mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
commit
5bb720c54d
13 changed files with 1154 additions and 1 deletions
41
packages/api/src/entity/webhook.ts
Normal file
41
packages/api/src/entity/webhook.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm'
|
||||
import { User } from './user'
|
||||
|
||||
@Entity({ name: 'webhooks' })
|
||||
export class Webhook {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string
|
||||
|
||||
@ManyToOne(() => User, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
user!: User
|
||||
|
||||
@Column('text')
|
||||
url!: string
|
||||
|
||||
@Column('text', { array: true })
|
||||
eventTypes!: string[]
|
||||
|
||||
@Column('text', { default: 'POST' })
|
||||
method!: string
|
||||
|
||||
@Column('text', { default: 'application/json' })
|
||||
contentType!: string
|
||||
|
||||
@Column('boolean', { default: true })
|
||||
enabled!: boolean
|
||||
|
||||
@CreateDateColumn({ default: () => 'CURRENT_TIMESTAMP' })
|
||||
createdAt!: Date
|
||||
|
||||
@UpdateDateColumn({ default: () => 'CURRENT_TIMESTAMP' })
|
||||
updatedAt!: Date
|
||||
}
|
||||
|
|
@ -502,6 +502,24 @@ export type DeleteReminderSuccess = {
|
|||
reminder: Reminder;
|
||||
};
|
||||
|
||||
export type DeleteWebhookError = {
|
||||
__typename?: 'DeleteWebhookError';
|
||||
errorCodes: Array<DeleteWebhookErrorCode>;
|
||||
};
|
||||
|
||||
export enum DeleteWebhookErrorCode {
|
||||
BadRequest = 'BAD_REQUEST',
|
||||
NotFound = 'NOT_FOUND',
|
||||
Unauthorized = 'UNAUTHORIZED'
|
||||
}
|
||||
|
||||
export type DeleteWebhookResult = DeleteWebhookError | DeleteWebhookSuccess;
|
||||
|
||||
export type DeleteWebhookSuccess = {
|
||||
__typename?: 'DeleteWebhookSuccess';
|
||||
webhook: Webhook;
|
||||
};
|
||||
|
||||
export type DeviceToken = {
|
||||
__typename?: 'DeviceToken';
|
||||
createdAt: Scalars['Date'];
|
||||
|
|
@ -817,6 +835,7 @@ export type Mutation = {
|
|||
deleteNewsletterEmail: DeleteNewsletterEmailResult;
|
||||
deleteReaction: DeleteReactionResult;
|
||||
deleteReminder: DeleteReminderResult;
|
||||
deleteWebhook: DeleteWebhookResult;
|
||||
generateApiKey: GenerateApiKeyResult;
|
||||
googleLogin: LoginResult;
|
||||
googleSignup: GoogleSignupResult;
|
||||
|
|
@ -836,6 +855,7 @@ export type Mutation = {
|
|||
setShareArticle: SetShareArticleResult;
|
||||
setShareHighlight: SetShareHighlightResult;
|
||||
setUserPersonalization: SetUserPersonalizationResult;
|
||||
setWebhook: SetWebhookResult;
|
||||
signup: SignupResult;
|
||||
subscribe: SubscribeResult;
|
||||
unsubscribe: UnsubscribeResult;
|
||||
|
|
@ -922,6 +942,11 @@ export type MutationDeleteReminderArgs = {
|
|||
};
|
||||
|
||||
|
||||
export type MutationDeleteWebhookArgs = {
|
||||
id: Scalars['ID'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationGenerateApiKeyArgs = {
|
||||
input: GenerateApiKeyInput;
|
||||
};
|
||||
|
|
@ -1012,6 +1037,11 @@ export type MutationSetUserPersonalizationArgs = {
|
|||
};
|
||||
|
||||
|
||||
export type MutationSetWebhookArgs = {
|
||||
input: SetWebhookInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationSignupArgs = {
|
||||
input: SignupInput;
|
||||
};
|
||||
|
|
@ -1180,6 +1210,8 @@ export type Query = {
|
|||
user: UserResult;
|
||||
users: UsersResult;
|
||||
validateUsername: Scalars['Boolean'];
|
||||
webhook: WebhookResult;
|
||||
webhooks: WebhooksResult;
|
||||
};
|
||||
|
||||
|
||||
|
|
@ -1256,6 +1288,11 @@ export type QueryValidateUsernameArgs = {
|
|||
username: Scalars['String'];
|
||||
};
|
||||
|
||||
|
||||
export type QueryWebhookArgs = {
|
||||
id: Scalars['ID'];
|
||||
};
|
||||
|
||||
export type Reaction = {
|
||||
__typename?: 'Reaction';
|
||||
code: ReactionType;
|
||||
|
|
@ -1611,6 +1648,34 @@ export type SetUserPersonalizationSuccess = {
|
|||
updatedUserPersonalization: UserPersonalization;
|
||||
};
|
||||
|
||||
export type SetWebhookError = {
|
||||
__typename?: 'SetWebhookError';
|
||||
errorCodes: Array<SetWebhookErrorCode>;
|
||||
};
|
||||
|
||||
export enum SetWebhookErrorCode {
|
||||
AlreadyExists = 'ALREADY_EXISTS',
|
||||
BadRequest = 'BAD_REQUEST',
|
||||
NotFound = 'NOT_FOUND',
|
||||
Unauthorized = 'UNAUTHORIZED'
|
||||
}
|
||||
|
||||
export type SetWebhookInput = {
|
||||
contentType?: InputMaybe<Scalars['String']>;
|
||||
enabled?: InputMaybe<Scalars['Boolean']>;
|
||||
eventTypes: Array<WebhookEvent>;
|
||||
id?: InputMaybe<Scalars['ID']>;
|
||||
method?: InputMaybe<Scalars['String']>;
|
||||
url: Scalars['String'];
|
||||
};
|
||||
|
||||
export type SetWebhookResult = SetWebhookError | SetWebhookSuccess;
|
||||
|
||||
export type SetWebhookSuccess = {
|
||||
__typename?: 'SetWebhookSuccess';
|
||||
webhook: Webhook;
|
||||
};
|
||||
|
||||
export type SharedArticleError = {
|
||||
__typename?: 'SharedArticleError';
|
||||
errorCodes: Array<SharedArticleErrorCode>;
|
||||
|
|
@ -2073,6 +2138,65 @@ export type UserSuccess = {
|
|||
user: User;
|
||||
};
|
||||
|
||||
export type Webhook = {
|
||||
__typename?: 'Webhook';
|
||||
contentType: Scalars['String'];
|
||||
createdAt: Scalars['Date'];
|
||||
enabled: Scalars['Boolean'];
|
||||
eventTypes: Array<WebhookEvent>;
|
||||
id: Scalars['ID'];
|
||||
method: Scalars['String'];
|
||||
updatedAt: Scalars['Date'];
|
||||
url: Scalars['String'];
|
||||
};
|
||||
|
||||
export type WebhookError = {
|
||||
__typename?: 'WebhookError';
|
||||
errorCodes: Array<WebhookErrorCode>;
|
||||
};
|
||||
|
||||
export enum WebhookErrorCode {
|
||||
BadRequest = 'BAD_REQUEST',
|
||||
NotFound = 'NOT_FOUND',
|
||||
Unauthorized = 'UNAUTHORIZED'
|
||||
}
|
||||
|
||||
export enum WebhookEvent {
|
||||
HighlightCreated = 'HIGHLIGHT_CREATED',
|
||||
HighlightDeleted = 'HIGHLIGHT_DELETED',
|
||||
HighlightUpdated = 'HIGHLIGHT_UPDATED',
|
||||
LabelCreated = 'LABEL_CREATED',
|
||||
LabelDeleted = 'LABEL_DELETED',
|
||||
LabelUpdated = 'LABEL_UPDATED',
|
||||
PageCreated = 'PAGE_CREATED',
|
||||
PageDeleted = 'PAGE_DELETED',
|
||||
PageUpdated = 'PAGE_UPDATED'
|
||||
}
|
||||
|
||||
export type WebhookResult = WebhookError | WebhookSuccess;
|
||||
|
||||
export type WebhooksError = {
|
||||
__typename?: 'WebhooksError';
|
||||
errorCodes: Array<WebhooksErrorCode>;
|
||||
};
|
||||
|
||||
export enum WebhooksErrorCode {
|
||||
BadRequest = 'BAD_REQUEST',
|
||||
Unauthorized = 'UNAUTHORIZED'
|
||||
}
|
||||
|
||||
export type WebhooksResult = WebhooksError | WebhooksSuccess;
|
||||
|
||||
export type WebhooksSuccess = {
|
||||
__typename?: 'WebhooksSuccess';
|
||||
webhooks: Array<Webhook>;
|
||||
};
|
||||
|
||||
export type WebhookSuccess = {
|
||||
__typename?: 'WebhookSuccess';
|
||||
webhook: Webhook;
|
||||
};
|
||||
|
||||
|
||||
|
||||
export type ResolverTypeWrapper<T> = Promise<T> | T;
|
||||
|
|
@ -2234,6 +2358,10 @@ export type ResolversTypes = {
|
|||
DeleteReminderErrorCode: DeleteReminderErrorCode;
|
||||
DeleteReminderResult: ResolversTypes['DeleteReminderError'] | ResolversTypes['DeleteReminderSuccess'];
|
||||
DeleteReminderSuccess: ResolverTypeWrapper<DeleteReminderSuccess>;
|
||||
DeleteWebhookError: ResolverTypeWrapper<DeleteWebhookError>;
|
||||
DeleteWebhookErrorCode: DeleteWebhookErrorCode;
|
||||
DeleteWebhookResult: ResolversTypes['DeleteWebhookError'] | ResolversTypes['DeleteWebhookSuccess'];
|
||||
DeleteWebhookSuccess: ResolverTypeWrapper<DeleteWebhookSuccess>;
|
||||
DeviceToken: ResolverTypeWrapper<DeviceToken>;
|
||||
FeedArticle: ResolverTypeWrapper<FeedArticle>;
|
||||
FeedArticleEdge: ResolverTypeWrapper<FeedArticleEdge>;
|
||||
|
|
@ -2367,6 +2495,11 @@ export type ResolversTypes = {
|
|||
SetUserPersonalizationInput: SetUserPersonalizationInput;
|
||||
SetUserPersonalizationResult: ResolversTypes['SetUserPersonalizationError'] | ResolversTypes['SetUserPersonalizationSuccess'];
|
||||
SetUserPersonalizationSuccess: ResolverTypeWrapper<SetUserPersonalizationSuccess>;
|
||||
SetWebhookError: ResolverTypeWrapper<SetWebhookError>;
|
||||
SetWebhookErrorCode: SetWebhookErrorCode;
|
||||
SetWebhookInput: SetWebhookInput;
|
||||
SetWebhookResult: ResolversTypes['SetWebhookError'] | ResolversTypes['SetWebhookSuccess'];
|
||||
SetWebhookSuccess: ResolverTypeWrapper<SetWebhookSuccess>;
|
||||
SharedArticleError: ResolverTypeWrapper<SharedArticleError>;
|
||||
SharedArticleErrorCode: SharedArticleErrorCode;
|
||||
SharedArticleResult: ResolversTypes['SharedArticleError'] | ResolversTypes['SharedArticleSuccess'];
|
||||
|
|
@ -2456,6 +2589,16 @@ export type ResolversTypes = {
|
|||
UsersResult: ResolversTypes['UsersError'] | ResolversTypes['UsersSuccess'];
|
||||
UsersSuccess: ResolverTypeWrapper<UsersSuccess>;
|
||||
UserSuccess: ResolverTypeWrapper<UserSuccess>;
|
||||
Webhook: ResolverTypeWrapper<Webhook>;
|
||||
WebhookError: ResolverTypeWrapper<WebhookError>;
|
||||
WebhookErrorCode: WebhookErrorCode;
|
||||
WebhookEvent: WebhookEvent;
|
||||
WebhookResult: ResolversTypes['WebhookError'] | ResolversTypes['WebhookSuccess'];
|
||||
WebhooksError: ResolverTypeWrapper<WebhooksError>;
|
||||
WebhooksErrorCode: WebhooksErrorCode;
|
||||
WebhooksResult: ResolversTypes['WebhooksError'] | ResolversTypes['WebhooksSuccess'];
|
||||
WebhooksSuccess: ResolverTypeWrapper<WebhooksSuccess>;
|
||||
WebhookSuccess: ResolverTypeWrapper<WebhookSuccess>;
|
||||
};
|
||||
|
||||
/** Mapping between all available schema types and the resolvers parents */
|
||||
|
|
@ -2531,6 +2674,9 @@ export type ResolversParentTypes = {
|
|||
DeleteReminderError: DeleteReminderError;
|
||||
DeleteReminderResult: ResolversParentTypes['DeleteReminderError'] | ResolversParentTypes['DeleteReminderSuccess'];
|
||||
DeleteReminderSuccess: DeleteReminderSuccess;
|
||||
DeleteWebhookError: DeleteWebhookError;
|
||||
DeleteWebhookResult: ResolversParentTypes['DeleteWebhookError'] | ResolversParentTypes['DeleteWebhookSuccess'];
|
||||
DeleteWebhookSuccess: DeleteWebhookSuccess;
|
||||
DeviceToken: DeviceToken;
|
||||
FeedArticle: FeedArticle;
|
||||
FeedArticleEdge: FeedArticleEdge;
|
||||
|
|
@ -2640,6 +2786,10 @@ export type ResolversParentTypes = {
|
|||
SetUserPersonalizationInput: SetUserPersonalizationInput;
|
||||
SetUserPersonalizationResult: ResolversParentTypes['SetUserPersonalizationError'] | ResolversParentTypes['SetUserPersonalizationSuccess'];
|
||||
SetUserPersonalizationSuccess: SetUserPersonalizationSuccess;
|
||||
SetWebhookError: SetWebhookError;
|
||||
SetWebhookInput: SetWebhookInput;
|
||||
SetWebhookResult: ResolversParentTypes['SetWebhookError'] | ResolversParentTypes['SetWebhookSuccess'];
|
||||
SetWebhookSuccess: SetWebhookSuccess;
|
||||
SharedArticleError: SharedArticleError;
|
||||
SharedArticleResult: ResolversParentTypes['SharedArticleError'] | ResolversParentTypes['SharedArticleSuccess'];
|
||||
SharedArticleSuccess: SharedArticleSuccess;
|
||||
|
|
@ -2708,6 +2858,13 @@ export type ResolversParentTypes = {
|
|||
UsersResult: ResolversParentTypes['UsersError'] | ResolversParentTypes['UsersSuccess'];
|
||||
UsersSuccess: UsersSuccess;
|
||||
UserSuccess: UserSuccess;
|
||||
Webhook: Webhook;
|
||||
WebhookError: WebhookError;
|
||||
WebhookResult: ResolversParentTypes['WebhookError'] | ResolversParentTypes['WebhookSuccess'];
|
||||
WebhooksError: WebhooksError;
|
||||
WebhooksResult: ResolversParentTypes['WebhooksError'] | ResolversParentTypes['WebhooksSuccess'];
|
||||
WebhooksSuccess: WebhooksSuccess;
|
||||
WebhookSuccess: WebhookSuccess;
|
||||
};
|
||||
|
||||
export type SanitizeDirectiveArgs = {
|
||||
|
|
@ -3051,6 +3208,20 @@ export type DeleteReminderSuccessResolvers<ContextType = ResolverContext, Parent
|
|||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type DeleteWebhookErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['DeleteWebhookError'] = ResolversParentTypes['DeleteWebhookError']> = {
|
||||
errorCodes?: Resolver<Array<ResolversTypes['DeleteWebhookErrorCode']>, ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type DeleteWebhookResultResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['DeleteWebhookResult'] = ResolversParentTypes['DeleteWebhookResult']> = {
|
||||
__resolveType: TypeResolveFn<'DeleteWebhookError' | 'DeleteWebhookSuccess', ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type DeleteWebhookSuccessResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['DeleteWebhookSuccess'] = ResolversParentTypes['DeleteWebhookSuccess']> = {
|
||||
webhook?: Resolver<ResolversTypes['Webhook'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type DeviceTokenResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['DeviceToken'] = ResolversParentTypes['DeviceToken']> = {
|
||||
createdAt?: Resolver<ResolversTypes['Date'], ParentType, ContextType>;
|
||||
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
|
||||
|
|
@ -3301,6 +3472,7 @@ export type MutationResolvers<ContextType = ResolverContext, ParentType extends
|
|||
deleteNewsletterEmail?: Resolver<ResolversTypes['DeleteNewsletterEmailResult'], ParentType, ContextType, RequireFields<MutationDeleteNewsletterEmailArgs, 'newsletterEmailId'>>;
|
||||
deleteReaction?: Resolver<ResolversTypes['DeleteReactionResult'], ParentType, ContextType, RequireFields<MutationDeleteReactionArgs, 'id'>>;
|
||||
deleteReminder?: Resolver<ResolversTypes['DeleteReminderResult'], ParentType, ContextType, RequireFields<MutationDeleteReminderArgs, 'id'>>;
|
||||
deleteWebhook?: Resolver<ResolversTypes['DeleteWebhookResult'], ParentType, ContextType, RequireFields<MutationDeleteWebhookArgs, 'id'>>;
|
||||
generateApiKey?: Resolver<ResolversTypes['GenerateApiKeyResult'], ParentType, ContextType, RequireFields<MutationGenerateApiKeyArgs, 'input'>>;
|
||||
googleLogin?: Resolver<ResolversTypes['LoginResult'], ParentType, ContextType, RequireFields<MutationGoogleLoginArgs, 'input'>>;
|
||||
googleSignup?: Resolver<ResolversTypes['GoogleSignupResult'], ParentType, ContextType, RequireFields<MutationGoogleSignupArgs, 'input'>>;
|
||||
|
|
@ -3320,6 +3492,7 @@ export type MutationResolvers<ContextType = ResolverContext, ParentType extends
|
|||
setShareArticle?: Resolver<ResolversTypes['SetShareArticleResult'], ParentType, ContextType, RequireFields<MutationSetShareArticleArgs, 'input'>>;
|
||||
setShareHighlight?: Resolver<ResolversTypes['SetShareHighlightResult'], ParentType, ContextType, RequireFields<MutationSetShareHighlightArgs, 'input'>>;
|
||||
setUserPersonalization?: Resolver<ResolversTypes['SetUserPersonalizationResult'], ParentType, ContextType, RequireFields<MutationSetUserPersonalizationArgs, 'input'>>;
|
||||
setWebhook?: Resolver<ResolversTypes['SetWebhookResult'], ParentType, ContextType, RequireFields<MutationSetWebhookArgs, 'input'>>;
|
||||
signup?: Resolver<ResolversTypes['SignupResult'], ParentType, ContextType, RequireFields<MutationSignupArgs, 'input'>>;
|
||||
subscribe?: Resolver<ResolversTypes['SubscribeResult'], ParentType, ContextType, RequireFields<MutationSubscribeArgs, 'name'>>;
|
||||
unsubscribe?: Resolver<ResolversTypes['UnsubscribeResult'], ParentType, ContextType, RequireFields<MutationUnsubscribeArgs, 'name'>>;
|
||||
|
|
@ -3410,6 +3583,8 @@ export type QueryResolvers<ContextType = ResolverContext, ParentType extends Res
|
|||
user?: Resolver<ResolversTypes['UserResult'], ParentType, ContextType, Partial<QueryUserArgs>>;
|
||||
users?: Resolver<ResolversTypes['UsersResult'], ParentType, ContextType>;
|
||||
validateUsername?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<QueryValidateUsernameArgs, 'username'>>;
|
||||
webhook?: Resolver<ResolversTypes['WebhookResult'], ParentType, ContextType, RequireFields<QueryWebhookArgs, 'id'>>;
|
||||
webhooks?: Resolver<ResolversTypes['WebhooksResult'], ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type ReactionResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['Reaction'] = ResolversParentTypes['Reaction']> = {
|
||||
|
|
@ -3639,6 +3814,20 @@ export type SetUserPersonalizationSuccessResolvers<ContextType = ResolverContext
|
|||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type SetWebhookErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['SetWebhookError'] = ResolversParentTypes['SetWebhookError']> = {
|
||||
errorCodes?: Resolver<Array<ResolversTypes['SetWebhookErrorCode']>, ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type SetWebhookResultResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['SetWebhookResult'] = ResolversParentTypes['SetWebhookResult']> = {
|
||||
__resolveType: TypeResolveFn<'SetWebhookError' | 'SetWebhookSuccess', ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type SetWebhookSuccessResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['SetWebhookSuccess'] = ResolversParentTypes['SetWebhookSuccess']> = {
|
||||
webhook?: Resolver<ResolversTypes['Webhook'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type SharedArticleErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['SharedArticleError'] = ResolversParentTypes['SharedArticleError']> = {
|
||||
errorCodes?: Resolver<Array<ResolversTypes['SharedArticleErrorCode']>, ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
|
|
@ -3928,6 +4117,46 @@ export type UserSuccessResolvers<ContextType = ResolverContext, ParentType exten
|
|||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type WebhookResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['Webhook'] = ResolversParentTypes['Webhook']> = {
|
||||
contentType?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
createdAt?: Resolver<ResolversTypes['Date'], ParentType, ContextType>;
|
||||
enabled?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
|
||||
eventTypes?: Resolver<Array<ResolversTypes['WebhookEvent']>, ParentType, ContextType>;
|
||||
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
|
||||
method?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
updatedAt?: Resolver<ResolversTypes['Date'], ParentType, ContextType>;
|
||||
url?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type WebhookErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['WebhookError'] = ResolversParentTypes['WebhookError']> = {
|
||||
errorCodes?: Resolver<Array<ResolversTypes['WebhookErrorCode']>, ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type WebhookResultResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['WebhookResult'] = ResolversParentTypes['WebhookResult']> = {
|
||||
__resolveType: TypeResolveFn<'WebhookError' | 'WebhookSuccess', ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type WebhooksErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['WebhooksError'] = ResolversParentTypes['WebhooksError']> = {
|
||||
errorCodes?: Resolver<Array<ResolversTypes['WebhooksErrorCode']>, ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type WebhooksResultResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['WebhooksResult'] = ResolversParentTypes['WebhooksResult']> = {
|
||||
__resolveType: TypeResolveFn<'WebhooksError' | 'WebhooksSuccess', ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type WebhooksSuccessResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['WebhooksSuccess'] = ResolversParentTypes['WebhooksSuccess']> = {
|
||||
webhooks?: Resolver<Array<ResolversTypes['Webhook']>, ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type WebhookSuccessResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['WebhookSuccess'] = ResolversParentTypes['WebhookSuccess']> = {
|
||||
webhook?: Resolver<ResolversTypes['Webhook'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type Resolvers<ContextType = ResolverContext> = {
|
||||
AddPopularReadError?: AddPopularReadErrorResolvers<ContextType>;
|
||||
AddPopularReadResult?: AddPopularReadResultResolvers<ContextType>;
|
||||
|
|
@ -3990,6 +4219,9 @@ export type Resolvers<ContextType = ResolverContext> = {
|
|||
DeleteReminderError?: DeleteReminderErrorResolvers<ContextType>;
|
||||
DeleteReminderResult?: DeleteReminderResultResolvers<ContextType>;
|
||||
DeleteReminderSuccess?: DeleteReminderSuccessResolvers<ContextType>;
|
||||
DeleteWebhookError?: DeleteWebhookErrorResolvers<ContextType>;
|
||||
DeleteWebhookResult?: DeleteWebhookResultResolvers<ContextType>;
|
||||
DeleteWebhookSuccess?: DeleteWebhookSuccessResolvers<ContextType>;
|
||||
DeviceToken?: DeviceTokenResolvers<ContextType>;
|
||||
FeedArticle?: FeedArticleResolvers<ContextType>;
|
||||
FeedArticleEdge?: FeedArticleEdgeResolvers<ContextType>;
|
||||
|
|
@ -4077,6 +4309,9 @@ export type Resolvers<ContextType = ResolverContext> = {
|
|||
SetUserPersonalizationError?: SetUserPersonalizationErrorResolvers<ContextType>;
|
||||
SetUserPersonalizationResult?: SetUserPersonalizationResultResolvers<ContextType>;
|
||||
SetUserPersonalizationSuccess?: SetUserPersonalizationSuccessResolvers<ContextType>;
|
||||
SetWebhookError?: SetWebhookErrorResolvers<ContextType>;
|
||||
SetWebhookResult?: SetWebhookResultResolvers<ContextType>;
|
||||
SetWebhookSuccess?: SetWebhookSuccessResolvers<ContextType>;
|
||||
SharedArticleError?: SharedArticleErrorResolvers<ContextType>;
|
||||
SharedArticleResult?: SharedArticleResultResolvers<ContextType>;
|
||||
SharedArticleSuccess?: SharedArticleSuccessResolvers<ContextType>;
|
||||
|
|
@ -4132,6 +4367,13 @@ export type Resolvers<ContextType = ResolverContext> = {
|
|||
UsersResult?: UsersResultResolvers<ContextType>;
|
||||
UsersSuccess?: UsersSuccessResolvers<ContextType>;
|
||||
UserSuccess?: UserSuccessResolvers<ContextType>;
|
||||
Webhook?: WebhookResolvers<ContextType>;
|
||||
WebhookError?: WebhookErrorResolvers<ContextType>;
|
||||
WebhookResult?: WebhookResultResolvers<ContextType>;
|
||||
WebhooksError?: WebhooksErrorResolvers<ContextType>;
|
||||
WebhooksResult?: WebhooksResultResolvers<ContextType>;
|
||||
WebhooksSuccess?: WebhooksSuccessResolvers<ContextType>;
|
||||
WebhookSuccess?: WebhookSuccessResolvers<ContextType>;
|
||||
};
|
||||
|
||||
export type DirectiveResolvers<ContextType = ResolverContext> = {
|
||||
|
|
|
|||
|
|
@ -440,6 +440,22 @@ type DeleteReminderSuccess {
|
|||
reminder: Reminder!
|
||||
}
|
||||
|
||||
type DeleteWebhookError {
|
||||
errorCodes: [DeleteWebhookErrorCode!]!
|
||||
}
|
||||
|
||||
enum DeleteWebhookErrorCode {
|
||||
BAD_REQUEST
|
||||
NOT_FOUND
|
||||
UNAUTHORIZED
|
||||
}
|
||||
|
||||
union DeleteWebhookResult = DeleteWebhookError | DeleteWebhookSuccess
|
||||
|
||||
type DeleteWebhookSuccess {
|
||||
webhook: Webhook!
|
||||
}
|
||||
|
||||
type DeviceToken {
|
||||
createdAt: Date!
|
||||
id: ID!
|
||||
|
|
@ -725,6 +741,7 @@ type Mutation {
|
|||
deleteNewsletterEmail(newsletterEmailId: ID!): DeleteNewsletterEmailResult!
|
||||
deleteReaction(id: ID!): DeleteReactionResult!
|
||||
deleteReminder(id: ID!): DeleteReminderResult!
|
||||
deleteWebhook(id: ID!): DeleteWebhookResult!
|
||||
generateApiKey(input: GenerateApiKeyInput!): GenerateApiKeyResult!
|
||||
googleLogin(input: GoogleLoginInput!): LoginResult!
|
||||
googleSignup(input: GoogleSignupInput!): GoogleSignupResult!
|
||||
|
|
@ -744,6 +761,7 @@ type Mutation {
|
|||
setShareArticle(input: SetShareArticleInput!): SetShareArticleResult!
|
||||
setShareHighlight(input: SetShareHighlightInput!): SetShareHighlightResult!
|
||||
setUserPersonalization(input: SetUserPersonalizationInput!): SetUserPersonalizationResult!
|
||||
setWebhook(input: SetWebhookInput!): SetWebhookResult!
|
||||
signup(input: SignupInput!): SignupResult!
|
||||
subscribe(name: String!): SubscribeResult!
|
||||
unsubscribe(name: String!): UnsubscribeResult!
|
||||
|
|
@ -856,6 +874,8 @@ type Query {
|
|||
user(userId: ID, username: String): UserResult!
|
||||
users: UsersResult!
|
||||
validateUsername(username: String!): Boolean!
|
||||
webhook(id: ID!): WebhookResult!
|
||||
webhooks: WebhooksResult!
|
||||
}
|
||||
|
||||
type Reaction {
|
||||
|
|
@ -1185,6 +1205,32 @@ type SetUserPersonalizationSuccess {
|
|||
updatedUserPersonalization: UserPersonalization!
|
||||
}
|
||||
|
||||
type SetWebhookError {
|
||||
errorCodes: [SetWebhookErrorCode!]!
|
||||
}
|
||||
|
||||
enum SetWebhookErrorCode {
|
||||
ALREADY_EXISTS
|
||||
BAD_REQUEST
|
||||
NOT_FOUND
|
||||
UNAUTHORIZED
|
||||
}
|
||||
|
||||
input SetWebhookInput {
|
||||
contentType: String
|
||||
enabled: Boolean
|
||||
eventTypes: [WebhookEvent!]!
|
||||
id: ID
|
||||
method: String
|
||||
url: String!
|
||||
}
|
||||
|
||||
union SetWebhookResult = SetWebhookError | SetWebhookSuccess
|
||||
|
||||
type SetWebhookSuccess {
|
||||
webhook: Webhook!
|
||||
}
|
||||
|
||||
type SharedArticleError {
|
||||
errorCodes: [SharedArticleErrorCode!]!
|
||||
}
|
||||
|
|
@ -1607,3 +1653,57 @@ type UsersSuccess {
|
|||
type UserSuccess {
|
||||
user: User!
|
||||
}
|
||||
|
||||
type Webhook {
|
||||
contentType: String!
|
||||
createdAt: Date!
|
||||
enabled: Boolean!
|
||||
eventTypes: [WebhookEvent!]!
|
||||
id: ID!
|
||||
method: String!
|
||||
updatedAt: Date!
|
||||
url: String!
|
||||
}
|
||||
|
||||
type WebhookError {
|
||||
errorCodes: [WebhookErrorCode!]!
|
||||
}
|
||||
|
||||
enum WebhookErrorCode {
|
||||
BAD_REQUEST
|
||||
NOT_FOUND
|
||||
UNAUTHORIZED
|
||||
}
|
||||
|
||||
enum WebhookEvent {
|
||||
HIGHLIGHT_CREATED
|
||||
HIGHLIGHT_DELETED
|
||||
HIGHLIGHT_UPDATED
|
||||
LABEL_CREATED
|
||||
LABEL_DELETED
|
||||
LABEL_UPDATED
|
||||
PAGE_CREATED
|
||||
PAGE_DELETED
|
||||
PAGE_UPDATED
|
||||
}
|
||||
|
||||
union WebhookResult = WebhookError | WebhookSuccess
|
||||
|
||||
type WebhooksError {
|
||||
errorCodes: [WebhooksErrorCode!]!
|
||||
}
|
||||
|
||||
enum WebhooksErrorCode {
|
||||
BAD_REQUEST
|
||||
UNAUTHORIZED
|
||||
}
|
||||
|
||||
union WebhooksResult = WebhooksError | WebhooksSuccess
|
||||
|
||||
type WebhooksSuccess {
|
||||
webhooks: [Webhook!]!
|
||||
}
|
||||
|
||||
type WebhookSuccess {
|
||||
webhook: Webhook!
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import {
|
|||
deleteLabelResolver,
|
||||
deleteNewsletterEmailResolver,
|
||||
deleteReminderResolver,
|
||||
deleteWebhookResolver,
|
||||
getAllUsersResolver,
|
||||
getArticleResolver,
|
||||
getArticlesResolver,
|
||||
|
|
@ -64,6 +65,7 @@ import {
|
|||
setShareArticleResolver,
|
||||
setShareHighlightResolver,
|
||||
setUserPersonalizationResolver,
|
||||
setWebhookResolver,
|
||||
signupResolver,
|
||||
subscribeResolver,
|
||||
subscriptionsResolver,
|
||||
|
|
@ -71,14 +73,16 @@ import {
|
|||
updateHighlightResolver,
|
||||
updateLabelResolver,
|
||||
updateLinkShareInfoResolver,
|
||||
updatePageResolver,
|
||||
updateReminderResolver,
|
||||
updateSharedCommentResolver,
|
||||
updateUserProfileResolver,
|
||||
updateUserResolver,
|
||||
uploadFileRequestResolver,
|
||||
validateUsernameResolver,
|
||||
updatePageResolver,
|
||||
addPopularReadResolver,
|
||||
webhookResolver,
|
||||
webhooksResolver,
|
||||
} from './index'
|
||||
import { getShareInfoForArticle } from '../datalayer/links/share_info'
|
||||
import {
|
||||
|
|
@ -151,6 +155,8 @@ export const functionResolvers = {
|
|||
updatePage: updatePageResolver,
|
||||
subscribe: subscribeResolver,
|
||||
addPopularRead: addPopularReadResolver,
|
||||
setWebhook: setWebhookResolver,
|
||||
deleteWebhook: deleteWebhookResolver,
|
||||
},
|
||||
Query: {
|
||||
me: getMeUserResolver,
|
||||
|
|
@ -170,6 +176,8 @@ export const functionResolvers = {
|
|||
labels: labelsResolver,
|
||||
search: searchResolver,
|
||||
subscriptions: subscriptionsResolver,
|
||||
webhooks: webhooksResolver,
|
||||
webhook: webhookResolver,
|
||||
},
|
||||
User: {
|
||||
async sharedArticles(
|
||||
|
|
@ -563,4 +571,8 @@ export const functionResolvers = {
|
|||
...resultResolveTypeResolver('UpdatePage'),
|
||||
...resultResolveTypeResolver('Subscribe'),
|
||||
...resultResolveTypeResolver('AddPopularRead'),
|
||||
...resultResolveTypeResolver('SetWebhook'),
|
||||
...resultResolveTypeResolver('Webhooks'),
|
||||
...resultResolveTypeResolver('DeleteWebhook'),
|
||||
...resultResolveTypeResolver('Webhook'),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,3 +17,4 @@ export * from './labels'
|
|||
export * from './subscriptions'
|
||||
export * from './update'
|
||||
export * from './popular_reads'
|
||||
export * from './webhooks'
|
||||
|
|
|
|||
240
packages/api/src/resolvers/webhooks/index.ts
Normal file
240
packages/api/src/resolvers/webhooks/index.ts
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
import { authorized } from '../../utils/helpers'
|
||||
import {
|
||||
DeleteWebhookError,
|
||||
DeleteWebhookErrorCode,
|
||||
DeleteWebhookSuccess,
|
||||
MutationDeleteWebhookArgs,
|
||||
MutationSetWebhookArgs,
|
||||
QueryWebhookArgs,
|
||||
SetWebhookError,
|
||||
SetWebhookErrorCode,
|
||||
SetWebhookSuccess,
|
||||
Webhook as WebhookResponse,
|
||||
WebhookError,
|
||||
WebhookErrorCode,
|
||||
WebhookEvent,
|
||||
WebhooksError,
|
||||
WebhooksErrorCode,
|
||||
WebhooksSuccess,
|
||||
WebhookSuccess,
|
||||
} from '../../generated/graphql'
|
||||
import { getRepository } from '../../entity/utils'
|
||||
import { User } from '../../entity/user'
|
||||
import { Webhook } from '../../entity/webhook'
|
||||
import { analytics } from '../../utils/analytics'
|
||||
import { env } from '../../env'
|
||||
|
||||
export const webhooksResolver = authorized<WebhooksSuccess, WebhooksError>(
|
||||
async (_obj, _params, { claims: { uid }, log }) => {
|
||||
log.info('webhooksResolver')
|
||||
|
||||
try {
|
||||
const user = await getRepository(User).findOneBy({ id: uid })
|
||||
if (!user) {
|
||||
return {
|
||||
errorCodes: [WebhooksErrorCode.Unauthorized],
|
||||
}
|
||||
}
|
||||
|
||||
const webhooks = await getRepository(Webhook).findBy({
|
||||
user: { id: uid },
|
||||
})
|
||||
|
||||
return {
|
||||
webhooks: webhooks.map((webhook) => webhookDataToResponse(webhook)),
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(error)
|
||||
|
||||
return {
|
||||
errorCodes: [WebhooksErrorCode.BadRequest],
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export const webhookResolver = authorized<
|
||||
WebhookSuccess,
|
||||
WebhookError,
|
||||
QueryWebhookArgs
|
||||
>(async (_, { id }, { claims: { uid }, log }) => {
|
||||
log.info('webhookResolver')
|
||||
|
||||
try {
|
||||
const user = await getRepository(User).findOneBy({ id: uid })
|
||||
if (!user) {
|
||||
return {
|
||||
errorCodes: [WebhookErrorCode.Unauthorized],
|
||||
}
|
||||
}
|
||||
|
||||
const webhook = await getRepository(Webhook).findOne({
|
||||
where: { id },
|
||||
relations: ['user'],
|
||||
})
|
||||
|
||||
if (!webhook) {
|
||||
return {
|
||||
errorCodes: [WebhookErrorCode.NotFound],
|
||||
}
|
||||
}
|
||||
|
||||
if (webhook.user.id !== uid) {
|
||||
return {
|
||||
errorCodes: [WebhookErrorCode.Unauthorized],
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
webhook: webhookDataToResponse(webhook),
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(error)
|
||||
|
||||
return {
|
||||
errorCodes: [WebhookErrorCode.BadRequest],
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const deleteWebhookResolver = authorized<
|
||||
DeleteWebhookSuccess,
|
||||
DeleteWebhookError,
|
||||
MutationDeleteWebhookArgs
|
||||
>(async (_, { id }, { claims: { uid }, log }) => {
|
||||
log.info('deleteWebhookResolver')
|
||||
|
||||
try {
|
||||
const user = await getRepository(User).findOneBy({ id: uid })
|
||||
if (!user) {
|
||||
return {
|
||||
errorCodes: [DeleteWebhookErrorCode.Unauthorized],
|
||||
}
|
||||
}
|
||||
|
||||
const webhook = await getRepository(Webhook).findOne({
|
||||
where: { id },
|
||||
relations: ['user'],
|
||||
})
|
||||
|
||||
if (!webhook) {
|
||||
return {
|
||||
errorCodes: [DeleteWebhookErrorCode.NotFound],
|
||||
}
|
||||
}
|
||||
|
||||
if (webhook.user.id !== uid) {
|
||||
return {
|
||||
errorCodes: [DeleteWebhookErrorCode.Unauthorized],
|
||||
}
|
||||
}
|
||||
|
||||
const deletedWebhook = await getRepository(Webhook).remove(webhook)
|
||||
deletedWebhook.id = id
|
||||
|
||||
analytics.track({
|
||||
userId: uid,
|
||||
event: 'webhook_delete',
|
||||
properties: {
|
||||
webhookId: webhook.id,
|
||||
env: env.server.apiEnv,
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
webhook: webhookDataToResponse(deletedWebhook),
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(error)
|
||||
|
||||
return {
|
||||
errorCodes: [DeleteWebhookErrorCode.BadRequest],
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const setWebhookResolver = authorized<
|
||||
SetWebhookSuccess,
|
||||
SetWebhookError,
|
||||
MutationSetWebhookArgs
|
||||
>(async (_, { input }, { claims: { uid }, log }) => {
|
||||
log.info('setWebhookResolver')
|
||||
|
||||
try {
|
||||
const user = await getRepository(User).findOneBy({ id: uid })
|
||||
if (!user) {
|
||||
return {
|
||||
errorCodes: [SetWebhookErrorCode.Unauthorized],
|
||||
}
|
||||
}
|
||||
|
||||
const webhookToSave: Partial<Webhook> = {
|
||||
url: input.url,
|
||||
eventTypes: input.eventTypes as string[],
|
||||
method: input.method || 'POST',
|
||||
contentType: input.contentType || 'application/json',
|
||||
enabled: input.enabled === null ? true : input.enabled,
|
||||
}
|
||||
|
||||
if (input.id) {
|
||||
// Update
|
||||
const existingWebhook = await getRepository(Webhook).findOne({
|
||||
where: { id: input.id },
|
||||
relations: ['user'],
|
||||
})
|
||||
if (!existingWebhook) {
|
||||
return {
|
||||
errorCodes: [SetWebhookErrorCode.NotFound],
|
||||
}
|
||||
}
|
||||
if (existingWebhook.user.id !== uid) {
|
||||
return {
|
||||
errorCodes: [SetWebhookErrorCode.Unauthorized],
|
||||
}
|
||||
}
|
||||
|
||||
webhookToSave.id = input.id
|
||||
} else {
|
||||
// Create
|
||||
const existingWebhook = await getRepository(Webhook).findOneBy({
|
||||
user: { id: uid },
|
||||
eventTypes: `{${input.eventTypes.join(',')}}`,
|
||||
})
|
||||
|
||||
if (existingWebhook) {
|
||||
return {
|
||||
errorCodes: [SetWebhookErrorCode.AlreadyExists],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const webhook = await getRepository(Webhook).save({
|
||||
user,
|
||||
...webhookToSave,
|
||||
})
|
||||
|
||||
analytics.track({
|
||||
userId: uid,
|
||||
event: 'webhook_set',
|
||||
properties: {
|
||||
webhookId: webhook.id,
|
||||
env: env.server.apiEnv,
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
webhook: webhookDataToResponse(webhook),
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(error)
|
||||
|
||||
return {
|
||||
errorCodes: [SetWebhookErrorCode.BadRequest],
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const webhookDataToResponse = (webhook: Webhook): WebhookResponse => ({
|
||||
...webhook,
|
||||
eventTypes: webhook.eventTypes as WebhookEvent[],
|
||||
})
|
||||
86
packages/api/src/routers/svc/webhooks.ts
Normal file
86
packages/api/src/routers/svc/webhooks.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
/* eslint-disable @typescript-eslint/no-misused-promises */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import express from 'express'
|
||||
import { readPushSubscription } from '../../datalayer/pubsub'
|
||||
import { getRepository } from '../../entity/utils'
|
||||
import { Webhook } from '../../entity/webhook'
|
||||
import axios, { Method } from 'axios'
|
||||
|
||||
export function webhooksServiceRouter() {
|
||||
const router = express.Router()
|
||||
|
||||
router.post('/trigger/:action', async (req, res) => {
|
||||
console.log('trigger webhook of action', req.params.action)
|
||||
const { message: msgStr, expired } = readPushSubscription(req)
|
||||
|
||||
if (!msgStr) {
|
||||
res.status(400).send('Bad Request')
|
||||
return
|
||||
}
|
||||
|
||||
if (expired) {
|
||||
console.log('discarding expired message')
|
||||
res.status(200).send('Expired')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(msgStr)
|
||||
const { userId, type } = data
|
||||
if (!userId || !type) {
|
||||
console.log('No userId or type found in message')
|
||||
res.status(400).send('Bad Request')
|
||||
return
|
||||
}
|
||||
|
||||
// example: PAGE_CREATED
|
||||
const eventType = `${type as string}_${req.params.action}`.toUpperCase()
|
||||
const webhooks = await getRepository(Webhook)
|
||||
.createQueryBuilder()
|
||||
.where('user_id = :userId', { userId })
|
||||
.andWhere(':eventType = ANY(event_types)', { eventType })
|
||||
.andWhere('enabled = true')
|
||||
.getMany()
|
||||
|
||||
if (webhooks.length <= 0) {
|
||||
console.log(
|
||||
'No active webhook found for user',
|
||||
userId,
|
||||
'and eventType',
|
||||
eventType
|
||||
)
|
||||
res.status(200).send('No webhook found')
|
||||
return
|
||||
}
|
||||
|
||||
// trigger webhooks
|
||||
for (const webhook of webhooks) {
|
||||
const url = webhook.url
|
||||
const method = webhook.method as Method
|
||||
const body = JSON.stringify({
|
||||
action: req.params.action,
|
||||
userId,
|
||||
[type]: data,
|
||||
})
|
||||
|
||||
console.log('triggering webhook', url, method, body)
|
||||
await axios.request({
|
||||
url,
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': webhook.contentType,
|
||||
},
|
||||
data: body,
|
||||
})
|
||||
}
|
||||
|
||||
res.status(200).send('OK')
|
||||
} catch (err) {
|
||||
console.log('trigger webhook failed', err)
|
||||
res.status(500).send(err)
|
||||
}
|
||||
})
|
||||
|
||||
return router
|
||||
}
|
||||
|
|
@ -1571,6 +1571,102 @@ const schema = gql`
|
|||
NOT_FOUND
|
||||
}
|
||||
|
||||
input SetWebhookInput {
|
||||
id: ID
|
||||
url: String!
|
||||
eventTypes: [WebhookEvent!]!
|
||||
contentType: String
|
||||
method: String
|
||||
enabled: Boolean
|
||||
}
|
||||
|
||||
enum WebhookEvent {
|
||||
PAGE_CREATED
|
||||
PAGE_UPDATED
|
||||
PAGE_DELETED
|
||||
HIGHLIGHT_CREATED
|
||||
HIGHLIGHT_UPDATED
|
||||
HIGHLIGHT_DELETED
|
||||
LABEL_CREATED
|
||||
LABEL_UPDATED
|
||||
LABEL_DELETED
|
||||
}
|
||||
|
||||
union SetWebhookResult = SetWebhookSuccess | SetWebhookError
|
||||
|
||||
type SetWebhookSuccess {
|
||||
webhook: Webhook!
|
||||
}
|
||||
|
||||
type Webhook {
|
||||
id: ID!
|
||||
url: String!
|
||||
eventTypes: [WebhookEvent!]!
|
||||
contentType: String!
|
||||
method: String!
|
||||
enabled: Boolean!
|
||||
createdAt: Date!
|
||||
updatedAt: Date!
|
||||
}
|
||||
|
||||
type SetWebhookError {
|
||||
errorCodes: [SetWebhookErrorCode!]!
|
||||
}
|
||||
|
||||
enum SetWebhookErrorCode {
|
||||
UNAUTHORIZED
|
||||
BAD_REQUEST
|
||||
ALREADY_EXISTS
|
||||
NOT_FOUND
|
||||
}
|
||||
|
||||
union DeleteWebhookResult = DeleteWebhookSuccess | DeleteWebhookError
|
||||
|
||||
type DeleteWebhookSuccess {
|
||||
webhook: Webhook!
|
||||
}
|
||||
|
||||
type DeleteWebhookError {
|
||||
errorCodes: [DeleteWebhookErrorCode!]!
|
||||
}
|
||||
|
||||
enum DeleteWebhookErrorCode {
|
||||
UNAUTHORIZED
|
||||
BAD_REQUEST
|
||||
NOT_FOUND
|
||||
}
|
||||
|
||||
union WebhookResult = WebhookSuccess | WebhookError
|
||||
|
||||
type WebhookSuccess {
|
||||
webhook: Webhook!
|
||||
}
|
||||
|
||||
type WebhookError {
|
||||
errorCodes: [WebhookErrorCode!]!
|
||||
}
|
||||
|
||||
enum WebhookErrorCode {
|
||||
UNAUTHORIZED
|
||||
BAD_REQUEST
|
||||
NOT_FOUND
|
||||
}
|
||||
|
||||
union WebhooksResult = WebhooksSuccess | WebhooksError
|
||||
|
||||
type WebhooksSuccess {
|
||||
webhooks: [Webhook!]!
|
||||
}
|
||||
|
||||
type WebhooksError {
|
||||
errorCodes: [WebhooksErrorCode!]!
|
||||
}
|
||||
|
||||
enum WebhooksErrorCode {
|
||||
UNAUTHORIZED
|
||||
BAD_REQUEST
|
||||
}
|
||||
|
||||
# Mutations
|
||||
type Mutation {
|
||||
googleLogin(input: GoogleLoginInput!): LoginResult!
|
||||
|
|
@ -1636,6 +1732,8 @@ const schema = gql`
|
|||
unsubscribe(name: String!): UnsubscribeResult!
|
||||
subscribe(name: String!): SubscribeResult!
|
||||
addPopularRead(name: String!): AddPopularReadResult!
|
||||
setWebhook(input: SetWebhookInput!): SetWebhookResult!
|
||||
deleteWebhook(id: ID!): DeleteWebhookResult!
|
||||
}
|
||||
|
||||
# FIXME: remove sort from feedArticles after all cached tabs are closed
|
||||
|
|
@ -1675,6 +1773,8 @@ const schema = gql`
|
|||
labels: LabelsResult!
|
||||
search(after: String, first: Int, query: String): SearchResult!
|
||||
subscriptions(sort: SortParams): SubscriptionsResult!
|
||||
webhooks: WebhooksResult!
|
||||
webhook(id: ID!): WebhookResult!
|
||||
}
|
||||
`
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ import { corsConfig } from './utils/corsConfig'
|
|||
import { initElasticsearch } from './elastic'
|
||||
import { uploadServiceRouter } from './routers/svc/upload'
|
||||
import rateLimit from 'express-rate-limit'
|
||||
import { webhooksServiceRouter } from './routers/svc/webhooks'
|
||||
|
||||
const PORT = process.env.PORT || 4000
|
||||
|
||||
|
|
@ -111,6 +112,7 @@ export const createApp = (): {
|
|||
app.use('/svc/pubsub/newsletters', newsletterServiceRouter())
|
||||
app.use('/svc/pubsub/emails', emailsServiceRouter())
|
||||
app.use('/svc/pubsub/upload', uploadServiceRouter())
|
||||
app.use('/svc/pubsub/webhooks', webhooksServiceRouter())
|
||||
app.use('/svc/reminders', remindersServiceRouter())
|
||||
app.use('/svc/pdf-attachments', pdfAttachmentsRouter())
|
||||
|
||||
|
|
|
|||
239
packages/api/test/resolvers/webhooks.test.ts
Normal file
239
packages/api/test/resolvers/webhooks.test.ts
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
import { createTestUser, deleteTestUser } from '../db'
|
||||
import { graphqlRequest, request } from '../util'
|
||||
import { expect } from 'chai'
|
||||
import 'mocha'
|
||||
import { User } from '../../src/entity/user'
|
||||
import { WebhookEvent } from '../../src/generated/graphql'
|
||||
import { Webhook } from '../../src/entity/webhook'
|
||||
import { getRepository } from '../../src/entity/utils'
|
||||
|
||||
describe('Webhooks API', () => {
|
||||
const username = 'fakeUser'
|
||||
|
||||
let user: User
|
||||
let authToken: string
|
||||
|
||||
before(async () => {
|
||||
// create test user and login
|
||||
user = await createTestUser(username)
|
||||
const res = await request
|
||||
.post('/local/debug/fake-user-login')
|
||||
.send({ fakeEmail: user.email })
|
||||
|
||||
authToken = res.body.authToken
|
||||
|
||||
// create test webhooks
|
||||
await getRepository(Webhook).save([
|
||||
{
|
||||
url: 'http://localhost:3000/webhooks/test',
|
||||
user: { id: user.id },
|
||||
eventTypes: [WebhookEvent.PageCreated],
|
||||
},
|
||||
{
|
||||
url: 'http://localhost:3000/webhooks/test',
|
||||
user: { id: user.id },
|
||||
eventTypes: [WebhookEvent.PageUpdated],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
// clean up
|
||||
await deleteTestUser(username)
|
||||
})
|
||||
|
||||
describe('Get webhook', () => {
|
||||
let webhook: Webhook
|
||||
|
||||
before(async () => {
|
||||
// create test webhooks
|
||||
webhook = await getRepository(Webhook).save({
|
||||
url: 'http://localhost:3000/webhooks/test',
|
||||
user: { id: user.id },
|
||||
eventTypes: [WebhookEvent.PageDeleted],
|
||||
})
|
||||
})
|
||||
|
||||
it('should return a webhook', async () => {
|
||||
const query = `
|
||||
query {
|
||||
webhook(id: "${webhook.id}") {
|
||||
... on WebhookSuccess {
|
||||
webhook {
|
||||
id
|
||||
url
|
||||
eventTypes
|
||||
enabled
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const res = await graphqlRequest(query, authToken)
|
||||
|
||||
expect(res.body.data.webhook.webhook.id).to.eql(webhook.id)
|
||||
expect(res.body.data.webhook.webhook.url).to.eql(webhook.url)
|
||||
expect(res.body.data.webhook.webhook.eventTypes).to.eql(
|
||||
webhook.eventTypes
|
||||
)
|
||||
expect(res.body.data.webhook.webhook.enabled).to.eql(webhook.enabled)
|
||||
})
|
||||
})
|
||||
|
||||
describe('List webhooks', () => {
|
||||
it('should return a list of webhooks', async () => {
|
||||
const query = `
|
||||
query {
|
||||
webhooks {
|
||||
... on WebhooksSuccess {
|
||||
webhooks {
|
||||
id
|
||||
url
|
||||
eventTypes
|
||||
enabled
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const res = await graphqlRequest(query, authToken)
|
||||
const webhooks = await getRepository(Webhook).findBy({
|
||||
user: { id: user.id },
|
||||
})
|
||||
|
||||
expect(res.body.data.webhooks.webhooks).to.eql(
|
||||
webhooks.map((w) => ({
|
||||
id: w.id,
|
||||
url: w.url,
|
||||
eventTypes: w.eventTypes,
|
||||
enabled: w.enabled,
|
||||
}))
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Set webhook', () => {
|
||||
let eventTypes: WebhookEvent[]
|
||||
let query: string
|
||||
let webhookUrl: string
|
||||
let webhookId: string
|
||||
let enabled: boolean
|
||||
|
||||
beforeEach(async () => {
|
||||
query = `
|
||||
mutation {
|
||||
setWebhook(
|
||||
input: {
|
||||
id: "${webhookId}",
|
||||
url: "${webhookUrl}",
|
||||
eventTypes: [${eventTypes}],
|
||||
enabled: ${enabled}
|
||||
}
|
||||
) {
|
||||
... on SetWebhookSuccess {
|
||||
webhook {
|
||||
id
|
||||
url
|
||||
eventTypes
|
||||
enabled
|
||||
}
|
||||
}
|
||||
... on SetWebhookError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
})
|
||||
|
||||
context('when id is not set', () => {
|
||||
before(() => {
|
||||
webhookId = ''
|
||||
webhookUrl = 'http://localhost:3000/webhooks/test'
|
||||
eventTypes = [WebhookEvent.HighlightCreated]
|
||||
enabled = true
|
||||
})
|
||||
|
||||
it('should create a webhook', async () => {
|
||||
const res = await graphqlRequest(query, authToken)
|
||||
|
||||
expect(res.body.data.setWebhook.webhook).to.be.an('object')
|
||||
expect(res.body.data.setWebhook.webhook.url).to.eql(webhookUrl)
|
||||
expect(res.body.data.setWebhook.webhook.eventTypes).to.eql(eventTypes)
|
||||
expect(res.body.data.setWebhook.webhook.enabled).to.be.true
|
||||
})
|
||||
})
|
||||
|
||||
context('when id is there', () => {
|
||||
before(async () => {
|
||||
const webhook = await getRepository(Webhook).save({
|
||||
url: 'http://localhost:3000/webhooks/test',
|
||||
user: { id: user.id },
|
||||
eventTypes: [WebhookEvent.HighlightUpdated],
|
||||
})
|
||||
webhookId = webhook.id
|
||||
webhookUrl = 'http://localhost:3000/webhooks/test_2'
|
||||
eventTypes = [
|
||||
WebhookEvent.HighlightUpdated,
|
||||
WebhookEvent.HighlightCreated,
|
||||
]
|
||||
enabled = false
|
||||
})
|
||||
|
||||
it('should update a webhook', async () => {
|
||||
const res = await graphqlRequest(query, authToken)
|
||||
|
||||
expect(res.body.data.setWebhook.webhook).to.be.an('object')
|
||||
expect(res.body.data.setWebhook.webhook.url).to.eql(webhookUrl)
|
||||
expect(res.body.data.setWebhook.webhook.eventTypes).to.eql(eventTypes)
|
||||
expect(res.body.data.setWebhook.webhook.enabled).to.be.false
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Delete webhook', () => {
|
||||
let query: string
|
||||
let webhookId: string
|
||||
|
||||
beforeEach(async () => {
|
||||
query = `
|
||||
mutation {
|
||||
deleteWebhook(id: "${webhookId}") {
|
||||
... on DeleteWebhookSuccess {
|
||||
webhook {
|
||||
id
|
||||
}
|
||||
}
|
||||
... on DeleteWebhookError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
})
|
||||
|
||||
context('when webhook exists', () => {
|
||||
before(async () => {
|
||||
const webhook = await getRepository(Webhook).save({
|
||||
url: 'http://localhost:3000/webhooks/test',
|
||||
user: { id: user.id },
|
||||
eventTypes: [WebhookEvent.LabelCreated],
|
||||
})
|
||||
webhookId = webhook.id
|
||||
})
|
||||
|
||||
it('should delete a webhook', async () => {
|
||||
const res = await graphqlRequest(query, authToken)
|
||||
const webhook = await getRepository(Webhook).findOneBy({
|
||||
id: webhookId,
|
||||
})
|
||||
|
||||
expect(res.body.data.deleteWebhook.webhook).to.be.an('object')
|
||||
expect(res.body.data.deleteWebhook.webhook.id).to.eql(webhookId)
|
||||
expect(webhook).to.be.null
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
58
packages/api/test/routers/webhooks.test.ts
Normal file
58
packages/api/test/routers/webhooks.test.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { createTestUser, deleteTestUser } from '../db'
|
||||
import { request } from '../util'
|
||||
import { User } from '../../src/entity/user'
|
||||
import 'mocha'
|
||||
import { getRepository } from '../../src/entity/utils'
|
||||
import { Webhook } from '../../src/entity/webhook'
|
||||
import { expect } from 'chai'
|
||||
import nock from 'nock'
|
||||
|
||||
describe('Webhooks Router', () => {
|
||||
const username = 'fakeUser'
|
||||
const token = process.env.PUBSUB_VERIFICATION_TOKEN || ''
|
||||
const webhookBaseUrl = 'https://localhost:3000'
|
||||
const webhookPath = `/webhooks`
|
||||
|
||||
let user: User
|
||||
let webhook: Webhook
|
||||
|
||||
before(async () => {
|
||||
// create test user and login
|
||||
user = await createTestUser(username)
|
||||
await request
|
||||
.post('/local/debug/fake-user-login')
|
||||
.send({ fakeEmail: user.email })
|
||||
|
||||
webhook = await getRepository(Webhook).save({
|
||||
url: webhookBaseUrl + webhookPath,
|
||||
user: { id: user.id },
|
||||
eventTypes: ['PAGE_CREATED'],
|
||||
})
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
// clean up
|
||||
await deleteTestUser(username)
|
||||
})
|
||||
|
||||
describe('trigger webhooks', () => {
|
||||
it('should trigger webhooks', async () => {
|
||||
const data = {
|
||||
message: {
|
||||
data: Buffer.from(
|
||||
JSON.stringify({ userId: user.id, type: 'page' })
|
||||
).toString('base64'),
|
||||
publishTime: new Date().toISOString(),
|
||||
},
|
||||
}
|
||||
|
||||
nock(webhookBaseUrl).post(webhookPath).reply(200)
|
||||
|
||||
const res = await request
|
||||
.post('/svc/pubsub/webhooks/trigger/created?token=' + token)
|
||||
.send(data)
|
||||
.expect(200)
|
||||
expect(res.text).to.eql('OK')
|
||||
})
|
||||
})
|
||||
})
|
||||
23
packages/db/migrations/0083.do.webhooks.sql
Executable file
23
packages/db/migrations/0083.do.webhooks.sql
Executable file
|
|
@ -0,0 +1,23 @@
|
|||
-- Type: DO
|
||||
-- Name: webhooks
|
||||
-- Description: webhooks model
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE omnivore.webhooks (
|
||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v1mc(),
|
||||
user_id uuid NOT NULL REFERENCES omnivore.user ON DELETE CASCADE,
|
||||
url text NOT NULL,
|
||||
method text NOT NULL DEFAULT 'POST',
|
||||
content_type text NOT NULL DEFAULT 'application/json',
|
||||
enabled boolean NOT NULL DEFAULT true,
|
||||
event_types text[] NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT current_timestamp,
|
||||
updated_at timestamptz NOT NULL DEFAULT current_timestamp,
|
||||
UNIQUE (user_id, event_types)
|
||||
);
|
||||
|
||||
CREATE TRIGGER update_webhook_modtime BEFORE UPDATE ON omnivore.webhooks
|
||||
FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column();
|
||||
|
||||
COMMIT;
|
||||
9
packages/db/migrations/0083.undo.webhooks.sql
Executable file
9
packages/db/migrations/0083.undo.webhooks.sql
Executable file
|
|
@ -0,0 +1,9 @@
|
|||
-- Type: UNDO
|
||||
-- Name: webhooks
|
||||
-- Description: webhooks model
|
||||
|
||||
BEGIN;
|
||||
|
||||
DROP TABLE IF EXISTS omnivore.webhooks;
|
||||
|
||||
COMMIT;
|
||||
Loading…
Reference in a new issue