Merge pull request #1493 from omnivore-app/recommend-api

recommend api
This commit is contained in:
Hongbo Wu
2022-12-02 13:51:55 +08:00
committed by GitHub
12 changed files with 432 additions and 7 deletions

View File

@ -3,17 +3,19 @@ import {
CreateDateColumn,
Entity,
JoinColumn,
OneToMany,
OneToOne,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm'
import { User } from '../user'
import { GroupMembership } from './group_membership'
@Entity()
export class Group {
@PrimaryGeneratedColumn('uuid')
id?: string
id!: string
@Column('text')
name!: string
@ -23,8 +25,11 @@ export class Group {
createdBy!: User
@CreateDateColumn()
createdAt?: Date
createdAt!: Date
@UpdateDateColumn()
updatedAt?: Date
updatedAt!: Date
@OneToMany(() => GroupMembership, (groupMembership) => groupMembership.group)
members!: GroupMembership[]
}

View File

@ -1,7 +1,9 @@
import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
ManyToOne,
OneToOne,
PrimaryGeneratedColumn,
UpdateDateColumn,
@ -20,7 +22,7 @@ export class GroupMembership {
@JoinColumn()
user!: User
@OneToOne(() => Group)
@ManyToOne(() => Group, (group) => group.members)
@JoinColumn()
group!: Group
@ -33,4 +35,7 @@ export class GroupMembership {
@UpdateDateColumn()
updatedAt?: Date
@Column('boolean', { default: false })
isAdmin!: boolean
}

View File

@ -33,4 +33,7 @@ export class Profile {
@UpdateDateColumn()
updatedAt!: Date
@Column('boolean', { default: false })
private!: boolean
}

View File

@ -275,6 +275,29 @@ export type CreateArticleSuccess = {
user: User;
};
export type CreateGroupError = {
__typename?: 'CreateGroupError';
errorCodes: Array<CreateGroupErrorCode>;
};
export enum CreateGroupErrorCode {
BadRequest = 'BAD_REQUEST',
Unauthorized = 'UNAUTHORIZED'
}
export type CreateGroupInput = {
expiresInDays?: InputMaybe<Scalars['Int']>;
maxMembers?: InputMaybe<Scalars['Int']>;
name: Scalars['String'];
};
export type CreateGroupResult = CreateGroupError | CreateGroupSuccess;
export type CreateGroupSuccess = {
__typename?: 'CreateGroupSuccess';
group: RecommendationGroup;
};
export type CreateHighlightError = {
__typename?: 'CreateHighlightError';
errorCodes: Array<CreateHighlightErrorCode>;
@ -824,6 +847,23 @@ export type GoogleSignupSuccess = {
me: User;
};
export type GroupsError = {
__typename?: 'GroupsError';
errorCodes: Array<GroupsErrorCode>;
};
export enum GroupsErrorCode {
BadRequest = 'BAD_REQUEST',
Unauthorized = 'UNAUTHORIZED'
}
export type GroupsResult = GroupsError | GroupsSuccess;
export type GroupsSuccess = {
__typename?: 'GroupsSuccess';
groups: Array<RecommendationGroup>;
};
export type Highlight = {
__typename?: 'Highlight';
annotation?: Maybe<Scalars['String']>;
@ -1066,6 +1106,7 @@ export type Mutation = {
addPopularRead: AddPopularReadResult;
createArticle: CreateArticleResult;
createArticleSavingRequest: CreateArticleSavingRequestResult;
createGroup: CreateGroupResult;
createHighlight: CreateHighlightResult;
createHighlightReply: CreateHighlightReplyResult;
createLabel: CreateLabelResult;
@ -1140,6 +1181,11 @@ export type MutationCreateArticleSavingRequestArgs = {
};
export type MutationCreateGroupArgs = {
input: CreateGroupInput;
};
export type MutationCreateHighlightArgs = {
input: CreateHighlightInput;
};
@ -1527,6 +1573,7 @@ export type Query = {
getFollowers: GetFollowersResult;
getFollowing: GetFollowingResult;
getUserPersonalization: GetUserPersonalizationResult;
groups: GroupsResult;
hello?: Maybe<Scalars['String']>;
integrations: IntegrationsResult;
labels: LabelsResult;
@ -1696,6 +1743,17 @@ export type RecentSearchesSuccess = {
searches: Array<RecentSearch>;
};
export type RecommendationGroup = {
__typename?: 'RecommendationGroup';
admins: Array<User>;
createdAt: Scalars['Date'];
id: Scalars['ID'];
inviteUrl: Scalars['String'];
members: Array<User>;
name: Scalars['String'];
updatedAt: Scalars['Date'];
};
export type Reminder = {
__typename?: 'Reminder';
archiveUntil: Scalars['Boolean'];
@ -2909,6 +2967,11 @@ export type ResolversTypes = {
CreateArticleSavingRequestResult: ResolversTypes['CreateArticleSavingRequestError'] | ResolversTypes['CreateArticleSavingRequestSuccess'];
CreateArticleSavingRequestSuccess: ResolverTypeWrapper<CreateArticleSavingRequestSuccess>;
CreateArticleSuccess: ResolverTypeWrapper<CreateArticleSuccess>;
CreateGroupError: ResolverTypeWrapper<CreateGroupError>;
CreateGroupErrorCode: CreateGroupErrorCode;
CreateGroupInput: CreateGroupInput;
CreateGroupResult: ResolversTypes['CreateGroupError'] | ResolversTypes['CreateGroupSuccess'];
CreateGroupSuccess: ResolverTypeWrapper<CreateGroupSuccess>;
CreateHighlightError: ResolverTypeWrapper<CreateHighlightError>;
CreateHighlightErrorCode: CreateHighlightErrorCode;
CreateHighlightInput: CreateHighlightInput;
@ -3023,6 +3086,10 @@ export type ResolversTypes = {
GoogleSignupInput: GoogleSignupInput;
GoogleSignupResult: ResolversTypes['GoogleSignupError'] | ResolversTypes['GoogleSignupSuccess'];
GoogleSignupSuccess: ResolverTypeWrapper<GoogleSignupSuccess>;
GroupsError: ResolverTypeWrapper<GroupsError>;
GroupsErrorCode: GroupsErrorCode;
GroupsResult: ResolversTypes['GroupsError'] | ResolversTypes['GroupsSuccess'];
GroupsSuccess: ResolverTypeWrapper<GroupsSuccess>;
Highlight: ResolverTypeWrapper<Highlight>;
HighlightReply: ResolverTypeWrapper<HighlightReply>;
HighlightStats: ResolverTypeWrapper<HighlightStats>;
@ -3090,6 +3157,7 @@ export type ResolversTypes = {
RecentSearchesErrorCode: RecentSearchesErrorCode;
RecentSearchesResult: ResolversTypes['RecentSearchesError'] | ResolversTypes['RecentSearchesSuccess'];
RecentSearchesSuccess: ResolverTypeWrapper<RecentSearchesSuccess>;
RecommendationGroup: ResolverTypeWrapper<RecommendationGroup>;
Reminder: ResolverTypeWrapper<Reminder>;
ReminderError: ResolverTypeWrapper<ReminderError>;
ReminderErrorCode: ReminderErrorCode;
@ -3331,6 +3399,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;
@ -3421,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;
@ -3475,6 +3550,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 +3906,20 @@ export type CreateArticleSuccessResolvers<ContextType = ResolverContext, ParentT
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type CreateGroupErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['CreateGroupError'] = ResolversParentTypes['CreateGroupError']> = {
errorCodes?: Resolver<Array<ResolversTypes['CreateGroupErrorCode']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type CreateGroupResultResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['CreateGroupResult'] = ResolversParentTypes['CreateGroupResult']> = {
__resolveType: TypeResolveFn<'CreateGroupError' | 'CreateGroupSuccess', ParentType, ContextType>;
};
export type CreateGroupSuccessResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['CreateGroupSuccess'] = ResolversParentTypes['CreateGroupSuccess']> = {
group?: Resolver<ResolversTypes['RecommendationGroup'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type CreateHighlightErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['CreateHighlightError'] = ResolversParentTypes['CreateHighlightError']> = {
errorCodes?: Resolver<Array<ResolversTypes['CreateHighlightErrorCode']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
@ -4234,6 +4324,20 @@ export type GoogleSignupSuccessResolvers<ContextType = ResolverContext, ParentTy
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type GroupsErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['GroupsError'] = ResolversParentTypes['GroupsError']> = {
errorCodes?: Resolver<Array<ResolversTypes['GroupsErrorCode']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type GroupsResultResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['GroupsResult'] = ResolversParentTypes['GroupsResult']> = {
__resolveType: TypeResolveFn<'GroupsError' | 'GroupsSuccess', ParentType, ContextType>;
};
export type GroupsSuccessResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['GroupsSuccess'] = ResolversParentTypes['GroupsSuccess']> = {
groups?: Resolver<Array<ResolversTypes['RecommendationGroup']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type HighlightResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['Highlight'] = ResolversParentTypes['Highlight']> = {
annotation?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
createdAt?: Resolver<ResolversTypes['Date'], ParentType, ContextType>;
@ -4417,6 +4521,7 @@ export type MutationResolvers<ContextType = ResolverContext, ParentType extends
addPopularRead?: Resolver<ResolversTypes['AddPopularReadResult'], ParentType, ContextType, RequireFields<MutationAddPopularReadArgs, 'name'>>;
createArticle?: Resolver<ResolversTypes['CreateArticleResult'], ParentType, ContextType, RequireFields<MutationCreateArticleArgs, 'input'>>;
createArticleSavingRequest?: Resolver<ResolversTypes['CreateArticleSavingRequestResult'], ParentType, ContextType, RequireFields<MutationCreateArticleSavingRequestArgs, 'input'>>;
createGroup?: Resolver<ResolversTypes['CreateGroupResult'], ParentType, ContextType, RequireFields<MutationCreateGroupArgs, 'input'>>;
createHighlight?: Resolver<ResolversTypes['CreateHighlightResult'], ParentType, ContextType, RequireFields<MutationCreateHighlightArgs, 'input'>>;
createHighlightReply?: Resolver<ResolversTypes['CreateHighlightReplyResult'], ParentType, ContextType, RequireFields<MutationCreateHighlightReplyArgs, 'input'>>;
createLabel?: Resolver<ResolversTypes['CreateLabelResult'], ParentType, ContextType, RequireFields<MutationCreateLabelArgs, 'input'>>;
@ -4557,6 +4662,7 @@ export type QueryResolvers<ContextType = ResolverContext, ParentType extends Res
getFollowers?: Resolver<ResolversTypes['GetFollowersResult'], ParentType, ContextType, Partial<QueryGetFollowersArgs>>;
getFollowing?: Resolver<ResolversTypes['GetFollowingResult'], ParentType, ContextType, Partial<QueryGetFollowingArgs>>;
getUserPersonalization?: Resolver<ResolversTypes['GetUserPersonalizationResult'], ParentType, ContextType>;
groups?: Resolver<ResolversTypes['GroupsResult'], ParentType, ContextType>;
hello?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
integrations?: Resolver<ResolversTypes['IntegrationsResult'], ParentType, ContextType>;
labels?: Resolver<ResolversTypes['LabelsResult'], ParentType, ContextType>;
@ -4616,6 +4722,17 @@ export type RecentSearchesSuccessResolvers<ContextType = ResolverContext, Parent
__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>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
inviteUrl?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
members?: Resolver<Array<ResolversTypes['User']>, ParentType, ContextType>;
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
updatedAt?: Resolver<ResolversTypes['Date'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type ReminderResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['Reminder'] = ResolversParentTypes['Reminder']> = {
archiveUntil?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
@ -5341,6 +5458,9 @@ export type Resolvers<ContextType = ResolverContext> = {
CreateArticleSavingRequestResult?: CreateArticleSavingRequestResultResolvers<ContextType>;
CreateArticleSavingRequestSuccess?: CreateArticleSavingRequestSuccessResolvers<ContextType>;
CreateArticleSuccess?: CreateArticleSuccessResolvers<ContextType>;
CreateGroupError?: CreateGroupErrorResolvers<ContextType>;
CreateGroupResult?: CreateGroupResultResolvers<ContextType>;
CreateGroupSuccess?: CreateGroupSuccessResolvers<ContextType>;
CreateHighlightError?: CreateHighlightErrorResolvers<ContextType>;
CreateHighlightReplyError?: CreateHighlightReplyErrorResolvers<ContextType>;
CreateHighlightReplyResult?: CreateHighlightReplyResultResolvers<ContextType>;
@ -5422,6 +5542,9 @@ export type Resolvers<ContextType = ResolverContext> = {
GoogleSignupError?: GoogleSignupErrorResolvers<ContextType>;
GoogleSignupResult?: GoogleSignupResultResolvers<ContextType>;
GoogleSignupSuccess?: GoogleSignupSuccessResolvers<ContextType>;
GroupsError?: GroupsErrorResolvers<ContextType>;
GroupsResult?: GroupsResultResolvers<ContextType>;
GroupsSuccess?: GroupsSuccessResolvers<ContextType>;
Highlight?: HighlightResolvers<ContextType>;
HighlightReply?: HighlightReplyResolvers<ContextType>;
HighlightStats?: HighlightStatsResolvers<ContextType>;
@ -5468,6 +5591,7 @@ export type Resolvers<ContextType = ResolverContext> = {
RecentSearchesError?: RecentSearchesErrorResolvers<ContextType>;
RecentSearchesResult?: RecentSearchesResultResolvers<ContextType>;
RecentSearchesSuccess?: RecentSearchesSuccessResolvers<ContextType>;
RecommendationGroup?: RecommendationGroupResolvers<ContextType>;
Reminder?: ReminderResolvers<ContextType>;
ReminderError?: ReminderErrorResolvers<ContextType>;
ReminderResult?: ReminderResultResolvers<ContextType>;

View File

@ -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!]!
}
@ -728,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!
@ -948,6 +984,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!
@ -1115,6 +1152,7 @@ type Query {
getFollowers(userId: ID): GetFollowersResult!
getFollowing(userId: ID): GetFollowingResult!
getUserPersonalization: GetUserPersonalizationResult!
groups: GroupsResult!
hello: String
integrations: IntegrationsResult!
labels: LabelsResult!
@ -1181,6 +1219,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!

View File

@ -26,6 +26,7 @@ import {
articleSavingRequestResolver,
createArticleResolver,
createArticleSavingRequestResolver,
createGroupResolver,
createHighlightResolver,
createLabelResolver,
createNewsletterEmailResolver,
@ -54,6 +55,7 @@ import {
getUserResolver,
googleLoginResolver,
googleSignupResolver,
groupsResolver,
integrationsResolver,
labelsResolver,
logOutResolver,
@ -186,6 +188,7 @@ export const functionResolvers = {
saveFilter: saveFilterResolver,
deleteFilter: deleteFilterResolver,
moveFilter: moveFilterResolver,
createGroup: createGroupResolver,
},
Query: {
me: getMeUserResolver,
@ -216,6 +219,7 @@ export const functionResolvers = {
rules: rulesResolver,
deviceTokens: deviceTokensResolver,
filters: filtersResolver,
groups: groupsResolver,
},
User: {
async sharedArticles(
@ -634,4 +638,6 @@ export const functionResolvers = {
...resultResolveTypeResolver('Filters'),
...resultResolveTypeResolver('DeleteFilter'),
...resultResolveTypeResolver('MoveFilter'),
...resultResolveTypeResolver('CreateGroup'),
...resultResolveTypeResolver('Groups'),
}

View File

@ -23,3 +23,4 @@ export * from './api_key'
export * from './integrations'
export * from './rules'
export * from './filters'
export * from './recommendations'

View File

@ -0,0 +1,118 @@
import {
CreateGroupError,
CreateGroupErrorCode,
CreateGroupSuccess,
GroupsError,
GroupsErrorCode,
GroupsSuccess,
MutationCreateGroupArgs,
} from '../../generated/graphql'
import {
createGroup,
getInviteUrl,
getRecommendationGroups,
} from '../../services/create_group'
import { authorized, userDataToUser } from '../../utils/helpers'
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 = getInviteUrl(invite)
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],
}
}
})
export const groupsResolver = authorized<GroupsSuccess, GroupsError>(
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],
}
}
}
)

View File

@ -2137,6 +2137,52 @@ 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
}
union GroupsResult = GroupsSuccess | GroupsError
type GroupsSuccess {
groups: [RecommendationGroup!]!
}
type GroupsError {
errorCodes: [GroupsErrorCode!]!
}
enum GroupsErrorCode {
UNAUTHORIZED
BAD_REQUEST
}
# Mutations
type Mutation {
googleLogin(input: GoogleLoginInput!): LoginResult!
@ -2214,6 +2260,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
@ -2269,6 +2316,7 @@ const schema = gql`
rules(enabled: Boolean): RulesResult!
deviceTokens: DeviceTokensResult!
filters: FiltersResult!
groups: GroupsResult!
}
`

View File

@ -4,15 +4,28 @@ 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
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) => {
// 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,
@ -28,7 +41,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,9 +49,45 @@ export const createGroup = async (input: {
user: input.admin,
group,
invite,
isAdmin: true,
})
return [group, invite]
}
)
return [group, invite]
}
export const getRecommendationGroups = async (
user: User
): Promise<RecommendationGroup[]> => {
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}`
}

View File

@ -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;

View File

@ -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;