From 25ca4c77913c66c5c1e5046dd50a1512c6c21f33 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Mon, 10 Jun 2024 17:15:23 +0800 Subject: [PATCH 1/8] create folder_policy table --- packages/api/src/entity/folder_policy.ts | 39 +++++++++++++++++++ .../db/migrations/0180.do.folder_policy.sql | 22 +++++++++++ .../db/migrations/0180.undo.folder_policy.sql | 9 +++++ 3 files changed, 70 insertions(+) create mode 100644 packages/api/src/entity/folder_policy.ts create mode 100755 packages/db/migrations/0180.do.folder_policy.sql create mode 100755 packages/db/migrations/0180.undo.folder_policy.sql diff --git a/packages/api/src/entity/folder_policy.ts b/packages/api/src/entity/folder_policy.ts new file mode 100644 index 000000000..2f77f4552 --- /dev/null +++ b/packages/api/src/entity/folder_policy.ts @@ -0,0 +1,39 @@ +import { + Column, + Entity, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, +} from 'typeorm' +import { User } from './user' + +@Entity({ name: 'folder_policy' }) +export class FolderPolicy { + @PrimaryGeneratedColumn('uuid') + id!: string + + @Column('uuid') + userId!: string + + @ManyToOne(() => User, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'user_id' }) + user!: User + + @Column('text') + folder!: string + + @Column('text') + action!: string + + @Column('int') + afterDays!: number + + @Column('int') + minimumItems!: number + + @Column('timestamptz') + createdAt!: Date + + @Column('timestamptz') + updatedAt!: Date +} diff --git a/packages/db/migrations/0180.do.folder_policy.sql b/packages/db/migrations/0180.do.folder_policy.sql new file mode 100755 index 000000000..aa9d7bd73 --- /dev/null +++ b/packages/db/migrations/0180.do.folder_policy.sql @@ -0,0 +1,22 @@ +-- Type: DO +-- Name: folder_policy +-- Description: Create a folder_policy table to contain the folder expiration policies for user and folder + +BEGIN; + +CREATE TABLE omnivore.folder_policy ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v1mc(), + user_id UUID NOT NULL REFERENCES omnivore.user(id) ON DELETE CASCADE, + folder TEXT NOT NULL, -- folder name in lowercase + action TEXT NOT NULL, -- delete or archive + after_days INT NOT NULL, -- number of days after which the action should be taken + minimum_items INT NOT NULL DEFAULT 0, -- minimum number of items to keep in the folder + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TRIGGER update_folder_policy_modtime BEFORE UPDATE ON omnivore.folder_policy FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column(); + +GRANT SELECT, INSERT, UPDATE, DELETE ON omnivore.folder_policy TO omnivore_user; + +COMMIT; diff --git a/packages/db/migrations/0180.undo.folder_policy.sql b/packages/db/migrations/0180.undo.folder_policy.sql new file mode 100755 index 000000000..20add6644 --- /dev/null +++ b/packages/db/migrations/0180.undo.folder_policy.sql @@ -0,0 +1,9 @@ +-- Type: UNDO +-- Name: folder_policy +-- Description: Create a folder_policy table to contain the folder expiration policies for user and folder + +BEGIN; + +DROP TABLE omnivore.folder_policy; + +COMMIT; From 7940c3f4eabd138e2873b2084a9f3b0462aaea9e Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Mon, 10 Jun 2024 19:25:58 +0800 Subject: [PATCH 2/8] add list folder policy api --- packages/api/src/entity/folder_policy.ts | 9 +- packages/api/src/generated/graphql.ts | 236 ++++++++++++++++++ packages/api/src/generated/schema.graphql | 93 +++++++ .../api/src/resolvers/folder_policy/index.ts | 23 ++ .../api/src/resolvers/function_resolvers.ts | 4 + packages/api/src/schema.ts | 103 ++++++++ packages/api/src/services/folder_policy.ts | 38 +++ .../api/test/resolvers/folder_policy.test.ts | 59 +++++ packages/api/test/util.ts | 8 + .../db/migrations/0180.do.folder_policy.sql | 7 +- .../db/migrations/0180.undo.folder_policy.sql | 2 + 11 files changed, 578 insertions(+), 4 deletions(-) create mode 100644 packages/api/src/resolvers/folder_policy/index.ts create mode 100644 packages/api/src/services/folder_policy.ts create mode 100644 packages/api/test/resolvers/folder_policy.test.ts diff --git a/packages/api/src/entity/folder_policy.ts b/packages/api/src/entity/folder_policy.ts index 2f77f4552..82bc0a160 100644 --- a/packages/api/src/entity/folder_policy.ts +++ b/packages/api/src/entity/folder_policy.ts @@ -7,6 +7,11 @@ import { } from 'typeorm' import { User } from './user' +export enum FolderPolicyAction { + DELETE = 'DELETE', + ARCHIVE = 'ARCHIVE', +} + @Entity({ name: 'folder_policy' }) export class FolderPolicy { @PrimaryGeneratedColumn('uuid') @@ -22,8 +27,8 @@ export class FolderPolicy { @Column('text') folder!: string - @Column('text') - action!: string + @Column('enum', { enum: FolderPolicyAction }) + action!: FolderPolicyAction @Column('int') afterDays!: number diff --git a/packages/api/src/generated/graphql.ts b/packages/api/src/generated/graphql.ts index 203364aa9..f388b1282 100644 --- a/packages/api/src/generated/graphql.ts +++ b/packages/api/src/generated/graphql.ts @@ -349,6 +349,30 @@ export type CreateArticleSuccess = { user: User; }; +export type CreateFolderPolicyError = { + __typename?: 'CreateFolderPolicyError'; + errorCodes: Array; +}; + +export enum CreateFolderPolicyErrorCode { + BadRequest = 'BAD_REQUEST', + Unauthorized = 'UNAUTHORIZED' +} + +export type CreateFolderPolicyInput = { + action: FolderPolicyAction; + afterDays: Scalars['Int']; + folder: Scalars['String']; + minimumItems?: InputMaybe; +}; + +export type CreateFolderPolicyResult = CreateFolderPolicyError | CreateFolderPolicySuccess; + +export type CreateFolderPolicySuccess = { + __typename?: 'CreateFolderPolicySuccess'; + policy: FolderPolicy; +}; + export type CreateGroupError = { __typename?: 'CreateGroupError'; errorCodes: Array; @@ -619,6 +643,23 @@ export type DeleteFilterSuccess = { filter: Filter; }; +export type DeleteFolderPolicyError = { + __typename?: 'DeleteFolderPolicyError'; + errorCodes: Array; +}; + +export enum DeleteFolderPolicyErrorCode { + BadRequest = 'BAD_REQUEST', + Unauthorized = 'UNAUTHORIZED' +} + +export type DeleteFolderPolicyResult = DeleteFolderPolicyError | DeleteFolderPolicySuccess; + +export type DeleteFolderPolicySuccess = { + __typename?: 'DeleteFolderPolicySuccess'; + success: Scalars['Boolean']; +}; + export type DeleteHighlightError = { __typename?: 'DeleteHighlightError'; errorCodes: Array; @@ -1081,6 +1122,39 @@ export type FiltersSuccess = { filters: Array; }; +export type FolderPoliciesError = { + __typename?: 'FolderPoliciesError'; + errorCodes: Array; +}; + +export enum FolderPoliciesErrorCode { + BadRequest = 'BAD_REQUEST', + Unauthorized = 'UNAUTHORIZED' +} + +export type FolderPoliciesResult = FolderPoliciesError | FolderPoliciesSuccess; + +export type FolderPoliciesSuccess = { + __typename?: 'FolderPoliciesSuccess'; + policies: Array; +}; + +export type FolderPolicy = { + __typename?: 'FolderPolicy'; + action: FolderPolicyAction; + afterDays: Scalars['Int']; + createdAt: Scalars['Date']; + folder: Scalars['String']; + id: Scalars['ID']; + minimumItems: Scalars['Int']; + updatedAt: Scalars['Date']; +}; + +export enum FolderPolicyAction { + Archive = 'ARCHIVE', + Delete = 'DELETE' +} + export type GenerateApiKeyError = { __typename?: 'GenerateApiKeyError'; errorCodes: Array; @@ -1723,6 +1797,7 @@ export type Mutation = { bulkAction: BulkActionResult; createArticle: CreateArticleResult; createArticleSavingRequest: CreateArticleSavingRequestResult; + createFolderPolicy: CreateFolderPolicyResult; createGroup: CreateGroupResult; createHighlight: CreateHighlightResult; createLabel: CreateLabelResult; @@ -1731,6 +1806,7 @@ export type Mutation = { deleteDiscoverArticle: DeleteDiscoverArticleResult; deleteDiscoverFeed: DeleteDiscoverFeedResult; deleteFilter: DeleteFilterResult; + deleteFolderPolicy: DeleteFolderPolicyResult; deleteHighlight: DeleteHighlightResult; deleteIntegration: DeleteIntegrationResult; deleteLabel: DeleteLabelResult; @@ -1780,6 +1856,7 @@ export type Mutation = { unsubscribe: UnsubscribeResult; updateEmail: UpdateEmailResult; updateFilter: UpdateFilterResult; + updateFolderPolicy: UpdateFolderPolicyResult; updateHighlight: UpdateHighlightResult; updateLabel: UpdateLabelResult; updateNewsletterEmail: UpdateNewsletterEmailResult; @@ -1822,6 +1899,11 @@ export type MutationCreateArticleSavingRequestArgs = { }; +export type MutationCreateFolderPolicyArgs = { + input: CreateFolderPolicyInput; +}; + + export type MutationCreateGroupArgs = { input: CreateGroupInput; }; @@ -1862,6 +1944,11 @@ export type MutationDeleteFilterArgs = { }; +export type MutationDeleteFolderPolicyArgs = { + id: Scalars['ID']; +}; + + export type MutationDeleteHighlightArgs = { highlightId: Scalars['ID']; }; @@ -2095,6 +2182,11 @@ export type MutationUpdateFilterArgs = { }; +export type MutationUpdateFolderPolicyArgs = { + input: UpdateFolderPolicyInput; +}; + + export type MutationUpdateHighlightArgs = { input: UpdateHighlightInput; }; @@ -2280,6 +2372,7 @@ export type Query = { discoverTopics: GetDiscoverTopicResults; feeds: FeedsResult; filters: FiltersResult; + folderPolicies: FolderPoliciesResult; getDiscoverFeedArticles: GetDiscoverFeedArticleResults; getUserPersonalization: GetUserPersonalizationResult; groups: GroupsResult; @@ -3555,6 +3648,30 @@ export type UpdateFilterSuccess = { filter: Filter; }; +export type UpdateFolderPolicyError = { + __typename?: 'UpdateFolderPolicyError'; + errorCodes: Array; +}; + +export enum UpdateFolderPolicyErrorCode { + BadRequest = 'BAD_REQUEST', + Unauthorized = 'UNAUTHORIZED' +} + +export type UpdateFolderPolicyInput = { + action?: InputMaybe; + afterDays?: InputMaybe; + id: Scalars['ID']; + minimumItems?: InputMaybe; +}; + +export type UpdateFolderPolicyResult = UpdateFolderPolicyError | UpdateFolderPolicySuccess; + +export type UpdateFolderPolicySuccess = { + __typename?: 'UpdateFolderPolicySuccess'; + policy: FolderPolicy; +}; + export type UpdateHighlightError = { __typename?: 'UpdateHighlightError'; errorCodes: Array; @@ -4180,6 +4297,11 @@ export type ResolversTypes = { CreateArticleSavingRequestResult: ResolversTypes['CreateArticleSavingRequestError'] | ResolversTypes['CreateArticleSavingRequestSuccess']; CreateArticleSavingRequestSuccess: ResolverTypeWrapper; CreateArticleSuccess: ResolverTypeWrapper; + CreateFolderPolicyError: ResolverTypeWrapper; + CreateFolderPolicyErrorCode: CreateFolderPolicyErrorCode; + CreateFolderPolicyInput: CreateFolderPolicyInput; + CreateFolderPolicyResult: ResolversTypes['CreateFolderPolicyError'] | ResolversTypes['CreateFolderPolicySuccess']; + CreateFolderPolicySuccess: ResolverTypeWrapper; CreateGroupError: ResolverTypeWrapper; CreateGroupErrorCode: CreateGroupErrorCode; CreateGroupInput: CreateGroupInput; @@ -4234,6 +4356,10 @@ export type ResolversTypes = { DeleteFilterErrorCode: DeleteFilterErrorCode; DeleteFilterResult: ResolversTypes['DeleteFilterError'] | ResolversTypes['DeleteFilterSuccess']; DeleteFilterSuccess: ResolverTypeWrapper; + DeleteFolderPolicyError: ResolverTypeWrapper; + DeleteFolderPolicyErrorCode: DeleteFolderPolicyErrorCode; + DeleteFolderPolicyResult: ResolversTypes['DeleteFolderPolicyError'] | ResolversTypes['DeleteFolderPolicySuccess']; + DeleteFolderPolicySuccess: ResolverTypeWrapper; DeleteHighlightError: ResolverTypeWrapper; DeleteHighlightErrorCode: DeleteHighlightErrorCode; DeleteHighlightReplyError: ResolverTypeWrapper; @@ -4324,6 +4450,12 @@ export type ResolversTypes = { FiltersResult: ResolversTypes['FiltersError'] | ResolversTypes['FiltersSuccess']; FiltersSuccess: ResolverTypeWrapper; Float: ResolverTypeWrapper; + FolderPoliciesError: ResolverTypeWrapper; + FolderPoliciesErrorCode: FolderPoliciesErrorCode; + FolderPoliciesResult: ResolversTypes['FolderPoliciesError'] | ResolversTypes['FolderPoliciesSuccess']; + FolderPoliciesSuccess: ResolverTypeWrapper; + FolderPolicy: ResolverTypeWrapper; + FolderPolicyAction: FolderPolicyAction; GenerateApiKeyError: ResolverTypeWrapper; GenerateApiKeyErrorCode: GenerateApiKeyErrorCode; GenerateApiKeyInput: GenerateApiKeyInput; @@ -4659,6 +4791,11 @@ export type ResolversTypes = { UpdateFilterInput: UpdateFilterInput; UpdateFilterResult: ResolversTypes['UpdateFilterError'] | ResolversTypes['UpdateFilterSuccess']; UpdateFilterSuccess: ResolverTypeWrapper; + UpdateFolderPolicyError: ResolverTypeWrapper; + UpdateFolderPolicyErrorCode: UpdateFolderPolicyErrorCode; + UpdateFolderPolicyInput: UpdateFolderPolicyInput; + UpdateFolderPolicyResult: ResolversTypes['UpdateFolderPolicyError'] | ResolversTypes['UpdateFolderPolicySuccess']; + UpdateFolderPolicySuccess: ResolverTypeWrapper; UpdateHighlightError: ResolverTypeWrapper; UpdateHighlightErrorCode: UpdateHighlightErrorCode; UpdateHighlightInput: UpdateHighlightInput; @@ -4794,6 +4931,10 @@ export type ResolversParentTypes = { CreateArticleSavingRequestResult: ResolversParentTypes['CreateArticleSavingRequestError'] | ResolversParentTypes['CreateArticleSavingRequestSuccess']; CreateArticleSavingRequestSuccess: CreateArticleSavingRequestSuccess; CreateArticleSuccess: CreateArticleSuccess; + CreateFolderPolicyError: CreateFolderPolicyError; + CreateFolderPolicyInput: CreateFolderPolicyInput; + CreateFolderPolicyResult: ResolversParentTypes['CreateFolderPolicyError'] | ResolversParentTypes['CreateFolderPolicySuccess']; + CreateFolderPolicySuccess: CreateFolderPolicySuccess; CreateGroupError: CreateGroupError; CreateGroupInput: CreateGroupInput; CreateGroupResult: ResolversParentTypes['CreateGroupError'] | ResolversParentTypes['CreateGroupSuccess']; @@ -4837,6 +4978,9 @@ export type ResolversParentTypes = { DeleteFilterError: DeleteFilterError; DeleteFilterResult: ResolversParentTypes['DeleteFilterError'] | ResolversParentTypes['DeleteFilterSuccess']; DeleteFilterSuccess: DeleteFilterSuccess; + DeleteFolderPolicyError: DeleteFolderPolicyError; + DeleteFolderPolicyResult: ResolversParentTypes['DeleteFolderPolicyError'] | ResolversParentTypes['DeleteFolderPolicySuccess']; + DeleteFolderPolicySuccess: DeleteFolderPolicySuccess; DeleteHighlightError: DeleteHighlightError; DeleteHighlightReplyError: DeleteHighlightReplyError; DeleteHighlightReplyResult: ResolversParentTypes['DeleteHighlightReplyError'] | ResolversParentTypes['DeleteHighlightReplySuccess']; @@ -4906,6 +5050,10 @@ export type ResolversParentTypes = { FiltersResult: ResolversParentTypes['FiltersError'] | ResolversParentTypes['FiltersSuccess']; FiltersSuccess: FiltersSuccess; Float: Scalars['Float']; + FolderPoliciesError: FolderPoliciesError; + FolderPoliciesResult: ResolversParentTypes['FolderPoliciesError'] | ResolversParentTypes['FolderPoliciesSuccess']; + FolderPoliciesSuccess: FolderPoliciesSuccess; + FolderPolicy: FolderPolicy; GenerateApiKeyError: GenerateApiKeyError; GenerateApiKeyInput: GenerateApiKeyInput; GenerateApiKeyResult: ResolversParentTypes['GenerateApiKeyError'] | ResolversParentTypes['GenerateApiKeySuccess']; @@ -5166,6 +5314,10 @@ export type ResolversParentTypes = { UpdateFilterInput: UpdateFilterInput; UpdateFilterResult: ResolversParentTypes['UpdateFilterError'] | ResolversParentTypes['UpdateFilterSuccess']; UpdateFilterSuccess: UpdateFilterSuccess; + UpdateFolderPolicyError: UpdateFolderPolicyError; + UpdateFolderPolicyInput: UpdateFolderPolicyInput; + UpdateFolderPolicyResult: ResolversParentTypes['UpdateFolderPolicyError'] | ResolversParentTypes['UpdateFolderPolicySuccess']; + UpdateFolderPolicySuccess: UpdateFolderPolicySuccess; UpdateHighlightError: UpdateHighlightError; UpdateHighlightInput: UpdateHighlightInput; UpdateHighlightReplyError: UpdateHighlightReplyError; @@ -5469,6 +5621,20 @@ export type CreateArticleSuccessResolvers; }; +export type CreateFolderPolicyErrorResolvers = { + errorCodes?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type CreateFolderPolicyResultResolvers = { + __resolveType: TypeResolveFn<'CreateFolderPolicyError' | 'CreateFolderPolicySuccess', ParentType, ContextType>; +}; + +export type CreateFolderPolicySuccessResolvers = { + policy?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type CreateGroupErrorResolvers = { errorCodes?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; @@ -5627,6 +5793,20 @@ export type DeleteFilterSuccessResolvers; }; +export type DeleteFolderPolicyErrorResolvers = { + errorCodes?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type DeleteFolderPolicyResultResolvers = { + __resolveType: TypeResolveFn<'DeleteFolderPolicyError' | 'DeleteFolderPolicySuccess', ParentType, ContextType>; +}; + +export type DeleteFolderPolicySuccessResolvers = { + success?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type DeleteHighlightErrorResolvers = { errorCodes?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; @@ -5992,6 +6172,31 @@ export type FiltersSuccessResolvers; }; +export type FolderPoliciesErrorResolvers = { + errorCodes?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type FolderPoliciesResultResolvers = { + __resolveType: TypeResolveFn<'FolderPoliciesError' | 'FolderPoliciesSuccess', ParentType, ContextType>; +}; + +export type FolderPoliciesSuccessResolvers = { + policies?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type FolderPolicyResolvers = { + action?: Resolver; + afterDays?: Resolver; + createdAt?: Resolver; + folder?: Resolver; + id?: Resolver; + minimumItems?: Resolver; + updatedAt?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type GenerateApiKeyErrorResolvers = { errorCodes?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; @@ -6486,6 +6691,7 @@ export type MutationResolvers>; createArticle?: Resolver>; createArticleSavingRequest?: Resolver>; + createFolderPolicy?: Resolver>; createGroup?: Resolver>; createHighlight?: Resolver>; createLabel?: Resolver>; @@ -6494,6 +6700,7 @@ export type MutationResolvers>; deleteDiscoverFeed?: Resolver>; deleteFilter?: Resolver>; + deleteFolderPolicy?: Resolver>; deleteHighlight?: Resolver>; deleteIntegration?: Resolver>; deleteLabel?: Resolver>; @@ -6543,6 +6750,7 @@ export type MutationResolvers>; updateEmail?: Resolver>; updateFilter?: Resolver>; + updateFolderPolicy?: Resolver>; updateHighlight?: Resolver>; updateLabel?: Resolver>; updateNewsletterEmail?: Resolver>; @@ -6639,6 +6847,7 @@ export type QueryResolvers; feeds?: Resolver>; filters?: Resolver; + folderPolicies?: Resolver; getDiscoverFeedArticles?: Resolver>; getUserPersonalization?: Resolver; groups?: Resolver; @@ -7391,6 +7600,20 @@ export type UpdateFilterSuccessResolvers; }; +export type UpdateFolderPolicyErrorResolvers = { + errorCodes?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type UpdateFolderPolicyResultResolvers = { + __resolveType: TypeResolveFn<'UpdateFolderPolicyError' | 'UpdateFolderPolicySuccess', ParentType, ContextType>; +}; + +export type UpdateFolderPolicySuccessResolvers = { + policy?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type UpdateHighlightErrorResolvers = { errorCodes?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; @@ -7734,6 +7957,9 @@ export type Resolvers = { CreateArticleSavingRequestResult?: CreateArticleSavingRequestResultResolvers; CreateArticleSavingRequestSuccess?: CreateArticleSavingRequestSuccessResolvers; CreateArticleSuccess?: CreateArticleSuccessResolvers; + CreateFolderPolicyError?: CreateFolderPolicyErrorResolvers; + CreateFolderPolicyResult?: CreateFolderPolicyResultResolvers; + CreateFolderPolicySuccess?: CreateFolderPolicySuccessResolvers; CreateGroupError?: CreateGroupErrorResolvers; CreateGroupResult?: CreateGroupResultResolvers; CreateGroupSuccess?: CreateGroupSuccessResolvers; @@ -7768,6 +7994,9 @@ export type Resolvers = { DeleteFilterError?: DeleteFilterErrorResolvers; DeleteFilterResult?: DeleteFilterResultResolvers; DeleteFilterSuccess?: DeleteFilterSuccessResolvers; + DeleteFolderPolicyError?: DeleteFolderPolicyErrorResolvers; + DeleteFolderPolicyResult?: DeleteFolderPolicyResultResolvers; + DeleteFolderPolicySuccess?: DeleteFolderPolicySuccessResolvers; DeleteHighlightError?: DeleteHighlightErrorResolvers; DeleteHighlightReplyError?: DeleteHighlightReplyErrorResolvers; DeleteHighlightReplyResult?: DeleteHighlightReplyResultResolvers; @@ -7833,6 +8062,10 @@ export type Resolvers = { FiltersError?: FiltersErrorResolvers; FiltersResult?: FiltersResultResolvers; FiltersSuccess?: FiltersSuccessResolvers; + FolderPoliciesError?: FolderPoliciesErrorResolvers; + FolderPoliciesResult?: FolderPoliciesResultResolvers; + FolderPoliciesSuccess?: FolderPoliciesSuccessResolvers; + FolderPolicy?: FolderPolicyResolvers; GenerateApiKeyError?: GenerateApiKeyErrorResolvers; GenerateApiKeyResult?: GenerateApiKeyResultResolvers; GenerateApiKeySuccess?: GenerateApiKeySuccessResolvers; @@ -8054,6 +8287,9 @@ export type Resolvers = { UpdateFilterError?: UpdateFilterErrorResolvers; UpdateFilterResult?: UpdateFilterResultResolvers; UpdateFilterSuccess?: UpdateFilterSuccessResolvers; + UpdateFolderPolicyError?: UpdateFolderPolicyErrorResolvers; + UpdateFolderPolicyResult?: UpdateFolderPolicyResultResolvers; + UpdateFolderPolicySuccess?: UpdateFolderPolicySuccessResolvers; UpdateHighlightError?: UpdateHighlightErrorResolvers; UpdateHighlightReplyError?: UpdateHighlightReplyErrorResolvers; UpdateHighlightReplyResult?: UpdateHighlightReplyResultResolvers; diff --git a/packages/api/src/generated/schema.graphql b/packages/api/src/generated/schema.graphql index 5692cb0aa..fa6d674b0 100644 --- a/packages/api/src/generated/schema.graphql +++ b/packages/api/src/generated/schema.graphql @@ -307,6 +307,28 @@ type CreateArticleSuccess { user: User! } +type CreateFolderPolicyError { + errorCodes: [CreateFolderPolicyErrorCode!]! +} + +enum CreateFolderPolicyErrorCode { + BAD_REQUEST + UNAUTHORIZED +} + +input CreateFolderPolicyInput { + action: FolderPolicyAction! + afterDays: Int! + folder: String! + minimumItems: Int +} + +union CreateFolderPolicyResult = CreateFolderPolicyError | CreateFolderPolicySuccess + +type CreateFolderPolicySuccess { + policy: FolderPolicy! +} + type CreateGroupError { errorCodes: [CreateGroupErrorCode!]! } @@ -557,6 +579,21 @@ type DeleteFilterSuccess { filter: Filter! } +type DeleteFolderPolicyError { + errorCodes: [DeleteFolderPolicyErrorCode!]! +} + +enum DeleteFolderPolicyErrorCode { + BAD_REQUEST + UNAUTHORIZED +} + +union DeleteFolderPolicyResult = DeleteFolderPolicyError | DeleteFolderPolicySuccess + +type DeleteFolderPolicySuccess { + success: Boolean! +} + type DeleteHighlightError { errorCodes: [DeleteHighlightErrorCode!]! } @@ -972,6 +1009,36 @@ type FiltersSuccess { filters: [Filter!]! } +type FolderPoliciesError { + errorCodes: [FolderPoliciesErrorCode!]! +} + +enum FolderPoliciesErrorCode { + BAD_REQUEST + UNAUTHORIZED +} + +union FolderPoliciesResult = FolderPoliciesError | FolderPoliciesSuccess + +type FolderPoliciesSuccess { + policies: [FolderPolicy!]! +} + +type FolderPolicy { + action: FolderPolicyAction! + afterDays: Int! + createdAt: Date! + folder: String! + id: ID! + minimumItems: Int! + updatedAt: Date! +} + +enum FolderPolicyAction { + ARCHIVE + DELETE +} + type GenerateApiKeyError { errorCodes: [GenerateApiKeyErrorCode!]! } @@ -1555,6 +1622,7 @@ type Mutation { bulkAction(action: BulkActionType!, arguments: JSON, async: Boolean, expectedCount: Int, labelIds: [ID!], query: String!): BulkActionResult! createArticle(input: CreateArticleInput!): CreateArticleResult! createArticleSavingRequest(input: CreateArticleSavingRequestInput!): CreateArticleSavingRequestResult! + createFolderPolicy(input: CreateFolderPolicyInput!): CreateFolderPolicyResult! createGroup(input: CreateGroupInput!): CreateGroupResult! createHighlight(input: CreateHighlightInput!): CreateHighlightResult! createLabel(input: CreateLabelInput!): CreateLabelResult! @@ -1563,6 +1631,7 @@ type Mutation { deleteDiscoverArticle(input: DeleteDiscoverArticleInput!): DeleteDiscoverArticleResult! deleteDiscoverFeed(input: DeleteDiscoverFeedInput!): DeleteDiscoverFeedResult! deleteFilter(id: ID!): DeleteFilterResult! + deleteFolderPolicy(id: ID!): DeleteFolderPolicyResult! deleteHighlight(highlightId: ID!): DeleteHighlightResult! deleteIntegration(id: ID!): DeleteIntegrationResult! deleteLabel(id: ID!): DeleteLabelResult! @@ -1612,6 +1681,7 @@ type Mutation { unsubscribe(name: String!, subscriptionId: ID): UnsubscribeResult! updateEmail(input: UpdateEmailInput!): UpdateEmailResult! updateFilter(input: UpdateFilterInput!): UpdateFilterResult! + updateFolderPolicy(input: UpdateFolderPolicyInput!): UpdateFolderPolicyResult! updateHighlight(input: UpdateHighlightInput!): UpdateHighlightResult! updateLabel(input: UpdateLabelInput!): UpdateLabelResult! updateNewsletterEmail(input: UpdateNewsletterEmailInput!): UpdateNewsletterEmailResult! @@ -1754,6 +1824,7 @@ type Query { discoverTopics: GetDiscoverTopicResults! feeds(input: FeedsInput!): FeedsResult! filters: FiltersResult! + folderPolicies: FolderPoliciesResult! getDiscoverFeedArticles(after: String, discoverTopicId: String!, feedId: ID, first: Int): GetDiscoverFeedArticleResults! getUserPersonalization: GetUserPersonalizationResult! groups: GroupsResult! @@ -2835,6 +2906,28 @@ type UpdateFilterSuccess { filter: Filter! } +type UpdateFolderPolicyError { + errorCodes: [UpdateFolderPolicyErrorCode!]! +} + +enum UpdateFolderPolicyErrorCode { + BAD_REQUEST + UNAUTHORIZED +} + +input UpdateFolderPolicyInput { + action: FolderPolicyAction + afterDays: Int + id: ID! + minimumItems: Int +} + +union UpdateFolderPolicyResult = UpdateFolderPolicyError | UpdateFolderPolicySuccess + +type UpdateFolderPolicySuccess { + policy: FolderPolicy! +} + type UpdateHighlightError { errorCodes: [UpdateHighlightErrorCode!]! } diff --git a/packages/api/src/resolvers/folder_policy/index.ts b/packages/api/src/resolvers/folder_policy/index.ts new file mode 100644 index 000000000..d9ad12c2b --- /dev/null +++ b/packages/api/src/resolvers/folder_policy/index.ts @@ -0,0 +1,23 @@ +import { FolderPolicy } from '../../entity/folder_policy' +import { + FolderPoliciesError, + FolderPoliciesSuccess, +} from '../../generated/graphql' +import { findFolderPoliciesByUserId } from '../../services/folder_policy' +import { Merge } from '../../util' +import { authorized } from '../../utils/gql-utils' + +type PartialFolderPoliciesSuccess = Merge< + FolderPoliciesSuccess, + { policies: FolderPolicy[] } +> +export const folderPoliciesResolver = authorized< + PartialFolderPoliciesSuccess, + FolderPoliciesError +>(async (_, __, { uid }) => { + const policies = await findFolderPoliciesByUserId(uid) + + return { + policies, + } +}) diff --git a/packages/api/src/resolvers/function_resolvers.ts b/packages/api/src/resolvers/function_resolvers.ts index dfb0e276c..b03b13e90 100644 --- a/packages/api/src/resolvers/function_resolvers.ts +++ b/packages/api/src/resolvers/function_resolvers.ts @@ -5,6 +5,7 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import { createHmac } from 'crypto' import { isError } from 'lodash' +import { FolderPolicy } from '../entity/folder_policy' import { Highlight } from '../entity/highlight' import { LibraryItem } from '../entity/library_item' import { @@ -52,6 +53,7 @@ import { saveDiscoverArticleResolver, } from './discover_feeds' import { optInFeatureResolver } from './features' +import { folderPoliciesResolver } from './folder_policy' import { highlightsResolver } from './highlight' import { hiddenHomeSectionResolver, @@ -342,6 +344,7 @@ export const functionResolvers = { subscription: subscriptionResolver, hiddenHomeSection: hiddenHomeSectionResolver, highlights: highlightsResolver, + folderPolicies: folderPoliciesResolver, }, User: { async intercomHash(user: User) { @@ -880,4 +883,5 @@ export const functionResolvers = { ...resultResolveTypeResolver('RefreshHome'), ...resultResolveTypeResolver('HiddenHomeSection'), ...resultResolveTypeResolver('Highlights'), + ...resultResolveTypeResolver('FolderPolicies'), } diff --git a/packages/api/src/schema.ts b/packages/api/src/schema.ts index 298bb7e90..1c0bead78 100755 --- a/packages/api/src/schema.ts +++ b/packages/api/src/schema.ts @@ -3247,6 +3247,101 @@ const schema = gql` BAD_REQUEST } + type FolderPolicy { + id: ID! + folder: String! + action: FolderPolicyAction! + afterDays: Int! + minimumItems: Int! + createdAt: Date! + updatedAt: Date! + } + + enum FolderPolicyAction { + ARCHIVE + DELETE + } + + union FolderPoliciesResult = FolderPoliciesSuccess | FolderPoliciesError + + type FolderPoliciesSuccess { + policies: [FolderPolicy!]! + } + + type FolderPoliciesError { + errorCodes: [FolderPoliciesErrorCode!]! + } + + enum FolderPoliciesErrorCode { + UNAUTHORIZED + BAD_REQUEST + } + + input CreateFolderPolicyInput { + folder: String! @sanitize(minLength: 1, maxLength: 255) + action: FolderPolicyAction! + afterDays: Int! + minimumItems: Int + } + + union CreateFolderPolicyResult = + CreateFolderPolicySuccess + | CreateFolderPolicyError + + type CreateFolderPolicySuccess { + policy: FolderPolicy! + } + + type CreateFolderPolicyError { + errorCodes: [CreateFolderPolicyErrorCode!]! + } + + enum CreateFolderPolicyErrorCode { + UNAUTHORIZED + BAD_REQUEST + } + + union DeleteFolderPolicyResult = + DeleteFolderPolicySuccess + | DeleteFolderPolicyError + + type DeleteFolderPolicySuccess { + success: Boolean! + } + + type DeleteFolderPolicyError { + errorCodes: [DeleteFolderPolicyErrorCode!]! + } + + enum DeleteFolderPolicyErrorCode { + UNAUTHORIZED + BAD_REQUEST + } + + union UpdateFolderPolicyResult = + UpdateFolderPolicySuccess + | UpdateFolderPolicyError + + type UpdateFolderPolicySuccess { + policy: FolderPolicy! + } + + type UpdateFolderPolicyError { + errorCodes: [UpdateFolderPolicyErrorCode!]! + } + + enum UpdateFolderPolicyErrorCode { + UNAUTHORIZED + BAD_REQUEST + } + + input UpdateFolderPolicyInput { + id: ID! + action: FolderPolicyAction + afterDays: Int + minimumItems: Int + } + # Mutations type Mutation { googleLogin(input: GoogleLoginInput!): LoginResult! @@ -3373,6 +3468,13 @@ const schema = gql` editDiscoverFeed(input: EditDiscoverFeedInput!): EditDiscoverFeedResult! emptyTrash: EmptyTrashResult! refreshHome: RefreshHomeResult! + createFolderPolicy( + input: CreateFolderPolicyInput! + ): CreateFolderPolicyResult! + updateFolderPolicy( + input: UpdateFolderPolicyInput! + ): UpdateFolderPolicyResult! + deleteFolderPolicy(id: ID!): DeleteFolderPolicyResult! } # FIXME: remove sort from feedArticles after all cached tabs are closed @@ -3447,6 +3549,7 @@ const schema = gql` subscription(id: ID!): SubscriptionResult! hiddenHomeSection: HiddenHomeSectionResult! highlights(after: String, first: Int, query: String): HighlightsResult! + folderPolicies: FolderPoliciesResult! } schema { diff --git a/packages/api/src/services/folder_policy.ts b/packages/api/src/services/folder_policy.ts new file mode 100644 index 000000000..b4477a6be --- /dev/null +++ b/packages/api/src/services/folder_policy.ts @@ -0,0 +1,38 @@ +import { FolderPolicy, FolderPolicyAction } from '../entity/folder_policy' +import { getRepository } from '../repository' + +export const createFolderPolicy = async (folderPolicy: { + userId: string + folder: string + action: FolderPolicyAction + afterDays: number + minimumItems: number +}) => { + return getRepository(FolderPolicy).save(folderPolicy) +} + +export const findFolderPoliciesByUserId = async (userId: string) => { + return getRepository(FolderPolicy).find({ + where: { userId }, + order: { folder: 'ASC' }, + }) +} + +export const updateFolderPolicy = async ( + id: string, + update: Partial +) => { + return getRepository(FolderPolicy).update(id, update) +} + +export const deleteFolderPolicy = async (id: string) => { + return getRepository(FolderPolicy).delete(id) +} + +export const findFolderPolicies = async () => { + return getRepository(FolderPolicy).find() +} + +export const findFolderPolicyById = async (id: string) => { + return getRepository(FolderPolicy).findOneBy({ id }) +} diff --git a/packages/api/test/resolvers/folder_policy.test.ts b/packages/api/test/resolvers/folder_policy.test.ts new file mode 100644 index 000000000..84254eb14 --- /dev/null +++ b/packages/api/test/resolvers/folder_policy.test.ts @@ -0,0 +1,59 @@ +import { FolderPolicyAction } from '../../src/entity/folder_policy' +import { User } from '../../src/entity/user' +import { createFolderPolicy } from '../../src/services/folder_policy' +import { deleteUser } from '../../src/services/user' +import { createTestUser } from '../db' +import { graphqlRequest, loginAndGetAuthToken } from '../util' +import { expect } from 'chai' + +describe('Folder Policy API', () => { + let loginUser: User + let authToken: string + + before(async () => { + // create test user and login + loginUser = await createTestUser('loginUser') + authToken = await loginAndGetAuthToken(loginUser.email) + }) + + after(async () => { + await deleteUser(loginUser.id) + }) + + describe('List Folder Policy', () => { + const query = ` + query { + folderPolicies { + ... on FolderPoliciesSuccess { + policies { + id + folder + action + createdAt + updatedAt + } + } + ... on FolderPoliciesError { + errorCodes + } + } + } + ` + + it('should return a list of folder policy of the user', async () => { + const existingPolicy = await createFolderPolicy({ + userId: loginUser.id, + folder: 'test-folder', + action: FolderPolicyAction.ARCHIVE, + afterDays: 30, + minimumItems: 10, + }) + + const res = await graphqlRequest(query, authToken).expect(200) + + const policies = res.body.data.folderPolicies.policies as any[] + expect(policies).to.have.lengthOf(1) + expect(policies[0].id).to.equal(existingPolicy.id) + }) + }) +}) diff --git a/packages/api/test/util.ts b/packages/api/test/util.ts index 42b5ecd60..6f043d98a 100644 --- a/packages/api/test/util.ts +++ b/packages/api/test/util.ts @@ -60,3 +60,11 @@ export const generateFakeUuid = () => { export const generateFakeShortId = () => { return nanoid(8) } + +export const loginAndGetAuthToken = async (email: string) => { + const res = await request + .post('/local/debug/fake-user-login') + .send({ fakeEmail: email }) + + return res.body.authToken as string +} diff --git a/packages/db/migrations/0180.do.folder_policy.sql b/packages/db/migrations/0180.do.folder_policy.sql index aa9d7bd73..b40391bd5 100755 --- a/packages/db/migrations/0180.do.folder_policy.sql +++ b/packages/db/migrations/0180.do.folder_policy.sql @@ -4,15 +4,18 @@ BEGIN; +CREATE TYPE folder_action AS ENUM ('DELETE', 'ARCHIVE'); + CREATE TABLE omnivore.folder_policy ( id UUID PRIMARY KEY DEFAULT uuid_generate_v1mc(), user_id UUID NOT NULL REFERENCES omnivore.user(id) ON DELETE CASCADE, folder TEXT NOT NULL, -- folder name in lowercase - action TEXT NOT NULL, -- delete or archive + action folder_action NOT NULL, -- delete or archive after_days INT NOT NULL, -- number of days after which the action should be taken minimum_items INT NOT NULL DEFAULT 0, -- minimum number of items to keep in the folder created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP + updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE (user_id, folder) -- only one policy per folder per user ); CREATE TRIGGER update_folder_policy_modtime BEFORE UPDATE ON omnivore.folder_policy FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column(); diff --git a/packages/db/migrations/0180.undo.folder_policy.sql b/packages/db/migrations/0180.undo.folder_policy.sql index 20add6644..ab2f0c829 100755 --- a/packages/db/migrations/0180.undo.folder_policy.sql +++ b/packages/db/migrations/0180.undo.folder_policy.sql @@ -6,4 +6,6 @@ BEGIN; DROP TABLE omnivore.folder_policy; +DROP TYPE folder_action; + COMMIT; From 2d757a48960b809ed7065f6c229580e296d7d31f Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Mon, 10 Jun 2024 20:30:13 +0800 Subject: [PATCH 3/8] add create folder policy api --- packages/api/src/entity/folder_policy.ts | 12 ++++- .../api/src/resolvers/folder_policy/index.ts | 30 ++++++++++- .../api/src/resolvers/function_resolvers.ts | 8 ++- .../api/test/resolvers/folder_policy.test.ts | 54 ++++++++++++++++++- 4 files changed, 96 insertions(+), 8 deletions(-) diff --git a/packages/api/src/entity/folder_policy.ts b/packages/api/src/entity/folder_policy.ts index 82bc0a160..94e72be5b 100644 --- a/packages/api/src/entity/folder_policy.ts +++ b/packages/api/src/entity/folder_policy.ts @@ -1,9 +1,11 @@ import { Column, + CreateDateColumn, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn, + UpdateDateColumn, } from 'typeorm' import { User } from './user' @@ -36,9 +38,15 @@ export class FolderPolicy { @Column('int') minimumItems!: number - @Column('timestamptz') + @CreateDateColumn({ + type: 'timestamptz', + default: () => 'CURRENT_TIMESTAMP', + }) createdAt!: Date - @Column('timestamptz') + @UpdateDateColumn({ + type: 'timestamptz', + default: () => 'CURRENT_TIMESTAMP', + }) updatedAt!: Date } diff --git a/packages/api/src/resolvers/folder_policy/index.ts b/packages/api/src/resolvers/folder_policy/index.ts index d9ad12c2b..737d655e2 100644 --- a/packages/api/src/resolvers/folder_policy/index.ts +++ b/packages/api/src/resolvers/folder_policy/index.ts @@ -1,9 +1,15 @@ -import { FolderPolicy } from '../../entity/folder_policy' +import { FolderPolicy, FolderPolicyAction } from '../../entity/folder_policy' import { + CreateFolderPolicyError, + CreateFolderPolicySuccess, FolderPoliciesError, FolderPoliciesSuccess, + MutationCreateFolderPolicyArgs, } from '../../generated/graphql' -import { findFolderPoliciesByUserId } from '../../services/folder_policy' +import { + createFolderPolicy, + findFolderPoliciesByUserId, +} from '../../services/folder_policy' import { Merge } from '../../util' import { authorized } from '../../utils/gql-utils' @@ -21,3 +27,23 @@ export const folderPoliciesResolver = authorized< policies, } }) + +export const createFolderPolicyResolver = authorized< + Merge, + CreateFolderPolicyError, + MutationCreateFolderPolicyArgs +>(async (_, { input }, { uid }) => { + const { folder, action, afterDays, minimumItems } = input + + const policy = await createFolderPolicy({ + userId: uid, + folder, + action: action as unknown as FolderPolicyAction, + afterDays, + minimumItems: minimumItems ?? 0, + }) + + return { + policy, + } +}) diff --git a/packages/api/src/resolvers/function_resolvers.ts b/packages/api/src/resolvers/function_resolvers.ts index b03b13e90..21a5d4d78 100644 --- a/packages/api/src/resolvers/function_resolvers.ts +++ b/packages/api/src/resolvers/function_resolvers.ts @@ -5,7 +5,6 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import { createHmac } from 'crypto' import { isError } from 'lodash' -import { FolderPolicy } from '../entity/folder_policy' import { Highlight } from '../entity/highlight' import { LibraryItem } from '../entity/library_item' import { @@ -53,7 +52,10 @@ import { saveDiscoverArticleResolver, } from './discover_feeds' import { optInFeatureResolver } from './features' -import { folderPoliciesResolver } from './folder_policy' +import { + createFolderPolicyResolver, + folderPoliciesResolver, +} from './folder_policy' import { highlightsResolver } from './highlight' import { hiddenHomeSectionResolver, @@ -309,6 +311,7 @@ export const functionResolvers = { exportToIntegration: exportToIntegrationResolver, replyToEmail: replyToEmailResolver, refreshHome: refreshHomeResolver, + createFolderPolicy: createFolderPolicyResolver, }, Query: { me: getMeUserResolver, @@ -884,4 +887,5 @@ export const functionResolvers = { ...resultResolveTypeResolver('HiddenHomeSection'), ...resultResolveTypeResolver('Highlights'), ...resultResolveTypeResolver('FolderPolicies'), + ...resultResolveTypeResolver('CreateFolderPolicy'), } diff --git a/packages/api/test/resolvers/folder_policy.test.ts b/packages/api/test/resolvers/folder_policy.test.ts index 84254eb14..f4d30925a 100644 --- a/packages/api/test/resolvers/folder_policy.test.ts +++ b/packages/api/test/resolvers/folder_policy.test.ts @@ -1,10 +1,15 @@ +import { expect } from 'chai' import { FolderPolicyAction } from '../../src/entity/folder_policy' import { User } from '../../src/entity/user' -import { createFolderPolicy } from '../../src/services/folder_policy' +import { FolderPolicy } from '../../src/generated/graphql' +import { + createFolderPolicy, + deleteFolderPolicy, + findFolderPolicyById, +} from '../../src/services/folder_policy' import { deleteUser } from '../../src/services/user' import { createTestUser } from '../db' import { graphqlRequest, loginAndGetAuthToken } from '../util' -import { expect } from 'chai' describe('Folder Policy API', () => { let loginUser: User @@ -54,6 +59,51 @@ describe('Folder Policy API', () => { const policies = res.body.data.folderPolicies.policies as any[] expect(policies).to.have.lengthOf(1) expect(policies[0].id).to.equal(existingPolicy.id) + + await deleteFolderPolicy(existingPolicy.id) + }) + }) + + describe('Create Folder Policy', () => { + const mutation = ` + mutation CreateFolderPolicy($input: CreateFolderPolicyInput!) { + createFolderPolicy(input: $input) { + ... on CreateFolderPolicySuccess { + policy { + id + folder + action + createdAt + updatedAt + } + } + ... on CreateFolderPolicyError { + errorCodes + } + } + } + ` + + it('should create a folder policy', async () => { + const input = { + folder: 'test-folder', + action: FolderPolicyAction.ARCHIVE, + afterDays: 30, + minimumItems: 10, + } + + const res = await graphqlRequest(mutation, authToken, { input }).expect( + 200 + ) + + const createdPolicy = res.body.data.createFolderPolicy + .policy as FolderPolicy + + const policy = await findFolderPolicyById(createdPolicy.id) + expect(policy).to.exist + expect(policy?.folder).to.equal(input.folder) + + await deleteFolderPolicy(createdPolicy.id) }) }) }) From 6138e078b19131038bf0e44f84a42f32398dfdd1 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Mon, 10 Jun 2024 20:51:35 +0800 Subject: [PATCH 4/8] add update folder policy api --- .../api/src/resolvers/folder_policy/index.ts | 56 +++++++++++++++ .../api/src/resolvers/function_resolvers.ts | 3 + packages/api/src/services/folder_policy.ts | 25 +++++-- .../api/test/resolvers/folder_policy.test.ts | 68 +++++++++++++++++-- 4 files changed, 142 insertions(+), 10 deletions(-) diff --git a/packages/api/src/resolvers/folder_policy/index.ts b/packages/api/src/resolvers/folder_policy/index.ts index 737d655e2..434133cbd 100644 --- a/packages/api/src/resolvers/folder_policy/index.ts +++ b/packages/api/src/resolvers/folder_policy/index.ts @@ -1,14 +1,21 @@ import { FolderPolicy, FolderPolicyAction } from '../../entity/folder_policy' import { CreateFolderPolicyError, + CreateFolderPolicyErrorCode, CreateFolderPolicySuccess, FolderPoliciesError, FolderPoliciesSuccess, MutationCreateFolderPolicyArgs, + MutationUpdateFolderPolicyArgs, + UpdateFolderPolicyError, + UpdateFolderPolicyErrorCode, + UpdateFolderPolicySuccess, } from '../../generated/graphql' import { createFolderPolicy, findFolderPoliciesByUserId, + findFolderPolicyById, + updateFolderPolicy, } from '../../services/folder_policy' import { Merge } from '../../util' import { authorized } from '../../utils/gql-utils' @@ -35,6 +42,12 @@ export const createFolderPolicyResolver = authorized< >(async (_, { input }, { uid }) => { const { folder, action, afterDays, minimumItems } = input + if (afterDays < 0 || (minimumItems && minimumItems < 0)) { + return { + errorCodes: [CreateFolderPolicyErrorCode.BadRequest], + } + } + const policy = await createFolderPolicy({ userId: uid, folder, @@ -47,3 +60,46 @@ export const createFolderPolicyResolver = authorized< policy, } }) + +export const updateFolderPolicyResolver = authorized< + Merge, + UpdateFolderPolicyError, + MutationUpdateFolderPolicyArgs +>(async (_, { input }, { uid }) => { + const { id, action, afterDays, minimumItems } = input + + if (!action && !afterDays && !minimumItems) { + return { + errorCodes: [UpdateFolderPolicyErrorCode.BadRequest], + } + } + + if ((afterDays && afterDays < 0) || (minimumItems && minimumItems < 0)) { + return { + errorCodes: [UpdateFolderPolicyErrorCode.BadRequest], + } + } + + const result = await updateFolderPolicy(uid, id, { + action: action ? (action as unknown as FolderPolicyAction) : undefined, + afterDays: afterDays ?? undefined, + minimumItems: minimumItems ?? undefined, + }) + + if (!result.affected) { + return { + errorCodes: [UpdateFolderPolicyErrorCode.Unauthorized], + } + } + + const policy = await findFolderPolicyById(uid, id) + if (!policy) { + return { + errorCodes: [UpdateFolderPolicyErrorCode.Unauthorized], + } + } + + return { + policy, + } +}) diff --git a/packages/api/src/resolvers/function_resolvers.ts b/packages/api/src/resolvers/function_resolvers.ts index 21a5d4d78..89c885500 100644 --- a/packages/api/src/resolvers/function_resolvers.ts +++ b/packages/api/src/resolvers/function_resolvers.ts @@ -55,6 +55,7 @@ import { optInFeatureResolver } from './features' import { createFolderPolicyResolver, folderPoliciesResolver, + updateFolderPolicyResolver, } from './folder_policy' import { highlightsResolver } from './highlight' import { @@ -312,6 +313,7 @@ export const functionResolvers = { replyToEmail: replyToEmailResolver, refreshHome: refreshHomeResolver, createFolderPolicy: createFolderPolicyResolver, + updateFolderPolicy: updateFolderPolicyResolver, }, Query: { me: getMeUserResolver, @@ -888,4 +890,5 @@ export const functionResolvers = { ...resultResolveTypeResolver('Highlights'), ...resultResolveTypeResolver('FolderPolicies'), ...resultResolveTypeResolver('CreateFolderPolicy'), + ...resultResolveTypeResolver('UpdateFolderPolicy'), } diff --git a/packages/api/src/services/folder_policy.ts b/packages/api/src/services/folder_policy.ts index b4477a6be..6358c5784 100644 --- a/packages/api/src/services/folder_policy.ts +++ b/packages/api/src/services/folder_policy.ts @@ -19,20 +19,33 @@ export const findFolderPoliciesByUserId = async (userId: string) => { } export const updateFolderPolicy = async ( - id: string, + userId: string, + folderPolicyId: string, update: Partial ) => { - return getRepository(FolderPolicy).update(id, update) + return getRepository(FolderPolicy).update( + { id: folderPolicyId, userId }, + update + ) } -export const deleteFolderPolicy = async (id: string) => { - return getRepository(FolderPolicy).delete(id) +export const deleteFolderPolicy = async ( + userId: string, + folderPolicyId: string +) => { + return getRepository(FolderPolicy).delete({ + id: folderPolicyId, + userId, + }) } export const findFolderPolicies = async () => { return getRepository(FolderPolicy).find() } -export const findFolderPolicyById = async (id: string) => { - return getRepository(FolderPolicy).findOneBy({ id }) +export const findFolderPolicyById = async ( + userId: string, + folderPolicyId: string +) => { + return getRepository(FolderPolicy).findOneBy({ id: folderPolicyId, userId }) } diff --git a/packages/api/test/resolvers/folder_policy.test.ts b/packages/api/test/resolvers/folder_policy.test.ts index f4d30925a..3612685e3 100644 --- a/packages/api/test/resolvers/folder_policy.test.ts +++ b/packages/api/test/resolvers/folder_policy.test.ts @@ -1,5 +1,8 @@ import { expect } from 'chai' -import { FolderPolicyAction } from '../../src/entity/folder_policy' +import { + FolderPolicy as FolderPolicyEntity, + FolderPolicyAction, +} from '../../src/entity/folder_policy' import { User } from '../../src/entity/user' import { FolderPolicy } from '../../src/generated/graphql' import { @@ -60,7 +63,7 @@ describe('Folder Policy API', () => { expect(policies).to.have.lengthOf(1) expect(policies[0].id).to.equal(existingPolicy.id) - await deleteFolderPolicy(existingPolicy.id) + await deleteFolderPolicy(loginUser.id, existingPolicy.id) }) }) @@ -99,11 +102,68 @@ describe('Folder Policy API', () => { const createdPolicy = res.body.data.createFolderPolicy .policy as FolderPolicy - const policy = await findFolderPolicyById(createdPolicy.id) + const policy = await findFolderPolicyById(loginUser.id, createdPolicy.id) expect(policy).to.exist expect(policy?.folder).to.equal(input.folder) - await deleteFolderPolicy(createdPolicy.id) + await deleteFolderPolicy(loginUser.id, createdPolicy.id) + }) + }) + + describe('Update Folder Policy', () => { + let existingPolicy: FolderPolicyEntity + + before(async () => { + existingPolicy = await createFolderPolicy({ + userId: loginUser.id, + folder: 'test-folder', + action: FolderPolicyAction.ARCHIVE, + afterDays: 30, + minimumItems: 10, + }) + }) + + after(async () => { + await deleteFolderPolicy(loginUser.id, existingPolicy.id) + }) + + const mutation = ` + mutation UpdateFolderPolicy($input: UpdateFolderPolicyInput!) { + updateFolderPolicy(input: $input) { + ... on UpdateFolderPolicySuccess { + policy { + id + folder + action + createdAt + updatedAt + } + } + ... on UpdateFolderPolicyError { + errorCodes + } + } + } + ` + + it('should update a folder policy', async () => { + const input = { + id: existingPolicy.id, + action: FolderPolicyAction.DELETE, + afterDays: 30, + minimumItems: 10, + } + + const res = await graphqlRequest(mutation, authToken, { input }).expect( + 200 + ) + + const updatedPolicy = res.body.data.updateFolderPolicy + .policy as FolderPolicy + + const policy = await findFolderPolicyById(loginUser.id, updatedPolicy.id) + expect(policy).to.exist + expect(policy?.action).to.equal(input.action) }) }) }) From 12fa6ca0b4b388800561170b5a115b65f2d48576 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Mon, 10 Jun 2024 20:59:26 +0800 Subject: [PATCH 5/8] add delete folder policy api --- packages/api/src/generated/graphql.ts | 1 - packages/api/src/generated/schema.graphql | 1 - .../api/src/resolvers/folder_policy/index.ts | 32 +++++++++- .../api/src/resolvers/function_resolvers.ts | 3 + packages/api/src/schema.ts | 1 - .../api/test/resolvers/folder_policy.test.ts | 61 +++++++++++++++++-- 6 files changed, 87 insertions(+), 12 deletions(-) diff --git a/packages/api/src/generated/graphql.ts b/packages/api/src/generated/graphql.ts index f388b1282..1e9e8940c 100644 --- a/packages/api/src/generated/graphql.ts +++ b/packages/api/src/generated/graphql.ts @@ -649,7 +649,6 @@ export type DeleteFolderPolicyError = { }; export enum DeleteFolderPolicyErrorCode { - BadRequest = 'BAD_REQUEST', Unauthorized = 'UNAUTHORIZED' } diff --git a/packages/api/src/generated/schema.graphql b/packages/api/src/generated/schema.graphql index fa6d674b0..3e0c83fe3 100644 --- a/packages/api/src/generated/schema.graphql +++ b/packages/api/src/generated/schema.graphql @@ -584,7 +584,6 @@ type DeleteFolderPolicyError { } enum DeleteFolderPolicyErrorCode { - BAD_REQUEST UNAUTHORIZED } diff --git a/packages/api/src/resolvers/folder_policy/index.ts b/packages/api/src/resolvers/folder_policy/index.ts index 434133cbd..7402161cd 100644 --- a/packages/api/src/resolvers/folder_policy/index.ts +++ b/packages/api/src/resolvers/folder_policy/index.ts @@ -3,9 +3,12 @@ import { CreateFolderPolicyError, CreateFolderPolicyErrorCode, CreateFolderPolicySuccess, + DeleteFolderPolicyError, + DeleteFolderPolicySuccess, FolderPoliciesError, FolderPoliciesSuccess, MutationCreateFolderPolicyArgs, + MutationDeleteFolderPolicyArgs, MutationUpdateFolderPolicyArgs, UpdateFolderPolicyError, UpdateFolderPolicyErrorCode, @@ -13,6 +16,7 @@ import { } from '../../generated/graphql' import { createFolderPolicy, + deleteFolderPolicy, findFolderPoliciesByUserId, findFolderPolicyById, updateFolderPolicy, @@ -22,7 +26,7 @@ import { authorized } from '../../utils/gql-utils' type PartialFolderPoliciesSuccess = Merge< FolderPoliciesSuccess, - { policies: FolderPolicy[] } + { policies: Array } > export const folderPoliciesResolver = authorized< PartialFolderPoliciesSuccess, @@ -39,10 +43,12 @@ export const createFolderPolicyResolver = authorized< Merge, CreateFolderPolicyError, MutationCreateFolderPolicyArgs ->(async (_, { input }, { uid }) => { +>(async (_, { input }, { uid, log }) => { const { folder, action, afterDays, minimumItems } = input if (afterDays < 0 || (minimumItems && minimumItems < 0)) { + log.error('Invalid values') + return { errorCodes: [CreateFolderPolicyErrorCode.BadRequest], } @@ -65,16 +71,20 @@ export const updateFolderPolicyResolver = authorized< Merge, UpdateFolderPolicyError, MutationUpdateFolderPolicyArgs ->(async (_, { input }, { uid }) => { +>(async (_, { input }, { log, uid }) => { const { id, action, afterDays, minimumItems } = input if (!action && !afterDays && !minimumItems) { + log.error('No fields to update') + return { errorCodes: [UpdateFolderPolicyErrorCode.BadRequest], } } if ((afterDays && afterDays < 0) || (minimumItems && minimumItems < 0)) { + log.error('Invalid values') + return { errorCodes: [UpdateFolderPolicyErrorCode.BadRequest], } @@ -87,6 +97,8 @@ export const updateFolderPolicyResolver = authorized< }) if (!result.affected) { + log.error('Policy not found') + return { errorCodes: [UpdateFolderPolicyErrorCode.Unauthorized], } @@ -94,6 +106,8 @@ export const updateFolderPolicyResolver = authorized< const policy = await findFolderPolicyById(uid, id) if (!policy) { + log.error('Policy not found') + return { errorCodes: [UpdateFolderPolicyErrorCode.Unauthorized], } @@ -103,3 +117,15 @@ export const updateFolderPolicyResolver = authorized< policy, } }) + +export const deleteFolderPolicyResolver = authorized< + DeleteFolderPolicySuccess, + DeleteFolderPolicyError, + MutationDeleteFolderPolicyArgs +>(async (_, { id }, { uid }) => { + const result = await deleteFolderPolicy(uid, id) + + return { + success: !!result.affected, + } +}) diff --git a/packages/api/src/resolvers/function_resolvers.ts b/packages/api/src/resolvers/function_resolvers.ts index 89c885500..2b257c03d 100644 --- a/packages/api/src/resolvers/function_resolvers.ts +++ b/packages/api/src/resolvers/function_resolvers.ts @@ -54,6 +54,7 @@ import { import { optInFeatureResolver } from './features' import { createFolderPolicyResolver, + deleteFolderPolicyResolver, folderPoliciesResolver, updateFolderPolicyResolver, } from './folder_policy' @@ -314,6 +315,7 @@ export const functionResolvers = { refreshHome: refreshHomeResolver, createFolderPolicy: createFolderPolicyResolver, updateFolderPolicy: updateFolderPolicyResolver, + deleteFolderPolicy: deleteFolderPolicyResolver, }, Query: { me: getMeUserResolver, @@ -891,4 +893,5 @@ export const functionResolvers = { ...resultResolveTypeResolver('FolderPolicies'), ...resultResolveTypeResolver('CreateFolderPolicy'), ...resultResolveTypeResolver('UpdateFolderPolicy'), + ...resultResolveTypeResolver('DeleteFolderPolicy'), } diff --git a/packages/api/src/schema.ts b/packages/api/src/schema.ts index 1c0bead78..49565f926 100755 --- a/packages/api/src/schema.ts +++ b/packages/api/src/schema.ts @@ -3315,7 +3315,6 @@ const schema = gql` enum DeleteFolderPolicyErrorCode { UNAUTHORIZED - BAD_REQUEST } union UpdateFolderPolicyResult = diff --git a/packages/api/test/resolvers/folder_policy.test.ts b/packages/api/test/resolvers/folder_policy.test.ts index 3612685e3..aa25447fd 100644 --- a/packages/api/test/resolvers/folder_policy.test.ts +++ b/packages/api/test/resolvers/folder_policy.test.ts @@ -4,7 +4,10 @@ import { FolderPolicyAction, } from '../../src/entity/folder_policy' import { User } from '../../src/entity/user' -import { FolderPolicy } from '../../src/generated/graphql' +import { + DeleteFolderPolicySuccess, + FolderPolicy, +} from '../../src/generated/graphql' import { createFolderPolicy, deleteFolderPolicy, @@ -48,10 +51,17 @@ describe('Folder Policy API', () => { } ` - it('should return a list of folder policy of the user', async () => { + it('should return a list of folder policy of the user in ascending order', async () => { const existingPolicy = await createFolderPolicy({ userId: loginUser.id, - folder: 'test-folder', + folder: 'inbox', + action: FolderPolicyAction.ARCHIVE, + afterDays: 30, + minimumItems: 10, + }) + const existingPolicy1 = await createFolderPolicy({ + userId: loginUser.id, + folder: 'following', action: FolderPolicyAction.ARCHIVE, afterDays: 30, minimumItems: 10, @@ -59,9 +69,11 @@ describe('Folder Policy API', () => { const res = await graphqlRequest(query, authToken).expect(200) - const policies = res.body.data.folderPolicies.policies as any[] - expect(policies).to.have.lengthOf(1) - expect(policies[0].id).to.equal(existingPolicy.id) + const policies = res.body.data.folderPolicies + .policies as Array + expect(policies).to.have.lengthOf(2) + expect(policies[0].id).to.equal(existingPolicy1.id) + expect(policies[1].id).to.equal(existingPolicy.id) await deleteFolderPolicy(loginUser.id, existingPolicy.id) }) @@ -166,4 +178,41 @@ describe('Folder Policy API', () => { expect(policy?.action).to.equal(input.action) }) }) + + describe('Delete Folder Policy', () => { + let existingPolicy: FolderPolicyEntity + + before(async () => { + existingPolicy = await createFolderPolicy({ + userId: loginUser.id, + folder: 'test-folder', + action: FolderPolicyAction.ARCHIVE, + afterDays: 30, + minimumItems: 10, + }) + }) + + const mutation = ` + mutation DeleteFolderPolicy($id: ID!) { + deleteFolderPolicy(id: $id) { + ... on DeleteFolderPolicySuccess { + success + } + ... on DeleteFolderPolicyError { + errorCodes + } + } + } + ` + + it('should delete a folder policy', async () => { + const res = await graphqlRequest(mutation, authToken, { + id: existingPolicy.id, + }).expect(200) + + const result = res.body.data + .deleteFolderPolicy as DeleteFolderPolicySuccess + expect(result.success).to.be.true + }) + }) }) From 6f496b93368b55e908bb24a8d6b02724ad55ebc0 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Mon, 10 Jun 2024 21:43:25 +0800 Subject: [PATCH 6/8] add folder expiration jobs --- packages/api/src/entity/folder_policy.ts | 4 +- packages/api/src/jobs/folder/expire.ts | 50 +++++++++++++++++++ packages/api/src/jobs/folder/expire_all.ts | 20 ++++++++ packages/api/src/queue-processor.ts | 9 ++++ packages/api/src/utils/createTask.ts | 45 +++++++++++++++++ .../api/test/resolvers/folder_policy.test.ts | 12 ++--- 6 files changed, 132 insertions(+), 8 deletions(-) create mode 100644 packages/api/src/jobs/folder/expire.ts create mode 100644 packages/api/src/jobs/folder/expire_all.ts diff --git a/packages/api/src/entity/folder_policy.ts b/packages/api/src/entity/folder_policy.ts index 94e72be5b..b49e8cb72 100644 --- a/packages/api/src/entity/folder_policy.ts +++ b/packages/api/src/entity/folder_policy.ts @@ -10,8 +10,8 @@ import { import { User } from './user' export enum FolderPolicyAction { - DELETE = 'DELETE', - ARCHIVE = 'ARCHIVE', + Delete = 'DELETE', + Archive = 'ARCHIVE', } @Entity({ name: 'folder_policy' }) diff --git a/packages/api/src/jobs/folder/expire.ts b/packages/api/src/jobs/folder/expire.ts new file mode 100644 index 000000000..ebe7ae36d --- /dev/null +++ b/packages/api/src/jobs/folder/expire.ts @@ -0,0 +1,50 @@ +import { FolderPolicyAction } from '../../entity/folder_policy' +import { BulkActionType } from '../../generated/graphql' +import { findFolderPolicyById } from '../../services/folder_policy' +import { batchUpdateLibraryItems } from '../../services/library_item' +import { logger } from '../../utils/logger' + +export const EXPIRE_FOLDER_JOB_NAME = 'EXPIRE_FOLDER_JOB' + +interface ExpireFolderJobData { + userId: string + folderPolicyId: string +} + +export const expireFolderJob = async (data: ExpireFolderJobData) => { + const { userId, folderPolicyId } = data + + const policy = await findFolderPolicyById(userId, folderPolicyId) + if (!policy) { + logger.error('Policy not found') + return + } + + logger.info(`Expiring items for policy ${policy.id}`) + + const getBulkActionType = (action: FolderPolicyAction) => { + switch (action) { + case FolderPolicyAction.Archive: + return BulkActionType.Archive + case FolderPolicyAction.Delete: + return BulkActionType.Delete + default: + logger.error('Unsupported action') + throw new Error('Unsupported action') + } + } + + const action = getBulkActionType(policy.action) + const savedAfter = new Date( + Date.now() - policy.afterDays * 24 * 60 * 60 * 1000 + ) + + await batchUpdateLibraryItems( + action, + { + useFolders: true, + query: `in:${policy.folder} saved:<${savedAfter.toISOString()}`, + }, + userId + ) +} diff --git a/packages/api/src/jobs/folder/expire_all.ts b/packages/api/src/jobs/folder/expire_all.ts new file mode 100644 index 000000000..9ea19b7a1 --- /dev/null +++ b/packages/api/src/jobs/folder/expire_all.ts @@ -0,0 +1,20 @@ +import { findFolderPolicies } from '../../services/folder_policy' +import { enqueueExpireFolderJob } from '../../utils/createTask' +import { logError } from '../../utils/logger' + +export const EXPIRE_ALL_FOLDERS_JOB_NAME = 'EXPIRE_ALL_FOLDERS_JOB' + +export const expireAllFoldersJob = async () => { + const policies = await findFolderPolicies() + + // sequentially enqueues a job to expire items for each policy + for (const policy of policies) { + try { + await enqueueExpireFolderJob(policy.userId, policy.id) + } catch (error) { + logError(error) + + continue + } + } +} diff --git a/packages/api/src/queue-processor.ts b/packages/api/src/queue-processor.ts index 00440da53..07240150f 100644 --- a/packages/api/src/queue-processor.ts +++ b/packages/api/src/queue-processor.ts @@ -32,6 +32,11 @@ import { } from './jobs/email/inbound_emails' import { sendEmailJob, SEND_EMAIL_JOB } from './jobs/email/send_email' import { findThumbnail, THUMBNAIL_JOB } from './jobs/find_thumbnail' +import { expireFolderJob, EXPIRE_FOLDER_JOB_NAME } from './jobs/folder/expire' +import { + expireAllFoldersJob, + EXPIRE_ALL_FOLDERS_JOB_NAME, +} from './jobs/folder/expire_all' import { generatePreviewContent, GENERATE_PREVIEW_CONTENT_JOB, @@ -217,6 +222,10 @@ export const createWorker = (connection: ConnectionOptions) => return generatePreviewContent(job.data) case PRUNE_TRASH_JOB: return pruneTrashJob(job.data) + case EXPIRE_ALL_FOLDERS_JOB_NAME: + return expireAllFoldersJob() + case EXPIRE_FOLDER_JOB_NAME: + return expireFolderJob(job.data) default: logger.warning(`[queue-processor] unhandled job: ${job.name}`) } diff --git a/packages/api/src/utils/createTask.ts b/packages/api/src/utils/createTask.ts index 2957926fd..76329c014 100644 --- a/packages/api/src/utils/createTask.ts +++ b/packages/api/src/utils/createTask.ts @@ -29,6 +29,8 @@ import { BulkActionData, BULK_ACTION_JOB_NAME } from '../jobs/bulk_action' import { CallWebhookJobData, CALL_WEBHOOK_JOB_NAME } from '../jobs/call_webhook' import { SendEmailJobData, SEND_EMAIL_JOB } from '../jobs/email/send_email' import { THUMBNAIL_JOB } from '../jobs/find_thumbnail' +import { EXPIRE_FOLDER_JOB_NAME } from '../jobs/folder/expire' +import { EXPIRE_ALL_FOLDERS_JOB_NAME } from '../jobs/folder/expire_all' import { GENERATE_PREVIEW_CONTENT_JOB } from '../jobs/generate_preview_content' import { EXPORT_ALL_ITEMS_JOB_NAME } from '../jobs/integration/export_all_items' import { @@ -114,6 +116,8 @@ export const getJobPriority = (jobName: string): number => { case THUMBNAIL_JOB: case GENERATE_PREVIEW_CONTENT_JOB: case PRUNE_TRASH_JOB: + case EXPIRE_ALL_FOLDERS_JOB_NAME: + case EXPIRE_FOLDER_JOB_NAME: return 100 default: @@ -1072,4 +1076,45 @@ export const enqueuePruneTrashJob = async (numDays: number) => { ) } +export const enqueueExpireAllFoldersJob = async () => { + const queue = await getBackendQueue() + if (!queue) { + return undefined + } + + return queue.add( + EXPIRE_ALL_FOLDERS_JOB_NAME, + {}, + { + jobId: `${EXPIRE_ALL_FOLDERS_JOB_NAME}_${JOB_VERSION}`, + removeOnComplete: true, + removeOnFail: true, + priority: getJobPriority(EXPIRE_ALL_FOLDERS_JOB_NAME), + attempts: 1, + } + ) +} + +export const enqueueExpireFolderJob = async ( + userId: string, + folderPolicyId: string +) => { + const queue = await getBackendQueue() + if (!queue) { + return undefined + } + + return queue.add( + EXPIRE_FOLDER_JOB_NAME, + { userId, folderPolicyId }, + { + jobId: `${EXPIRE_FOLDER_JOB_NAME}_${folderPolicyId}_${JOB_VERSION}`, + removeOnComplete: true, + removeOnFail: true, + priority: getJobPriority(EXPIRE_FOLDER_JOB_NAME), + attempts: 3, + } + ) +} + export default createHttpTaskWithToken diff --git a/packages/api/test/resolvers/folder_policy.test.ts b/packages/api/test/resolvers/folder_policy.test.ts index aa25447fd..cafcabbb2 100644 --- a/packages/api/test/resolvers/folder_policy.test.ts +++ b/packages/api/test/resolvers/folder_policy.test.ts @@ -55,14 +55,14 @@ describe('Folder Policy API', () => { const existingPolicy = await createFolderPolicy({ userId: loginUser.id, folder: 'inbox', - action: FolderPolicyAction.ARCHIVE, + action: FolderPolicyAction.Archive, afterDays: 30, minimumItems: 10, }) const existingPolicy1 = await createFolderPolicy({ userId: loginUser.id, folder: 'following', - action: FolderPolicyAction.ARCHIVE, + action: FolderPolicyAction.Archive, afterDays: 30, minimumItems: 10, }) @@ -102,7 +102,7 @@ describe('Folder Policy API', () => { it('should create a folder policy', async () => { const input = { folder: 'test-folder', - action: FolderPolicyAction.ARCHIVE, + action: FolderPolicyAction.Archive, afterDays: 30, minimumItems: 10, } @@ -129,7 +129,7 @@ describe('Folder Policy API', () => { existingPolicy = await createFolderPolicy({ userId: loginUser.id, folder: 'test-folder', - action: FolderPolicyAction.ARCHIVE, + action: FolderPolicyAction.Archive, afterDays: 30, minimumItems: 10, }) @@ -161,7 +161,7 @@ describe('Folder Policy API', () => { it('should update a folder policy', async () => { const input = { id: existingPolicy.id, - action: FolderPolicyAction.DELETE, + action: FolderPolicyAction.Delete, afterDays: 30, minimumItems: 10, } @@ -186,7 +186,7 @@ describe('Folder Policy API', () => { existingPolicy = await createFolderPolicy({ userId: loginUser.id, folder: 'test-folder', - action: FolderPolicyAction.ARCHIVE, + action: FolderPolicyAction.Archive, afterDays: 30, minimumItems: 10, }) From 88e3d648c8aa2201c040c3899198f262d7ff8453 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Thu, 13 Jun 2024 17:57:29 +0800 Subject: [PATCH 7/8] add a REST API to trigger the expire folder job --- packages/api/src/generated/graphql.ts | 4 -- packages/api/src/generated/schema.graphql | 3 - packages/api/src/jobs/expire_folders.ts | 7 ++ packages/api/src/jobs/folder/expire.ts | 50 -------------- packages/api/src/jobs/folder/expire_all.ts | 20 ------ packages/api/src/queue-processor.ts | 15 ++-- .../api/src/resolvers/folder_policy/index.ts | 12 ++-- packages/api/src/routers/svc/links.ts | 21 ++++++ packages/api/src/schema.ts | 3 - packages/api/src/services/folder_policy.ts | 5 -- packages/api/src/utils/createTask.ts | 36 ++-------- .../api/test/resolvers/folder_policy.test.ts | 4 -- .../db/migrations/0180.do.folder_policy.sql | 25 ------- .../db/migrations/0181.do.folder_policy.sql | 68 +++++++++++++++++++ ...policy.sql => 0181.undo.folder_policy.sql} | 2 + 15 files changed, 115 insertions(+), 160 deletions(-) create mode 100644 packages/api/src/jobs/expire_folders.ts delete mode 100644 packages/api/src/jobs/folder/expire.ts delete mode 100644 packages/api/src/jobs/folder/expire_all.ts delete mode 100755 packages/db/migrations/0180.do.folder_policy.sql create mode 100755 packages/db/migrations/0181.do.folder_policy.sql rename packages/db/migrations/{0180.undo.folder_policy.sql => 0181.undo.folder_policy.sql} (83%) diff --git a/packages/api/src/generated/graphql.ts b/packages/api/src/generated/graphql.ts index 1e9e8940c..ecc2e59ba 100644 --- a/packages/api/src/generated/graphql.ts +++ b/packages/api/src/generated/graphql.ts @@ -363,7 +363,6 @@ export type CreateFolderPolicyInput = { action: FolderPolicyAction; afterDays: Scalars['Int']; folder: Scalars['String']; - minimumItems?: InputMaybe; }; export type CreateFolderPolicyResult = CreateFolderPolicyError | CreateFolderPolicySuccess; @@ -1145,7 +1144,6 @@ export type FolderPolicy = { createdAt: Scalars['Date']; folder: Scalars['String']; id: Scalars['ID']; - minimumItems: Scalars['Int']; updatedAt: Scalars['Date']; }; @@ -3661,7 +3659,6 @@ export type UpdateFolderPolicyInput = { action?: InputMaybe; afterDays?: InputMaybe; id: Scalars['ID']; - minimumItems?: InputMaybe; }; export type UpdateFolderPolicyResult = UpdateFolderPolicyError | UpdateFolderPolicySuccess; @@ -6191,7 +6188,6 @@ export type FolderPolicyResolvers; folder?: Resolver; id?: Resolver; - minimumItems?: Resolver; updatedAt?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; diff --git a/packages/api/src/generated/schema.graphql b/packages/api/src/generated/schema.graphql index 3e0c83fe3..a64449a29 100644 --- a/packages/api/src/generated/schema.graphql +++ b/packages/api/src/generated/schema.graphql @@ -320,7 +320,6 @@ input CreateFolderPolicyInput { action: FolderPolicyAction! afterDays: Int! folder: String! - minimumItems: Int } union CreateFolderPolicyResult = CreateFolderPolicyError | CreateFolderPolicySuccess @@ -1029,7 +1028,6 @@ type FolderPolicy { createdAt: Date! folder: String! id: ID! - minimumItems: Int! updatedAt: Date! } @@ -2918,7 +2916,6 @@ input UpdateFolderPolicyInput { action: FolderPolicyAction afterDays: Int id: ID! - minimumItems: Int } union UpdateFolderPolicyResult = UpdateFolderPolicyError | UpdateFolderPolicySuccess diff --git a/packages/api/src/jobs/expire_folders.ts b/packages/api/src/jobs/expire_folders.ts new file mode 100644 index 000000000..8c823b60a --- /dev/null +++ b/packages/api/src/jobs/expire_folders.ts @@ -0,0 +1,7 @@ +import { appDataSource } from '../data_source' + +export const EXPIRE_FOLDERS_JOB_NAME = 'expire-folders' + +export const expireFoldersJob = async () => { + await appDataSource.query('CALL omnivore.expire_folders()') +} diff --git a/packages/api/src/jobs/folder/expire.ts b/packages/api/src/jobs/folder/expire.ts deleted file mode 100644 index ebe7ae36d..000000000 --- a/packages/api/src/jobs/folder/expire.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { FolderPolicyAction } from '../../entity/folder_policy' -import { BulkActionType } from '../../generated/graphql' -import { findFolderPolicyById } from '../../services/folder_policy' -import { batchUpdateLibraryItems } from '../../services/library_item' -import { logger } from '../../utils/logger' - -export const EXPIRE_FOLDER_JOB_NAME = 'EXPIRE_FOLDER_JOB' - -interface ExpireFolderJobData { - userId: string - folderPolicyId: string -} - -export const expireFolderJob = async (data: ExpireFolderJobData) => { - const { userId, folderPolicyId } = data - - const policy = await findFolderPolicyById(userId, folderPolicyId) - if (!policy) { - logger.error('Policy not found') - return - } - - logger.info(`Expiring items for policy ${policy.id}`) - - const getBulkActionType = (action: FolderPolicyAction) => { - switch (action) { - case FolderPolicyAction.Archive: - return BulkActionType.Archive - case FolderPolicyAction.Delete: - return BulkActionType.Delete - default: - logger.error('Unsupported action') - throw new Error('Unsupported action') - } - } - - const action = getBulkActionType(policy.action) - const savedAfter = new Date( - Date.now() - policy.afterDays * 24 * 60 * 60 * 1000 - ) - - await batchUpdateLibraryItems( - action, - { - useFolders: true, - query: `in:${policy.folder} saved:<${savedAfter.toISOString()}`, - }, - userId - ) -} diff --git a/packages/api/src/jobs/folder/expire_all.ts b/packages/api/src/jobs/folder/expire_all.ts deleted file mode 100644 index 9ea19b7a1..000000000 --- a/packages/api/src/jobs/folder/expire_all.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { findFolderPolicies } from '../../services/folder_policy' -import { enqueueExpireFolderJob } from '../../utils/createTask' -import { logError } from '../../utils/logger' - -export const EXPIRE_ALL_FOLDERS_JOB_NAME = 'EXPIRE_ALL_FOLDERS_JOB' - -export const expireAllFoldersJob = async () => { - const policies = await findFolderPolicies() - - // sequentially enqueues a job to expire items for each policy - for (const policy of policies) { - try { - await enqueueExpireFolderJob(policy.userId, policy.id) - } catch (error) { - logError(error) - - continue - } - } -} diff --git a/packages/api/src/queue-processor.ts b/packages/api/src/queue-processor.ts index 07240150f..a31dd6dfa 100644 --- a/packages/api/src/queue-processor.ts +++ b/packages/api/src/queue-processor.ts @@ -31,12 +31,11 @@ import { SAVE_NEWSLETTER_JOB, } from './jobs/email/inbound_emails' import { sendEmailJob, SEND_EMAIL_JOB } from './jobs/email/send_email' -import { findThumbnail, THUMBNAIL_JOB } from './jobs/find_thumbnail' -import { expireFolderJob, EXPIRE_FOLDER_JOB_NAME } from './jobs/folder/expire' import { - expireAllFoldersJob, - EXPIRE_ALL_FOLDERS_JOB_NAME, -} from './jobs/folder/expire_all' + expireFoldersJob, + EXPIRE_FOLDERS_JOB_NAME, +} from './jobs/expire_folders' +import { findThumbnail, THUMBNAIL_JOB } from './jobs/find_thumbnail' import { generatePreviewContent, GENERATE_PREVIEW_CONTENT_JOB, @@ -222,10 +221,8 @@ export const createWorker = (connection: ConnectionOptions) => return generatePreviewContent(job.data) case PRUNE_TRASH_JOB: return pruneTrashJob(job.data) - case EXPIRE_ALL_FOLDERS_JOB_NAME: - return expireAllFoldersJob() - case EXPIRE_FOLDER_JOB_NAME: - return expireFolderJob(job.data) + case EXPIRE_FOLDERS_JOB_NAME: + return expireFoldersJob() default: logger.warning(`[queue-processor] unhandled job: ${job.name}`) } diff --git a/packages/api/src/resolvers/folder_policy/index.ts b/packages/api/src/resolvers/folder_policy/index.ts index 7402161cd..ca4869477 100644 --- a/packages/api/src/resolvers/folder_policy/index.ts +++ b/packages/api/src/resolvers/folder_policy/index.ts @@ -44,9 +44,9 @@ export const createFolderPolicyResolver = authorized< CreateFolderPolicyError, MutationCreateFolderPolicyArgs >(async (_, { input }, { uid, log }) => { - const { folder, action, afterDays, minimumItems } = input + const { folder, action, afterDays } = input - if (afterDays < 0 || (minimumItems && minimumItems < 0)) { + if (afterDays < 0) { log.error('Invalid values') return { @@ -59,7 +59,6 @@ export const createFolderPolicyResolver = authorized< folder, action: action as unknown as FolderPolicyAction, afterDays, - minimumItems: minimumItems ?? 0, }) return { @@ -72,9 +71,9 @@ export const updateFolderPolicyResolver = authorized< UpdateFolderPolicyError, MutationUpdateFolderPolicyArgs >(async (_, { input }, { log, uid }) => { - const { id, action, afterDays, minimumItems } = input + const { id, action, afterDays } = input - if (!action && !afterDays && !minimumItems) { + if (!action && !afterDays) { log.error('No fields to update') return { @@ -82,7 +81,7 @@ export const updateFolderPolicyResolver = authorized< } } - if ((afterDays && afterDays < 0) || (minimumItems && minimumItems < 0)) { + if (afterDays && afterDays < 0) { log.error('Invalid values') return { @@ -93,7 +92,6 @@ export const updateFolderPolicyResolver = authorized< const result = await updateFolderPolicy(uid, id, { action: action ? (action as unknown as FolderPolicyAction) : undefined, afterDays: afterDays ?? undefined, - minimumItems: minimumItems ?? undefined, }) if (!result.affected) { diff --git a/packages/api/src/routers/svc/links.ts b/packages/api/src/routers/svc/links.ts index bd35351cc..9f7910325 100644 --- a/packages/api/src/routers/svc/links.ts +++ b/packages/api/src/routers/svc/links.ts @@ -6,6 +6,7 @@ import { readPushSubscription } from '../../pubsub' import { userRepository } from '../../repository/user' import { createPageSaveRequest } from '../../services/create_page_save_request' import { enqueuePruneTrashJob } from '../../utils/createTask' +import { enqueueExpireFoldersJob } from '../../utils/createTask' import { logger } from '../../utils/logger' interface CreateLinkRequestMessage { @@ -92,5 +93,25 @@ export function linkServiceRouter() { } }) + router.post('/expireFolders', async (req, res) => { + const { expired } = readPushSubscription(req) + + if (expired) { + logger.info('discarding expired message') + return res.status(200).send('Expired') + } + + try { + const job = await enqueueExpireFoldersJob() + logger.info('enqueue job', { id: job?.id }) + + return res.sendStatus(200) + } catch (error) { + logger.error('error expire folders', error) + + return res.sendStatus(500) + } + }) + return router } diff --git a/packages/api/src/schema.ts b/packages/api/src/schema.ts index 49565f926..b6e8b1270 100755 --- a/packages/api/src/schema.ts +++ b/packages/api/src/schema.ts @@ -3252,7 +3252,6 @@ const schema = gql` folder: String! action: FolderPolicyAction! afterDays: Int! - minimumItems: Int! createdAt: Date! updatedAt: Date! } @@ -3281,7 +3280,6 @@ const schema = gql` folder: String! @sanitize(minLength: 1, maxLength: 255) action: FolderPolicyAction! afterDays: Int! - minimumItems: Int } union CreateFolderPolicyResult = @@ -3338,7 +3336,6 @@ const schema = gql` id: ID! action: FolderPolicyAction afterDays: Int - minimumItems: Int } # Mutations diff --git a/packages/api/src/services/folder_policy.ts b/packages/api/src/services/folder_policy.ts index 6358c5784..08640cf67 100644 --- a/packages/api/src/services/folder_policy.ts +++ b/packages/api/src/services/folder_policy.ts @@ -6,7 +6,6 @@ export const createFolderPolicy = async (folderPolicy: { folder: string action: FolderPolicyAction afterDays: number - minimumItems: number }) => { return getRepository(FolderPolicy).save(folderPolicy) } @@ -39,10 +38,6 @@ export const deleteFolderPolicy = async ( }) } -export const findFolderPolicies = async () => { - return getRepository(FolderPolicy).find() -} - export const findFolderPolicyById = async ( userId: string, folderPolicyId: string diff --git a/packages/api/src/utils/createTask.ts b/packages/api/src/utils/createTask.ts index 76329c014..e95de4fd5 100644 --- a/packages/api/src/utils/createTask.ts +++ b/packages/api/src/utils/createTask.ts @@ -28,9 +28,8 @@ import { import { BulkActionData, BULK_ACTION_JOB_NAME } from '../jobs/bulk_action' import { CallWebhookJobData, CALL_WEBHOOK_JOB_NAME } from '../jobs/call_webhook' import { SendEmailJobData, SEND_EMAIL_JOB } from '../jobs/email/send_email' +import { EXPIRE_FOLDERS_JOB_NAME } from '../jobs/expire_folders' import { THUMBNAIL_JOB } from '../jobs/find_thumbnail' -import { EXPIRE_FOLDER_JOB_NAME } from '../jobs/folder/expire' -import { EXPIRE_ALL_FOLDERS_JOB_NAME } from '../jobs/folder/expire_all' import { GENERATE_PREVIEW_CONTENT_JOB } from '../jobs/generate_preview_content' import { EXPORT_ALL_ITEMS_JOB_NAME } from '../jobs/integration/export_all_items' import { @@ -116,8 +115,7 @@ export const getJobPriority = (jobName: string): number => { case THUMBNAIL_JOB: case GENERATE_PREVIEW_CONTENT_JOB: case PRUNE_TRASH_JOB: - case EXPIRE_ALL_FOLDERS_JOB_NAME: - case EXPIRE_FOLDER_JOB_NAME: + case EXPIRE_FOLDERS_JOB_NAME: return 100 default: @@ -1076,42 +1074,20 @@ export const enqueuePruneTrashJob = async (numDays: number) => { ) } -export const enqueueExpireAllFoldersJob = async () => { +export const enqueueExpireFoldersJob = async () => { const queue = await getBackendQueue() if (!queue) { return undefined } return queue.add( - EXPIRE_ALL_FOLDERS_JOB_NAME, + EXPIRE_FOLDERS_JOB_NAME, {}, { - jobId: `${EXPIRE_ALL_FOLDERS_JOB_NAME}_${JOB_VERSION}`, + jobId: `${EXPIRE_FOLDERS_JOB_NAME}_${JOB_VERSION}`, removeOnComplete: true, removeOnFail: true, - priority: getJobPriority(EXPIRE_ALL_FOLDERS_JOB_NAME), - attempts: 1, - } - ) -} - -export const enqueueExpireFolderJob = async ( - userId: string, - folderPolicyId: string -) => { - const queue = await getBackendQueue() - if (!queue) { - return undefined - } - - return queue.add( - EXPIRE_FOLDER_JOB_NAME, - { userId, folderPolicyId }, - { - jobId: `${EXPIRE_FOLDER_JOB_NAME}_${folderPolicyId}_${JOB_VERSION}`, - removeOnComplete: true, - removeOnFail: true, - priority: getJobPriority(EXPIRE_FOLDER_JOB_NAME), + priority: getJobPriority(EXPIRE_FOLDERS_JOB_NAME), attempts: 3, } ) diff --git a/packages/api/test/resolvers/folder_policy.test.ts b/packages/api/test/resolvers/folder_policy.test.ts index cafcabbb2..c65955566 100644 --- a/packages/api/test/resolvers/folder_policy.test.ts +++ b/packages/api/test/resolvers/folder_policy.test.ts @@ -57,14 +57,12 @@ describe('Folder Policy API', () => { folder: 'inbox', action: FolderPolicyAction.Archive, afterDays: 30, - minimumItems: 10, }) const existingPolicy1 = await createFolderPolicy({ userId: loginUser.id, folder: 'following', action: FolderPolicyAction.Archive, afterDays: 30, - minimumItems: 10, }) const res = await graphqlRequest(query, authToken).expect(200) @@ -131,7 +129,6 @@ describe('Folder Policy API', () => { folder: 'test-folder', action: FolderPolicyAction.Archive, afterDays: 30, - minimumItems: 10, }) }) @@ -188,7 +185,6 @@ describe('Folder Policy API', () => { folder: 'test-folder', action: FolderPolicyAction.Archive, afterDays: 30, - minimumItems: 10, }) }) diff --git a/packages/db/migrations/0180.do.folder_policy.sql b/packages/db/migrations/0180.do.folder_policy.sql deleted file mode 100755 index b40391bd5..000000000 --- a/packages/db/migrations/0180.do.folder_policy.sql +++ /dev/null @@ -1,25 +0,0 @@ --- Type: DO --- Name: folder_policy --- Description: Create a folder_policy table to contain the folder expiration policies for user and folder - -BEGIN; - -CREATE TYPE folder_action AS ENUM ('DELETE', 'ARCHIVE'); - -CREATE TABLE omnivore.folder_policy ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v1mc(), - user_id UUID NOT NULL REFERENCES omnivore.user(id) ON DELETE CASCADE, - folder TEXT NOT NULL, -- folder name in lowercase - action folder_action NOT NULL, -- delete or archive - after_days INT NOT NULL, -- number of days after which the action should be taken - minimum_items INT NOT NULL DEFAULT 0, -- minimum number of items to keep in the folder - created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - UNIQUE (user_id, folder) -- only one policy per folder per user -); - -CREATE TRIGGER update_folder_policy_modtime BEFORE UPDATE ON omnivore.folder_policy FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column(); - -GRANT SELECT, INSERT, UPDATE, DELETE ON omnivore.folder_policy TO omnivore_user; - -COMMIT; diff --git a/packages/db/migrations/0181.do.folder_policy.sql b/packages/db/migrations/0181.do.folder_policy.sql new file mode 100755 index 000000000..5fb0654b2 --- /dev/null +++ b/packages/db/migrations/0181.do.folder_policy.sql @@ -0,0 +1,68 @@ +-- Type: DO +-- Name: folder_policy +-- Description: Create a folder_policy table to contain the folder expiration policies for user and folder + +BEGIN; + +CREATE TYPE folder_action AS ENUM ('DELETE', 'ARCHIVE'); + +CREATE TABLE omnivore.folder_policy ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v1mc(), + user_id UUID NOT NULL REFERENCES omnivore.user(id) ON DELETE CASCADE, + folder TEXT NOT NULL, -- folder name in lowercase + action folder_action NOT NULL, -- delete or archive + after_days INT NOT NULL, -- number of days after which the action should be taken + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE (user_id, folder, action) -- only one policy per user and folder action +); + +CREATE TRIGGER update_folder_policy_modtime BEFORE UPDATE ON omnivore.folder_policy FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column(); + +GRANT SELECT, INSERT, UPDATE, DELETE ON omnivore.folder_policy TO omnivore_user; + +CREATE PROCEDURE omnivore.expire_folders() +LANGUAGE plpgsql +AS $$ +DECLARE + folder_record RECORD; + folder_name TEXT; + folder_action folder_action; + folder_user_id UUID; + folder_after_days INT; + old_states library_item_state[]; + new_state library_item_state; + column_name TEXT; + folder_policy_cursor CURSOR FOR SELECT id, user_id, folder, action, after_days FROM omnivore.folder_policy; +BEGIN + FOR folder_record IN folder_policy_cursor LOOP + folder_user_id := folder_record.user_id; + folder_name := folder_record.folder; + folder_action := folder_record.action; + folder_after_days := folder_record.after_days; + + IF folder_action = 'DELETE' THEN + old_states := ARRAY['SUCCEEDED', 'FAILED', 'ARCHIVED', 'PROCESSING', 'CONTENT_NOT_FETCHED'::library_item_state]; + new_state := 'DELETED'; + column_name := 'deleted_at'; + ELSIF folder_action = 'ARCHIVE' THEN + old_states := ARRAY['SUCCEEDED', 'FAILED', 'PROCESSING', 'CONTENT_NOT_FETCHED'::library_item_state]; + new_state := 'ARCHIVED'; + column_name := 'archived_at'; + END IF; + + BEGIN + PERFORM omnivore.set_claims(folder_user_id, 'omnivore_user'); + + EXECUTE format('UPDATE omnivore.library_item ' + 'SET state = $1, %I = CURRENT_TIMESTAMP ' + 'WHERE user_id = $2 AND state = ANY ($3) AND folder = $4 AND created_at < CURRENT_TIMESTAMP - INTERVAL ''$5 days''', column_name) + USING new_state, folder_user_id, old_states, folder_name, folder_after_days; + + COMMIT; + END; + END LOOP; +END; +$$; + +COMMIT; diff --git a/packages/db/migrations/0180.undo.folder_policy.sql b/packages/db/migrations/0181.undo.folder_policy.sql similarity index 83% rename from packages/db/migrations/0180.undo.folder_policy.sql rename to packages/db/migrations/0181.undo.folder_policy.sql index ab2f0c829..2f499eb66 100755 --- a/packages/db/migrations/0180.undo.folder_policy.sql +++ b/packages/db/migrations/0181.undo.folder_policy.sql @@ -8,4 +8,6 @@ DROP TABLE omnivore.folder_policy; DROP TYPE folder_action; +DROP PROCEDURE omnivore.expire_folders(); + COMMIT; From 31f04344fccd3bf3f4a7d27b3b7294300293e859 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Thu, 13 Jun 2024 18:41:35 +0800 Subject: [PATCH 8/8] resolve rebase conflicts --- packages/api/src/entity/folder_policy.ts | 3 --- packages/api/test/resolvers/folder_policy.test.ts | 2 -- .../{0181.do.folder_policy.sql => 0182.do.folder_policy.sql} | 0 ...0181.undo.folder_policy.sql => 0182.undo.folder_policy.sql} | 0 4 files changed, 5 deletions(-) rename packages/db/migrations/{0181.do.folder_policy.sql => 0182.do.folder_policy.sql} (100%) rename packages/db/migrations/{0181.undo.folder_policy.sql => 0182.undo.folder_policy.sql} (100%) diff --git a/packages/api/src/entity/folder_policy.ts b/packages/api/src/entity/folder_policy.ts index b49e8cb72..11427003b 100644 --- a/packages/api/src/entity/folder_policy.ts +++ b/packages/api/src/entity/folder_policy.ts @@ -35,9 +35,6 @@ export class FolderPolicy { @Column('int') afterDays!: number - @Column('int') - minimumItems!: number - @CreateDateColumn({ type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP', diff --git a/packages/api/test/resolvers/folder_policy.test.ts b/packages/api/test/resolvers/folder_policy.test.ts index c65955566..4112acb4f 100644 --- a/packages/api/test/resolvers/folder_policy.test.ts +++ b/packages/api/test/resolvers/folder_policy.test.ts @@ -102,7 +102,6 @@ describe('Folder Policy API', () => { folder: 'test-folder', action: FolderPolicyAction.Archive, afterDays: 30, - minimumItems: 10, } const res = await graphqlRequest(mutation, authToken, { input }).expect( @@ -160,7 +159,6 @@ describe('Folder Policy API', () => { id: existingPolicy.id, action: FolderPolicyAction.Delete, afterDays: 30, - minimumItems: 10, } const res = await graphqlRequest(mutation, authToken, { input }).expect( diff --git a/packages/db/migrations/0181.do.folder_policy.sql b/packages/db/migrations/0182.do.folder_policy.sql similarity index 100% rename from packages/db/migrations/0181.do.folder_policy.sql rename to packages/db/migrations/0182.do.folder_policy.sql diff --git a/packages/db/migrations/0181.undo.folder_policy.sql b/packages/db/migrations/0182.undo.folder_policy.sql similarity index 100% rename from packages/db/migrations/0181.undo.folder_policy.sql rename to packages/db/migrations/0182.undo.folder_policy.sql