Add recommend gql api

This commit is contained in:
Hongbo Wu 2022-12-02 19:45:26 +08:00
parent 0e3d1ced46
commit 7707f100f2
10 changed files with 207 additions and 15 deletions

View file

@ -1,10 +1,10 @@
import { Group, Page, PageContext } from './types'
import { Page, PageContext, Recommendation } from './types'
import { createPage, getPageByParam, updatePage } from './pages'
export const addRecommendation = async (
ctx: PageContext,
page: Page,
group: Group
recommendation: Recommendation
): Promise<string | undefined> => {
try {
const userId = ctx.uid
@ -15,7 +15,9 @@ export const addRecommendation = async (
})
if (existingPage) {
// update recommendedBy in the existing page
const recommendedBy = (existingPage.recommendedBy || []).concat(group)
const recommendedBy = (existingPage.recommendedBy || []).concat(
recommendation
)
await updatePage(
existingPage.id,
@ -30,7 +32,7 @@ export const addRecommendation = async (
// create a new page
const newPage = {
...page,
recommendedBy: [group],
recommendedBy: [recommendation],
userId,
}

View file

@ -188,7 +188,7 @@ export interface Highlight {
highlightPositionAnchorIndex?: number | null
}
export interface Group {
export interface Recommendation {
id: string
name: string
recommendedAt: Date
@ -230,7 +230,7 @@ export interface Page {
readAt?: Date
listenedAt?: Date
wordsCount?: number
recommendedBy?: Group[]
recommendedBy?: Recommendation[]
}
export interface SearchItem {
@ -263,6 +263,7 @@ export interface SearchItem {
wordsCount?: number
siteName?: string
siteIcon?: string
recommendedBy?: Recommendation[]
}
const keys = ['_id', 'url', 'slug', 'userId', 'uploadFileId', 'state'] as const

View file

@ -16,7 +16,7 @@ import { Invite } from './invite'
@Entity()
export class GroupMembership {
@PrimaryGeneratedColumn('uuid')
id?: string
id!: string
@OneToOne(() => User)
@JoinColumn()

View file

@ -1132,6 +1132,7 @@ export type Mutation = {
moveFilter: MoveFilterResult;
moveLabel: MoveLabelResult;
optInFeature: OptInFeatureResult;
recommend: RecommendResult;
reportItem: ReportItemResult;
revokeApiKey: RevokeApiKeyResult;
saveArticleReadingProgress: SaveArticleReadingProgressResult;
@ -1301,6 +1302,11 @@ export type MutationOptInFeatureArgs = {
};
export type MutationRecommendArgs = {
input: RecommendInput;
};
export type MutationReportItemArgs = {
input: ReportItemInput;
};
@ -1743,6 +1749,29 @@ export type RecentSearchesSuccess = {
searches: Array<RecentSearch>;
};
export type RecommendError = {
__typename?: 'RecommendError';
errorCodes: Array<RecommendErrorCode>;
};
export enum RecommendErrorCode {
BadRequest = 'BAD_REQUEST',
NotFound = 'NOT_FOUND',
Unauthorized = 'UNAUTHORIZED'
}
export type RecommendInput = {
groupIds: Array<Scalars['ID']>;
pageId: Scalars['ID'];
};
export type RecommendResult = RecommendError | RecommendSuccess;
export type RecommendSuccess = {
__typename?: 'RecommendSuccess';
taskNames: Array<Scalars['String']>;
};
export type RecommendationGroup = {
__typename?: 'RecommendationGroup';
admins: Array<User>;
@ -3157,6 +3186,11 @@ export type ResolversTypes = {
RecentSearchesErrorCode: RecentSearchesErrorCode;
RecentSearchesResult: ResolversTypes['RecentSearchesError'] | ResolversTypes['RecentSearchesSuccess'];
RecentSearchesSuccess: ResolverTypeWrapper<RecentSearchesSuccess>;
RecommendError: ResolverTypeWrapper<RecommendError>;
RecommendErrorCode: RecommendErrorCode;
RecommendInput: RecommendInput;
RecommendResult: ResolversTypes['RecommendError'] | ResolversTypes['RecommendSuccess'];
RecommendSuccess: ResolverTypeWrapper<RecommendSuccess>;
RecommendationGroup: ResolverTypeWrapper<RecommendationGroup>;
Reminder: ResolverTypeWrapper<Reminder>;
ReminderError: ResolverTypeWrapper<ReminderError>;
@ -3550,6 +3584,10 @@ export type ResolversParentTypes = {
RecentSearchesError: RecentSearchesError;
RecentSearchesResult: ResolversParentTypes['RecentSearchesError'] | ResolversParentTypes['RecentSearchesSuccess'];
RecentSearchesSuccess: RecentSearchesSuccess;
RecommendError: RecommendError;
RecommendInput: RecommendInput;
RecommendResult: ResolversParentTypes['RecommendError'] | ResolversParentTypes['RecommendSuccess'];
RecommendSuccess: RecommendSuccess;
RecommendationGroup: RecommendationGroup;
Reminder: Reminder;
ReminderError: ReminderError;
@ -4547,6 +4585,7 @@ export type MutationResolvers<ContextType = ResolverContext, ParentType extends
moveFilter?: Resolver<ResolversTypes['MoveFilterResult'], ParentType, ContextType, RequireFields<MutationMoveFilterArgs, 'input'>>;
moveLabel?: Resolver<ResolversTypes['MoveLabelResult'], ParentType, ContextType, RequireFields<MutationMoveLabelArgs, 'input'>>;
optInFeature?: Resolver<ResolversTypes['OptInFeatureResult'], ParentType, ContextType, RequireFields<MutationOptInFeatureArgs, 'input'>>;
recommend?: Resolver<ResolversTypes['RecommendResult'], ParentType, ContextType, RequireFields<MutationRecommendArgs, 'input'>>;
reportItem?: Resolver<ResolversTypes['ReportItemResult'], ParentType, ContextType, RequireFields<MutationReportItemArgs, 'input'>>;
revokeApiKey?: Resolver<ResolversTypes['RevokeApiKeyResult'], ParentType, ContextType, RequireFields<MutationRevokeApiKeyArgs, 'id'>>;
saveArticleReadingProgress?: Resolver<ResolversTypes['SaveArticleReadingProgressResult'], ParentType, ContextType, RequireFields<MutationSaveArticleReadingProgressArgs, 'input'>>;
@ -4722,6 +4761,20 @@ export type RecentSearchesSuccessResolvers<ContextType = ResolverContext, Parent
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type RecommendErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['RecommendError'] = ResolversParentTypes['RecommendError']> = {
errorCodes?: Resolver<Array<ResolversTypes['RecommendErrorCode']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type RecommendResultResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['RecommendResult'] = ResolversParentTypes['RecommendResult']> = {
__resolveType: TypeResolveFn<'RecommendError' | 'RecommendSuccess', ParentType, ContextType>;
};
export type RecommendSuccessResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['RecommendSuccess'] = ResolversParentTypes['RecommendSuccess']> = {
taskNames?: Resolver<Array<ResolversTypes['String']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type RecommendationGroupResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['RecommendationGroup'] = ResolversParentTypes['RecommendationGroup']> = {
admins?: Resolver<Array<ResolversTypes['User']>, ParentType, ContextType>;
createdAt?: Resolver<ResolversTypes['Date'], ParentType, ContextType>;
@ -5591,6 +5644,9 @@ export type Resolvers<ContextType = ResolverContext> = {
RecentSearchesError?: RecentSearchesErrorResolvers<ContextType>;
RecentSearchesResult?: RecentSearchesResultResolvers<ContextType>;
RecentSearchesSuccess?: RecentSearchesSuccessResolvers<ContextType>;
RecommendError?: RecommendErrorResolvers<ContextType>;
RecommendResult?: RecommendResultResolvers<ContextType>;
RecommendSuccess?: RecommendSuccessResolvers<ContextType>;
RecommendationGroup?: RecommendationGroupResolvers<ContextType>;
Reminder?: ReminderResolvers<ContextType>;
ReminderError?: ReminderErrorResolvers<ContextType>;

View file

@ -1010,6 +1010,7 @@ type Mutation {
moveFilter(input: MoveFilterInput!): MoveFilterResult!
moveLabel(input: MoveLabelInput!): MoveLabelResult!
optInFeature(input: OptInFeatureInput!): OptInFeatureResult!
recommend(input: RecommendInput!): RecommendResult!
reportItem(input: ReportItemInput!): ReportItemResult!
revokeApiKey(id: ID!): RevokeApiKeyResult!
saveArticleReadingProgress(input: SaveArticleReadingProgressInput!): SaveArticleReadingProgressResult!
@ -1219,6 +1220,27 @@ type RecentSearchesSuccess {
searches: [RecentSearch!]!
}
type RecommendError {
errorCodes: [RecommendErrorCode!]!
}
enum RecommendErrorCode {
BAD_REQUEST
NOT_FOUND
UNAUTHORIZED
}
input RecommendInput {
groupIds: [ID!]!
pageId: ID!
}
union RecommendResult = RecommendError | RecommendSuccess
type RecommendSuccess {
taskNames: [String!]!
}
type RecommendationGroup {
admins: [User!]!
createdAt: Date!

View file

@ -63,6 +63,7 @@ import {
moveFilterResolver,
moveLabelResolver,
newsletterEmailsResolver,
recommendResolver,
reminderResolver,
reportItemResolver,
revokeApiKeyResolver,
@ -189,6 +190,7 @@ export const functionResolvers = {
deleteFilter: deleteFilterResolver,
moveFilter: moveFilterResolver,
createGroup: createGroupResolver,
recommend: recommendResolver,
},
Query: {
me: getMeUserResolver,
@ -640,4 +642,5 @@ export const functionResolvers = {
...resultResolveTypeResolver('MoveFilter'),
...resultResolveTypeResolver('CreateGroup'),
...resultResolveTypeResolver('Groups'),
...resultResolveTypeResolver('Recommend'),
}

View file

@ -6,6 +6,10 @@ import {
GroupsErrorCode,
GroupsSuccess,
MutationCreateGroupArgs,
MutationRecommendArgs,
RecommendError,
RecommendErrorCode,
RecommendSuccess,
} from '../../generated/graphql'
import {
createGroup,
@ -15,6 +19,10 @@ import {
import { authorized, userDataToUser } from '../../utils/helpers'
import { getRepository } from '../../entity/utils'
import { User } from '../../entity/user'
import { Group } from '../../entity/groups/group'
import { In } from 'typeorm'
import { getPageByParam } from '../../elastic/pages'
import { enqueueRecommendation } from '../../utils/createTask'
export const createGroupResolver = authorized<
CreateGroupSuccess,
@ -116,3 +124,77 @@ export const groupsResolver = authorized<GroupsSuccess, GroupsError>(
}
}
)
export const recommendResolver = authorized<
RecommendSuccess,
RecommendError,
MutationRecommendArgs
>(async (_, { input }, { claims: { uid }, log }) => {
log.info('Recommend', {
input,
labels: {
source: 'resolver',
resolver: 'recommendResolver',
uid,
},
})
try {
const user = await getRepository(User).findOneBy({
id: uid,
})
if (!user) {
return {
errorCodes: [RecommendErrorCode.Unauthorized],
}
}
const groups = await getRepository(Group).find({
where: { id: In(input.groupIds) },
})
if (groups.length === 0) {
return {
errorCodes: [RecommendErrorCode.NotFound],
}
}
const page = await getPageByParam({ _id: input.pageId, userId: uid })
if (!page) {
return {
errorCodes: [RecommendErrorCode.NotFound],
}
}
const taskNames = await Promise.all(
groups
.map((group) =>
group.members
.filter((member) => member.id !== uid)
.map((member) =>
enqueueRecommendation(member.id, page.id, {
...group,
recommendedAt: new Date(),
})
)
)
.flat()
)
return {
taskNames,
}
} catch (error) {
log.error('Error recommending', {
error,
labels: {
source: 'resolver',
resolver: 'recommendResolver',
uid,
},
})
return {
errorCodes: [RecommendErrorCode.BadRequest],
}
}
})

View file

@ -29,7 +29,7 @@ import {
import { Claims } from '../resolvers/types'
import { createPage, getPageByParam, updatePage } from '../elastic/pages'
import { createPubSubClient } from '../datalayer/pubsub'
import { Group } from '../elastic/types'
import { Recommendation } from '../elastic/types'
import { addRecommendation } from '../elastic/recommendation'
const logger = buildLogger('app.dispatch')
@ -166,13 +166,13 @@ export function pageRouter() {
const {
userId: recommendedUserId,
pageId,
group,
recommendation,
} = req.body as {
userId: string
pageId: string
group: Group
recommendation: Recommendation
}
if (!recommendedUserId || !pageId || !group) {
if (!recommendedUserId || !pageId || !recommendation) {
return res.status(400).send({ errorCode: 'BAD_DATA' })
}
@ -189,7 +189,11 @@ export function pageRouter() {
return res.status(404).send({ errorCode: 'NOT_FOUND' })
}
const recommendedPageId = await addRecommendation(ctx, page, group)
const recommendedPageId = await addRecommendation(
ctx,
page,
recommendation
)
if (!recommendedPageId) {
logger.error('Failed to add recommendation to page')
return res.sendStatus(500)

View file

@ -2183,6 +2183,27 @@ const schema = gql`
BAD_REQUEST
}
input RecommendInput {
pageId: ID!
groupIds: [ID!]!
}
union RecommendResult = RecommendSuccess | RecommendError
type RecommendSuccess {
taskNames: [String!]!
}
type RecommendError {
errorCodes: [RecommendErrorCode!]!
}
enum RecommendErrorCode {
UNAUTHORIZED
BAD_REQUEST
NOT_FOUND
}
# Mutations
type Mutation {
googleLogin(input: GoogleLoginInput!): LoginResult!
@ -2261,6 +2282,7 @@ const schema = gql`
deleteFilter(id: ID!): DeleteFilterResult!
moveFilter(input: MoveFilterInput!): MoveFilterResult!
createGroup(input: CreateGroupInput!): CreateGroupResult!
recommend(input: RecommendInput!): RecommendResult!
}
# FIXME: remove sort from feedArticles after all cached tabs are closed

View file

@ -10,7 +10,7 @@ import { nanoid } from 'nanoid'
import { google } from '@google-cloud/tasks/build/protos/protos'
import { IntegrationType } from '../entity/integration'
import { signFeatureToken } from '../services/features'
import { Group } from '../elastic/types'
import { Recommendation } from '../elastic/types'
import View = google.cloud.tasks.v2.Task.View
const logger = buildLogger('app.dispatch')
@ -409,13 +409,13 @@ export const enqueueTextToSpeech = async ({
export const enqueueRecommendation = async (
userId: string,
pageId: string,
group: Group
recommendation: Recommendation
): Promise<string> => {
const { GOOGLE_CLOUD_PROJECT } = process.env
const payload = {
userId,
pageId,
group,
recommendation,
}
// If there is no Google Cloud Project Id exposed, it means that we are in local environment