From 7f04a381a1ac988a2eacfdac5d8c1deb9e1f9d71 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Thu, 1 Dec 2022 17:47:12 +0800 Subject: [PATCH 1/4] Add is_admin field to group_membership table --- packages/api/src/entity/groups/group_membership.ts | 4 ++++ .../0101.do.add_is_admin_to_group_membership.sql | 9 +++++++++ .../0101.undo.add_is_admin_to_group_membership.sql | 9 +++++++++ 3 files changed, 22 insertions(+) create mode 100755 packages/db/migrations/0101.do.add_is_admin_to_group_membership.sql create mode 100755 packages/db/migrations/0101.undo.add_is_admin_to_group_membership.sql diff --git a/packages/api/src/entity/groups/group_membership.ts b/packages/api/src/entity/groups/group_membership.ts index 1cb1e537c..7362f8edc 100644 --- a/packages/api/src/entity/groups/group_membership.ts +++ b/packages/api/src/entity/groups/group_membership.ts @@ -1,4 +1,5 @@ import { + Column, CreateDateColumn, Entity, JoinColumn, @@ -33,4 +34,7 @@ export class GroupMembership { @UpdateDateColumn() updatedAt?: Date + + @Column('boolean', { default: false }) + isAdmin!: boolean } diff --git a/packages/db/migrations/0101.do.add_is_admin_to_group_membership.sql b/packages/db/migrations/0101.do.add_is_admin_to_group_membership.sql new file mode 100755 index 000000000..2967aa992 --- /dev/null +++ b/packages/db/migrations/0101.do.add_is_admin_to_group_membership.sql @@ -0,0 +1,9 @@ +-- Type: DO +-- Name: add_is_admin_to_group_membership +-- Description: Add is_admin field to group_membership table + +BEGIN; + +ALTER TABLE omnivore.group_membership ADD COLUMN is_admin BOOLEAN NOT NULL DEFAULT FALSE; + +COMMIT; diff --git a/packages/db/migrations/0101.undo.add_is_admin_to_group_membership.sql b/packages/db/migrations/0101.undo.add_is_admin_to_group_membership.sql new file mode 100755 index 000000000..07ed5affb --- /dev/null +++ b/packages/db/migrations/0101.undo.add_is_admin_to_group_membership.sql @@ -0,0 +1,9 @@ +-- Type: UNDO +-- Name: add_is_admin_to_group_membership +-- Description: Add is_admin field to group_membership table + +BEGIN; + +ALTER TABLE omnivore.group_membership DROP COLUMN IF EXISTS is_admin; + +COMMIT; From 7285ebb940394eeb58c7329090cd16008ca6e83f Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Thu, 1 Dec 2022 18:41:00 +0800 Subject: [PATCH 2/4] Add createGroup API --- packages/api/src/entity/groups/group.ts | 6 +- packages/api/src/entity/profile.ts | 3 + packages/api/src/generated/graphql.ts | 81 +++++++++++++++++++ packages/api/src/generated/schema.graphql | 32 ++++++++ .../api/src/resolvers/function_resolvers.ts | 3 + packages/api/src/resolvers/index.ts | 1 + .../src/resolvers/recommendations/index.ts | 70 ++++++++++++++++ packages/api/src/schema.ts | 32 ++++++++ packages/api/src/services/create_group.ts | 7 +- 9 files changed, 229 insertions(+), 6 deletions(-) create mode 100644 packages/api/src/resolvers/recommendations/index.ts diff --git a/packages/api/src/entity/groups/group.ts b/packages/api/src/entity/groups/group.ts index bce8d2234..4d300d762 100644 --- a/packages/api/src/entity/groups/group.ts +++ b/packages/api/src/entity/groups/group.ts @@ -13,7 +13,7 @@ import { User } from '../user' @Entity() export class Group { @PrimaryGeneratedColumn('uuid') - id?: string + id!: string @Column('text') name!: string @@ -23,8 +23,8 @@ export class Group { createdBy!: User @CreateDateColumn() - createdAt?: Date + createdAt!: Date @UpdateDateColumn() - updatedAt?: Date + updatedAt!: Date } diff --git a/packages/api/src/entity/profile.ts b/packages/api/src/entity/profile.ts index a30d057cf..b5f04b48d 100644 --- a/packages/api/src/entity/profile.ts +++ b/packages/api/src/entity/profile.ts @@ -33,4 +33,7 @@ export class Profile { @UpdateDateColumn() updatedAt!: Date + + @Column('boolean', { default: false }) + private!: boolean } diff --git a/packages/api/src/generated/graphql.ts b/packages/api/src/generated/graphql.ts index 946be9a8c..e63a31b11 100644 --- a/packages/api/src/generated/graphql.ts +++ b/packages/api/src/generated/graphql.ts @@ -275,6 +275,29 @@ export type CreateArticleSuccess = { user: User; }; +export type CreateGroupError = { + __typename?: 'CreateGroupError'; + errorCodes: Array; +}; + +export enum CreateGroupErrorCode { + BadRequest = 'BAD_REQUEST', + Unauthorized = 'UNAUTHORIZED' +} + +export type CreateGroupInput = { + expiresInDays?: InputMaybe; + maxMembers?: InputMaybe; + name: Scalars['String']; +}; + +export type CreateGroupResult = CreateGroupError | CreateGroupSuccess; + +export type CreateGroupSuccess = { + __typename?: 'CreateGroupSuccess'; + group: RecommendationGroup; +}; + export type CreateHighlightError = { __typename?: 'CreateHighlightError'; errorCodes: Array; @@ -1066,6 +1089,7 @@ export type Mutation = { addPopularRead: AddPopularReadResult; createArticle: CreateArticleResult; createArticleSavingRequest: CreateArticleSavingRequestResult; + createGroup: CreateGroupResult; createHighlight: CreateHighlightResult; createHighlightReply: CreateHighlightReplyResult; createLabel: CreateLabelResult; @@ -1140,6 +1164,11 @@ export type MutationCreateArticleSavingRequestArgs = { }; +export type MutationCreateGroupArgs = { + input: CreateGroupInput; +}; + + export type MutationCreateHighlightArgs = { input: CreateHighlightInput; }; @@ -1696,6 +1725,17 @@ export type RecentSearchesSuccess = { searches: Array; }; +export type RecommendationGroup = { + __typename?: 'RecommendationGroup'; + admins: Array; + createdAt: Scalars['Date']; + id: Scalars['ID']; + inviteUrl: Scalars['String']; + members: Array; + name: Scalars['String']; + updatedAt: Scalars['Date']; +}; + export type Reminder = { __typename?: 'Reminder'; archiveUntil: Scalars['Boolean']; @@ -2909,6 +2949,11 @@ export type ResolversTypes = { CreateArticleSavingRequestResult: ResolversTypes['CreateArticleSavingRequestError'] | ResolversTypes['CreateArticleSavingRequestSuccess']; CreateArticleSavingRequestSuccess: ResolverTypeWrapper; CreateArticleSuccess: ResolverTypeWrapper; + CreateGroupError: ResolverTypeWrapper; + CreateGroupErrorCode: CreateGroupErrorCode; + CreateGroupInput: CreateGroupInput; + CreateGroupResult: ResolversTypes['CreateGroupError'] | ResolversTypes['CreateGroupSuccess']; + CreateGroupSuccess: ResolverTypeWrapper; CreateHighlightError: ResolverTypeWrapper; CreateHighlightErrorCode: CreateHighlightErrorCode; CreateHighlightInput: CreateHighlightInput; @@ -3090,6 +3135,7 @@ export type ResolversTypes = { RecentSearchesErrorCode: RecentSearchesErrorCode; RecentSearchesResult: ResolversTypes['RecentSearchesError'] | ResolversTypes['RecentSearchesSuccess']; RecentSearchesSuccess: ResolverTypeWrapper; + RecommendationGroup: ResolverTypeWrapper; Reminder: ResolverTypeWrapper; ReminderError: ResolverTypeWrapper; ReminderErrorCode: ReminderErrorCode; @@ -3331,6 +3377,10 @@ export type ResolversParentTypes = { CreateArticleSavingRequestResult: ResolversParentTypes['CreateArticleSavingRequestError'] | ResolversParentTypes['CreateArticleSavingRequestSuccess']; CreateArticleSavingRequestSuccess: CreateArticleSavingRequestSuccess; CreateArticleSuccess: CreateArticleSuccess; + CreateGroupError: CreateGroupError; + CreateGroupInput: CreateGroupInput; + CreateGroupResult: ResolversParentTypes['CreateGroupError'] | ResolversParentTypes['CreateGroupSuccess']; + CreateGroupSuccess: CreateGroupSuccess; CreateHighlightError: CreateHighlightError; CreateHighlightInput: CreateHighlightInput; CreateHighlightReplyError: CreateHighlightReplyError; @@ -3475,6 +3525,7 @@ export type ResolversParentTypes = { RecentSearchesError: RecentSearchesError; RecentSearchesResult: ResolversParentTypes['RecentSearchesError'] | ResolversParentTypes['RecentSearchesSuccess']; RecentSearchesSuccess: RecentSearchesSuccess; + RecommendationGroup: RecommendationGroup; Reminder: Reminder; ReminderError: ReminderError; ReminderResult: ResolversParentTypes['ReminderError'] | ResolversParentTypes['ReminderSuccess']; @@ -3830,6 +3881,20 @@ export type CreateArticleSuccessResolvers; }; +export type CreateGroupErrorResolvers = { + errorCodes?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type CreateGroupResultResolvers = { + __resolveType: TypeResolveFn<'CreateGroupError' | 'CreateGroupSuccess', ParentType, ContextType>; +}; + +export type CreateGroupSuccessResolvers = { + group?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type CreateHighlightErrorResolvers = { errorCodes?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; @@ -4417,6 +4482,7 @@ export type MutationResolvers>; createArticle?: Resolver>; createArticleSavingRequest?: Resolver>; + createGroup?: Resolver>; createHighlight?: Resolver>; createHighlightReply?: Resolver>; createLabel?: Resolver>; @@ -4616,6 +4682,17 @@ export type RecentSearchesSuccessResolvers; }; +export type RecommendationGroupResolvers = { + admins?: Resolver, ParentType, ContextType>; + createdAt?: Resolver; + id?: Resolver; + inviteUrl?: Resolver; + members?: Resolver, ParentType, ContextType>; + name?: Resolver; + updatedAt?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type ReminderResolvers = { archiveUntil?: Resolver; id?: Resolver; @@ -5341,6 +5418,9 @@ export type Resolvers = { CreateArticleSavingRequestResult?: CreateArticleSavingRequestResultResolvers; CreateArticleSavingRequestSuccess?: CreateArticleSavingRequestSuccessResolvers; CreateArticleSuccess?: CreateArticleSuccessResolvers; + CreateGroupError?: CreateGroupErrorResolvers; + CreateGroupResult?: CreateGroupResultResolvers; + CreateGroupSuccess?: CreateGroupSuccessResolvers; CreateHighlightError?: CreateHighlightErrorResolvers; CreateHighlightReplyError?: CreateHighlightReplyErrorResolvers; CreateHighlightReplyResult?: CreateHighlightReplyResultResolvers; @@ -5468,6 +5548,7 @@ export type Resolvers = { RecentSearchesError?: RecentSearchesErrorResolvers; RecentSearchesResult?: RecentSearchesResultResolvers; RecentSearchesSuccess?: RecentSearchesSuccessResolvers; + RecommendationGroup?: RecommendationGroupResolvers; Reminder?: ReminderResolvers; ReminderError?: ReminderErrorResolvers; ReminderResult?: ReminderResultResolvers; diff --git a/packages/api/src/generated/schema.graphql b/packages/api/src/generated/schema.graphql index 62eee676c..6c024eefa 100644 --- a/packages/api/src/generated/schema.graphql +++ b/packages/api/src/generated/schema.graphql @@ -232,6 +232,27 @@ type CreateArticleSuccess { user: User! } +type CreateGroupError { + errorCodes: [CreateGroupErrorCode!]! +} + +enum CreateGroupErrorCode { + BAD_REQUEST + UNAUTHORIZED +} + +input CreateGroupInput { + expiresInDays: Int + maxMembers: Int + name: String! +} + +union CreateGroupResult = CreateGroupError | CreateGroupSuccess + +type CreateGroupSuccess { + group: RecommendationGroup! +} + type CreateHighlightError { errorCodes: [CreateHighlightErrorCode!]! } @@ -948,6 +969,7 @@ type Mutation { addPopularRead(name: String!): AddPopularReadResult! createArticle(input: CreateArticleInput!): CreateArticleResult! createArticleSavingRequest(input: CreateArticleSavingRequestInput!): CreateArticleSavingRequestResult! + createGroup(input: CreateGroupInput!): CreateGroupResult! createHighlight(input: CreateHighlightInput!): CreateHighlightResult! createHighlightReply(input: CreateHighlightReplyInput!): CreateHighlightReplyResult! createLabel(input: CreateLabelInput!): CreateLabelResult! @@ -1181,6 +1203,16 @@ type RecentSearchesSuccess { searches: [RecentSearch!]! } +type RecommendationGroup { + admins: [User!]! + createdAt: Date! + id: ID! + inviteUrl: String! + members: [User!]! + name: String! + updatedAt: Date! +} + type Reminder { archiveUntil: Boolean! id: ID! diff --git a/packages/api/src/resolvers/function_resolvers.ts b/packages/api/src/resolvers/function_resolvers.ts index 2648160a7..c23b683a7 100644 --- a/packages/api/src/resolvers/function_resolvers.ts +++ b/packages/api/src/resolvers/function_resolvers.ts @@ -26,6 +26,7 @@ import { articleSavingRequestResolver, createArticleResolver, createArticleSavingRequestResolver, + createGroupResolver, createHighlightResolver, createLabelResolver, createNewsletterEmailResolver, @@ -186,6 +187,7 @@ export const functionResolvers = { saveFilter: saveFilterResolver, deleteFilter: deleteFilterResolver, moveFilter: moveFilterResolver, + createGroup: createGroupResolver, }, Query: { me: getMeUserResolver, @@ -634,4 +636,5 @@ export const functionResolvers = { ...resultResolveTypeResolver('Filters'), ...resultResolveTypeResolver('DeleteFilter'), ...resultResolveTypeResolver('MoveFilter'), + ...resultResolveTypeResolver('CreateGroup'), } diff --git a/packages/api/src/resolvers/index.ts b/packages/api/src/resolvers/index.ts index ee324050e..f1cf2d8e2 100644 --- a/packages/api/src/resolvers/index.ts +++ b/packages/api/src/resolvers/index.ts @@ -23,3 +23,4 @@ export * from './api_key' export * from './integrations' export * from './rules' export * from './filters' +export * from './recommendations' diff --git a/packages/api/src/resolvers/recommendations/index.ts b/packages/api/src/resolvers/recommendations/index.ts new file mode 100644 index 000000000..a040188f6 --- /dev/null +++ b/packages/api/src/resolvers/recommendations/index.ts @@ -0,0 +1,70 @@ +import { + CreateGroupError, + CreateGroupErrorCode, + CreateGroupSuccess, + MutationCreateGroupArgs, +} from '../../generated/graphql' +import { createGroup } from '../../services/create_group' +import { authorized, userDataToUser } from '../../utils/helpers' +import { homePageURL } from '../../env' +import { getRepository } from '../../entity/utils' +import { User } from '../../entity/user' + +export const createGroupResolver = authorized< + CreateGroupSuccess, + CreateGroupError, + MutationCreateGroupArgs +>(async (_, { input }, { claims: { uid }, log }) => { + log.info('Creating group', { + input, + labels: { + source: 'resolver', + resolver: 'createGroupResolver', + uid, + }, + }) + + try { + const userData = await getRepository(User).findOne({ + where: { id: uid }, + relations: ['profile'], + }) + if (!userData) { + return { + errorCodes: [CreateGroupErrorCode.Unauthorized], + } + } + + const [group, invite] = await createGroup({ + admin: userData, + name: input.name, + maxMembers: input.maxMembers, + expiresInDays: input.expiresInDays, + }) + + const inviteUrl = `${homePageURL()}/invite/${invite.code}` + const user = userDataToUser(userData) + + return { + group: { + ...group, + inviteUrl, + admins: [user], + members: [user], + }, + } + } catch (error) { + log.error('Error creating group', { + error, + labels: { + source: 'resolver', + resolver: 'createGroupResolver', + uid, + }, + }) + + return { + errorCodes: [CreateGroupErrorCode.BadRequest], + } + } +}) diff --git a/packages/api/src/schema.ts b/packages/api/src/schema.ts index 13e67f122..4da7c1c8d 100755 --- a/packages/api/src/schema.ts +++ b/packages/api/src/schema.ts @@ -2137,6 +2137,37 @@ const schema = gql` NOT_FOUND } + input CreateGroupInput { + name: String! @sanitize(maxLength: 140) + maxMembers: Int + expiresInDays: Int + } + + union CreateGroupResult = CreateGroupSuccess | CreateGroupError + + type CreateGroupSuccess { + group: RecommendationGroup! + } + + type RecommendationGroup { + id: ID! + name: String! + inviteUrl: String! + admins: [User!]! + members: [User!]! + createdAt: Date! + updatedAt: Date! + } + + type CreateGroupError { + errorCodes: [CreateGroupErrorCode!]! + } + + enum CreateGroupErrorCode { + UNAUTHORIZED + BAD_REQUEST + } + # Mutations type Mutation { googleLogin(input: GoogleLoginInput!): LoginResult! @@ -2214,6 +2245,7 @@ const schema = gql` saveFilter(input: SaveFilterInput!): SaveFilterResult! deleteFilter(id: ID!): DeleteFilterResult! moveFilter(input: MoveFilterInput!): MoveFilterResult! + createGroup(input: CreateGroupInput!): CreateGroupResult! } # FIXME: remove sort from feedArticles after all cached tabs are closed diff --git a/packages/api/src/services/create_group.ts b/packages/api/src/services/create_group.ts index a8897460b..6a50cd130 100644 --- a/packages/api/src/services/create_group.ts +++ b/packages/api/src/services/create_group.ts @@ -8,8 +8,8 @@ import { AppDataSource } from '../server' export const createGroup = async (input: { admin: User name: string - maxMembers?: number - expiresInDays?: number + maxMembers?: number | null + expiresInDays?: number | null }): Promise<[Group, Invite]> => { const [group, invite] = await AppDataSource.transaction<[Group, Invite]>( async (t) => { @@ -28,7 +28,7 @@ export const createGroup = async (input: { group, code, createdBy: input.admin, - maxMembers: input.maxMembers || 50, + maxMembers: input.maxMembers || 12, expirationTime: expirationTime, }) // Add the admin to the group as its first user @@ -36,6 +36,7 @@ export const createGroup = async (input: { user: input.admin, group, invite, + isAdmin: true, }) return [group, invite] } From da3d9cf76b7016e2370c321ae756d603992b3e88 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Fri, 2 Dec 2022 12:32:26 +0800 Subject: [PATCH 3/4] Add groups api --- packages/api/src/entity/groups/group.ts | 5 ++ .../api/src/entity/groups/group_membership.ts | 3 +- packages/api/src/generated/graphql.ts | 43 +++++++++++++++ packages/api/src/generated/schema.graphql | 16 ++++++ .../api/src/resolvers/function_resolvers.ts | 3 ++ .../src/resolvers/recommendations/index.ts | 54 +++++++++++++++++-- packages/api/src/schema.ts | 16 ++++++ packages/api/src/services/create_group.ts | 39 ++++++++++++++ 8 files changed, 175 insertions(+), 4 deletions(-) diff --git a/packages/api/src/entity/groups/group.ts b/packages/api/src/entity/groups/group.ts index 4d300d762..4aa22e1ee 100644 --- a/packages/api/src/entity/groups/group.ts +++ b/packages/api/src/entity/groups/group.ts @@ -3,12 +3,14 @@ import { CreateDateColumn, Entity, JoinColumn, + OneToMany, OneToOne, PrimaryGeneratedColumn, UpdateDateColumn, } from 'typeorm' import { User } from '../user' +import { GroupMembership } from './group_membership' @Entity() export class Group { @@ -27,4 +29,7 @@ export class Group { @UpdateDateColumn() updatedAt!: Date + + @OneToMany(() => GroupMembership, (groupMembership) => groupMembership.group) + members!: GroupMembership[] } diff --git a/packages/api/src/entity/groups/group_membership.ts b/packages/api/src/entity/groups/group_membership.ts index 7362f8edc..30b92d943 100644 --- a/packages/api/src/entity/groups/group_membership.ts +++ b/packages/api/src/entity/groups/group_membership.ts @@ -3,6 +3,7 @@ import { CreateDateColumn, Entity, JoinColumn, + ManyToOne, OneToOne, PrimaryGeneratedColumn, UpdateDateColumn, @@ -21,7 +22,7 @@ export class GroupMembership { @JoinColumn() user!: User - @OneToOne(() => Group) + @ManyToOne(() => Group, (group) => group.members) @JoinColumn() group!: Group diff --git a/packages/api/src/generated/graphql.ts b/packages/api/src/generated/graphql.ts index e63a31b11..f9db0f7e3 100644 --- a/packages/api/src/generated/graphql.ts +++ b/packages/api/src/generated/graphql.ts @@ -847,6 +847,23 @@ export type GoogleSignupSuccess = { me: User; }; +export type GroupsError = { + __typename?: 'GroupsError'; + errorCodes: Array; +}; + +export enum GroupsErrorCode { + BadRequest = 'BAD_REQUEST', + Unauthorized = 'UNAUTHORIZED' +} + +export type GroupsResult = GroupsError | GroupsSuccess; + +export type GroupsSuccess = { + __typename?: 'GroupsSuccess'; + groups: Array; +}; + export type Highlight = { __typename?: 'Highlight'; annotation?: Maybe; @@ -1556,6 +1573,7 @@ export type Query = { getFollowers: GetFollowersResult; getFollowing: GetFollowingResult; getUserPersonalization: GetUserPersonalizationResult; + groups: GroupsResult; hello?: Maybe; integrations: IntegrationsResult; labels: LabelsResult; @@ -3068,6 +3086,10 @@ export type ResolversTypes = { GoogleSignupInput: GoogleSignupInput; GoogleSignupResult: ResolversTypes['GoogleSignupError'] | ResolversTypes['GoogleSignupSuccess']; GoogleSignupSuccess: ResolverTypeWrapper; + GroupsError: ResolverTypeWrapper; + GroupsErrorCode: GroupsErrorCode; + GroupsResult: ResolversTypes['GroupsError'] | ResolversTypes['GroupsSuccess']; + GroupsSuccess: ResolverTypeWrapper; Highlight: ResolverTypeWrapper; HighlightReply: ResolverTypeWrapper; HighlightStats: ResolverTypeWrapper; @@ -3471,6 +3493,9 @@ export type ResolversParentTypes = { GoogleSignupInput: GoogleSignupInput; GoogleSignupResult: ResolversParentTypes['GoogleSignupError'] | ResolversParentTypes['GoogleSignupSuccess']; GoogleSignupSuccess: GoogleSignupSuccess; + GroupsError: GroupsError; + GroupsResult: ResolversParentTypes['GroupsError'] | ResolversParentTypes['GroupsSuccess']; + GroupsSuccess: GroupsSuccess; Highlight: Highlight; HighlightReply: HighlightReply; HighlightStats: HighlightStats; @@ -4299,6 +4324,20 @@ export type GoogleSignupSuccessResolvers; }; +export type GroupsErrorResolvers = { + errorCodes?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type GroupsResultResolvers = { + __resolveType: TypeResolveFn<'GroupsError' | 'GroupsSuccess', ParentType, ContextType>; +}; + +export type GroupsSuccessResolvers = { + groups?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type HighlightResolvers = { annotation?: Resolver, ParentType, ContextType>; createdAt?: Resolver; @@ -4623,6 +4662,7 @@ export type QueryResolvers>; getFollowing?: Resolver>; getUserPersonalization?: Resolver; + groups?: Resolver; hello?: Resolver, ParentType, ContextType>; integrations?: Resolver; labels?: Resolver; @@ -5502,6 +5542,9 @@ export type Resolvers = { GoogleSignupError?: GoogleSignupErrorResolvers; GoogleSignupResult?: GoogleSignupResultResolvers; GoogleSignupSuccess?: GoogleSignupSuccessResolvers; + GroupsError?: GroupsErrorResolvers; + GroupsResult?: GroupsResultResolvers; + GroupsSuccess?: GroupsSuccessResolvers; Highlight?: HighlightResolvers; HighlightReply?: HighlightReplyResolvers; HighlightStats?: HighlightStatsResolvers; diff --git a/packages/api/src/generated/schema.graphql b/packages/api/src/generated/schema.graphql index 6c024eefa..447b3a776 100644 --- a/packages/api/src/generated/schema.graphql +++ b/packages/api/src/generated/schema.graphql @@ -749,6 +749,21 @@ type GoogleSignupSuccess { me: User! } +type GroupsError { + errorCodes: [GroupsErrorCode!]! +} + +enum GroupsErrorCode { + BAD_REQUEST + UNAUTHORIZED +} + +union GroupsResult = GroupsError | GroupsSuccess + +type GroupsSuccess { + groups: [RecommendationGroup!]! +} + type Highlight { annotation: String createdAt: Date! @@ -1137,6 +1152,7 @@ type Query { getFollowers(userId: ID): GetFollowersResult! getFollowing(userId: ID): GetFollowingResult! getUserPersonalization: GetUserPersonalizationResult! + groups: GroupsResult! hello: String integrations: IntegrationsResult! labels: LabelsResult! diff --git a/packages/api/src/resolvers/function_resolvers.ts b/packages/api/src/resolvers/function_resolvers.ts index c23b683a7..3d7a05808 100644 --- a/packages/api/src/resolvers/function_resolvers.ts +++ b/packages/api/src/resolvers/function_resolvers.ts @@ -55,6 +55,7 @@ import { getUserResolver, googleLoginResolver, googleSignupResolver, + groupsResolver, integrationsResolver, labelsResolver, logOutResolver, @@ -218,6 +219,7 @@ export const functionResolvers = { rules: rulesResolver, deviceTokens: deviceTokensResolver, filters: filtersResolver, + groups: groupsResolver, }, User: { async sharedArticles( @@ -637,4 +639,5 @@ export const functionResolvers = { ...resultResolveTypeResolver('DeleteFilter'), ...resultResolveTypeResolver('MoveFilter'), ...resultResolveTypeResolver('CreateGroup'), + ...resultResolveTypeResolver('Groups'), } diff --git a/packages/api/src/resolvers/recommendations/index.ts b/packages/api/src/resolvers/recommendations/index.ts index a040188f6..68cd8a04a 100644 --- a/packages/api/src/resolvers/recommendations/index.ts +++ b/packages/api/src/resolvers/recommendations/index.ts @@ -2,11 +2,17 @@ import { CreateGroupError, CreateGroupErrorCode, CreateGroupSuccess, + GroupsError, + GroupsErrorCode, + GroupsSuccess, MutationCreateGroupArgs, } from '../../generated/graphql' -import { createGroup } from '../../services/create_group' +import { + createGroup, + getInviteUrl, + getRecommendationGroups, +} from '../../services/create_group' import { authorized, userDataToUser } from '../../utils/helpers' -import { homePageURL } from '../../env' import { getRepository } from '../../entity/utils' import { User } from '../../entity/user' @@ -42,7 +48,7 @@ export const createGroupResolver = authorized< expiresInDays: input.expiresInDays, }) - const inviteUrl = `${homePageURL()}/invite/${invite.code}` + const inviteUrl = getInviteUrl(invite) const user = userDataToUser(userData) return { @@ -68,3 +74,45 @@ export const createGroupResolver = authorized< } } }) + +export const groupsResolver = authorized( + async (_, __, { claims: { uid }, log }) => { + log.info('Getting groups', { + labels: { + source: 'resolver', + resolver: 'groupsResolver', + uid, + }, + }) + + try { + const user = await getRepository(User).findOneBy({ + id: uid, + }) + if (!user) { + return { + errorCodes: [GroupsErrorCode.Unauthorized], + } + } + + const groups = await getRecommendationGroups(user) + + return { + groups, + } + } catch (error) { + log.error('Error getting groups', { + error, + labels: { + source: 'resolver', + resolver: 'groupsResolver', + uid, + }, + }) + + return { + errorCodes: [GroupsErrorCode.BadRequest], + } + } + } +) diff --git a/packages/api/src/schema.ts b/packages/api/src/schema.ts index 4da7c1c8d..0a5ed9910 100755 --- a/packages/api/src/schema.ts +++ b/packages/api/src/schema.ts @@ -2168,6 +2168,21 @@ const schema = gql` BAD_REQUEST } + union GroupsResult = GroupsSuccess | GroupsError + + type GroupsSuccess { + groups: [RecommendationGroup!]! + } + + type GroupsError { + errorCodes: [GroupsErrorCode!]! + } + + enum GroupsErrorCode { + UNAUTHORIZED + BAD_REQUEST + } + # Mutations type Mutation { googleLogin(input: GoogleLoginInput!): LoginResult! @@ -2301,6 +2316,7 @@ const schema = gql` rules(enabled: Boolean): RulesResult! deviceTokens: DeviceTokensResult! filters: FiltersResult! + groups: GroupsResult! } ` diff --git a/packages/api/src/services/create_group.ts b/packages/api/src/services/create_group.ts index 6a50cd130..d49b9901a 100644 --- a/packages/api/src/services/create_group.ts +++ b/packages/api/src/services/create_group.ts @@ -4,6 +4,10 @@ import { Invite } from '../entity/groups/invite' import { GroupMembership } from '../entity/groups/group_membership' import { nanoid } from 'nanoid' import { AppDataSource } from '../server' +import { RecommendationGroup, User as GraphqlUser } from '../generated/graphql' +import { getRepository } from '../entity/utils' +import { homePageURL } from '../env' +import { userDataToUser } from '../utils/helpers' export const createGroup = async (input: { admin: User @@ -43,3 +47,38 @@ export const createGroup = async (input: { ) return [group, invite] } + +export const getRecommendationGroups = async ( + user: User +): Promise => { + const groupMembers = await getRepository(GroupMembership).find({ + where: { user: { id: user.id } }, + relations: ['invite', 'group.members.user.profile'], + }) + + return groupMembers.map((gm) => { + const admins: GraphqlUser[] = [] + const members: GraphqlUser[] = [] + gm.group.members.forEach((m) => { + const user = userDataToUser(m.user) + if (m.isAdmin) { + admins.push(user) + } + members.push(user) + }) + + return { + id: gm.group.id, + name: gm.group.name, + createdAt: gm.group.createdAt, + updatedAt: gm.group.updatedAt, + inviteUrl: getInviteUrl(gm.invite), + admins, + members, + } + }) +} + +export const getInviteUrl = (invite: Invite) => { + return `${homePageURL()}/invite/${invite.code}` +} From 93c6c4202a1fca6d69863c93245c179bdbe5f2b5 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Fri, 2 Dec 2022 12:36:18 +0800 Subject: [PATCH 4/4] Max number of group per user is 3 --- packages/api/src/services/create_group.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/api/src/services/create_group.ts b/packages/api/src/services/create_group.ts index d49b9901a..62557049b 100644 --- a/packages/api/src/services/create_group.ts +++ b/packages/api/src/services/create_group.ts @@ -17,6 +17,15 @@ export const createGroup = async (input: { }): Promise<[Group, Invite]> => { const [group, invite] = await AppDataSource.transaction<[Group, Invite]>( async (t) => { + // Max number of groups a user can create + const maxGroups = 3 + const groupCount = await getRepository(Group).countBy({ + createdBy: { id: input.admin.id }, + }) + if (groupCount >= maxGroups) { + throw new Error('Max groups reached') + } + const group = await t.getRepository(Group).save({ name: input.name, createdBy: input.admin,