Removing Prisma and Grapql-nestjs-prisma resolvers (#2574)

* Some cleaning

* Fix seeds

* Fix all sign in, sign up flow and apiKey optimistic rendering

* Fix
This commit is contained in:
Charles Bochet
2023-11-19 18:25:47 +01:00
committed by GitHub
parent 18dac1a2b6
commit f5e1d7825a
616 changed files with 2220 additions and 23073 deletions

View File

@ -0,0 +1,33 @@
import { Field, ID, ObjectType } from '@nestjs/graphql';
import { IDField } from '@ptc-org/nestjs-query-graphql';
@ObjectType('UserWorkspaceMemberName')
export class UserWorkspaceMemberName {
@Field({ nullable: false })
firstName: string;
@Field({ nullable: false })
lastName: string;
}
@ObjectType('UserWorkspaceMember')
export class UserWorkspaceMember {
@IDField(() => ID)
id: string;
@Field(() => UserWorkspaceMemberName)
name: UserWorkspaceMemberName;
@Field({ nullable: false })
colorScheme: string;
@Field({ nullable: true })
avatarUrl: string;
@Field({ nullable: false })
locale: string;
@Field({ nullable: false })
allowImpersonation: boolean;
}

View File

@ -1,8 +1,7 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { PrismaService } from 'src/database/prisma.service';
import { prismaMock } from 'src/database/client-mock/jest-prisma-singleton';
import { WorkspaceService } from 'src/core/workspace/services/workspace.service';
import { User } from 'src/core/user/user.entity';
import { UserService } from './user.service';
@ -14,11 +13,7 @@ describe('UserService', () => {
providers: [
UserService,
{
provide: PrismaService,
useValue: prismaMock,
},
{
provide: WorkspaceService,
provide: getRepositoryToken(User),
useValue: {},
},
],

View File

@ -0,0 +1,85 @@
import { InjectRepository } from '@nestjs/typeorm';
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
import { Repository } from 'typeorm';
import { assert } from 'src/utils/assert';
import { User } from 'src/core/user/user.entity';
import { UserWorkspaceMember } from 'src/core/user/dtos/workspace-member.dto';
import { DataSourceService } from 'src/metadata/data-source/data-source.service';
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
export class UserService extends TypeOrmQueryService<User> {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
private readonly dataSourceService: DataSourceService,
private readonly typeORMService: TypeORMService,
) {
super(userRepository);
}
async loadWorkspaceMember(user: User) {
const dataSourceMetadata =
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
user.defaultWorkspace.id,
);
const workspaceDataSource = await this.typeORMService.connectToDataSource(
dataSourceMetadata,
);
const workspaceMembers = await workspaceDataSource?.query(
`SELECT * FROM ${dataSourceMetadata.schema}."workspaceMember" WHERE "userId" = '${user.id}'`,
);
assert(workspaceMembers.length === 1, 'WorkspaceMember not found');
const userWorkspaceMember = new UserWorkspaceMember();
userWorkspaceMember.id = workspaceMembers[0].id;
userWorkspaceMember.colorScheme = workspaceMembers[0].colorScheme;
userWorkspaceMember.locale = workspaceMembers[0].locale;
userWorkspaceMember.allowImpersonation =
workspaceMembers[0].allowImpersonation;
userWorkspaceMember.avatarUrl = workspaceMembers[0].avatarUrl;
userWorkspaceMember.name = {
firstName: workspaceMembers[0].nameFirstName,
lastName: workspaceMembers[0].nameLastName,
};
return userWorkspaceMember;
}
async createWorkspaceMember(user: User, avatarUrl?: string) {
const dataSourceMetadata =
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
user.defaultWorkspace.id,
);
const workspaceDataSource = await this.typeORMService.connectToDataSource(
dataSourceMetadata,
);
await workspaceDataSource?.query(
`INSERT INTO ${dataSourceMetadata.schema}."workspaceMember"
("nameFirstName", "nameLastName", "colorScheme", "userId", "allowImpersonation", "avatarUrl")
VALUES ('${user.firstName}', '${user.lastName}', 'Light', '${
user.id
}', true, '${avatarUrl ?? ''}')`,
);
}
async deleteUser({
workspaceId: _workspaceId,
userId,
}: {
workspaceId: string;
userId: string;
}) {
const user = await this.userRepository.findBy({ id: userId });
assert(user, 'User not found');
return user;
}
}

View File

