Merge pull request #3086 from omnivore-app/feature/add-fields-to-user-personalization

feature/add fields to user personalization
This commit is contained in:
Hongbo Wu
2023-11-08 10:34:51 +08:00
committed by GitHub
9 changed files with 238 additions and 11 deletions

View File

@ -53,4 +53,7 @@ export class UserPersonalization {
@UpdateDateColumn({ default: () => 'CURRENT_TIMESTAMP' })
updatedAt!: Date
@Column('json')
fields?: any | null
}

View File

@ -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<Scalars['JSON']>;
fontFamily?: InputMaybe<Scalars['String']>;
fontSize?: InputMaybe<Scalars['Int']>;
libraryLayoutType?: InputMaybe<Scalars['String']>;
@ -3159,6 +3161,7 @@ export enum UserErrorCode {
export type UserPersonalization = {
__typename?: 'UserPersonalization';
fields?: Maybe<Scalars['JSON']>;
fontFamily?: Maybe<Scalars['String']>;
fontSize?: Maybe<Scalars['Int']>;
id?: Maybe<Scalars['ID']>;
@ -3511,6 +3514,7 @@ export type ResolversTypes = {
IntegrationsErrorCode: IntegrationsErrorCode;
IntegrationsResult: ResolversTypes['IntegrationsError'] | ResolversTypes['IntegrationsSuccess'];
IntegrationsSuccess: ResolverTypeWrapper<IntegrationsSuccess>;
JSON: ResolverTypeWrapper<Scalars['JSON']>;
JoinGroupError: ResolverTypeWrapper<JoinGroupError>;
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<ContextType = ResolverContext, ParentTy
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export interface JsonScalarConfig extends GraphQLScalarTypeConfig<ResolversTypes['JSON'], any> {
name: 'JSON';
}
export type JoinGroupErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['JoinGroupError'] = ResolversParentTypes['JoinGroupError']> = {
errorCodes?: Resolver<Array<ResolversTypes['JoinGroupErrorCode']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
@ -6108,6 +6117,7 @@ export type UserErrorResolvers<ContextType = ResolverContext, ParentType extends
};
export type UserPersonalizationResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['UserPersonalization'] = ResolversParentTypes['UserPersonalization']> = {
fields?: Resolver<Maybe<ResolversTypes['JSON']>, ParentType, ContextType>;
fontFamily?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
fontSize?: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;
id?: Resolver<Maybe<ResolversTypes['ID']>, ParentType, ContextType>;
@ -6314,6 +6324,7 @@ export type Resolvers<ContextType = ResolverContext> = {
IntegrationsError?: IntegrationsErrorResolvers<ContextType>;
IntegrationsResult?: IntegrationsResultResolvers<ContextType>;
IntegrationsSuccess?: IntegrationsSuccessResolvers<ContextType>;
JSON?: GraphQLScalarType;
JoinGroupError?: JoinGroupErrorResolvers<ContextType>;
JoinGroupResult?: JoinGroupResultResolvers<ContextType>;
JoinGroupSuccess?: JoinGroupSuccessResolvers<ContextType>;

View File

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

View File

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

View File

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

View File

@ -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<UserPersonalization>
) => {
return authTrx(
(t) => t.getRepository(UserPersonalization).save(userPersonalization),
undefined,
userId
)
}

View File

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

View File

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

View File

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