Merge pull request #3086 from omnivore-app/feature/add-fields-to-user-personalization
feature/add fields to user personalization
This commit is contained in:
@ -53,4 +53,7 @@ export class UserPersonalization {
|
||||
|
||||
@UpdateDateColumn({ default: () => 'CURRENT_TIMESTAMP' })
|
||||
updatedAt!: Date
|
||||
|
||||
@Column('json')
|
||||
fields?: any | null
|
||||
}
|
||||
|
||||
@ -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>;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 } }
|
||||
})
|
||||
|
||||
@ -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
|
||||
|
||||
36
packages/api/src/services/user_personalization.ts
Normal file
36
packages/api/src/services/user_personalization.ts
Normal 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
|
||||
)
|
||||
}
|
||||
159
packages/api/test/resolvers/user_personalization.test.ts
Normal file
159
packages/api/test/resolvers/user_personalization.test.ts
Normal 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,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
9
packages/db/migrations/0145.do.add_fields_to_user_personalization.sql
Executable file
9
packages/db/migrations/0145.do.add_fields_to_user_personalization.sql
Executable 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;
|
||||
9
packages/db/migrations/0145.undo.add_fields_to_user_personalization.sql
Executable file
9
packages/db/migrations/0145.undo.add_fields_to_user_personalization.sql
Executable 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;
|
||||
Reference in New Issue
Block a user