diff --git a/packages/api/src/generated/graphql.ts b/packages/api/src/generated/graphql.ts index fd414ea98..d26ef5448 100644 --- a/packages/api/src/generated/graphql.ts +++ b/packages/api/src/generated/graphql.ts @@ -823,6 +823,7 @@ export type Mutation = { updateHighlightReply: UpdateHighlightReplyResult; updateLabel: UpdateLabelResult; updateLinkShareInfo: UpdateLinkShareInfoResult; + updatePage: UpdatePageResult; updateReminder: UpdateReminderResult; updateSharedComment: UpdateSharedCommentResult; updateUser: UpdateUserResult; @@ -1021,6 +1022,11 @@ export type MutationUpdateLinkShareInfoArgs = { }; +export type MutationUpdatePageArgs = { + input: UpdatePageInput; +}; + + export type MutationUpdateReminderArgs = { input: UpdateReminderInput; }; @@ -1822,6 +1828,32 @@ export type UpdateLinkShareInfoSuccess = { message: Scalars['String']; }; +export type UpdatePageError = { + __typename?: 'UpdatePageError'; + errorCodes: Array; +}; + +export enum UpdatePageErrorCode { + BadRequest = 'BAD_REQUEST', + Forbidden = 'FORBIDDEN', + NotFound = 'NOT_FOUND', + Unauthorized = 'UNAUTHORIZED', + UpdateFailed = 'UPDATE_FAILED' +} + +export type UpdatePageInput = { + description?: InputMaybe; + pageId: Scalars['ID']; + title?: InputMaybe; +}; + +export type UpdatePageResult = UpdatePageError | UpdatePageSuccess; + +export type UpdatePageSuccess = { + __typename?: 'UpdatePageSuccess'; + updatedPage: Page; +}; + export type UpdateReminderError = { __typename?: 'UpdateReminderError'; errorCodes: Array; @@ -2352,6 +2384,11 @@ export type ResolversTypes = { UpdateLinkShareInfoInput: UpdateLinkShareInfoInput; UpdateLinkShareInfoResult: ResolversTypes['UpdateLinkShareInfoError'] | ResolversTypes['UpdateLinkShareInfoSuccess']; UpdateLinkShareInfoSuccess: ResolverTypeWrapper; + UpdatePageError: ResolverTypeWrapper; + UpdatePageErrorCode: UpdatePageErrorCode; + UpdatePageInput: UpdatePageInput; + UpdatePageResult: ResolversTypes['UpdatePageError'] | ResolversTypes['UpdatePageSuccess']; + UpdatePageSuccess: ResolverTypeWrapper; UpdateReminderError: ResolverTypeWrapper; UpdateReminderErrorCode: UpdateReminderErrorCode; UpdateReminderInput: UpdateReminderInput; @@ -2605,6 +2642,10 @@ export type ResolversParentTypes = { UpdateLinkShareInfoInput: UpdateLinkShareInfoInput; UpdateLinkShareInfoResult: ResolversParentTypes['UpdateLinkShareInfoError'] | ResolversParentTypes['UpdateLinkShareInfoSuccess']; UpdateLinkShareInfoSuccess: UpdateLinkShareInfoSuccess; + UpdatePageError: UpdatePageError; + UpdatePageInput: UpdatePageInput; + UpdatePageResult: ResolversParentTypes['UpdatePageError'] | ResolversParentTypes['UpdatePageSuccess']; + UpdatePageSuccess: UpdatePageSuccess; UpdateReminderError: UpdateReminderError; UpdateReminderInput: UpdateReminderInput; UpdateReminderResult: ResolversParentTypes['UpdateReminderError'] | ResolversParentTypes['UpdateReminderSuccess']; @@ -3236,6 +3277,7 @@ export type MutationResolvers>; updateLabel?: Resolver>; updateLinkShareInfo?: Resolver>; + updatePage?: Resolver>; updateReminder?: Resolver>; updateSharedComment?: Resolver>; updateUser?: Resolver>; @@ -3692,6 +3734,20 @@ export type UpdateLinkShareInfoSuccessResolvers; }; +export type UpdatePageErrorResolvers = { + errorCodes?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type UpdatePageResultResolvers = { + __resolveType: TypeResolveFn<'UpdatePageError' | 'UpdatePageSuccess', ParentType, ContextType>; +}; + +export type UpdatePageSuccessResolvers = { + updatedPage?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type UpdateReminderErrorResolvers = { errorCodes?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; @@ -3996,6 +4052,9 @@ export type Resolvers = { UpdateLinkShareInfoError?: UpdateLinkShareInfoErrorResolvers; UpdateLinkShareInfoResult?: UpdateLinkShareInfoResultResolvers; UpdateLinkShareInfoSuccess?: UpdateLinkShareInfoSuccessResolvers; + UpdatePageError?: UpdatePageErrorResolvers; + UpdatePageResult?: UpdatePageResultResolvers; + UpdatePageSuccess?: UpdatePageSuccessResolvers; UpdateReminderError?: UpdateReminderErrorResolvers; UpdateReminderResult?: UpdateReminderResultResolvers; UpdateReminderSuccess?: UpdateReminderSuccessResolvers; diff --git a/packages/api/src/generated/schema.graphql b/packages/api/src/generated/schema.graphql index a399f9dbd..b031777b9 100644 --- a/packages/api/src/generated/schema.graphql +++ b/packages/api/src/generated/schema.graphql @@ -733,6 +733,7 @@ type Mutation { updateHighlightReply(input: UpdateHighlightReplyInput!): UpdateHighlightReplyResult! updateLabel(input: UpdateLabelInput!): UpdateLabelResult! updateLinkShareInfo(input: UpdateLinkShareInfoInput!): UpdateLinkShareInfoResult! + updatePage(input: UpdatePageInput!): UpdatePageResult! updateReminder(input: UpdateReminderInput!): UpdateReminderResult! updateSharedComment(input: UpdateSharedCommentInput!): UpdateSharedCommentResult! updateUser(input: UpdateUserInput!): UpdateUserResult! @@ -1388,6 +1389,30 @@ type UpdateLinkShareInfoSuccess { message: String! } +type UpdatePageError { + errorCodes: [UpdatePageErrorCode!]! +} + +enum UpdatePageErrorCode { + BAD_REQUEST + FORBIDDEN + NOT_FOUND + UNAUTHORIZED + UPDATE_FAILED +} + +input UpdatePageInput { + description: String + pageId: ID! + title: String +} + +union UpdatePageResult = UpdatePageError | UpdatePageSuccess + +type UpdatePageSuccess { + updatedPage: Page! +} + type UpdateReminderError { errorCodes: [UpdateReminderErrorCode!]! } diff --git a/packages/api/src/resolvers/function_resolvers.ts b/packages/api/src/resolvers/function_resolvers.ts index a5b52fe1c..e3e8081c2 100644 --- a/packages/api/src/resolvers/function_resolvers.ts +++ b/packages/api/src/resolvers/function_resolvers.ts @@ -77,6 +77,7 @@ import { updateUserResolver, uploadFileRequestResolver, validateUsernameResolver, + updatePageResolver, } from './index' import { getShareInfoForArticle } from '../datalayer/links/share_info' import { @@ -146,6 +147,7 @@ export const functionResolvers = { setLabels: setLabelsResolver, generateApiKey: generateApiKeyResolver, unsubscribe: unsubscribeResolver, + updatePage: updatePageResolver, subscribe: subscribeResolver, }, Query: { @@ -556,5 +558,6 @@ export const functionResolvers = { ...resultResolveTypeResolver('Subscriptions'), ...resultResolveTypeResolver('Unsubscribe'), ...resultResolveTypeResolver('UpdateLabel'), + ...resultResolveTypeResolver('UpdatePage'), ...resultResolveTypeResolver('Subscribe'), } diff --git a/packages/api/src/resolvers/index.ts b/packages/api/src/resolvers/index.ts index 1aa83dff4..07cdfea55 100644 --- a/packages/api/src/resolvers/index.ts +++ b/packages/api/src/resolvers/index.ts @@ -15,3 +15,4 @@ export * from './reminders' export * from './user_device_tokens' export * from './labels' export * from './subscriptions' +export * from './update' diff --git a/packages/api/src/resolvers/update/index.ts b/packages/api/src/resolvers/update/index.ts new file mode 100644 index 000000000..46607d1a7 --- /dev/null +++ b/packages/api/src/resolvers/update/index.ts @@ -0,0 +1,52 @@ +import { + UpdatePageError, + UpdatePageErrorCode, + UpdatePageSuccess, + MutationUpdatePageArgs, +} from '../../generated/graphql' +import { authorized, userDataToUser } from '../../utils/helpers' +import { updatePage, getPageById } from '../../elastic/pages' +import { Page } from '../../entity/page' +import { Merge } from '../../util' + +export type UpdatePageSuccessPartial = Merge< + UpdatePageSuccess, + { updatedPage: Partial } +> + +export const updatePageResolver = authorized< + UpdatePageSuccessPartial, + UpdatePageError, + MutationUpdatePageArgs +>(async (_, { input }, ctx) => { + const { + models, + claims: { uid }, + } = ctx + + const user = userDataToUser(await models.user.get(uid)) + if (!user) { + return { errorCodes: [UpdatePageErrorCode.Unauthorized] } + } + + const page = await getPageById(input.pageId) + + if (!page) return { errorCodes: [UpdatePageErrorCode.NotFound] } + else if (page.userId !== user.id) + return { errorCodes: [UpdatePageErrorCode.Unauthorized] } + + const pageData = { + id: input.pageId, + title: input.title || undefined, + description: input.description || '', + } + + const updateResult = await updatePage(input.pageId, pageData, { ...ctx, uid }) + if (!updateResult) return { errorCodes: [UpdatePageErrorCode.UpdateFailed] } + + const updatedPage = (await getPageById(input.pageId)) as unknown as Page + return { + updatedPage: updatedPage, + __typename: 'UpdatePageSuccess', + } +}) diff --git a/packages/api/src/schema.ts b/packages/api/src/schema.ts index f910324de..d3c7c5643 100755 --- a/packages/api/src/schema.ts +++ b/packages/api/src/schema.ts @@ -498,6 +498,30 @@ const schema = gql` union SaveResult = SaveSuccess | SaveError + input UpdatePageInput { + pageId: ID! + title: String + description: String + } + + type UpdatePageSuccess { + updatedPage: Page! + } + + enum UpdatePageErrorCode { + UPDATE_FAILED + UNAUTHORIZED + BAD_REQUEST + NOT_FOUND + FORBIDDEN + } + + type UpdatePageError { + errorCodes: [UpdatePageErrorCode!]! + } + + union UpdatePageResult = UpdatePageSuccess | UpdatePageError + # Mutation: setFollow union SetFollowResult = SetFollowSuccess | SetFollowError input SetFollowInput { @@ -1577,6 +1601,7 @@ const schema = gql` deleteNewsletterEmail(newsletterEmailId: ID!): DeleteNewsletterEmailResult! saveUrl(input: SaveUrlInput!): SaveResult! savePage(input: SavePageInput!): SaveResult! + updatePage(input: UpdatePageInput!): UpdatePageResult! saveFile(input: SaveFileInput!): SaveResult! createReminder(input: CreateReminderInput!): CreateReminderResult! updateReminder(input: UpdateReminderInput!): UpdateReminderResult! diff --git a/packages/api/test/resolvers/update.test.ts b/packages/api/test/resolvers/update.test.ts new file mode 100644 index 000000000..abdccdf21 --- /dev/null +++ b/packages/api/test/resolvers/update.test.ts @@ -0,0 +1,72 @@ +import { createTestUser, deleteTestUser } from '../db' +import { + createTestElasticPage, + graphqlRequest, + request, +} from '../util' +import { expect } from 'chai' +import 'mocha' +import { User } from '../../src/entity/user' +import { Page } from '../../src/elastic/types' + +describe('Update API', () => { + const username = 'fakeUser' + + let user: User + let authToken: string + let page: Page + + 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 + page = await createTestElasticPage(user) + }) + + after(async () => { + // clean up + await deleteTestUser(username) + }) + + describe('update page', () => { + let query: string + let title = "New Title" + let description = "New Description" + + beforeEach(() => { + query = ` + mutation { + updatePage( + input: { + pageId: "${page.id}" + title: "${title}" + description: "${description}" + } + ) { + ... on UpdatePageSuccess { + updatedPage { + title + description + } + } + ... on UpdatePageError { + errorCodes + } + } + } + ` + }) + + it('should update page', async () => { + const res = await graphqlRequest(query, authToken).expect(200) + + const updatedPage = res?.body.data.updatePage.updatedPage + expect(updatedPage?.title).to.eql(title) + expect(updatedPage?.description).to.eql(description) + }) + }) +})