mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Add joinGroup API
This commit is contained in:
parent
8d123cf52d
commit
714f0f1f63
9 changed files with 198 additions and 1 deletions
|
|
@ -14,7 +14,7 @@ import { Group } from './group'
|
|||
@Entity()
|
||||
export class Invite {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id?: string
|
||||
id!: string
|
||||
|
||||
@Column('text')
|
||||
code!: string
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue