Merge pull request #4041 from omnivore-app/feature/folder-policy-api

feat: folder expiration policy api
This commit is contained in:
Hongbo Wu
2024-06-14 14:44:21 +08:00
committed by GitHub
15 changed files with 1013 additions and 0 deletions

View File

@ -0,0 +1,49 @@
import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm'
import { User } from './user'
export enum FolderPolicyAction {
Delete = 'DELETE',
Archive = 'ARCHIVE',
}
@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('enum', { enum: FolderPolicyAction })
action!: FolderPolicyAction
@Column('int')
afterDays!: number
@CreateDateColumn({
type: 'timestamptz',
default: () => 'CURRENT_TIMESTAMP',
})
createdAt!: Date
@UpdateDateColumn({
type: 'timestamptz',
default: () => 'CURRENT_TIMESTAMP',
})
updatedAt!: Date
}

View File

@ -349,6 +349,29 @@ export type CreateArticleSuccess = {
user: User;
};
export type CreateFolderPolicyError = {
__typename?: 'CreateFolderPolicyError';
errorCodes: Array<CreateFolderPolicyErrorCode>;
};
export enum CreateFolderPolicyErrorCode {
BadRequest = 'BAD_REQUEST',
Unauthorized = 'UNAUTHORIZED'
}
export type CreateFolderPolicyInput = {
action: FolderPolicyAction;
afterDays: Scalars['Int'];
folder: Scalars['String'];
};
export type CreateFolderPolicyResult = CreateFolderPolicyError | CreateFolderPolicySuccess;
export type CreateFolderPolicySuccess = {
__typename?: 'CreateFolderPolicySuccess';
policy: FolderPolicy;
};
export type CreateGroupError = {
__typename?: 'CreateGroupError';
errorCodes: Array<CreateGroupErrorCode>;
@ -619,6 +642,22 @@ export type DeleteFilterSuccess = {
filter: Filter;
};
export type DeleteFolderPolicyError = {
__typename?: 'DeleteFolderPolicyError';
errorCodes: Array<DeleteFolderPolicyErrorCode>;
};
export enum DeleteFolderPolicyErrorCode {
Unauthorized = 'UNAUTHORIZED'
}
export type DeleteFolderPolicyResult = DeleteFolderPolicyError | DeleteFolderPolicySuccess;
export type DeleteFolderPolicySuccess = {
__typename?: 'DeleteFolderPolicySuccess';
success: Scalars['Boolean'];
};
export type DeleteHighlightError = {
__typename?: 'DeleteHighlightError';
errorCodes: Array<DeleteHighlightErrorCode>;
@ -1081,6 +1120,38 @@ export type FiltersSuccess = {
filters: Array<Filter>;
};
export type FolderPoliciesError = {
__typename?: 'FolderPoliciesError';
errorCodes: Array<FolderPoliciesErrorCode>;
};
export enum FolderPoliciesErrorCode {
BadRequest = 'BAD_REQUEST',
Unauthorized = 'UNAUTHORIZED'
}
export type FolderPoliciesResult = FolderPoliciesError | FolderPoliciesSuccess;
export type FolderPoliciesSuccess = {
__typename?: 'FolderPoliciesSuccess';
policies: Array<FolderPolicy>;
};
export type FolderPolicy = {
__typename?: 'FolderPolicy';
action: FolderPolicyAction;
afterDays: Scalars['Int'];
createdAt: Scalars['Date'];
folder: Scalars['String'];
id: Scalars['ID'];
updatedAt: Scalars['Date'];
};
export enum FolderPolicyAction {
Archive = 'ARCHIVE',
Delete = 'DELETE'
}
export type GenerateApiKeyError = {
__typename?: 'GenerateApiKeyError';
errorCodes: Array<GenerateApiKeyErrorCode>;
@ -1723,6 +1794,7 @@ export type Mutation = {
bulkAction: BulkActionResult;
createArticle: CreateArticleResult;
createArticleSavingRequest: CreateArticleSavingRequestResult;
createFolderPolicy: CreateFolderPolicyResult;
createGroup: CreateGroupResult;
createHighlight: CreateHighlightResult;
createLabel: CreateLabelResult;
@ -1731,6 +1803,7 @@ export type Mutation = {
deleteDiscoverArticle: DeleteDiscoverArticleResult;
deleteDiscoverFeed: DeleteDiscoverFeedResult;
deleteFilter: DeleteFilterResult;
deleteFolderPolicy: DeleteFolderPolicyResult;
deleteHighlight: DeleteHighlightResult;
deleteIntegration: DeleteIntegrationResult;
deleteLabel: DeleteLabelResult;
@ -1780,6 +1853,7 @@ export type Mutation = {
unsubscribe: UnsubscribeResult;
updateEmail: UpdateEmailResult;
updateFilter: UpdateFilterResult;
updateFolderPolicy: UpdateFolderPolicyResult;
updateHighlight: UpdateHighlightResult;
updateLabel: UpdateLabelResult;
updateNewsletterEmail: UpdateNewsletterEmailResult;
@ -1822,6 +1896,11 @@ export type MutationCreateArticleSavingRequestArgs = {
};
export type MutationCreateFolderPolicyArgs = {
input: CreateFolderPolicyInput;
};
export type MutationCreateGroupArgs = {
input: CreateGroupInput;
};
@ -1862,6 +1941,11 @@ export type MutationDeleteFilterArgs = {
};
export type MutationDeleteFolderPolicyArgs = {
id: Scalars['ID'];
};
export type MutationDeleteHighlightArgs = {
highlightId: Scalars['ID'];
};
@ -2095,6 +2179,11 @@ export type MutationUpdateFilterArgs = {
};
export type MutationUpdateFolderPolicyArgs = {
input: UpdateFolderPolicyInput;
};
export type MutationUpdateHighlightArgs = {
input: UpdateHighlightInput;
};
@ -2280,6 +2369,7 @@ export type Query = {
discoverTopics: GetDiscoverTopicResults;
feeds: FeedsResult;
filters: FiltersResult;
folderPolicies: FolderPoliciesResult;
getDiscoverFeedArticles: GetDiscoverFeedArticleResults;
getUserPersonalization: GetUserPersonalizationResult;
groups: GroupsResult;
@ -3555,6 +3645,29 @@ export type UpdateFilterSuccess = {
filter: Filter;
};
export type UpdateFolderPolicyError = {
__typename?: 'UpdateFolderPolicyError';
errorCodes: Array<UpdateFolderPolicyErrorCode>;
};
export enum UpdateFolderPolicyErrorCode {
BadRequest = 'BAD_REQUEST',
Unauthorized = 'UNAUTHORIZED'
}
export type UpdateFolderPolicyInput = {
action?: InputMaybe<FolderPolicyAction>;
afterDays?: InputMaybe<Scalars['Int']>;
id: Scalars['ID'];
};
export type UpdateFolderPolicyResult = UpdateFolderPolicyError | UpdateFolderPolicySuccess;
export type UpdateFolderPolicySuccess = {
__typename?: 'UpdateFolderPolicySuccess';
policy: FolderPolicy;
};
export type UpdateHighlightError = {
__typename?: 'UpdateHighlightError';
errorCodes: Array<UpdateHighlightErrorCode>;
@ -4180,6 +4293,11 @@ export type ResolversTypes = {
CreateArticleSavingRequestResult: ResolversTypes['CreateArticleSavingRequestError'] | ResolversTypes['CreateArticleSavingRequestSuccess'];
CreateArticleSavingRequestSuccess: ResolverTypeWrapper<CreateArticleSavingRequestSuccess>;
CreateArticleSuccess: ResolverTypeWrapper<CreateArticleSuccess>;
CreateFolderPolicyError: ResolverTypeWrapper<CreateFolderPolicyError>;
CreateFolderPolicyErrorCode: CreateFolderPolicyErrorCode;
CreateFolderPolicyInput: CreateFolderPolicyInput;
CreateFolderPolicyResult: ResolversTypes['CreateFolderPolicyError'] | ResolversTypes['CreateFolderPolicySuccess'];
CreateFolderPolicySuccess: ResolverTypeWrapper<CreateFolderPolicySuccess>;
CreateGroupError: ResolverTypeWrapper<CreateGroupError>;
CreateGroupErrorCode: CreateGroupErrorCode;
CreateGroupInput: CreateGroupInput;
@ -4234,6 +4352,10 @@ export type ResolversTypes = {
DeleteFilterErrorCode: DeleteFilterErrorCode;
DeleteFilterResult: ResolversTypes['DeleteFilterError'] | ResolversTypes['DeleteFilterSuccess'];
DeleteFilterSuccess: ResolverTypeWrapper<DeleteFilterSuccess>;
DeleteFolderPolicyError: ResolverTypeWrapper<DeleteFolderPolicyError>;
DeleteFolderPolicyErrorCode: DeleteFolderPolicyErrorCode;
DeleteFolderPolicyResult: ResolversTypes['DeleteFolderPolicyError'] | ResolversTypes['DeleteFolderPolicySuccess'];
DeleteFolderPolicySuccess: ResolverTypeWrapper<DeleteFolderPolicySuccess>;
DeleteHighlightError: ResolverTypeWrapper<DeleteHighlightError>;
DeleteHighlightErrorCode: DeleteHighlightErrorCode;
DeleteHighlightReplyError: ResolverTypeWrapper<DeleteHighlightReplyError>;
@ -4324,6 +4446,12 @@ export type ResolversTypes = {
FiltersResult: ResolversTypes['FiltersError'] | ResolversTypes['FiltersSuccess'];
FiltersSuccess: ResolverTypeWrapper<FiltersSuccess>;
Float: ResolverTypeWrapper<Scalars['Float']>;
FolderPoliciesError: ResolverTypeWrapper<FolderPoliciesError>;
FolderPoliciesErrorCode: FolderPoliciesErrorCode;
FolderPoliciesResult: ResolversTypes['FolderPoliciesError'] | ResolversTypes['FolderPoliciesSuccess'];
FolderPoliciesSuccess: ResolverTypeWrapper<FolderPoliciesSuccess>;
FolderPolicy: ResolverTypeWrapper<FolderPolicy>;
FolderPolicyAction: FolderPolicyAction;
GenerateApiKeyError: ResolverTypeWrapper<GenerateApiKeyError>;
GenerateApiKeyErrorCode: GenerateApiKeyErrorCode;
GenerateApiKeyInput: GenerateApiKeyInput;
@ -4659,6 +4787,11 @@ export type ResolversTypes = {
UpdateFilterInput: UpdateFilterInput;
UpdateFilterResult: ResolversTypes['UpdateFilterError'] | ResolversTypes['UpdateFilterSuccess'];
UpdateFilterSuccess: ResolverTypeWrapper<UpdateFilterSuccess>;
UpdateFolderPolicyError: ResolverTypeWrapper<UpdateFolderPolicyError>;
UpdateFolderPolicyErrorCode: UpdateFolderPolicyErrorCode;
UpdateFolderPolicyInput: UpdateFolderPolicyInput;
UpdateFolderPolicyResult: ResolversTypes['UpdateFolderPolicyError'] | ResolversTypes['UpdateFolderPolicySuccess'];
UpdateFolderPolicySuccess: ResolverTypeWrapper<UpdateFolderPolicySuccess>;
UpdateHighlightError: ResolverTypeWrapper<UpdateHighlightError>;
UpdateHighlightErrorCode: UpdateHighlightErrorCode;
UpdateHighlightInput: UpdateHighlightInput;
@ -4794,6 +4927,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 +4974,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 +5046,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 +5310,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 +5617,20 @@ export type CreateArticleSuccessResolvers<ContextType = ResolverContext, ParentT
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type CreateFolderPolicyErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['CreateFolderPolicyError'] = ResolversParentTypes['CreateFolderPolicyError']> = {
errorCodes?: Resolver<Array<ResolversTypes['CreateFolderPolicyErrorCode']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type CreateFolderPolicyResultResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['CreateFolderPolicyResult'] = ResolversParentTypes['CreateFolderPolicyResult']> = {
__resolveType: TypeResolveFn<'CreateFolderPolicyError' | 'CreateFolderPolicySuccess', ParentType, ContextType>;
};
export type CreateFolderPolicySuccessResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['CreateFolderPolicySuccess'] = ResolversParentTypes['CreateFolderPolicySuccess']> = {
policy?: Resolver<ResolversTypes['FolderPolicy'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type CreateGroupErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['CreateGroupError'] = ResolversParentTypes['CreateGroupError']> = {
errorCodes?: Resolver<Array<ResolversTypes['CreateGroupErrorCode']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
@ -5627,6 +5789,20 @@ export type DeleteFilterSuccessResolvers<ContextType = ResolverContext, ParentTy
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type DeleteFolderPolicyErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['DeleteFolderPolicyError'] = ResolversParentTypes['DeleteFolderPolicyError']> = {
errorCodes?: Resolver<Array<ResolversTypes['DeleteFolderPolicyErrorCode']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type DeleteFolderPolicyResultResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['DeleteFolderPolicyResult'] = ResolversParentTypes['DeleteFolderPolicyResult']> = {
__resolveType: TypeResolveFn<'DeleteFolderPolicyError' | 'DeleteFolderPolicySuccess', ParentType, ContextType>;
};
export type DeleteFolderPolicySuccessResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['DeleteFolderPolicySuccess'] = ResolversParentTypes['DeleteFolderPolicySuccess']> = {
success?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type DeleteHighlightErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['DeleteHighlightError'] = ResolversParentTypes['DeleteHighlightError']> = {
errorCodes?: Resolver<Array<ResolversTypes['DeleteHighlightErrorCode']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
@ -5992,6 +6168,30 @@ export type FiltersSuccessResolvers<ContextType = ResolverContext, ParentType ex
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type FolderPoliciesErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['FolderPoliciesError'] = ResolversParentTypes['FolderPoliciesError']> = {
errorCodes?: Resolver<Array<ResolversTypes['FolderPoliciesErrorCode']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type FolderPoliciesResultResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['FolderPoliciesResult'] = ResolversParentTypes['FolderPoliciesResult']> = {
__resolveType: TypeResolveFn<'FolderPoliciesError' | 'FolderPoliciesSuccess', ParentType, ContextType>;
};
export type FolderPoliciesSuccessResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['FolderPoliciesSuccess'] = ResolversParentTypes['FolderPoliciesSuccess']> = {
policies?: Resolver<Array<ResolversTypes['FolderPolicy']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type FolderPolicyResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['FolderPolicy'] = ResolversParentTypes['FolderPolicy']> = {
action?: Resolver<ResolversTypes['FolderPolicyAction'], ParentType, ContextType>;
afterDays?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
createdAt?: Resolver<ResolversTypes['Date'], ParentType, ContextType>;
folder?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
updatedAt?: Resolver<ResolversTypes['Date'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type GenerateApiKeyErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['GenerateApiKeyError'] = ResolversParentTypes['GenerateApiKeyError']> = {
errorCodes?: Resolver<Array<ResolversTypes['GenerateApiKeyErrorCode']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
@ -6486,6 +6686,7 @@ export type MutationResolvers<ContextType = ResolverContext, ParentType extends
bulkAction?: Resolver<ResolversTypes['BulkActionResult'], ParentType, ContextType, RequireFields<MutationBulkActionArgs, 'action' | 'query'>>;
createArticle?: Resolver<ResolversTypes['CreateArticleResult'], ParentType, ContextType, RequireFields<MutationCreateArticleArgs, 'input'>>;
createArticleSavingRequest?: Resolver<ResolversTypes['CreateArticleSavingRequestResult'], ParentType, ContextType, RequireFields<MutationCreateArticleSavingRequestArgs, 'input'>>;
createFolderPolicy?: Resolver<ResolversTypes['CreateFolderPolicyResult'], ParentType, ContextType, RequireFields<MutationCreateFolderPolicyArgs, 'input'>>;
createGroup?: Resolver<ResolversTypes['CreateGroupResult'], ParentType, ContextType, RequireFields<MutationCreateGroupArgs, 'input'>>;
createHighlight?: Resolver<ResolversTypes['CreateHighlightResult'], ParentType, ContextType, RequireFields<MutationCreateHighlightArgs, 'input'>>;
createLabel?: Resolver<ResolversTypes['CreateLabelResult'], ParentType, ContextType, RequireFields<MutationCreateLabelArgs, 'input'>>;
@ -6494,6 +6695,7 @@ export type MutationResolvers<ContextType = ResolverContext, ParentType extends
deleteDiscoverArticle?: Resolver<ResolversTypes['DeleteDiscoverArticleResult'], ParentType, ContextType, RequireFields<MutationDeleteDiscoverArticleArgs, 'input'>>;
deleteDiscoverFeed?: Resolver<ResolversTypes['DeleteDiscoverFeedResult'], ParentType, ContextType, RequireFields<MutationDeleteDiscoverFeedArgs, 'input'>>;
deleteFilter?: Resolver<ResolversTypes['DeleteFilterResult'], ParentType, ContextType, RequireFields<MutationDeleteFilterArgs, 'id'>>;
deleteFolderPolicy?: Resolver<ResolversTypes['DeleteFolderPolicyResult'], ParentType, ContextType, RequireFields<MutationDeleteFolderPolicyArgs, 'id'>>;
deleteHighlight?: Resolver<ResolversTypes['DeleteHighlightResult'], ParentType, ContextType, RequireFields<MutationDeleteHighlightArgs, 'highlightId'>>;
deleteIntegration?: Resolver<ResolversTypes['DeleteIntegrationResult'], ParentType, ContextType, RequireFields<MutationDeleteIntegrationArgs, 'id'>>;
deleteLabel?: Resolver<ResolversTypes['DeleteLabelResult'], ParentType, ContextType, RequireFields<MutationDeleteLabelArgs, 'id'>>;
@ -6543,6 +6745,7 @@ export type MutationResolvers<ContextType = ResolverContext, ParentType extends
unsubscribe?: Resolver<ResolversTypes['UnsubscribeResult'], ParentType, ContextType, RequireFields<MutationUnsubscribeArgs, 'name'>>;
updateEmail?: Resolver<ResolversTypes['UpdateEmailResult'], ParentType, ContextType, RequireFields<MutationUpdateEmailArgs, 'input'>>;
updateFilter?: Resolver<ResolversTypes['UpdateFilterResult'], ParentType, ContextType, RequireFields<MutationUpdateFilterArgs, 'input'>>;
updateFolderPolicy?: Resolver<ResolversTypes['UpdateFolderPolicyResult'], ParentType, ContextType, RequireFields<MutationUpdateFolderPolicyArgs, 'input'>>;
updateHighlight?: Resolver<ResolversTypes['UpdateHighlightResult'], ParentType, ContextType, RequireFields<MutationUpdateHighlightArgs, 'input'>>;
updateLabel?: Resolver<ResolversTypes['UpdateLabelResult'], ParentType, ContextType, RequireFields<MutationUpdateLabelArgs, 'input'>>;
updateNewsletterEmail?: Resolver<ResolversTypes['UpdateNewsletterEmailResult'], ParentType, ContextType, RequireFields<MutationUpdateNewsletterEmailArgs, 'input'>>;
@ -6639,6 +6842,7 @@ export type QueryResolvers<ContextType = ResolverContext, ParentType extends Res
discoverTopics?: Resolver<ResolversTypes['GetDiscoverTopicResults'], ParentType, ContextType>;
feeds?: Resolver<ResolversTypes['FeedsResult'], ParentType, ContextType, RequireFields<QueryFeedsArgs, 'input'>>;
filters?: Resolver<ResolversTypes['FiltersResult'], ParentType, ContextType>;
folderPolicies?: Resolver<ResolversTypes['FolderPoliciesResult'], ParentType, ContextType>;
getDiscoverFeedArticles?: Resolver<ResolversTypes['GetDiscoverFeedArticleResults'], ParentType, ContextType, RequireFields<QueryGetDiscoverFeedArticlesArgs, 'discoverTopicId'>>;
getUserPersonalization?: Resolver<ResolversTypes['GetUserPersonalizationResult'], ParentType, ContextType>;
groups?: Resolver<ResolversTypes['GroupsResult'], ParentType, ContextType>;
@ -7391,6 +7595,20 @@ export type UpdateFilterSuccessResolvers<ContextType = ResolverContext, ParentTy
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type UpdateFolderPolicyErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['UpdateFolderPolicyError'] = ResolversParentTypes['UpdateFolderPolicyError']> = {
errorCodes?: Resolver<Array<ResolversTypes['UpdateFolderPolicyErrorCode']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type UpdateFolderPolicyResultResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['UpdateFolderPolicyResult'] = ResolversParentTypes['UpdateFolderPolicyResult']> = {
__resolveType: TypeResolveFn<'UpdateFolderPolicyError' | 'UpdateFolderPolicySuccess', ParentType, ContextType>;
};
export type UpdateFolderPolicySuccessResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['UpdateFolderPolicySuccess'] = ResolversParentTypes['UpdateFolderPolicySuccess']> = {
policy?: Resolver<ResolversTypes['FolderPolicy'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type UpdateHighlightErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['UpdateHighlightError'] = ResolversParentTypes['UpdateHighlightError']> = {
errorCodes?: Resolver<Array<ResolversTypes['UpdateHighlightErrorCode']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
@ -7734,6 +7952,9 @@ export type Resolvers<ContextType = ResolverContext> = {
CreateArticleSavingRequestResult?: CreateArticleSavingRequestResultResolvers<ContextType>;
CreateArticleSavingRequestSuccess?: CreateArticleSavingRequestSuccessResolvers<ContextType>;
CreateArticleSuccess?: CreateArticleSuccessResolvers<ContextType>;
CreateFolderPolicyError?: CreateFolderPolicyErrorResolvers<ContextType>;
CreateFolderPolicyResult?: CreateFolderPolicyResultResolvers<ContextType>;
CreateFolderPolicySuccess?: CreateFolderPolicySuccessResolvers<ContextType>;
CreateGroupError?: CreateGroupErrorResolvers<ContextType>;
CreateGroupResult?: CreateGroupResultResolvers<ContextType>;
CreateGroupSuccess?: CreateGroupSuccessResolvers<ContextType>;
@ -7768,6 +7989,9 @@ export type Resolvers<ContextType = ResolverContext> = {
DeleteFilterError?: DeleteFilterErrorResolvers<ContextType>;
DeleteFilterResult?: DeleteFilterResultResolvers<ContextType>;
DeleteFilterSuccess?: DeleteFilterSuccessResolvers<ContextType>;
DeleteFolderPolicyError?: DeleteFolderPolicyErrorResolvers<ContextType>;
DeleteFolderPolicyResult?: DeleteFolderPolicyResultResolvers<ContextType>;
DeleteFolderPolicySuccess?: DeleteFolderPolicySuccessResolvers<ContextType>;
DeleteHighlightError?: DeleteHighlightErrorResolvers<ContextType>;
DeleteHighlightReplyError?: DeleteHighlightReplyErrorResolvers<ContextType>;
DeleteHighlightReplyResult?: DeleteHighlightReplyResultResolvers<ContextType>;
@ -7833,6 +8057,10 @@ export type Resolvers<ContextType = ResolverContext> = {
FiltersError?: FiltersErrorResolvers<ContextType>;
FiltersResult?: FiltersResultResolvers<ContextType>;
FiltersSuccess?: FiltersSuccessResolvers<ContextType>;
FolderPoliciesError?: FolderPoliciesErrorResolvers<ContextType>;
FolderPoliciesResult?: FolderPoliciesResultResolvers<ContextType>;
FolderPoliciesSuccess?: FolderPoliciesSuccessResolvers<ContextType>;
FolderPolicy?: FolderPolicyResolvers<ContextType>;
GenerateApiKeyError?: GenerateApiKeyErrorResolvers<ContextType>;
GenerateApiKeyResult?: GenerateApiKeyResultResolvers<ContextType>;
GenerateApiKeySuccess?: GenerateApiKeySuccessResolvers<ContextType>;
@ -8054,6 +8282,9 @@ export type Resolvers<ContextType = ResolverContext> = {
UpdateFilterError?: UpdateFilterErrorResolvers<ContextType>;
UpdateFilterResult?: UpdateFilterResultResolvers<ContextType>;
UpdateFilterSuccess?: UpdateFilterSuccessResolvers<ContextType>;
UpdateFolderPolicyError?: UpdateFolderPolicyErrorResolvers<ContextType>;
UpdateFolderPolicyResult?: UpdateFolderPolicyResultResolvers<ContextType>;
UpdateFolderPolicySuccess?: UpdateFolderPolicySuccessResolvers<ContextType>;
UpdateHighlightError?: UpdateHighlightErrorResolvers<ContextType>;
UpdateHighlightReplyError?: UpdateHighlightReplyErrorResolvers<ContextType>;
UpdateHighlightReplyResult?: UpdateHighlightReplyResultResolvers<ContextType>;

View File

@ -307,6 +307,27 @@ type CreateArticleSuccess {
user: User!
}
type CreateFolderPolicyError {
errorCodes: [CreateFolderPolicyErrorCode!]!
}
enum CreateFolderPolicyErrorCode {
BAD_REQUEST
UNAUTHORIZED
}
input CreateFolderPolicyInput {
action: FolderPolicyAction!
afterDays: Int!
folder: String!
}
union CreateFolderPolicyResult = CreateFolderPolicyError | CreateFolderPolicySuccess
type CreateFolderPolicySuccess {
policy: FolderPolicy!
}
type CreateGroupError {
errorCodes: [CreateGroupErrorCode!]!
}
@ -557,6 +578,20 @@ type DeleteFilterSuccess {
filter: Filter!
}
type DeleteFolderPolicyError {
errorCodes: [DeleteFolderPolicyErrorCode!]!
}
enum DeleteFolderPolicyErrorCode {
UNAUTHORIZED
}
union DeleteFolderPolicyResult = DeleteFolderPolicyError | DeleteFolderPolicySuccess
type DeleteFolderPolicySuccess {
success: Boolean!
}
type DeleteHighlightError {
errorCodes: [DeleteHighlightErrorCode!]!
}
@ -972,6 +1007,35 @@ 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!
updatedAt: Date!
}
enum FolderPolicyAction {
ARCHIVE
DELETE
}
type GenerateApiKeyError {
errorCodes: [GenerateApiKeyErrorCode!]!
}
@ -1555,6 +1619,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 +1628,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 +1678,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 +1821,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 +2903,27 @@ type UpdateFilterSuccess {
filter: Filter!
}
type UpdateFolderPolicyError {
errorCodes: [UpdateFolderPolicyErrorCode!]!
}
enum UpdateFolderPolicyErrorCode {
BAD_REQUEST
UNAUTHORIZED
}
input UpdateFolderPolicyInput {
action: FolderPolicyAction
afterDays: Int
id: ID!
}
union UpdateFolderPolicyResult = UpdateFolderPolicyError | UpdateFolderPolicySuccess
type UpdateFolderPolicySuccess {
policy: FolderPolicy!
}
type UpdateHighlightError {
errorCodes: [UpdateHighlightErrorCode!]!
}

View File

@ -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()')
}

View File

@ -31,6 +31,10 @@ import {
SAVE_NEWSLETTER_JOB,
} from './jobs/email/inbound_emails'
import { sendEmailJob, SEND_EMAIL_JOB } from './jobs/email/send_email'
import {
expireFoldersJob,
EXPIRE_FOLDERS_JOB_NAME,
} from './jobs/expire_folders'
import { findThumbnail, THUMBNAIL_JOB } from './jobs/find_thumbnail'
import {
generatePreviewContent,
@ -217,6 +221,8 @@ export const createWorker = (connection: ConnectionOptions) =>
return generatePreviewContent(job.data)
case PRUNE_TRASH_JOB:
return pruneTrashJob(job.data)
case EXPIRE_FOLDERS_JOB_NAME:
return expireFoldersJob()
default:
logger.warning(`[queue-processor] unhandled job: ${job.name}`)
}

View File

@ -0,0 +1,129 @@
import { FolderPolicy, FolderPolicyAction } from '../../entity/folder_policy'
import {
CreateFolderPolicyError,
CreateFolderPolicyErrorCode,
CreateFolderPolicySuccess,
DeleteFolderPolicyError,
DeleteFolderPolicySuccess,
FolderPoliciesError,
FolderPoliciesSuccess,
MutationCreateFolderPolicyArgs,
MutationDeleteFolderPolicyArgs,
MutationUpdateFolderPolicyArgs,
UpdateFolderPolicyError,
UpdateFolderPolicyErrorCode,
UpdateFolderPolicySuccess,
} from '../../generated/graphql'
import {
createFolderPolicy,
deleteFolderPolicy,
findFolderPoliciesByUserId,
findFolderPolicyById,
updateFolderPolicy,
} from '../../services/folder_policy'
import { Merge } from '../../util'
import { authorized } from '../../utils/gql-utils'
type PartialFolderPoliciesSuccess = Merge<
FolderPoliciesSuccess,
{ policies: Array<FolderPolicy> }
>
export const folderPoliciesResolver = authorized<
PartialFolderPoliciesSuccess,
FolderPoliciesError
>(async (_, __, { uid }) => {
const policies = await findFolderPoliciesByUserId(uid)
return {
policies,
}
})
export const createFolderPolicyResolver = authorized<
Merge<CreateFolderPolicySuccess, { policy: FolderPolicy }>,
CreateFolderPolicyError,
MutationCreateFolderPolicyArgs
>(async (_, { input }, { uid, log }) => {
const { folder, action, afterDays } = input
if (afterDays < 0) {
log.error('Invalid values')
return {
errorCodes: [CreateFolderPolicyErrorCode.BadRequest],
}
}
const policy = await createFolderPolicy({
userId: uid,
folder,
action: action as unknown as FolderPolicyAction,
afterDays,
})
return {
policy,
}
})
export const updateFolderPolicyResolver = authorized<
Merge<UpdateFolderPolicySuccess, { policy: FolderPolicy }>,
UpdateFolderPolicyError,
MutationUpdateFolderPolicyArgs
>(async (_, { input }, { log, uid }) => {
const { id, action, afterDays } = input
if (!action && !afterDays) {
log.error('No fields to update')
return {
errorCodes: [UpdateFolderPolicyErrorCode.BadRequest],
}
}
if (afterDays && afterDays < 0) {
log.error('Invalid values')
return {
errorCodes: [UpdateFolderPolicyErrorCode.BadRequest],
}
}
const result = await updateFolderPolicy(uid, id, {
action: action ? (action as unknown as FolderPolicyAction) : undefined,
afterDays: afterDays ?? undefined,
})
if (!result.affected) {
log.error('Policy not found')
return {
errorCodes: [UpdateFolderPolicyErrorCode.Unauthorized],
}
}
const policy = await findFolderPolicyById(uid, id)
if (!policy) {
log.error('Policy not found')
return {
errorCodes: [UpdateFolderPolicyErrorCode.Unauthorized],
}
}
return {
policy,
}
})
export const deleteFolderPolicyResolver = authorized<
DeleteFolderPolicySuccess,
DeleteFolderPolicyError,
MutationDeleteFolderPolicyArgs
>(async (_, { id }, { uid }) => {
const result = await deleteFolderPolicy(uid, id)
return {
success: !!result.affected,
}
})

View File

@ -52,6 +52,12 @@ import {
saveDiscoverArticleResolver,
} from './discover_feeds'
import { optInFeatureResolver } from './features'
import {
createFolderPolicyResolver,
deleteFolderPolicyResolver,
folderPoliciesResolver,
updateFolderPolicyResolver,
} from './folder_policy'
import { highlightsResolver } from './highlight'
import {
hiddenHomeSectionResolver,
@ -307,6 +313,9 @@ export const functionResolvers = {
exportToIntegration: exportToIntegrationResolver,
replyToEmail: replyToEmailResolver,
refreshHome: refreshHomeResolver,
createFolderPolicy: createFolderPolicyResolver,
updateFolderPolicy: updateFolderPolicyResolver,
deleteFolderPolicy: deleteFolderPolicyResolver,
},
Query: {
me: getMeUserResolver,
@ -342,6 +351,7 @@ export const functionResolvers = {
subscription: subscriptionResolver,
hiddenHomeSection: hiddenHomeSectionResolver,
highlights: highlightsResolver,
folderPolicies: folderPoliciesResolver,
},
User: {
async intercomHash(user: User) {
@ -882,4 +892,8 @@ export const functionResolvers = {
...resultResolveTypeResolver('RefreshHome'),
...resultResolveTypeResolver('HiddenHomeSection'),
...resultResolveTypeResolver('Highlights'),
...resultResolveTypeResolver('FolderPolicies'),
...resultResolveTypeResolver('CreateFolderPolicy'),
...resultResolveTypeResolver('UpdateFolderPolicy'),
...resultResolveTypeResolver('DeleteFolderPolicy'),
}

View File

@ -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
}

View File

@ -3247,6 +3247,97 @@ const schema = gql`
BAD_REQUEST
}
type FolderPolicy {
id: ID!
folder: String!
action: FolderPolicyAction!
afterDays: 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!
}
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
}
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
}
# Mutations
type Mutation {
googleLogin(input: GoogleLoginInput!): LoginResult!
@ -3373,6 +3464,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 +3545,7 @@ const schema = gql`
subscription(id: ID!): SubscriptionResult!
hiddenHomeSection: HiddenHomeSectionResult!
highlights(after: String, first: Int, query: String): HighlightsResult!
folderPolicies: FolderPoliciesResult!
}
schema {

View File

@ -0,0 +1,46 @@
import { FolderPolicy, FolderPolicyAction } from '../entity/folder_policy'
import { getRepository } from '../repository'
export const createFolderPolicy = async (folderPolicy: {
userId: string
folder: string
action: FolderPolicyAction
afterDays: 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 (
userId: string,
folderPolicyId: string,
update: Partial<FolderPolicy>
) => {
return getRepository(FolderPolicy).update(
{ id: folderPolicyId, userId },
update
)
}
export const deleteFolderPolicy = async (
userId: string,
folderPolicyId: string
) => {
return getRepository(FolderPolicy).delete({
id: folderPolicyId,
userId,
})
}
export const findFolderPolicyById = async (
userId: string,
folderPolicyId: string
) => {
return getRepository(FolderPolicy).findOneBy({ id: folderPolicyId, userId })
}

View File

@ -28,6 +28,7 @@ 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 { GENERATE_PREVIEW_CONTENT_JOB } from '../jobs/generate_preview_content'
import { EXPORT_ALL_ITEMS_JOB_NAME } from '../jobs/integration/export_all_items'
@ -114,6 +115,7 @@ export const getJobPriority = (jobName: string): number => {
case THUMBNAIL_JOB:
case GENERATE_PREVIEW_CONTENT_JOB:
case PRUNE_TRASH_JOB:
case EXPIRE_FOLDERS_JOB_NAME:
return 100
default:
@ -1072,4 +1074,23 @@ export const enqueuePruneTrashJob = async (numDays: number) => {
)
}
export const enqueueExpireFoldersJob = async () => {
const queue = await getBackendQueue()
if (!queue) {
return undefined
}
return queue.add(
EXPIRE_FOLDERS_JOB_NAME,
{},
{
jobId: `${EXPIRE_FOLDERS_JOB_NAME}_${JOB_VERSION}`,
removeOnComplete: true,
removeOnFail: true,
priority: getJobPriority(EXPIRE_FOLDERS_JOB_NAME),
attempts: 3,
}
)
}
export default createHttpTaskWithToken

View File

@ -0,0 +1,212 @@
import { expect } from 'chai'
import {
FolderPolicy as FolderPolicyEntity,
FolderPolicyAction,
} from '../../src/entity/folder_policy'
import { User } from '../../src/entity/user'
import {
DeleteFolderPolicySuccess,
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'
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 in ascending order', async () => {
const existingPolicy = await createFolderPolicy({
userId: loginUser.id,
folder: 'inbox',
action: FolderPolicyAction.Archive,
afterDays: 30,
})
const existingPolicy1 = await createFolderPolicy({
userId: loginUser.id,
folder: 'following',
action: FolderPolicyAction.Archive,
afterDays: 30,
})
const res = await graphqlRequest(query, authToken).expect(200)
const policies = res.body.data.folderPolicies
.policies as Array<FolderPolicy>
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)
})
})
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,
}
const res = await graphqlRequest(mutation, authToken, { input }).expect(
200
)
const createdPolicy = res.body.data.createFolderPolicy
.policy as FolderPolicy
const policy = await findFolderPolicyById(loginUser.id, createdPolicy.id)
expect(policy).to.exist
expect(policy?.folder).to.equal(input.folder)
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,
})
})
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,
}
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)
})
})
describe('Delete Folder Policy', () => {
let existingPolicy: FolderPolicyEntity
before(async () => {
existingPolicy = await createFolderPolicy({
userId: loginUser.id,
folder: 'test-folder',
action: FolderPolicyAction.Archive,
afterDays: 30,
})
})
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
})
})
})

View File

@ -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
}

View File

@ -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;

View File

@ -0,0 +1,13 @@
-- 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;
DROP TYPE folder_action;
DROP PROCEDURE omnivore.expire_folders();
COMMIT;