@ -0,0 +1,38 @@
import {
AutoResolverOpts,
ReadResolverOpts,
PagingStrategies,
} from '@ptc-org/nestjs-query-graphql';
import { User } from 'src/core/user/user.entity';
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
export const userAutoResolverOpts: AutoResolverOpts<
any,
any,
unknown,
unknown,
ReadResolverOpts<any>,
PagingStrategies
>[] = [
{
EntityClass: User,
DTOClass: User,
enableTotalCount: true,
pagingStrategy: PagingStrategies.CURSOR,
read: {
many: { disabled: true },
one: { disabled: true },
},
create: {
many: { disabled: true },
one: { disabled: true },
},
update: {
many: { disabled: true },
one: { disabled: true },
},
delete: { many: { disabled: true }, one: { disabled: true } },
guards: [JwtAuthGuard],
},
];

View File

@ -0,0 +1,74 @@
import { ID, Field, ObjectType } from '@nestjs/graphql';
import {
Entity,
Column,
PrimaryGeneratedColumn,
CreateDateColumn,
UpdateDateColumn,
OneToMany,
ManyToOne,
} from 'typeorm';
import { IDField } from '@ptc-org/nestjs-query-graphql';
import { RefreshToken } from 'src/core/refresh-token/refresh-token.entity';
import { Workspace } from 'src/core/workspace/workspace.entity';
import { UserWorkspaceMember } from 'src/core/user/dtos/workspace-member.dto';
@Entity({ name: 'user', schema: 'core' })
@ObjectType('User')
export class User {
@IDField(() => ID)
@PrimaryGeneratedColumn('uuid')
id: string;
@Field()
@Column({ nullable: true })
firstName: string;
@Field()
@Column({ nullable: true })
lastName: string;
@Field()
@Column()
email: string;
@Field()
@Column({ default: false })
emailVerified: boolean;
@Field({ nullable: true })
@Column({ default: false })
disabled: boolean;
@Field({ nullable: true })
@Column({ nullable: true })
passwordHash: string;
@Field()
@Column({ default: false })
canImpersonate: boolean;
@Field()
@CreateDateColumn({ type: 'timestamp with time zone' })
createdAt: Date;
@Field()
@UpdateDateColumn({ type: 'timestamp with time zone' })
updatedAt: Date;
@Field({ nullable: true })
@Column({ nullable: true })
deletedAt: Date;
@Field(() => Workspace, { nullable: false })
@ManyToOne(() => Workspace, (workspace) => workspace.users)
defaultWorkspace: Workspace;
@OneToMany(() => RefreshToken, (refreshToken) => refreshToken.user)
refreshTokens: RefreshToken[];
@Field(() => UserWorkspaceMember, { nullable: false })
workspaceMember: UserWorkspaceMember;
}

View File

@ -1,23 +1,34 @@
/* eslint-disable no-restricted-imports */
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { NestjsQueryGraphQLModule } from '@ptc-org/nestjs-query-graphql';
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { FileModule } from 'src/core/file/file.module';
import { WorkspaceModule } from 'src/core/workspace/workspace.module';
import { EnvironmentModule } from 'src/integrations/environment/environment.module';
import { AbilityModule } from 'src/ability/ability.module';
import { PrismaModule } from 'src/database/prisma.module';
import { User } from 'src/core/user/user.entity';
import { UserResolver } from 'src/core/user/user.resolver';
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
import { DataSourceModule } from 'src/metadata/data-source/data-source.module';
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { UserService } from './user.service';
import { UserResolver } from './user.resolver';
import config from '../../../ormconfig';
import { userAutoResolverOpts } from './user.auto-resolver-opts';
import { UserService } from './services/user.service';
@Module({
imports: [
TypeOrmModule.forRoot(config),
NestjsQueryGraphQLModule.forFeature({
imports: [NestjsQueryTypeOrmModule.forFeature([User]), TypeORMModule],
resolvers: userAutoResolverOpts,
}),
DataSourceModule,
FileModule,
WorkspaceModule,
EnvironmentModule,
AbilityModule,
PrismaModule,
],
providers: [UserService, UserResolver],
exports: [UserService],
providers: [UserService, UserResolver, TypeORMService],
})
export class UserModule {}

View File

@ -1,42 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AbilityFactory } from 'src/ability/ability.factory';
import { FileUploadService } from 'src/core/file/services/file-upload.service';
import { EnvironmentService } from 'src/integrations/environment/environment.service';
import { UserResolver } from './user.resolver';
import { UserService } from './user.service';
describe('UserResolver', () => {
let resolver: UserResolver;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UserResolver,
{
provide: UserService,
useValue: {},
},
{
provide: AbilityFactory,
useValue: {},
},
{
provide: FileUploadService,
useValue: {},
},
{
provide: EnvironmentService,
useValue: {},
},
],
}).compile();
resolver = module.get<UserResolver>(UserResolver);
});
it('should be defined', () => {
expect(resolver).toBeDefined();
});
});

View File

@ -1,48 +1,32 @@
import {
Args,
Resolver,
Query,
ResolveField,
Args,
Parent,
ResolveField,
Mutation,
} from '@nestjs/graphql';
import { UseFilters, UseGuards } from '@nestjs/common';
import { UseGuards } from '@nestjs/common';
import crypto from 'crypto';
import { accessibleBy } from '@casl/prisma';
import { Prisma, Workspace } from '@prisma/client';
import { FileUpload, GraphQLUpload } from 'graphql-upload';
import { FileFolder } from 'src/core/file/interfaces/file-folder.interface';
import { SupportDriver } from 'src/integrations/environment/interfaces/support.interface';
import { FileFolder } from 'src/core/file/interfaces/file-folder.interface';
import { FindManyUserArgs } from 'src/core/@generated/user/find-many-user.args';
import { User } from 'src/core/@generated/user/user.model';
import { ExceptionFilter } from 'src/filters/exception.filter';
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
import {
PrismaSelect,
PrismaSelector,
} from 'src/decorators/prisma-select.decorator';
import { AbilityGuard } from 'src/guards/ability.guard';
import { CheckAbilities } from 'src/decorators/check-abilities.decorator';
import {
DeleteUserAbilityHandler,
ReadUserAbilityHandler,
UpdateUserAbilityHandler,
} from 'src/ability/handlers/user.ability-handler';
import { UserAbility } from 'src/decorators/user-ability.decorator';
import { AppAbility } from 'src/ability/ability.factory';
import { AuthUser } from 'src/decorators/auth-user.decorator';
import { assert } from 'src/utils/assert';
import { UpdateOneUserArgs } from 'src/core/@generated/user/update-one-user.args';
import { EnvironmentService } from 'src/integrations/environment/environment.service';
import { streamToBuffer } from 'src/utils/stream-to-buffer';
import { FileUploadService } from 'src/core/file/services/file-upload.service';
import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator';
import { EnvironmentService } from 'src/integrations/environment/environment.service';
import { assert } from 'src/utils/assert';
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
import { User } from 'src/core/user/user.entity';
import { Workspace } from 'src/core/workspace/workspace.entity';
import { UserWorkspaceMember } from 'src/core/user/dtos/workspace-member.dto';
import { UserService } from './user.service';
import { UserService } from './services/user.service';
const getHMACKey = (email?: string, key?: string | null) => {
if (!email || !key) return null;
@ -56,85 +40,24 @@ const getHMACKey = (email?: string, key?: string | null) => {
export class UserResolver {
constructor(
private readonly userService: UserService,
private readonly environmentService: EnvironmentService,
private readonly fileUploadService: FileUploadService,
private environmentService: EnvironmentService,
) {}
@Query(() => User)
async currentUser(
@AuthUser() { id }: User,
@PrismaSelector({ modelName: 'User' })
prismaSelect: PrismaSelect<'User'>,
) {
const select = prismaSelect.value;
const user = await this.userService.findUnique({
where: {
id,
},
select,
async currentUser(@AuthUser() { id }: User) {
const user = await this.userService.findById(id, {
relations: [{ name: 'defaultWorkspace', query: {} }],
});
assert(user, 'User not found');
return user;
}
@UseFilters(ExceptionFilter)
@Query(() => [User], {
@ResolveField(() => UserWorkspaceMember, {
nullable: false,
})
@UseGuards(AbilityGuard)
@CheckAbilities(ReadUserAbilityHandler)
async findManyUser(
@Args() args: FindManyUserArgs,
@UserAbility() ability: AppAbility,
@PrismaSelector({ modelName: 'User' })
prismaSelect: PrismaSelect<'User'>,
): Promise<Partial<User>[]> {
return await this.userService.findMany({
where: args.where
? {
AND: [args.where, accessibleBy(ability).User],
}
: accessibleBy(ability).User,
orderBy: args.orderBy,
cursor: args.cursor,
take: args.take,
skip: args.skip,
distinct: args.distinct,
select: prismaSelect.value,
});
}
@Mutation(() => User)
@UseGuards(AbilityGuard)
@CheckAbilities(UpdateUserAbilityHandler)
async updateUser(
@Args() args: UpdateOneUserArgs,
@AuthUser() { id }: User,
@PrismaSelector({ modelName: 'User' })
prismaSelect: PrismaSelect<'User'>,
) {
const user = await this.userService.findUnique({
where: {
id,
},
select: prismaSelect.value,
});
assert(user, 'User not found');
return this.userService.update({
where: args.where,
data: args.data,
select: prismaSelect.value,
} as Prisma.UserUpdateArgs);
}
@ResolveField(() => String, {
nullable: false,
})
displayName(@Parent() parent: User): string {
return `${parent.firstName ?? ''} ${parent.lastName ?? ''}`;
async workspaceMember(@Parent() user: User): Promise<UserWorkspaceMember> {
return this.userService.loadWorkspaceMember(user);
}
@ResolveField(() => String, {
@ -154,6 +77,10 @@ export class UserResolver {
@Args({ name: 'file', type: () => GraphQLUpload })
{ createReadStream, filename, mimetype }: FileUpload,
): Promise<string> {
if (!id) {
throw new Error('User not found');
}
const stream = createReadStream();
const buffer = await streamToBuffer(stream);
const fileFolder = FileFolder.ProfilePicture;
@ -165,20 +92,11 @@ export class UserResolver {
fileFolder,
});
await this.userService.update({
where: { id },
data: {
avatarUrl: paths[0],
},
});
return paths[0];
}
@Mutation(() => User)
@UseGuards(AbilityGuard)
@CheckAbilities(DeleteUserAbilityHandler)
async deleteUserAccount(
async deleteUser(
@AuthUser() { id: userId }: User,
@AuthWorkspace() { id: workspaceId }: Workspace,
) {

View File

@ -1,140 +0,0 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from 'src/database/prisma.service';
import { assert } from 'src/utils/assert';
import { WorkspaceService } from 'src/core/workspace/services/workspace.service';
export type UserPayload = {
displayName: string | undefined | null;
email: string;
};
@Injectable()
export class UserService {
constructor(
private readonly prismaService: PrismaService,
private readonly workspaceService: WorkspaceService,
) {}
// Find
findFirst = this.prismaService.client.user.findFirst;
findFirstOrThrow = this.prismaService.client.user.findFirstOrThrow;
findUnique = this.prismaService.client.user.findUnique;
findUniqueOrThrow = this.prismaService.client.user.findUniqueOrThrow;
findMany = this.prismaService.client.user.findMany;
// Create
create = this.prismaService.client.user.create;
createMany = this.prismaService.client.user.createMany;
// Update
update = this.prismaService.client.user.update;
upsert = this.prismaService.client.user.upsert;
updateMany = this.prismaService.client.user.updateMany;
// Delete
delete = this.prismaService.client.user.delete;
deleteMany = this.prismaService.client.user.deleteMany;
// Aggregate
aggregate = this.prismaService.client.user.aggregate;
// Count
count = this.prismaService.client.user.count;
// GroupBy
groupBy = this.prismaService.client.user.groupBy;
// Customs
async createUser<T extends Prisma.UserCreateArgs>(
args: Prisma.SelectSubset<T, Prisma.UserCreateArgs>,
workspaceId?: string,
): Promise<Prisma.UserGetPayload<T>> {
assert(args.data.email, 'email is missing', BadRequestException);
// Create workspace if not exists
const workspace = workspaceId
? await this.workspaceService.findUnique({
where: {
id: workspaceId,
},
})
: await this.workspaceService.createDefaultWorkspace();
assert(workspace, 'workspace is missing', BadRequestException);
// Create user
const user = await this.prismaService.client.user.upsert({
where: {
email: args.data.email,
},
create: {
...(args.data as Prisma.UserCreateInput),
defaultWorkspaceId: workspace.id,
},
update: {},
...(args.select ? { select: args.select } : {}),
...(args.include ? { include: args.include } : {}),
} as Prisma.UserUpsertArgs);
return user as Prisma.UserGetPayload<T>;
}
async deleteUser({
workspaceId,
userId,
}: {
workspaceId: string;
userId: string;
}) {
const { workspaceMember, refreshToken } = this.prismaService.client;
const user = await this.findUnique({
where: {
id: userId,
},
select: {
id: true,
},
});
assert(user, 'User not found');
const workspace = await this.workspaceService.findUnique({
where: { id: workspaceId },
select: { id: true },
});
assert(workspace, 'Workspace not found');
const workSpaceMembers = await workspaceMember.findMany({
where: {
workspaceId,
},
});
const isLastMember =
workSpaceMembers.length === 1 && workSpaceMembers[0].userId === userId;
if (isLastMember) {
// Delete entire workspace
await this.workspaceService.deleteWorkspace({
workspaceId,
});
} else {
await this.prismaService.client.$transaction([
workspaceMember.deleteMany({
where: { userId },
}),
refreshToken.deleteMany({
where: { userId },
}),
this.delete({ where: { id: userId } }),
]);
}
return user;
}
}