Add joinGroup API

This commit is contained in:
Hongbo Wu 2022-12-05 12:44:18 +08:00
parent 8d123cf52d
commit 714f0f1f63
9 changed files with 198 additions and 1 deletions

View file

@ -14,7 +14,7 @@ import { Group } from './group'
@Entity()
export class Invite {
@PrimaryGeneratedColumn('uuid')
id?: string
id!: string
@Column('text')
code!: string

View file

@ -931,6 +931,24 @@ export type IntegrationsSuccess = {
integrations: Array<Integration>;
};
export type JoinGroupError = {
__typename?: 'JoinGroupError';
errorCodes: Array<JoinGroupErrorCode>;
};
export enum JoinGroupErrorCode {
BadRequest = 'BAD_REQUEST',
NotFound = 'NOT_FOUND',
Unauthorized = 'UNAUTHORIZED'
}
export type JoinGroupResult = JoinGroupError | JoinGroupSuccess;
export type JoinGroupSuccess = {
__typename?: 'JoinGroupSuccess';
group: RecommendationGroup;
};
export type Label = {
__typename?: 'Label';
color: Scalars['String'];
@ -1127,6 +1145,7 @@ export type Mutation = {
generateApiKey: GenerateApiKeyResult;
googleLogin: LoginResult;
googleSignup: GoogleSignupResult;
joinGroup: JoinGroupResult;
logOut: LogOutResult;
mergeHighlight: MergeHighlightResult;
moveFilter: MoveFilterResult;
@ -1282,6 +1301,11 @@ export type MutationGoogleSignupArgs = {
};
export type MutationJoinGroupArgs = {
inviteCode: Scalars['String'];
};
export type MutationMergeHighlightArgs = {
input: MergeHighlightInput;
};
@ -3130,6 +3154,10 @@ export type ResolversTypes = {
IntegrationsErrorCode: IntegrationsErrorCode;
IntegrationsResult: ResolversTypes['IntegrationsError'] | ResolversTypes['IntegrationsSuccess'];
IntegrationsSuccess: ResolverTypeWrapper<IntegrationsSuccess>;
JoinGroupError: ResolverTypeWrapper<JoinGroupError>;
JoinGroupErrorCode: JoinGroupErrorCode;
JoinGroupResult: ResolversTypes['JoinGroupError'] | ResolversTypes['JoinGroupSuccess'];
JoinGroupSuccess: ResolverTypeWrapper<JoinGroupSuccess>;
Label: ResolverTypeWrapper<Label>;
LabelsError: ResolverTypeWrapper<LabelsError>;
LabelsErrorCode: LabelsErrorCode;
@ -3539,6 +3567,9 @@ export type ResolversParentTypes = {
IntegrationsError: IntegrationsError;
IntegrationsResult: ResolversParentTypes['IntegrationsError'] | ResolversParentTypes['IntegrationsSuccess'];
IntegrationsSuccess: IntegrationsSuccess;
JoinGroupError: JoinGroupError;
JoinGroupResult: ResolversParentTypes['JoinGroupError'] | ResolversParentTypes['JoinGroupSuccess'];
JoinGroupSuccess: JoinGroupSuccess;
Label: Label;
LabelsError: LabelsError;
LabelsResult: ResolversParentTypes['LabelsError'] | ResolversParentTypes['LabelsSuccess'];
@ -4436,6 +4467,20 @@ export type IntegrationsSuccessResolvers<ContextType = ResolverContext, ParentTy
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type JoinGroupErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['JoinGroupError'] = ResolversParentTypes['JoinGroupError']> = {
errorCodes?: Resolver<Array<ResolversTypes['JoinGroupErrorCode']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type JoinGroupResultResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['JoinGroupResult'] = ResolversParentTypes['JoinGroupResult']> = {
__resolveType: TypeResolveFn<'JoinGroupError' | 'JoinGroupSuccess', ParentType, ContextType>;
};
export type JoinGroupSuccessResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['JoinGroupSuccess'] = ResolversParentTypes['JoinGroupSuccess']> = {
group?: Resolver<ResolversTypes['RecommendationGroup'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type LabelResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['Label'] = ResolversParentTypes['Label']> = {
color?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
createdAt?: Resolver<Maybe<ResolversTypes['Date']>, ParentType, ContextType>;
@ -4580,6 +4625,7 @@ export type MutationResolvers<ContextType = ResolverContext, ParentType extends
generateApiKey?: Resolver<ResolversTypes['GenerateApiKeyResult'], ParentType, ContextType, RequireFields<MutationGenerateApiKeyArgs, 'input'>>;
googleLogin?: Resolver<ResolversTypes['LoginResult'], ParentType, ContextType, RequireFields<MutationGoogleLoginArgs, 'input'>>;
googleSignup?: Resolver<ResolversTypes['GoogleSignupResult'], ParentType, ContextType, RequireFields<MutationGoogleSignupArgs, 'input'>>;
joinGroup?: Resolver<ResolversTypes['JoinGroupResult'], ParentType, ContextType, RequireFields<MutationJoinGroupArgs, 'inviteCode'>>;
logOut?: Resolver<ResolversTypes['LogOutResult'], ParentType, ContextType>;
mergeHighlight?: Resolver<ResolversTypes['MergeHighlightResult'], ParentType, ContextType, RequireFields<MutationMergeHighlightArgs, 'input'>>;
moveFilter?: Resolver<ResolversTypes['MoveFilterResult'], ParentType, ContextType, RequireFields<MutationMoveFilterArgs, 'input'>>;
@ -5605,6 +5651,9 @@ export type Resolvers<ContextType = ResolverContext> = {
IntegrationsError?: IntegrationsErrorResolvers<ContextType>;
IntegrationsResult?: IntegrationsResultResolvers<ContextType>;
IntegrationsSuccess?: IntegrationsSuccessResolvers<ContextType>;
JoinGroupError?: JoinGroupErrorResolvers<ContextType>;
JoinGroupResult?: JoinGroupResultResolvers<ContextType>;
JoinGroupSuccess?: JoinGroupSuccessResolvers<ContextType>;
Label?: LabelResolvers<ContextType>;
LabelsError?: LabelsErrorResolvers<ContextType>;
LabelsResult?: LabelsResultResolvers<ContextType>;

View file

@ -825,6 +825,22 @@ type IntegrationsSuccess {
integrations: [Integration!]!
}
type JoinGroupError {
errorCodes: [JoinGroupErrorCode!]!
}
enum JoinGroupErrorCode {
BAD_REQUEST
NOT_FOUND
UNAUTHORIZED
}
union JoinGroupResult = JoinGroupError | JoinGroupSuccess
type JoinGroupSuccess {
group: RecommendationGroup!
}
type Label {
color: String!
createdAt: Date
@ -1005,6 +1021,7 @@ type Mutation {
generateApiKey(input: GenerateApiKeyInput!): GenerateApiKeyResult!
googleLogin(input: GoogleLoginInput!): LoginResult!
googleSignup(input: GoogleSignupInput!): GoogleSignupResult!
joinGroup(inviteCode: String!): JoinGroupResult!
logOut: LogOutResult!
mergeHighlight(input: MergeHighlightInput!): MergeHighlightResult!
moveFilter(input: MoveFilterInput!): MoveFilterResult!

View file

@ -57,6 +57,7 @@ import {
googleSignupResolver,
groupsResolver,
integrationsResolver,
joinGroupResolver,
labelsResolver,
logOutResolver,
mergeHighlightResolver,
@ -191,6 +192,7 @@ export const functionResolvers = {
moveFilter: moveFilterResolver,
createGroup: createGroupResolver,
recommend: recommendResolver,
joinGroup: joinGroupResolver,
},
Query: {
me: getMeUserResolver,
@ -643,4 +645,5 @@ export const functionResolvers = {
...resultResolveTypeResolver('CreateGroup'),
...resultResolveTypeResolver('Groups'),
...resultResolveTypeResolver('Recommend'),
...resultResolveTypeResolver('JoinGroup'),
}

View file

@ -5,16 +5,21 @@ import {
GroupsError,
GroupsErrorCode,
GroupsSuccess,
JoinGroupError,
JoinGroupErrorCode,
JoinGroupSuccess,
MutationCreateGroupArgs,
MutationRecommendArgs,
RecommendError,
RecommendErrorCode,
RecommendSuccess,
MutationJoinGroupArgs,
} from '../../generated/graphql'
import {
createGroup,
getInviteUrl,
getRecommendationGroups,
joinGroup,
} from '../../services/create_group'
import { authorized, userDataToUser } from '../../utils/helpers'
import { getRepository } from '../../entity/utils'
@ -207,3 +212,49 @@ export const recommendResolver = authorized<
}
}
})
export const joinGroupResolver = authorized<
JoinGroupSuccess,
JoinGroupError,
MutationJoinGroupArgs
>(async (_, { inviteCode }, { claims: { uid }, log }) => {
log.info('Joining group', {
inviteCode,
labels: {
source: 'resolver',
resolver: 'joinGroupResolver',
uid,
},
})
try {
const user = await getRepository(User).findOne({
where: { id: uid },
relations: ['profile'],
})
if (!user) {
return {
errorCodes: [JoinGroupErrorCode.Unauthorized],
}
}
const group = await joinGroup(user, inviteCode)
return {
group,
}
} catch (error) {
log.error('Error joining group', {
error,
labels: {
source: 'resolver',
resolver: 'joinGroupResolver',
uid,
},
})
return {
errorCodes: [JoinGroupErrorCode.BadRequest],
}
}
})

View file

@ -2204,6 +2204,22 @@ const schema = gql`
NOT_FOUND
}
union JoinGroupResult = JoinGroupSuccess | JoinGroupError
type JoinGroupSuccess {
group: RecommendationGroup!
}
type JoinGroupError {
errorCodes: [JoinGroupErrorCode!]!
}
enum JoinGroupErrorCode {
UNAUTHORIZED
BAD_REQUEST
NOT_FOUND
}
# Mutations
type Mutation {
googleLogin(input: GoogleLoginInput!): LoginResult!
@ -2283,6 +2299,7 @@ const schema = gql`
moveFilter(input: MoveFilterInput!): MoveFilterResult!
createGroup(input: CreateGroupInput!): CreateGroupResult!
recommend(input: RecommendInput!): RecommendResult!
joinGroup(inviteCode: String!): JoinGroupResult!
}
# FIXME: remove sort from feedArticles after all cached tabs are closed

View file

@ -91,3 +91,59 @@ export const getRecommendationGroups = async (
export const getInviteUrl = (invite: Invite) => {
return `${homePageURL()}/invite/${invite.code}`
}
export const joinGroup = async (
user: User,
inviteCode: string
): Promise<RecommendationGroup> => {
const invite = await AppDataSource.transaction<Invite>(async (t) => {
// Check if the invite exists
const invite = await t
.getRepository(Invite)
.createQueryBuilder('invite')
.setLock('pessimistic_write')
.innerJoinAndSelect('invite.group', 'group')
.where('invite.code = :inviteCode AND invite.expiration_time >= NOW()', {
inviteCode,
})
.getOne()
if (!invite) {
throw new Error('Invite not found')
}
// Check if exceeded max members considering concurrent requests
await t.query(
`
insert into omnivore.group_membership (user_id, group_id, invite_id)
select $1, $2, $3
from omnivore.group_membership
where group_id = $2
having count(*) < $4`,
[user.id, invite.group.id, invite.id, invite.maxMembers]
)
return invite
})
const group = await getRepository(Group).findOneOrFail({
where: { id: invite.group.id },
relations: ['members', 'members.user.profile'],
})
const admins: GraphqlUser[] = []
const members: GraphqlUser[] = []
group.members.forEach((m) => {
const user = userDataToUser(m.user)
if (m.isAdmin) {
admins.push(user)
}
members.push(user)
})
return {
...group,
inviteUrl: getInviteUrl(invite),
admins,
members,
}
}

View file

@ -10,4 +10,6 @@ GRANT UPDATE ON TABLE omnivore.invite TO omnivore_user;
GRANT UPDATE ON TABLE omnivore.group TO omnivore_user;
GRANT UPDATE ON TABLE omnivore.group_membership TO omnivore_user;
COMMIT;

View file

@ -10,4 +10,6 @@ REVOKE UPDATE ON omnivore.invite FROM omnivore_user;
REVOKE UPDATE ON omnivore.group FROM omnivore_user;
REVOKE UPDATE ON omnivore.group_membership FROM omnivore_user;
COMMIT;