diff --git a/packages/backend/server/src/core/user/resolver.ts b/packages/backend/server/src/core/user/resolver.ts index 73b2fe885c..aa3f30b06e 100644 --- a/packages/backend/server/src/core/user/resolver.ts +++ b/packages/backend/server/src/core/user/resolver.ts @@ -1,9 +1,11 @@ import { Args, + createUnionType, Field, InputType, Int, Mutation, + ObjectType, Query, Resolver, } from '@nestjs/graphql'; @@ -168,9 +170,29 @@ class CreateUserInput { email!: string; @Field(() => String, { nullable: true }) - name!: string | null; + name?: string; } +@InputType() +class ImportUsersInput { + @Field(() => [CreateUserInput]) + users!: CreateUserInput[]; +} + +@ObjectType() +class UserImportFailedType { + @Field(() => String) + email!: string; + + @Field(() => String) + error!: string; +} + +const UserImportResultType = createUnionType({ + name: 'UserImportResultType', + types: () => [UserType, UserImportFailedType], +}); + @Admin() @Resolver(() => UserType) export class UserManagementResolver { @@ -245,6 +267,27 @@ export class UserManagementResolver { return this.getUser(id); } + @Mutation(() => [UserImportResultType], { + description: 'import users', + }) + async importUsers( + @Args({ name: 'input', type: () => ImportUsersInput }) + input: ImportUsersInput + ): Promise<(typeof UserImportResultType)[]> { + const results = await this.models.user.importUsers(input.users); + + return results.map((result, i) => { + if (result.status === 'fulfilled') { + return sessionUser(result.value); + } else { + return { + email: input.users[i].email, + error: result.reason.message, + }; + } + }); + } + @Mutation(() => DeleteAccount, { description: 'Delete a user account', }) diff --git a/packages/backend/server/src/models/user.ts b/packages/backend/server/src/models/user.ts index a176fccc0f..94aa9ec0b1 100644 --- a/packages/backend/server/src/models/user.ts +++ b/packages/backend/server/src/models/user.ts @@ -177,6 +177,17 @@ export class UserModel extends BaseModel { return user; } + async importUsers(inputs: CreateUserInput[]) { + return await Promise.allSettled( + inputs.map(async input => { + return await this.create({ + ...input, + registered: true, + }); + }) + ); + } + @Transactional() async update(id: string, data: UpdateUserInput) { if (data.password) { diff --git a/packages/backend/server/src/schema.gql b/packages/backend/server/src/schema.gql index 4ce5d2426b..14e3c826c6 100644 --- a/packages/backend/server/src/schema.gql +++ b/packages/backend/server/src/schema.gql @@ -506,6 +506,10 @@ type GraphqlBadRequestDataType { message: String! } +input ImportUsersInput { + users: [CreateUserInput!]! +} + type InvalidEmailDataType { email: String! } @@ -824,6 +828,9 @@ type Mutation { generateLicenseKey(sessionId: String!): String! grantDocUserRoles(input: GrantDocUserRolesInput!): Boolean! grantMember(permission: Permission!, userId: String!, workspaceId: String!): Boolean! + + """import users""" + importUsers(input: ImportUsersInput!): [UserImportResultType!]! invite(email: String!, permission: Permission @deprecated(reason: "never used"), sendInviteMail: Boolean, workspaceId: String!): String! inviteBatch(emails: [String!]!, sendInviteMail: Boolean, workspaceId: String!): [InviteResult!]! leaveWorkspace(sendLeaveMail: Boolean, workspaceId: String!, workspaceName: String @deprecated(reason: "no longer used")): Boolean! @@ -1333,6 +1340,13 @@ input UpdateWorkspaceInput { """The `Upload` scalar type represents a file upload.""" scalar Upload +type UserImportFailedType { + email: String! + error: String! +} + +union UserImportResultType = UserImportFailedType | UserType + union UserOrLimitedUser = LimitedUserType | UserType type UserQuotaHumanReadableType {