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] }