mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Add recommend gql api
This commit is contained in:
parent
0e3d1ced46
commit
7707f100f2
10 changed files with 207 additions and 15 deletions
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import { Invite } from './invite'
|
|||
@Entity()
|
||||
export class GroupMembership {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id?: string
|
||||
id!: string
|
||||
|
||||
@OneToOne(() => User)
|
||||
@JoinColumn()
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue