From 4d845ef4139362bf3dffc3cda6568095555aa7ff Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Wed, 30 Nov 2022 20:38:30 +0800 Subject: [PATCH 1/4] Add search_filter table and entity --- packages/api/src/entity/search_filter.ts | 37 +++++++++++++++++++ .../db/migrations/0100.do.search_filters.sql | 23 ++++++++++++ .../migrations/0100.undo.search_filters.sql | 9 +++++ 3 files changed, 69 insertions(+) create mode 100644 packages/api/src/entity/search_filter.ts create mode 100755 packages/db/migrations/0100.do.search_filters.sql create mode 100755 packages/db/migrations/0100.undo.search_filters.sql diff --git a/packages/api/src/entity/search_filter.ts b/packages/api/src/entity/search_filter.ts new file mode 100644 index 000000000..8f4ed528f --- /dev/null +++ b/packages/api/src/entity/search_filter.ts @@ -0,0 +1,37 @@ +import { + Column, + CreateDateColumn, + Entity, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, + Unique, + UpdateDateColumn, +} from 'typeorm' +import { User } from './user' + +@Entity({ name: 'search_filter' }) +@Unique('search_filter_unique_key', ['user', 'name']) +export class SearchFilter { + @PrimaryGeneratedColumn('uuid') + id!: string + + @ManyToOne(() => User, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'user_id' }) + user!: User + + @Column('varchar', { length: 255 }) + name!: string + + @Column('varchar', { length: 255, nullable: true }) + description?: string | null + + @Column('varchar', { length: 255 }) + filter!: string + + @CreateDateColumn({ default: () => 'CURRENT_TIMESTAMP' }) + createdAt!: Date + + @UpdateDateColumn({ default: () => 'CURRENT_TIMESTAMP' }) + updatedAt!: Date +} diff --git a/packages/db/migrations/0100.do.search_filters.sql b/packages/db/migrations/0100.do.search_filters.sql new file mode 100755 index 000000000..dcf717340 --- /dev/null +++ b/packages/db/migrations/0100.do.search_filters.sql @@ -0,0 +1,23 @@ +-- Type: DO +-- Name: search_filters +-- Description: Create search_filters table + +BEGIN; + +CREATE TABLE omnivore.search_filters ( + id uuid PRIMARY KEY DEFAULT uuid_generate_v1mc(), + user_id uuid NOT NULL REFERENCES omnivore.user ON DELETE CASCADE, + name character varying(255) NOT NULL, + description character varying(255), + filter character varying(255) NOT NULL, + created_at timestamptz NOT NULL DEFAULT current_timestamp, + updated_at timestamptz NOT NULL DEFAULT current_timestamp, + UNIQUE (user_id, name) +); + +CREATE TRIGGER search_filters_modtime BEFORE UPDATE ON omnivore.search_filters + FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column(); + +GRANT SELECT, INSERT, UPDATE, DELETE ON omnivore.search_filters TO omnivore_user; + +COMMIT; diff --git a/packages/db/migrations/0100.undo.search_filters.sql b/packages/db/migrations/0100.undo.search_filters.sql new file mode 100755 index 000000000..b08361352 --- /dev/null +++ b/packages/db/migrations/0100.undo.search_filters.sql @@ -0,0 +1,9 @@ +-- Type: UNDO +-- Name: search_filters +-- Description: Create search_filters table + +BEGIN; + +DROP TABLE IF EXISTS omnivore.search_filters; + +COMMIT; From 1d514a05307863a8e0589ac2bbe4dfa5bc62c936 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Wed, 30 Nov 2022 20:39:09 +0800 Subject: [PATCH 2/4] Update table name --- packages/api/src/entity/search_filter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api/src/entity/search_filter.ts b/packages/api/src/entity/search_filter.ts index 8f4ed528f..1997a14ca 100644 --- a/packages/api/src/entity/search_filter.ts +++ b/packages/api/src/entity/search_filter.ts @@ -10,7 +10,7 @@ import { } from 'typeorm' import { User } from './user' -@Entity({ name: 'search_filter' }) +@Entity({ name: 'search_filters' }) @Unique('search_filter_unique_key', ['user', 'name']) export class SearchFilter { @PrimaryGeneratedColumn('uuid') From 0487d8325ca8b15bcde5ea0bb685ed806ffdaa77 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Wed, 30 Nov 2022 21:41:59 +0800 Subject: [PATCH 3/4] Add saved filter api --- .../entity/{search_filter.ts => filter.ts} | 6 +- packages/api/src/generated/graphql.ts | 173 ++++++++++++++++++ packages/api/src/generated/schema.graphql | 66 +++++++ packages/api/src/resolvers/filters/index.ts | 159 ++++++++++++++++ .../api/src/resolvers/function_resolvers.ts | 9 + packages/api/src/resolvers/index.ts | 1 + packages/api/src/schema.ts | 66 +++++++ ...search_filters.sql => 0100.do.filters.sql} | 6 +- ...arch_filters.sql => 0100.undo.filters.sql} | 2 +- 9 files changed, 481 insertions(+), 7 deletions(-) rename packages/api/src/entity/{search_filter.ts => filter.ts} (85%) create mode 100644 packages/api/src/resolvers/filters/index.ts rename packages/db/migrations/{0100.do.search_filters.sql => 0100.do.filters.sql} (73%) rename packages/db/migrations/{0100.undo.search_filters.sql => 0100.undo.filters.sql} (68%) diff --git a/packages/api/src/entity/search_filter.ts b/packages/api/src/entity/filter.ts similarity index 85% rename from packages/api/src/entity/search_filter.ts rename to packages/api/src/entity/filter.ts index 1997a14ca..4452d172e 100644 --- a/packages/api/src/entity/search_filter.ts +++ b/packages/api/src/entity/filter.ts @@ -10,9 +10,9 @@ import { } from 'typeorm' import { User } from './user' -@Entity({ name: 'search_filters' }) -@Unique('search_filter_unique_key', ['user', 'name']) -export class SearchFilter { +@Entity({ name: 'filters' }) +@Unique('filter_unique_key', ['user', 'name']) +export class Filter { @PrimaryGeneratedColumn('uuid') id!: string diff --git a/packages/api/src/generated/graphql.ts b/packages/api/src/generated/graphql.ts index d59c7f75c..c21ab2ca9 100644 --- a/packages/api/src/generated/graphql.ts +++ b/packages/api/src/generated/graphql.ts @@ -445,6 +445,24 @@ export type DeleteAccountSuccess = { userID: Scalars['ID']; }; +export type DeleteFilterError = { + __typename?: 'DeleteFilterError'; + errorCodes: Array; +}; + +export enum DeleteFilterErrorCode { + BadRequest = 'BAD_REQUEST', + NotFound = 'NOT_FOUND', + Unauthorized = 'UNAUTHORIZED' +} + +export type DeleteFilterResult = DeleteFilterError | DeleteFilterSuccess; + +export type DeleteFilterSuccess = { + __typename?: 'DeleteFilterSuccess'; + filter: Filter; +}; + export type DeleteHighlightError = { __typename?: 'DeleteHighlightError'; errorCodes: Array; @@ -679,6 +697,33 @@ export type FeedArticlesSuccess = { pageInfo: PageInfo; }; +export type Filter = { + __typename?: 'Filter'; + createdAt: Scalars['Date']; + description?: Maybe; + filter: Scalars['String']; + id: Scalars['ID']; + name: Scalars['String']; + updatedAt: Scalars['Date']; +}; + +export type FiltersError = { + __typename?: 'FiltersError'; + errorCodes: Array; +}; + +export enum FiltersErrorCode { + BadRequest = 'BAD_REQUEST', + Unauthorized = 'UNAUTHORIZED' +} + +export type FiltersResult = FiltersError | FiltersSuccess; + +export type FiltersSuccess = { + __typename?: 'FiltersSuccess'; + filters: Array; +}; + export type GenerateApiKeyError = { __typename?: 'GenerateApiKeyError'; errorCodes: Array; @@ -1004,6 +1049,7 @@ export type Mutation = { createReaction: CreateReactionResult; createReminder: CreateReminderResult; deleteAccount: DeleteAccountResult; + deleteFilter: DeleteFilterResult; deleteHighlight: DeleteHighlightResult; deleteHighlightReply: DeleteHighlightReplyResult; deleteIntegration: DeleteIntegrationResult; @@ -1024,6 +1070,7 @@ export type Mutation = { revokeApiKey: RevokeApiKeyResult; saveArticleReadingProgress: SaveArticleReadingProgressResult; saveFile: SaveResult; + saveFilter: SaveFilterResult; savePage: SaveResult; saveUrl: SaveResult; setBookmarkArticle: SetBookmarkArticleResult; @@ -1098,6 +1145,11 @@ export type MutationDeleteAccountArgs = { }; +export type MutationDeleteFilterArgs = { + id: Scalars['ID']; +}; + + export type MutationDeleteHighlightArgs = { highlightId: Scalars['ID']; }; @@ -1193,6 +1245,11 @@ export type MutationSaveFileArgs = { }; +export type MutationSaveFilterArgs = { + input: SaveFilterInput; +}; + + export type MutationSavePageArgs = { input: SavePageInput; }; @@ -1436,6 +1493,7 @@ export type Query = { articles: ArticlesResult; deviceTokens: DeviceTokensResult; feedArticles: FeedArticlesResult; + filters: FiltersResult; getFollowers: GetFollowersResult; getFollowing: GetFollowingResult; getUserPersonalization: GetUserPersonalizationResult; @@ -1761,6 +1819,31 @@ export type SaveFileInput = { url: Scalars['String']; }; +export type SaveFilterError = { + __typename?: 'SaveFilterError'; + errorCodes: Array; +}; + +export enum SaveFilterErrorCode { + BadRequest = 'BAD_REQUEST', + NotFound = 'NOT_FOUND', + Unauthorized = 'UNAUTHORIZED' +} + +export type SaveFilterInput = { + description?: InputMaybe; + filter: Scalars['String']; + id?: InputMaybe; + name: Scalars['String']; +}; + +export type SaveFilterResult = SaveFilterError | SaveFilterSuccess; + +export type SaveFilterSuccess = { + __typename?: 'SaveFilterSuccess'; + filter: Filter; +}; + export type SavePageInput = { clientRequestId: Scalars['ID']; originalContent: Scalars['String']; @@ -2829,6 +2912,10 @@ export type ResolversTypes = { DeleteAccountErrorCode: DeleteAccountErrorCode; DeleteAccountResult: ResolversTypes['DeleteAccountError'] | ResolversTypes['DeleteAccountSuccess']; DeleteAccountSuccess: ResolverTypeWrapper; + DeleteFilterError: ResolverTypeWrapper; + DeleteFilterErrorCode: DeleteFilterErrorCode; + DeleteFilterResult: ResolversTypes['DeleteFilterError'] | ResolversTypes['DeleteFilterSuccess']; + DeleteFilterSuccess: ResolverTypeWrapper; DeleteHighlightError: ResolverTypeWrapper; DeleteHighlightErrorCode: DeleteHighlightErrorCode; DeleteHighlightReplyError: ResolverTypeWrapper; @@ -2877,6 +2964,11 @@ export type ResolversTypes = { FeedArticlesErrorCode: FeedArticlesErrorCode; FeedArticlesResult: ResolversTypes['FeedArticlesError'] | ResolversTypes['FeedArticlesSuccess']; FeedArticlesSuccess: ResolverTypeWrapper; + Filter: ResolverTypeWrapper; + FiltersError: ResolverTypeWrapper; + FiltersErrorCode: FiltersErrorCode; + FiltersResult: ResolversTypes['FiltersError'] | ResolversTypes['FiltersSuccess']; + FiltersSuccess: ResolverTypeWrapper; Float: ResolverTypeWrapper; GenerateApiKeyError: ResolverTypeWrapper; GenerateApiKeyErrorCode: GenerateApiKeyErrorCode; @@ -2990,6 +3082,11 @@ export type ResolversTypes = { SaveError: ResolverTypeWrapper; SaveErrorCode: SaveErrorCode; SaveFileInput: SaveFileInput; + SaveFilterError: ResolverTypeWrapper; + SaveFilterErrorCode: SaveFilterErrorCode; + SaveFilterInput: SaveFilterInput; + SaveFilterResult: ResolversTypes['SaveFilterError'] | ResolversTypes['SaveFilterSuccess']; + SaveFilterSuccess: ResolverTypeWrapper; SavePageInput: SavePageInput; SaveResult: ResolversTypes['SaveError'] | ResolversTypes['SaveSuccess']; SaveSuccess: ResolverTypeWrapper; @@ -3225,6 +3322,9 @@ export type ResolversParentTypes = { DeleteAccountError: DeleteAccountError; DeleteAccountResult: ResolversParentTypes['DeleteAccountError'] | ResolversParentTypes['DeleteAccountSuccess']; DeleteAccountSuccess: DeleteAccountSuccess; + DeleteFilterError: DeleteFilterError; + DeleteFilterResult: ResolversParentTypes['DeleteFilterError'] | ResolversParentTypes['DeleteFilterSuccess']; + DeleteFilterSuccess: DeleteFilterSuccess; DeleteHighlightError: DeleteHighlightError; DeleteHighlightReplyError: DeleteHighlightReplyError; DeleteHighlightReplyResult: ResolversParentTypes['DeleteHighlightReplyError'] | ResolversParentTypes['DeleteHighlightReplySuccess']; @@ -3262,6 +3362,10 @@ export type ResolversParentTypes = { FeedArticlesError: FeedArticlesError; FeedArticlesResult: ResolversParentTypes['FeedArticlesError'] | ResolversParentTypes['FeedArticlesSuccess']; FeedArticlesSuccess: FeedArticlesSuccess; + Filter: Filter; + FiltersError: FiltersError; + FiltersResult: ResolversParentTypes['FiltersError'] | ResolversParentTypes['FiltersSuccess']; + FiltersSuccess: FiltersSuccess; Float: Scalars['Float']; GenerateApiKeyError: GenerateApiKeyError; GenerateApiKeyInput: GenerateApiKeyInput; @@ -3352,6 +3456,10 @@ export type ResolversParentTypes = { SaveArticleReadingProgressSuccess: SaveArticleReadingProgressSuccess; SaveError: SaveError; SaveFileInput: SaveFileInput; + SaveFilterError: SaveFilterError; + SaveFilterInput: SaveFilterInput; + SaveFilterResult: ResolversParentTypes['SaveFilterError'] | ResolversParentTypes['SaveFilterSuccess']; + SaveFilterSuccess: SaveFilterSuccess; SavePageInput: SavePageInput; SaveResult: ResolversParentTypes['SaveError'] | ResolversParentTypes['SaveSuccess']; SaveSuccess: SaveSuccess; @@ -3784,6 +3892,20 @@ export type DeleteAccountSuccessResolvers; }; +export type DeleteFilterErrorResolvers = { + errorCodes?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type DeleteFilterResultResolvers = { + __resolveType: TypeResolveFn<'DeleteFilterError' | 'DeleteFilterSuccess', ParentType, ContextType>; +}; + +export type DeleteFilterSuccessResolvers = { + filter?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type DeleteHighlightErrorResolvers = { errorCodes?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; @@ -3977,6 +4099,30 @@ export type FeedArticlesSuccessResolvers; }; +export type FilterResolvers = { + createdAt?: Resolver; + description?: Resolver, ParentType, ContextType>; + filter?: Resolver; + id?: Resolver; + name?: Resolver; + updatedAt?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type FiltersErrorResolvers = { + errorCodes?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type FiltersResultResolvers = { + __resolveType: TypeResolveFn<'FiltersError' | 'FiltersSuccess', ParentType, ContextType>; +}; + +export type FiltersSuccessResolvers = { + filters?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type GenerateApiKeyErrorResolvers = { errorCodes?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; @@ -4223,6 +4369,7 @@ export type MutationResolvers>; createReminder?: Resolver>; deleteAccount?: Resolver>; + deleteFilter?: Resolver>; deleteHighlight?: Resolver>; deleteHighlightReply?: Resolver>; deleteIntegration?: Resolver>; @@ -4243,6 +4390,7 @@ export type MutationResolvers>; saveArticleReadingProgress?: Resolver>; saveFile?: Resolver>; + saveFilter?: Resolver>; savePage?: Resolver>; saveUrl?: Resolver>; setBookmarkArticle?: Resolver>; @@ -4349,6 +4497,7 @@ export type QueryResolvers>; deviceTokens?: Resolver; feedArticles?: Resolver>; + filters?: Resolver; getFollowers?: Resolver>; getFollowing?: Resolver>; getUserPersonalization?: Resolver; @@ -4503,6 +4652,20 @@ export type SaveErrorResolvers; }; +export type SaveFilterErrorResolvers = { + errorCodes?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type SaveFilterResultResolvers = { + __resolveType: TypeResolveFn<'SaveFilterError' | 'SaveFilterSuccess', ParentType, ContextType>; +}; + +export type SaveFilterSuccessResolvers = { + filter?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type SaveResultResolvers = { __resolveType: TypeResolveFn<'SaveError' | 'SaveSuccess', ParentType, ContextType>; }; @@ -5144,6 +5307,9 @@ export type Resolvers = { DeleteAccountError?: DeleteAccountErrorResolvers; DeleteAccountResult?: DeleteAccountResultResolvers; DeleteAccountSuccess?: DeleteAccountSuccessResolvers; + DeleteFilterError?: DeleteFilterErrorResolvers; + DeleteFilterResult?: DeleteFilterResultResolvers; + DeleteFilterSuccess?: DeleteFilterSuccessResolvers; DeleteHighlightError?: DeleteHighlightErrorResolvers; DeleteHighlightReplyError?: DeleteHighlightReplyErrorResolvers; DeleteHighlightReplyResult?: DeleteHighlightReplyResultResolvers; @@ -5181,6 +5347,10 @@ export type Resolvers = { FeedArticlesError?: FeedArticlesErrorResolvers; FeedArticlesResult?: FeedArticlesResultResolvers; FeedArticlesSuccess?: FeedArticlesSuccessResolvers; + Filter?: FilterResolvers; + FiltersError?: FiltersErrorResolvers; + FiltersResult?: FiltersResultResolvers; + FiltersSuccess?: FiltersSuccessResolvers; GenerateApiKeyError?: GenerateApiKeyErrorResolvers; GenerateApiKeyResult?: GenerateApiKeyResultResolvers; GenerateApiKeySuccess?: GenerateApiKeySuccessResolvers; @@ -5256,6 +5426,9 @@ export type Resolvers = { SaveArticleReadingProgressResult?: SaveArticleReadingProgressResultResolvers; SaveArticleReadingProgressSuccess?: SaveArticleReadingProgressSuccessResolvers; SaveError?: SaveErrorResolvers; + SaveFilterError?: SaveFilterErrorResolvers; + SaveFilterResult?: SaveFilterResultResolvers; + SaveFilterSuccess?: SaveFilterSuccessResolvers; SaveResult?: SaveResultResolvers; SaveSuccess?: SaveSuccessResolvers; SearchError?: SearchErrorResolvers; diff --git a/packages/api/src/generated/schema.graphql b/packages/api/src/generated/schema.graphql index aa6ac5d79..0b36bf4ad 100644 --- a/packages/api/src/generated/schema.graphql +++ b/packages/api/src/generated/schema.graphql @@ -390,6 +390,22 @@ type DeleteAccountSuccess { userID: ID! } +type DeleteFilterError { + errorCodes: [DeleteFilterErrorCode!]! +} + +enum DeleteFilterErrorCode { + BAD_REQUEST + NOT_FOUND + UNAUTHORIZED +} + +union DeleteFilterResult = DeleteFilterError | DeleteFilterSuccess + +type DeleteFilterSuccess { + filter: Filter! +} + type DeleteHighlightError { errorCodes: [DeleteHighlightErrorCode!]! } @@ -598,6 +614,30 @@ type FeedArticlesSuccess { pageInfo: PageInfo! } +type Filter { + createdAt: Date! + description: String + filter: String! + id: ID! + name: String! + updatedAt: Date! +} + +type FiltersError { + errorCodes: [FiltersErrorCode!]! +} + +enum FiltersErrorCode { + BAD_REQUEST + UNAUTHORIZED +} + +union FiltersResult = FiltersError | FiltersSuccess + +type FiltersSuccess { + filters: [Filter!]! +} + type GenerateApiKeyError { errorCodes: [GenerateApiKeyErrorCode!]! } @@ -893,6 +933,7 @@ type Mutation { createReaction(input: CreateReactionInput!): CreateReactionResult! createReminder(input: CreateReminderInput!): CreateReminderResult! deleteAccount(userID: ID!): DeleteAccountResult! + deleteFilter(id: ID!): DeleteFilterResult! deleteHighlight(highlightId: ID!): DeleteHighlightResult! deleteHighlightReply(highlightReplyId: ID!): DeleteHighlightReplyResult! deleteIntegration(id: ID!): DeleteIntegrationResult! @@ -913,6 +954,7 @@ type Mutation { revokeApiKey(id: ID!): RevokeApiKeyResult! saveArticleReadingProgress(input: SaveArticleReadingProgressInput!): SaveArticleReadingProgressResult! saveFile(input: SaveFileInput!): SaveResult! + saveFilter(input: SaveFilterInput!): SaveFilterResult! savePage(input: SavePageInput!): SaveResult! saveUrl(input: SaveUrlInput!): SaveResult! setBookmarkArticle(input: SetBookmarkArticleInput!): SetBookmarkArticleResult! @@ -1046,6 +1088,7 @@ type Query { articles(after: String, first: Int, includePending: Boolean, query: String, sharedOnly: Boolean, sort: SortParams): ArticlesResult! deviceTokens: DeviceTokensResult! feedArticles(after: String, first: Int, sharedByUser: ID, sort: SortParams): FeedArticlesResult! + filters: FiltersResult! getFollowers(userId: ID): GetFollowersResult! getFollowing(userId: ID): GetFollowingResult! getUserPersonalization: GetUserPersonalizationResult! @@ -1255,6 +1298,29 @@ input SaveFileInput { url: String! } +type SaveFilterError { + errorCodes: [SaveFilterErrorCode!]! +} + +enum SaveFilterErrorCode { + BAD_REQUEST + NOT_FOUND + UNAUTHORIZED +} + +input SaveFilterInput { + description: String + filter: String! + id: ID + name: String! +} + +union SaveFilterResult = SaveFilterError | SaveFilterSuccess + +type SaveFilterSuccess { + filter: Filter! +} + input SavePageInput { clientRequestId: ID! originalContent: String! diff --git a/packages/api/src/resolvers/filters/index.ts b/packages/api/src/resolvers/filters/index.ts new file mode 100644 index 000000000..87cc44590 --- /dev/null +++ b/packages/api/src/resolvers/filters/index.ts @@ -0,0 +1,159 @@ +import { authorized } from '../../utils/helpers' +import { + DeleteFilterError, + DeleteFilterErrorCode, + DeleteFilterSuccess, + FiltersError, + FiltersErrorCode, + FiltersSuccess, + MutationDeleteFilterArgs, + MutationSaveFilterArgs, + SaveFilterError, + SaveFilterErrorCode, + SaveFilterSuccess, +} from '../../generated/graphql' +import { Filter } from '../../entity/filter' +import { getRepository } from '../../entity/utils' +import { User } from '../../entity/user' + +export const saveFilterResolver = authorized< + SaveFilterSuccess, + SaveFilterError, + MutationSaveFilterArgs +>(async (_, { input }, { claims, log }) => { + log.info('Saving filters', { + input, + labels: { + source: 'resolver', + resolver: 'saveFilterResolver', + uid: claims.uid, + }, + }) + + try { + const user = await getRepository(User).findOneBy({ id: claims.uid }) + if (!user) { + return { + errorCodes: [SaveFilterErrorCode.Unauthorized], + } + } + + const filter = await getRepository(Filter).save({ + ...input, + id: input.id ?? undefined, + user: { id: claims.uid }, + }) + + return { + filter, + } + } catch (error) { + log.error('Error saving filters', { + error, + labels: { + source: 'resolver', + resolver: 'saveFilterResolver', + uid: claims.uid, + }, + }) + + return { + errorCodes: [SaveFilterErrorCode.BadRequest], + } + } +}) + +export const deleteFilterResolver = authorized< + DeleteFilterSuccess, + DeleteFilterError, + MutationDeleteFilterArgs +>(async (_, { id }, { claims, log }) => { + log.info('Deleting filters', { + id, + labels: { + source: 'resolver', + resolver: 'deleteFilterResolver', + uid: claims.uid, + }, + }) + + try { + const user = await getRepository(User).findOneBy({ id: claims.uid }) + if (!user) { + return { + errorCodes: [DeleteFilterErrorCode.Unauthorized], + } + } + + const filter = await getRepository(Filter).findOneBy({ + id, + user: { id: claims.uid }, + }) + if (!filter) { + return { + errorCodes: [DeleteFilterErrorCode.NotFound], + } + } + + await getRepository(Filter).delete({ id }) + + return { + filter, + } + } catch (error) { + log.error('Error deleting filters', { + error, + labels: { + source: 'resolver', + resolver: 'deleteFilterResolver', + uid: claims.uid, + }, + }) + + return { + errorCodes: [DeleteFilterErrorCode.BadRequest], + } + } +}) + +export const filtersResolver = authorized( + async (_, __, { claims, log }) => { + log.info('Getting filters', { + labels: { + source: 'resolver', + resolver: 'filtersResolver', + uid: claims.uid, + }, + }) + + try { + const user = await getRepository(User).findOneBy({ id: claims.uid }) + if (!user) { + return { + errorCodes: [FiltersErrorCode.Unauthorized], + } + } + + const filters = await getRepository(Filter).findBy({ + user: { id: claims.uid }, + }) + + return { + filters, + } + } catch (error) { + log.error('Error getting filters', { + error, + labels: { + source: 'resolver', + resolver: 'filtersResolver', + uid: claims.uid, + }, + }) + + return { + errorCodes: [FiltersErrorCode.BadRequest], + } + } + } +) diff --git a/packages/api/src/resolvers/function_resolvers.ts b/packages/api/src/resolvers/function_resolvers.ts index a9beca613..a0143ea84 100644 --- a/packages/api/src/resolvers/function_resolvers.ts +++ b/packages/api/src/resolvers/function_resolvers.ts @@ -31,6 +31,7 @@ import { createNewsletterEmailResolver, createReminderResolver, deleteAccountResolver, + deleteFilterResolver, deleteHighlightResolver, deleteIntegrationResolver, deleteLabelResolver, @@ -39,6 +40,7 @@ import { deleteRuleResolver, deleteWebhookResolver, deviceTokensResolver, + filtersResolver, generateApiKeyResolver, getAllUsersResolver, getArticleResolver, @@ -64,6 +66,7 @@ import { rulesResolver, saveArticleReadingProgressResolver, saveFileResolver, + saveFilterResolver, savePageResolver, saveUrlResolver, searchResolver, @@ -179,6 +182,8 @@ export const functionResolvers = { optInFeature: optInFeatureResolver, setRule: setRuleResolver, deleteRule: deleteRuleResolver, + saveFilter: saveFilterResolver, + deleteFilter: deleteFilterResolver, }, Query: { me: getMeUserResolver, @@ -208,6 +213,7 @@ export const functionResolvers = { recentSearches: recentSearchesResolver, rules: rulesResolver, deviceTokens: deviceTokensResolver, + filters: filtersResolver, }, User: { async sharedArticles( @@ -622,4 +628,7 @@ export const functionResolvers = { ...resultResolveTypeResolver('Rules'), ...resultResolveTypeResolver('DeviceTokens'), ...resultResolveTypeResolver('DeleteRule'), + ...resultResolveTypeResolver('SaveFilter'), + ...resultResolveTypeResolver('Filters'), + ...resultResolveTypeResolver('DeleteFilter'), } diff --git a/packages/api/src/resolvers/index.ts b/packages/api/src/resolvers/index.ts index f303cbc3d..ee324050e 100644 --- a/packages/api/src/resolvers/index.ts +++ b/packages/api/src/resolvers/index.ts @@ -22,3 +22,4 @@ export * from './webhooks' export * from './api_key' export * from './integrations' export * from './rules' +export * from './filters' diff --git a/packages/api/src/schema.ts b/packages/api/src/schema.ts index 5823f48ee..663268439 100755 --- a/packages/api/src/schema.ts +++ b/packages/api/src/schema.ts @@ -2051,6 +2051,69 @@ const schema = gql` BAD_REQUEST } + input SaveFilterInput { + id: ID + name: String! + filter: String! + description: String + } + + union SaveFilterResult = SaveFilterSuccess | SaveFilterError + + type SaveFilterSuccess { + filter: Filter! + } + + type Filter { + id: ID! + name: String! + filter: String! + description: String + createdAt: Date! + updatedAt: Date! + } + + type SaveFilterError { + errorCodes: [SaveFilterErrorCode!]! + } + + enum SaveFilterErrorCode { + UNAUTHORIZED + BAD_REQUEST + NOT_FOUND + } + + union FiltersResult = FiltersSuccess | FiltersError + + type FiltersSuccess { + filters: [Filter!]! + } + + type FiltersError { + errorCodes: [FiltersErrorCode!]! + } + + enum FiltersErrorCode { + UNAUTHORIZED + BAD_REQUEST + } + + union DeleteFilterResult = DeleteFilterSuccess | DeleteFilterError + + type DeleteFilterSuccess { + filter: Filter! + } + + type DeleteFilterError { + errorCodes: [DeleteFilterErrorCode!]! + } + + enum DeleteFilterErrorCode { + UNAUTHORIZED + BAD_REQUEST + NOT_FOUND + } + # Mutations type Mutation { googleLogin(input: GoogleLoginInput!): LoginResult! @@ -2125,6 +2188,8 @@ const schema = gql` optInFeature(input: OptInFeatureInput!): OptInFeatureResult! setRule(input: SetRuleInput!): SetRuleResult! deleteRule(id: ID!): DeleteRuleResult! + saveFilter(input: SaveFilterInput!): SaveFilterResult! + deleteFilter(id: ID!): DeleteFilterResult! } # FIXME: remove sort from feedArticles after all cached tabs are closed @@ -2179,6 +2244,7 @@ const schema = gql` recentSearches: RecentSearchesResult! rules(enabled: Boolean): RulesResult! deviceTokens: DeviceTokensResult! + filters: FiltersResult! } ` diff --git a/packages/db/migrations/0100.do.search_filters.sql b/packages/db/migrations/0100.do.filters.sql similarity index 73% rename from packages/db/migrations/0100.do.search_filters.sql rename to packages/db/migrations/0100.do.filters.sql index dcf717340..629dbf610 100755 --- a/packages/db/migrations/0100.do.search_filters.sql +++ b/packages/db/migrations/0100.do.filters.sql @@ -4,7 +4,7 @@ BEGIN; -CREATE TABLE omnivore.search_filters ( +CREATE TABLE omnivore.filters ( id uuid PRIMARY KEY DEFAULT uuid_generate_v1mc(), user_id uuid NOT NULL REFERENCES omnivore.user ON DELETE CASCADE, name character varying(255) NOT NULL, @@ -15,9 +15,9 @@ CREATE TABLE omnivore.search_filters ( UNIQUE (user_id, name) ); -CREATE TRIGGER search_filters_modtime BEFORE UPDATE ON omnivore.search_filters +CREATE TRIGGER filters_modtime BEFORE UPDATE ON omnivore.filters FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column(); -GRANT SELECT, INSERT, UPDATE, DELETE ON omnivore.search_filters TO omnivore_user; +GRANT SELECT, INSERT, UPDATE, DELETE ON omnivore.filters TO omnivore_user; COMMIT; diff --git a/packages/db/migrations/0100.undo.search_filters.sql b/packages/db/migrations/0100.undo.filters.sql similarity index 68% rename from packages/db/migrations/0100.undo.search_filters.sql rename to packages/db/migrations/0100.undo.filters.sql index b08361352..1837c1580 100755 --- a/packages/db/migrations/0100.undo.search_filters.sql +++ b/packages/db/migrations/0100.undo.filters.sql @@ -4,6 +4,6 @@ BEGIN; -DROP TABLE IF EXISTS omnivore.search_filters; +DROP TABLE IF EXISTS omnivore.filters; COMMIT; From 0eebfe923aa786e4063ff58a1dd2dec498d9ff59 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Thu, 1 Dec 2022 10:26:32 +0800 Subject: [PATCH 4/4] Add position in filter table and move filter api --- packages/api/src/entity/filter.ts | 3 + packages/api/src/generated/graphql.ts | 58 +++++++ packages/api/src/generated/schema.graphql | 23 +++ packages/api/src/resolvers/filters/index.ts | 144 +++++++++++++++++- .../api/src/resolvers/function_resolvers.ts | 3 + packages/api/src/schema.ts | 23 +++ packages/db/migrations/0100.do.filters.sql | 30 +++- packages/db/migrations/0100.undo.filters.sql | 2 + 8 files changed, 282 insertions(+), 4 deletions(-) diff --git a/packages/api/src/entity/filter.ts b/packages/api/src/entity/filter.ts index 4452d172e..54583167a 100644 --- a/packages/api/src/entity/filter.ts +++ b/packages/api/src/entity/filter.ts @@ -29,6 +29,9 @@ export class Filter { @Column('varchar', { length: 255 }) filter!: string + @Column('integer', { default: 0 }) + position!: number + @CreateDateColumn({ default: () => 'CURRENT_TIMESTAMP' }) createdAt!: Date diff --git a/packages/api/src/generated/graphql.ts b/packages/api/src/generated/graphql.ts index c21ab2ca9..8498e29bd 100644 --- a/packages/api/src/generated/graphql.ts +++ b/packages/api/src/generated/graphql.ts @@ -704,6 +704,7 @@ export type Filter = { filter: Scalars['String']; id: Scalars['ID']; name: Scalars['String']; + position: Scalars['Int']; updatedAt: Scalars['Date']; }; @@ -1014,6 +1015,29 @@ export type MergeHighlightSuccess = { overlapHighlightIdList: Array; }; +export type MoveFilterError = { + __typename?: 'MoveFilterError'; + errorCodes: Array; +}; + +export enum MoveFilterErrorCode { + BadRequest = 'BAD_REQUEST', + NotFound = 'NOT_FOUND', + Unauthorized = 'UNAUTHORIZED' +} + +export type MoveFilterInput = { + afterFilterId?: InputMaybe; + filterId: Scalars['ID']; +}; + +export type MoveFilterResult = MoveFilterError | MoveFilterSuccess; + +export type MoveFilterSuccess = { + __typename?: 'MoveFilterSuccess'; + filter: Filter; +}; + export type MoveLabelError = { __typename?: 'MoveLabelError'; errorCodes: Array; @@ -1064,6 +1088,7 @@ export type Mutation = { googleSignup: GoogleSignupResult; logOut: LogOutResult; mergeHighlight: MergeHighlightResult; + moveFilter: MoveFilterResult; moveLabel: MoveLabelResult; optInFeature: OptInFeatureResult; reportItem: ReportItemResult; @@ -1215,6 +1240,11 @@ export type MutationMergeHighlightArgs = { }; +export type MutationMoveFilterArgs = { + input: MoveFilterInput; +}; + + export type MutationMoveLabelArgs = { input: MoveLabelInput; }; @@ -3023,6 +3053,11 @@ export type ResolversTypes = { MergeHighlightInput: MergeHighlightInput; MergeHighlightResult: ResolversTypes['MergeHighlightError'] | ResolversTypes['MergeHighlightSuccess']; MergeHighlightSuccess: ResolverTypeWrapper; + MoveFilterError: ResolverTypeWrapper; + MoveFilterErrorCode: MoveFilterErrorCode; + MoveFilterInput: MoveFilterInput; + MoveFilterResult: ResolversTypes['MoveFilterError'] | ResolversTypes['MoveFilterSuccess']; + MoveFilterSuccess: ResolverTypeWrapper; MoveLabelError: ResolverTypeWrapper; MoveLabelErrorCode: MoveLabelErrorCode; MoveLabelInput: MoveLabelInput; @@ -3410,6 +3445,10 @@ export type ResolversParentTypes = { MergeHighlightInput: MergeHighlightInput; MergeHighlightResult: ResolversParentTypes['MergeHighlightError'] | ResolversParentTypes['MergeHighlightSuccess']; MergeHighlightSuccess: MergeHighlightSuccess; + MoveFilterError: MoveFilterError; + MoveFilterInput: MoveFilterInput; + MoveFilterResult: ResolversParentTypes['MoveFilterError'] | ResolversParentTypes['MoveFilterSuccess']; + MoveFilterSuccess: MoveFilterSuccess; MoveLabelError: MoveLabelError; MoveLabelInput: MoveLabelInput; MoveLabelResult: ResolversParentTypes['MoveLabelError'] | ResolversParentTypes['MoveLabelSuccess']; @@ -4105,6 +4144,7 @@ export type FilterResolvers; id?: Resolver; name?: Resolver; + position?: Resolver; updatedAt?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; @@ -4344,6 +4384,20 @@ export type MergeHighlightSuccessResolvers; }; +export type MoveFilterErrorResolvers = { + errorCodes?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type MoveFilterResultResolvers = { + __resolveType: TypeResolveFn<'MoveFilterError' | 'MoveFilterSuccess', ParentType, ContextType>; +}; + +export type MoveFilterSuccessResolvers = { + filter?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type MoveLabelErrorResolvers = { errorCodes?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; @@ -4384,6 +4438,7 @@ export type MutationResolvers>; logOut?: Resolver; mergeHighlight?: Resolver>; + moveFilter?: Resolver>; moveLabel?: Resolver>; optInFeature?: Resolver>; reportItem?: Resolver>; @@ -5388,6 +5443,9 @@ export type Resolvers = { MergeHighlightError?: MergeHighlightErrorResolvers; MergeHighlightResult?: MergeHighlightResultResolvers; MergeHighlightSuccess?: MergeHighlightSuccessResolvers; + MoveFilterError?: MoveFilterErrorResolvers; + MoveFilterResult?: MoveFilterResultResolvers; + MoveFilterSuccess?: MoveFilterSuccessResolvers; MoveLabelError?: MoveLabelErrorResolvers; MoveLabelResult?: MoveLabelResultResolvers; MoveLabelSuccess?: MoveLabelSuccessResolvers; diff --git a/packages/api/src/generated/schema.graphql b/packages/api/src/generated/schema.graphql index 0b36bf4ad..7f9087672 100644 --- a/packages/api/src/generated/schema.graphql +++ b/packages/api/src/generated/schema.graphql @@ -620,6 +620,7 @@ type Filter { filter: String! id: ID! name: String! + position: Int! updatedAt: Date! } @@ -901,6 +902,27 @@ type MergeHighlightSuccess { overlapHighlightIdList: [String!]! } +type MoveFilterError { + errorCodes: [MoveFilterErrorCode!]! +} + +enum MoveFilterErrorCode { + BAD_REQUEST + NOT_FOUND + UNAUTHORIZED +} + +input MoveFilterInput { + afterFilterId: ID + filterId: ID! +} + +union MoveFilterResult = MoveFilterError | MoveFilterSuccess + +type MoveFilterSuccess { + filter: Filter! +} + type MoveLabelError { errorCodes: [MoveLabelErrorCode!]! } @@ -948,6 +970,7 @@ type Mutation { googleSignup(input: GoogleSignupInput!): GoogleSignupResult! logOut: LogOutResult! mergeHighlight(input: MergeHighlightInput!): MergeHighlightResult! + moveFilter(input: MoveFilterInput!): MoveFilterResult! moveLabel(input: MoveLabelInput!): MoveLabelResult! optInFeature(input: OptInFeatureInput!): OptInFeatureResult! reportItem(input: ReportItemInput!): ReportItemResult! diff --git a/packages/api/src/resolvers/filters/index.ts b/packages/api/src/resolvers/filters/index.ts index 87cc44590..19619b5f1 100644 --- a/packages/api/src/resolvers/filters/index.ts +++ b/packages/api/src/resolvers/filters/index.ts @@ -6,15 +6,23 @@ import { FiltersError, FiltersErrorCode, FiltersSuccess, + MoveFilterError, + MoveFilterErrorCode, + MoveFilterSuccess, MutationDeleteFilterArgs, + MutationMoveFilterArgs, MutationSaveFilterArgs, SaveFilterError, SaveFilterErrorCode, SaveFilterSuccess, } from '../../generated/graphql' import { Filter } from '../../entity/filter' -import { getRepository } from '../../entity/utils' +import { getRepository, setClaims } from '../../entity/utils' import { User } from '../../entity/user' +import { AppDataSource } from '../../server' +import { Between } from 'typeorm' +import { analytics } from '../../utils/analytics' +import { env } from '../../env' export const saveFilterResolver = authorized< SaveFilterSuccess, @@ -134,8 +142,9 @@ export const filtersResolver = authorized( } } - const filters = await getRepository(Filter).findBy({ - user: { id: claims.uid }, + const filters = await getRepository(Filter).find({ + where: { user: { id: claims.uid } }, + order: { position: 'ASC' }, }) return { @@ -157,3 +166,132 @@ export const filtersResolver = authorized( } } ) + +export const moveFilterResolver = authorized< + MoveFilterSuccess, + MoveFilterError, + MutationMoveFilterArgs +>(async (_, { input }, { claims: { uid }, log }) => { + log.info('Moving filters', { + input, + filters: { + source: 'resolver', + resolver: 'moveFilterResolver', + uid, + }, + }) + + const { filterId, afterFilterId } = input + + try { + const user = await getRepository(User).findOneBy({ id: uid }) + if (!user) { + return { + errorCodes: [MoveFilterErrorCode.Unauthorized], + } + } + + const filter = await getRepository(Filter).findOne({ + where: { id: filterId }, + relations: ['user'], + }) + if (!filter) { + return { + errorCodes: [MoveFilterErrorCode.NotFound], + } + } + if (filter.user.id !== uid) { + return { + errorCodes: [MoveFilterErrorCode.Unauthorized], + } + } + + if (filter.id === afterFilterId) { + // nothing to do + return { filter } + } + + const oldPosition = filter.position + // if afterFilterId is not provided, move to the top + let newPosition = 1 + if (afterFilterId) { + const afterFilter = await getRepository(Filter).findOne({ + where: { id: afterFilterId }, + relations: ['user'], + }) + if (!afterFilter) { + return { + errorCodes: [MoveFilterErrorCode.NotFound], + } + } + if (afterFilter.user.id !== uid) { + return { + errorCodes: [MoveFilterErrorCode.Unauthorized], + } + } + newPosition = afterFilter.position + } + const moveUp = newPosition < oldPosition + + // move filter to the new position + const updated = await AppDataSource.transaction(async (t) => { + await setClaims(t, uid) + + // update the position of the other filters + const updated = await t.getRepository(Filter).update( + { + user: { id: uid }, + position: Between( + Math.min(newPosition, oldPosition), + Math.max(newPosition, oldPosition) + ), + }, + { + position: () => `position + ${moveUp ? 1 : -1}`, + } + ) + if (!updated.affected) { + return null + } + + // update the position of the filter + return t.getRepository(Filter).save({ + ...filter, + position: newPosition, + }) + }) + + if (!updated) { + return { + errorCodes: [MoveFilterErrorCode.BadRequest], + } + } + + analytics.track({ + userId: uid, + event: 'filter_moved', + properties: { + filterId, + afterFilterId, + env: env.server.apiEnv, + }, + }) + + return { + filter: updated, + } + } catch (error) { + log.error('Error moving filters', { + error, + labels: { + source: 'resolver', + resolver: 'moveFilterResolver', + uid, + }, + }) + + return { + errorCodes: [MoveFilterErrorCode.BadRequest], + } + } +}) diff --git a/packages/api/src/resolvers/function_resolvers.ts b/packages/api/src/resolvers/function_resolvers.ts index a0143ea84..2648160a7 100644 --- a/packages/api/src/resolvers/function_resolvers.ts +++ b/packages/api/src/resolvers/function_resolvers.ts @@ -58,6 +58,7 @@ import { labelsResolver, logOutResolver, mergeHighlightResolver, + moveFilterResolver, moveLabelResolver, newsletterEmailsResolver, reminderResolver, @@ -184,6 +185,7 @@ export const functionResolvers = { deleteRule: deleteRuleResolver, saveFilter: saveFilterResolver, deleteFilter: deleteFilterResolver, + moveFilter: moveFilterResolver, }, Query: { me: getMeUserResolver, @@ -631,4 +633,5 @@ export const functionResolvers = { ...resultResolveTypeResolver('SaveFilter'), ...resultResolveTypeResolver('Filters'), ...resultResolveTypeResolver('DeleteFilter'), + ...resultResolveTypeResolver('MoveFilter'), } diff --git a/packages/api/src/schema.ts b/packages/api/src/schema.ts index 663268439..294949f51 100755 --- a/packages/api/src/schema.ts +++ b/packages/api/src/schema.ts @@ -2068,6 +2068,7 @@ const schema = gql` id: ID! name: String! filter: String! + position: Int! description: String createdAt: Date! updatedAt: Date! @@ -2114,6 +2115,27 @@ const schema = gql` NOT_FOUND } + input MoveFilterInput { + filterId: ID! + afterFilterId: ID # null to move to the top + } + + union MoveFilterResult = MoveFilterSuccess | MoveFilterError + + type MoveFilterSuccess { + filter: Filter! + } + + type MoveFilterError { + errorCodes: [MoveFilterErrorCode!]! + } + + enum MoveFilterErrorCode { + UNAUTHORIZED + BAD_REQUEST + NOT_FOUND + } + # Mutations type Mutation { googleLogin(input: GoogleLoginInput!): LoginResult! @@ -2190,6 +2212,7 @@ const schema = gql` deleteRule(id: ID!): DeleteRuleResult! saveFilter(input: SaveFilterInput!): SaveFilterResult! deleteFilter(id: ID!): DeleteFilterResult! + moveFilter(input: MoveFilterInput!): MoveFilterResult! } # FIXME: remove sort from feedArticles after all cached tabs are closed diff --git a/packages/db/migrations/0100.do.filters.sql b/packages/db/migrations/0100.do.filters.sql index 629dbf610..82516e745 100755 --- a/packages/db/migrations/0100.do.filters.sql +++ b/packages/db/migrations/0100.do.filters.sql @@ -10,14 +10,42 @@ CREATE TABLE omnivore.filters ( name character varying(255) NOT NULL, description character varying(255), filter character varying(255) NOT NULL, + position integer NOT NULL DEFAULT 0, created_at timestamptz NOT NULL DEFAULT current_timestamp, updated_at timestamptz NOT NULL DEFAULT current_timestamp, UNIQUE (user_id, name) ); -CREATE TRIGGER filters_modtime BEFORE UPDATE ON omnivore.filters +CREATE OR REPLACE FUNCTION update_filter_position() + RETURNS TRIGGER AS $$ +DECLARE + new_position INTEGER; +BEGIN + IF (TG_OP = 'DELETE') THEN + UPDATE omnivore.filters SET position = position - 1 WHERE user_id = OLD.user_id AND position > OLD.position; + RETURN OLD; + ELSIF (TG_OP = 'INSERT') THEN + SELECT COALESCE(MAX(position), 0) + 1 INTO new_position FROM omnivore.filters WHERE user_id = NEW.user_id AND name < NEW.name; + UPDATE omnivore.filters SET position = position + 1 WHERE user_id = NEW.user_id AND position >= new_position; + NEW.position = new_position; + RETURN NEW; + END IF; +END; +$$ LANGUAGE 'plpgsql'; + +CREATE TRIGGER update_filter_modtime BEFORE UPDATE ON omnivore.filters FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column(); +CREATE TRIGGER increment_filter_position + BEFORE INSERT ON omnivore.filters + FOR EACH ROW +EXECUTE FUNCTION update_filter_position(); + +CREATE TRIGGER decrement_filter_position + AFTER DELETE ON omnivore.filters + FOR EACH ROW +EXECUTE FUNCTION update_filter_position(); + GRANT SELECT, INSERT, UPDATE, DELETE ON omnivore.filters TO omnivore_user; COMMIT; diff --git a/packages/db/migrations/0100.undo.filters.sql b/packages/db/migrations/0100.undo.filters.sql index 1837c1580..78b173e61 100755 --- a/packages/db/migrations/0100.undo.filters.sql +++ b/packages/db/migrations/0100.undo.filters.sql @@ -4,6 +4,8 @@ BEGIN; +DROP FUNCTION IF EXISTS update_filter_position; + DROP TABLE IF EXISTS omnivore.filters; COMMIT;