From 0a4758105afb3aaa0b9b28a9bdedda9005a60f3d Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Wed, 20 Dec 2023 11:29:10 +0800 Subject: [PATCH] feat: update newsletter email api --- packages/api/src/generated/graphql.ts | 57 +++++++++++++++++++ packages/api/src/generated/schema.graphql | 23 ++++++++ .../api/src/resolvers/function_resolvers.ts | 11 +++- .../api/src/resolvers/newsletters/index.ts | 41 +++++++++++++ packages/api/src/schema.ts | 27 +++++++++ packages/api/src/services/newsletters.ts | 26 +++++++++ .../api/test/resolvers/newsletters.test.ts | 54 ++++++++++++++++++ 7 files changed, 237 insertions(+), 2 deletions(-) diff --git a/packages/api/src/generated/graphql.ts b/packages/api/src/generated/graphql.ts index 080b4614e..62ba7be07 100644 --- a/packages/api/src/generated/graphql.ts +++ b/packages/api/src/generated/graphql.ts @@ -1391,6 +1391,7 @@ export type Mutation = { updateFilter: UpdateFilterResult; updateHighlight: UpdateHighlightResult; updateLabel: UpdateLabelResult; + updateNewsletterEmail: UpdateNewsletterEmailResult; updatePage: UpdatePageResult; updateSubscription: UpdateSubscriptionResult; updateUser: UpdateUserResult; @@ -1677,6 +1678,11 @@ export type MutationUpdateLabelArgs = { }; +export type MutationUpdateNewsletterEmailArgs = { + input: UpdateNewsletterEmailInput; +}; + + export type MutationUpdatePageArgs = { input: UpdatePageInput; }; @@ -3059,6 +3065,30 @@ export type UpdateLinkShareInfoSuccess = { message: Scalars['String']; }; +export type UpdateNewsletterEmailError = { + __typename?: 'UpdateNewsletterEmailError'; + errorCodes: Array; +}; + +export enum UpdateNewsletterEmailErrorCode { + BadRequest = 'BAD_REQUEST', + Unauthorized = 'UNAUTHORIZED' +} + +export type UpdateNewsletterEmailInput = { + description?: InputMaybe; + folder?: InputMaybe; + id: Scalars['ID']; + name?: InputMaybe; +}; + +export type UpdateNewsletterEmailResult = UpdateNewsletterEmailError | UpdateNewsletterEmailSuccess; + +export type UpdateNewsletterEmailSuccess = { + __typename?: 'UpdateNewsletterEmailSuccess'; + newsletterEmail: NewsletterEmail; +}; + export type UpdatePageError = { __typename?: 'UpdatePageError'; errorCodes: Array; @@ -3962,6 +3992,11 @@ export type ResolversTypes = { UpdateLinkShareInfoInput: UpdateLinkShareInfoInput; UpdateLinkShareInfoResult: ResolversTypes['UpdateLinkShareInfoError'] | ResolversTypes['UpdateLinkShareInfoSuccess']; UpdateLinkShareInfoSuccess: ResolverTypeWrapper; + UpdateNewsletterEmailError: ResolverTypeWrapper; + UpdateNewsletterEmailErrorCode: UpdateNewsletterEmailErrorCode; + UpdateNewsletterEmailInput: UpdateNewsletterEmailInput; + UpdateNewsletterEmailResult: ResolversTypes['UpdateNewsletterEmailError'] | ResolversTypes['UpdateNewsletterEmailSuccess']; + UpdateNewsletterEmailSuccess: ResolverTypeWrapper; UpdatePageError: ResolverTypeWrapper; UpdatePageErrorCode: UpdatePageErrorCode; UpdatePageInput: UpdatePageInput; @@ -4392,6 +4427,10 @@ export type ResolversParentTypes = { UpdateLinkShareInfoInput: UpdateLinkShareInfoInput; UpdateLinkShareInfoResult: ResolversParentTypes['UpdateLinkShareInfoError'] | ResolversParentTypes['UpdateLinkShareInfoSuccess']; UpdateLinkShareInfoSuccess: UpdateLinkShareInfoSuccess; + UpdateNewsletterEmailError: UpdateNewsletterEmailError; + UpdateNewsletterEmailInput: UpdateNewsletterEmailInput; + UpdateNewsletterEmailResult: ResolversParentTypes['UpdateNewsletterEmailError'] | ResolversParentTypes['UpdateNewsletterEmailSuccess']; + UpdateNewsletterEmailSuccess: UpdateNewsletterEmailSuccess; UpdatePageError: UpdatePageError; UpdatePageInput: UpdatePageInput; UpdatePageResult: ResolversParentTypes['UpdatePageError'] | ResolversParentTypes['UpdatePageSuccess']; @@ -5460,6 +5499,7 @@ export type MutationResolvers>; updateHighlight?: Resolver>; updateLabel?: Resolver>; + updateNewsletterEmail?: Resolver>; updatePage?: Resolver>; updateSubscription?: Resolver>; updateUser?: Resolver>; @@ -6267,6 +6307,20 @@ export type UpdateLinkShareInfoSuccessResolvers; }; +export type UpdateNewsletterEmailErrorResolvers = { + errorCodes?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type UpdateNewsletterEmailResultResolvers = { + __resolveType: TypeResolveFn<'UpdateNewsletterEmailError' | 'UpdateNewsletterEmailSuccess', ParentType, ContextType>; +}; + +export type UpdateNewsletterEmailSuccessResolvers = { + newsletterEmail?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type UpdatePageErrorResolvers = { errorCodes?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; @@ -6807,6 +6861,9 @@ export type Resolvers = { UpdateLinkShareInfoError?: UpdateLinkShareInfoErrorResolvers; UpdateLinkShareInfoResult?: UpdateLinkShareInfoResultResolvers; UpdateLinkShareInfoSuccess?: UpdateLinkShareInfoSuccessResolvers; + UpdateNewsletterEmailError?: UpdateNewsletterEmailErrorResolvers; + UpdateNewsletterEmailResult?: UpdateNewsletterEmailResultResolvers; + UpdateNewsletterEmailSuccess?: UpdateNewsletterEmailSuccessResolvers; UpdatePageError?: UpdatePageErrorResolvers; UpdatePageResult?: UpdatePageResultResolvers; UpdatePageSuccess?: UpdatePageSuccessResolvers; diff --git a/packages/api/src/generated/schema.graphql b/packages/api/src/generated/schema.graphql index 97fbbe750..cbcdc1808 100644 --- a/packages/api/src/generated/schema.graphql +++ b/packages/api/src/generated/schema.graphql @@ -1252,6 +1252,7 @@ type Mutation { updateFilter(input: UpdateFilterInput!): UpdateFilterResult! updateHighlight(input: UpdateHighlightInput!): UpdateHighlightResult! updateLabel(input: UpdateLabelInput!): UpdateLabelResult! + updateNewsletterEmail(input: UpdateNewsletterEmailInput!): UpdateNewsletterEmailResult! updatePage(input: UpdatePageInput!): UpdatePageResult! updateSubscription(input: UpdateSubscriptionInput!): UpdateSubscriptionResult! updateUser(input: UpdateUserInput!): UpdateUserResult! @@ -2442,6 +2443,28 @@ type UpdateLinkShareInfoSuccess { message: String! } +type UpdateNewsletterEmailError { + errorCodes: [UpdateNewsletterEmailErrorCode!]! +} + +enum UpdateNewsletterEmailErrorCode { + BAD_REQUEST + UNAUTHORIZED +} + +input UpdateNewsletterEmailInput { + description: String + folder: String + id: ID! + name: String +} + +union UpdateNewsletterEmailResult = UpdateNewsletterEmailError | UpdateNewsletterEmailSuccess + +type UpdateNewsletterEmailSuccess { + newsletterEmail: NewsletterEmail! +} + type UpdatePageError { errorCodes: [UpdatePageErrorCode!]! } diff --git a/packages/api/src/resolvers/function_resolvers.ts b/packages/api/src/resolvers/function_resolvers.ts index cdcd4a8bc..ba86626b8 100644 --- a/packages/api/src/resolvers/function_resolvers.ts +++ b/packages/api/src/resolvers/function_resolvers.ts @@ -131,6 +131,7 @@ import { validateUsernameResolver, webhookResolver, webhooksResolver, + updateNewsletterEmailResolver, } from './index' import { markEmailAsItemResolver, recentEmailsResolver } from './recent_emails' import { recentSearchesResolver } from './recent_searches' @@ -226,6 +227,7 @@ export const functionResolvers = { updateFilter: updateFilterResolver, updateEmail: updateEmailResolver, moveToFolder: moveToFolderResolver, + updateNewsletterEmail: updateNewsletterEmailResolver, }, Query: { me: getMeUserResolver, @@ -445,8 +447,12 @@ export const functionResolvers = { subscription.icon && createImageProxyUrl(subscription.icon, 128, 128) ) }, - folder(subscription: { folder?: string | null }) { - return subscription.folder || DEFAULT_SUBSCRIPTION_FOLDER + folder(subscription: Subscription) { + return ( + subscription.folder || + subscription.newsletterEmail?.folder || + DEFAULT_SUBSCRIPTION_FOLDER + ) }, }, NewsletterEmail: { @@ -550,4 +556,5 @@ export const functionResolvers = { ...resultResolveTypeResolver('UpdateEmail'), ...resultResolveTypeResolver('ScanFeeds'), ...resultResolveTypeResolver('MoveToFolder'), + ...resultResolveTypeResolver('UpdateNewsletterEmail'), } diff --git a/packages/api/src/resolvers/newsletters/index.ts b/packages/api/src/resolvers/newsletters/index.ts index 34b887dea..a7fc5a447 100644 --- a/packages/api/src/resolvers/newsletters/index.ts +++ b/packages/api/src/resolvers/newsletters/index.ts @@ -12,15 +12,20 @@ import { DeleteNewsletterEmailSuccess, MutationCreateNewsletterEmailArgs, MutationDeleteNewsletterEmailArgs, + MutationUpdateNewsletterEmailArgs, NewsletterEmailsError, NewsletterEmailsErrorCode, NewsletterEmailsSuccess, + UpdateNewsletterEmailError, + UpdateNewsletterEmailErrorCode, + UpdateNewsletterEmailSuccess, } from '../../generated/graphql' import { getRepository } from '../../repository' import { createNewsletterEmail, deleteNewsletterEmail, getNewsletterEmails, + updateNewsletterEmail, } from '../../services/newsletters' import { unsubscribeAll } from '../../services/subscriptions' import { Merge } from '../../util' @@ -143,3 +148,39 @@ export const deleteNewsletterEmailResolver = authorized< } } }) + +export type UpdateNewsletterEmailSuccessPartial = Merge< + UpdateNewsletterEmailSuccess, + { newsletterEmail: NewsletterEmail } +> +export const updateNewsletterEmailResolver = authorized< + UpdateNewsletterEmailSuccessPartial, + UpdateNewsletterEmailError, + MutationUpdateNewsletterEmailArgs +>(async (_parent, { input }, { uid, log }) => { + analytics.track({ + userId: uid, + event: 'newsletter_email_updated', + properties: { + env: env.server.apiEnv, + ...input, + }, + }) + + const updatedNewsletterEmail = await updateNewsletterEmail(input.id, uid, { + name: input.name, + description: input.description, + folder: input.folder, + }) + if (!updatedNewsletterEmail) { + log.error('failed to update newsletter email') + + return { + errorCodes: [UpdateNewsletterEmailErrorCode.Unauthorized], + } + } + + return { + newsletterEmail: updatedNewsletterEmail, + } +}) diff --git a/packages/api/src/schema.ts b/packages/api/src/schema.ts index 49c64e669..4df9a5b35 100755 --- a/packages/api/src/schema.ts +++ b/packages/api/src/schema.ts @@ -2737,6 +2737,30 @@ const schema = gql` BAD_REQUEST } + input UpdateNewsletterEmailInput { + id: ID! + name: String + description: String + folder: String + } + + union UpdateNewsletterEmailResult = + UpdateNewsletterEmailSuccess + | UpdateNewsletterEmailError + + type UpdateNewsletterEmailSuccess { + newsletterEmail: NewsletterEmail! + } + + type UpdateNewsletterEmailError { + errorCodes: [UpdateNewsletterEmailErrorCode!]! + } + + enum UpdateNewsletterEmailErrorCode { + UNAUTHORIZED + BAD_REQUEST + } + # Mutations type Mutation { googleLogin(input: GoogleLoginInput!): LoginResult! @@ -2845,6 +2869,9 @@ const schema = gql` ): UpdateSubscriptionResult! moveToFolder(id: ID!, folder: String!): MoveToFolderResult! fetchContent(id: ID!): FetchContentResult! + updateNewsletterEmail( + input: UpdateNewsletterEmailInput! + ): UpdateNewsletterEmailResult! } # FIXME: remove sort from feedArticles after all cached tabs are closed diff --git a/packages/api/src/services/newsletters.ts b/packages/api/src/services/newsletters.ts index e5e382624..eb30ac267 100644 --- a/packages/api/src/services/newsletters.ts +++ b/packages/api/src/services/newsletters.ts @@ -7,6 +7,7 @@ import { } from '../generated/graphql' import { getRepository } from '../repository' import { userRepository } from '../repository/user' +import { keysToCamelCase } from '../utils/helpers' import addressparser = require('nodemailer/lib/addressparser') const parsedAddress = (emailAddress: string) => { @@ -114,3 +115,28 @@ export const findNewsletterEmailById = async ( ): Promise => { return getRepository(NewsletterEmail).findOneBy({ id }) } + +export const updateNewsletterEmail = async ( + id: string, + userId: string, + newsletterEmail: Partial +): Promise => { + const repo = getRepository(NewsletterEmail) + const result = await repo + .createQueryBuilder() + .where('id = :id', { id }) + .andWhere('user_id = :userId', { userId }) + .update(newsletterEmail) + .returning('*') + .execute() + + if ( + !result.affected || + !Array.isArray(result.raw) || + result.raw.length === 0 + ) { + return null + } + + return keysToCamelCase(result.raw[0]) as NewsletterEmail +} diff --git a/packages/api/test/resolvers/newsletters.test.ts b/packages/api/test/resolvers/newsletters.test.ts index 941a0365f..6f48e3ea6 100644 --- a/packages/api/test/resolvers/newsletters.test.ts +++ b/packages/api/test/resolvers/newsletters.test.ts @@ -9,6 +9,7 @@ import { import { getRepository } from '../../src/repository' import { createNewsletterEmail, + deleteNewsletterEmail, findNewsletterEmailByAddress, findNewsletterEmailById, } from '../../src/services/newsletters' @@ -286,4 +287,57 @@ describe('Newsletters API', () => { return graphqlRequest(query, invalidAuthToken).expect(500) }) }) + + describe('Update newsletter email', () => { + const query = ` + mutation UpdateNewsletterEmail($input: UpdateNewsletterEmailInput!) { + updateNewsletterEmail(input: $input) { + ... on UpdateNewsletterEmailSuccess { + newsletterEmail { + id + address + folder + } + } + ... on UpdateNewsletterEmailError { + errorCodes + } + } + } + ` + + context('when newsletter email exists', () => { + let newsletterEmailId = 'Newsletter email id' + + before(async () => { + // create test newsletter emails + const newsletterEmail = await createNewsletterEmail( + user.id, + undefined, + 'inbox' + ) + newsletterEmailId = newsletterEmail.id + }) + + after(async () => { + // clean up + await deleteNewsletterEmail(newsletterEmailId) + }) + + it('responds with status code 200', async () => { + const folder = 'following' + const response = await graphqlRequest(query, authToken, { + input: { + id: newsletterEmailId, + folder, + }, + }).expect(200) + expect( + response.body.data.updateNewsletterEmail.newsletterEmail.folder + ).to.eql(folder) + const newsletterEmail = await findNewsletterEmailById(newsletterEmailId) + expect(newsletterEmail?.folder).to.eql(folder) + }) + }) + }) })