From 39586cfb9184f3243138fcea7c6715b63319c297 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Tue, 7 Nov 2023 13:50:00 +0800 Subject: [PATCH 1/2] add fields column of type:json to the user_personalization table --- .../0145.do.add_fields_to_user_personalization.sql | 9 +++++++++ .../0145.undo.add_fields_to_user_personalization.sql | 9 +++++++++ 2 files changed, 18 insertions(+) create mode 100755 packages/db/migrations/0145.do.add_fields_to_user_personalization.sql create mode 100755 packages/db/migrations/0145.undo.add_fields_to_user_personalization.sql diff --git a/packages/db/migrations/0145.do.add_fields_to_user_personalization.sql b/packages/db/migrations/0145.do.add_fields_to_user_personalization.sql new file mode 100755 index 000000000..8101445ba --- /dev/null +++ b/packages/db/migrations/0145.do.add_fields_to_user_personalization.sql @@ -0,0 +1,9 @@ +-- Type: DO +-- Name: add_fields_to_user_personalization +-- Description: Add fields column to the user_personalization table + +BEGIN; + +ALTER TABLE omnivore.user_personalization ADD COLUMN fields json; + +COMMIT; diff --git a/packages/db/migrations/0145.undo.add_fields_to_user_personalization.sql b/packages/db/migrations/0145.undo.add_fields_to_user_personalization.sql new file mode 100755 index 000000000..e610d6867 --- /dev/null +++ b/packages/db/migrations/0145.undo.add_fields_to_user_personalization.sql @@ -0,0 +1,9 @@ +-- Type: UNDO +-- Name: add_fields_to_user_personalization +-- Description: Add fields column to the user_personalization table + +BEGIN; + +ALTER TABLE omnivore.user_personalization DROP COLUMN fields; + +COMMIT; From 3e9a049306b9658c908242303621fb5a61097e4f Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Tue, 7 Nov 2023 15:39:56 +0800 Subject: [PATCH 2/2] add fields to the response of user personalization api --- .../api/src/entity/user_personalization.ts | 3 + packages/api/src/generated/graphql.ts | 11 ++ packages/api/src/generated/schema.graphql | 4 + .../resolvers/user_personalization/index.ts | 15 +- packages/api/src/schema.ts | 3 + .../api/src/services/user_personalization.ts | 36 ++++ .../resolvers/user_personalization.test.ts | 159 ++++++++++++++++++ 7 files changed, 220 insertions(+), 11 deletions(-) create mode 100644 packages/api/src/services/user_personalization.ts create mode 100644 packages/api/test/resolvers/user_personalization.test.ts diff --git a/packages/api/src/entity/user_personalization.ts b/packages/api/src/entity/user_personalization.ts index 76f6a4d1b..788b7050b 100644 --- a/packages/api/src/entity/user_personalization.ts +++ b/packages/api/src/entity/user_personalization.ts @@ -53,4 +53,7 @@ export class UserPersonalization { @UpdateDateColumn({ default: () => 'CURRENT_TIMESTAMP' }) updatedAt!: Date + + @Column('json') + fields?: any | null } diff --git a/packages/api/src/generated/graphql.ts b/packages/api/src/generated/graphql.ts index 8041b6beb..773e67137 100644 --- a/packages/api/src/generated/graphql.ts +++ b/packages/api/src/generated/graphql.ts @@ -14,6 +14,7 @@ export type Scalars = { Int: number; Float: number; Date: any; + JSON: any; }; export type AddPopularReadError = { @@ -2514,6 +2515,7 @@ export enum SetUserPersonalizationErrorCode { } export type SetUserPersonalizationInput = { + fields?: InputMaybe; fontFamily?: InputMaybe; fontSize?: InputMaybe; libraryLayoutType?: InputMaybe; @@ -3159,6 +3161,7 @@ export enum UserErrorCode { export type UserPersonalization = { __typename?: 'UserPersonalization'; + fields?: Maybe; fontFamily?: Maybe; fontSize?: Maybe; id?: Maybe; @@ -3511,6 +3514,7 @@ export type ResolversTypes = { IntegrationsErrorCode: IntegrationsErrorCode; IntegrationsResult: ResolversTypes['IntegrationsError'] | ResolversTypes['IntegrationsSuccess']; IntegrationsSuccess: ResolverTypeWrapper; + JSON: ResolverTypeWrapper; JoinGroupError: ResolverTypeWrapper; JoinGroupErrorCode: JoinGroupErrorCode; JoinGroupResult: ResolversTypes['JoinGroupError'] | ResolversTypes['JoinGroupSuccess']; @@ -3978,6 +3982,7 @@ export type ResolversParentTypes = { IntegrationsError: IntegrationsError; IntegrationsResult: ResolversParentTypes['IntegrationsError'] | ResolversParentTypes['IntegrationsSuccess']; IntegrationsSuccess: IntegrationsSuccess; + JSON: Scalars['JSON']; JoinGroupError: JoinGroupError; JoinGroupResult: ResolversParentTypes['JoinGroupError'] | ResolversParentTypes['JoinGroupSuccess']; JoinGroupSuccess: JoinGroupSuccess; @@ -4955,6 +4960,10 @@ export type IntegrationsSuccessResolvers; }; +export interface JsonScalarConfig extends GraphQLScalarTypeConfig { + name: 'JSON'; +} + export type JoinGroupErrorResolvers = { errorCodes?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; @@ -6108,6 +6117,7 @@ export type UserErrorResolvers = { + fields?: Resolver, ParentType, ContextType>; fontFamily?: Resolver, ParentType, ContextType>; fontSize?: Resolver, ParentType, ContextType>; id?: Resolver, ParentType, ContextType>; @@ -6314,6 +6324,7 @@ export type Resolvers = { IntegrationsError?: IntegrationsErrorResolvers; IntegrationsResult?: IntegrationsResultResolvers; IntegrationsSuccess?: IntegrationsSuccessResolvers; + JSON?: GraphQLScalarType; JoinGroupError?: JoinGroupErrorResolvers; JoinGroupResult?: JoinGroupResultResolvers; JoinGroupSuccess?: JoinGroupSuccessResolvers; diff --git a/packages/api/src/generated/schema.graphql b/packages/api/src/generated/schema.graphql index 335c93409..09150be0c 100644 --- a/packages/api/src/generated/schema.graphql +++ b/packages/api/src/generated/schema.graphql @@ -901,6 +901,8 @@ type IntegrationsSuccess { integrations: [Integration!]! } +scalar JSON + type JoinGroupError { errorCodes: [JoinGroupErrorCode!]! } @@ -1963,6 +1965,7 @@ enum SetUserPersonalizationErrorCode { } input SetUserPersonalizationInput { + fields: JSON fontFamily: String fontSize: Int libraryLayoutType: String @@ -2557,6 +2560,7 @@ enum UserErrorCode { } type UserPersonalization { + fields: JSON fontFamily: String fontSize: Int id: ID diff --git a/packages/api/src/resolvers/user_personalization/index.ts b/packages/api/src/resolvers/user_personalization/index.ts index ad0e8c306..e9bb00a89 100644 --- a/packages/api/src/resolvers/user_personalization/index.ts +++ b/packages/api/src/resolvers/user_personalization/index.ts @@ -14,9 +14,7 @@ export const setUserPersonalizationResolver = authorized< SetUserPersonalizationSuccess, SetUserPersonalizationError, MutationSetUserPersonalizationArgs ->(async (_, { input }, { authTrx, claims: { uid }, log }) => { - log.info('setUserPersonalizationResolver', { uid, input }) - +>(async (_, { input }, { authTrx, uid }) => { const result = await authTrx(async (t) => { return t.getRepository(UserPersonalization).upsert( { @@ -40,10 +38,8 @@ export const setUserPersonalizationResolver = authorized< ) // Cast SortOrder from string to enum - const librarySortOrder = updatedUserPersonalization?.librarySortOrder as - | SortOrder - | null - | undefined + const librarySortOrder = + updatedUserPersonalization?.librarySortOrder as SortOrder return { updatedUserPersonalization: { @@ -64,10 +60,7 @@ export const getUserPersonalizationResolver = authorized< ) // Cast SortOrder from string to enum - const librarySortOrder = userPersonalization?.librarySortOrder as - | SortOrder - | null - | undefined + const librarySortOrder = userPersonalization?.librarySortOrder as SortOrder return { userPersonalization: { ...userPersonalization, librarySortOrder } } }) diff --git a/packages/api/src/schema.ts b/packages/api/src/schema.ts index c6bb63f9d..4e0692bcc 100755 --- a/packages/api/src/schema.ts +++ b/packages/api/src/schema.ts @@ -4,6 +4,7 @@ const schema = gql` # Scalars scalar Date + scalar JSON directive @sanitize( allowedTags: [String] @@ -1050,6 +1051,7 @@ const schema = gql` speechSecondaryVoice: String speechRate: String speechVolume: String + fields: JSON } # Query: UserPersonalization @@ -1091,6 +1093,7 @@ const schema = gql` speechSecondaryVoice: String speechRate: String speechVolume: String + fields: JSON } # Type: ArticleSavingRequest diff --git a/packages/api/src/services/user_personalization.ts b/packages/api/src/services/user_personalization.ts new file mode 100644 index 000000000..cc9cd82c4 --- /dev/null +++ b/packages/api/src/services/user_personalization.ts @@ -0,0 +1,36 @@ +import { DeepPartial } from 'typeorm' +import { UserPersonalization } from '../entity/user_personalization' +import { authTrx } from '../repository' + +export const findUserPersonalization = async (id: string, userId: string) => { + return authTrx( + (t) => + t.getRepository(UserPersonalization).findOneBy({ + id, + }), + undefined, + userId + ) +} + +export const deleteUserPersonalization = async (id: string, userId: string) => { + return authTrx( + (t) => + t.getRepository(UserPersonalization).delete({ + id, + }), + undefined, + userId + ) +} + +export const saveUserPersonalization = async ( + userId: string, + userPersonalization: DeepPartial +) => { + return authTrx( + (t) => t.getRepository(UserPersonalization).save(userPersonalization), + undefined, + userId + ) +} diff --git a/packages/api/test/resolvers/user_personalization.test.ts b/packages/api/test/resolvers/user_personalization.test.ts new file mode 100644 index 000000000..4f940d05f --- /dev/null +++ b/packages/api/test/resolvers/user_personalization.test.ts @@ -0,0 +1,159 @@ +import { expect } from 'chai' +import 'mocha' +import { User } from '../../src/entity/user' +import { UserPersonalization } from '../../src/entity/user_personalization' +import { deleteUser } from '../../src/services/user' +import { + deleteUserPersonalization, + findUserPersonalization, + saveUserPersonalization, +} from '../../src/services/user_personalization' +import { createTestUser } from '../db' +import { graphqlRequest, request } from '../util' + +describe('User Personalization API', () => { + let authToken: string + let user: User + + before(async () => { + // create test user and login + user = await createTestUser('fakeUser') + const res = await request + .post('/local/debug/fake-user-login') + .send({ fakeEmail: user.email }) + + authToken = res.body.authToken + }) + + after(async () => { + // clean up + await deleteUser(user.id) + }) + + describe('Set user personalization', () => { + const query = ` + mutation SetUserPersonalization($input: SetUserPersonalizationInput!) { + setUserPersonalization(input: $input) { + ... on SetUserPersonalizationSuccess { + updatedUserPersonalization { + id + fields + } + } + ... on SetUserPersonalizationError { + errorCodes + } + } + } + ` + + context('when user personalization does not exist', () => { + it('creates a new user personalization', async () => { + const fields = { + testField: 'testValue', + } + + const res = await graphqlRequest(query, authToken, { + input: { fields }, + }).expect(200) + + expect( + res.body.data.setUserPersonalization.updatedUserPersonalization.fields + ).to.eql(fields) + + const userPersonalization = await findUserPersonalization( + res.body.data.setUserPersonalization.updatedUserPersonalization.id, + user.id + ) + expect(userPersonalization).to.not.be.null + + // clean up + await deleteUserPersonalization( + res.body.data.setUserPersonalization.updatedUserPersonalization.id, + user.id + ) + }) + }) + + context('when user personalization exists', () => { + let existingUserPersonalization: UserPersonalization + + before(async () => { + existingUserPersonalization = await saveUserPersonalization(user.id, { + user: { id: user.id }, + fields: { + testField: 'testValue', + }, + }) + }) + + after(async () => { + // clean up + await deleteUserPersonalization(existingUserPersonalization.id, user.id) + }) + + it('updates the user personalization', async () => { + const newFields = { + testField: 'testValue1', + } + + const res = await graphqlRequest(query, authToken, { + input: { fields: newFields }, + }).expect(200) + + expect( + res.body.data.setUserPersonalization.updatedUserPersonalization.fields + ).to.eql(newFields) + + const updatedUserPersonalization = await findUserPersonalization( + existingUserPersonalization.id, + user.id + ) + expect(updatedUserPersonalization?.fields).to.eql(newFields) + }) + }) + }) + + describe('Get user personalization', () => { + let existingUserPersonalization: UserPersonalization + + before(async () => { + existingUserPersonalization = await saveUserPersonalization(user.id, { + user: { id: user.id }, + fields: { + testField: 'testValue', + }, + }) + }) + + after(async () => { + // clean up + await deleteUserPersonalization(existingUserPersonalization.id, user.id) + }) + + const query = ` + query GetUserPersonalization { + getUserPersonalization { + ... on GetUserPersonalizationSuccess { + userPersonalization { + id + fields + } + } + ... on GetUserPersonalizationError { + errorCodes + } + } + } + ` + + it('returns the user personalization', async () => { + const res = await graphqlRequest(query, authToken).expect(200) + + expect(res.body.data.getUserPersonalization.userPersonalization).to.eql({ + id: existingUserPersonalization.id, + fields: existingUserPersonalization.fields, + }) + }) + }) +})