feat: wip server folder structure (#4573)

* feat: wip server folder structure

* fix: merge

* fix: wrong merge

* fix: remove unused file

* fix: comment

* fix: lint

* fix: merge

* fix: remove console.log

* fix: metadata graphql arguments broken
This commit is contained in:
Jérémy M
2024-03-20 16:23:46 +01:00
committed by GitHub
parent da12710fe9
commit e5c1309e8c
461 changed files with 1396 additions and 1322 deletions

View File

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

View File

@ -0,0 +1,43 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { User } from 'src/engine/core-modules/user/user.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { UserService } from './user.service';
describe('UserService', () => {
let service: UserService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UserService,
{
provide: getRepositoryToken(User, 'core'),
useValue: {},
},
{
provide: getRepositoryToken(UserWorkspace, 'core'),
useValue: {},
},
{
provide: DataSourceService,
useValue: {},
},
{
provide: TypeORMService,
useValue: {},
},
],
}).compile();
service = module.get<UserService>(UserService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,116 @@
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/engine/core-modules/user/user.entity';
import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
export class UserService extends TypeOrmQueryService<User> {
constructor(
@InjectRepository(User, 'core')
private readonly userRepository: Repository<User>,
@InjectRepository(UserWorkspace, 'core')
private readonly userWorkspaceRepository: Repository<UserWorkspace>,
private readonly dataSourceService: DataSourceService,
private readonly typeORMService: TypeORMService,
) {
super(userRepository);
}
async loadWorkspaceMember(user: User) {
const dataSourcesMetadata =
await this.dataSourceService.getDataSourcesMetadataFromWorkspaceId(
user.defaultWorkspace.id,
);
if (!dataSourcesMetadata.length) {
return;
}
if (dataSourcesMetadata.length > 1) {
throw new Error(
`user '${user.id}' default workspace '${user.defaultWorkspace.id}' has multiple data source metadata`,
);
}
const dataSourceMetadata = dataSourcesMetadata[0];
const workspaceDataSource =
await this.typeORMService.connectToDataSource(dataSourceMetadata);
const workspaceMembers = await workspaceDataSource?.query(
`SELECT * FROM ${dataSourceMetadata.schema}."workspaceMember" WHERE "userId" = '${user.id}'`,
);
if (!workspaceMembers.length) {
return;
}
assert(
workspaceMembers.length === 1,
'WorkspaceMember not found or too many found',
);
const userWorkspaceMember = new WorkspaceMember();
userWorkspaceMember.id = workspaceMembers[0].id;
userWorkspaceMember.colorScheme = workspaceMembers[0].colorScheme;
userWorkspaceMember.locale = workspaceMembers[0].locale;
userWorkspaceMember.avatarUrl = workspaceMembers[0].avatarUrl;
userWorkspaceMember.name = {
firstName: workspaceMembers[0].nameFirstName,
lastName: workspaceMembers[0].nameLastName,
};
return userWorkspaceMember;
}
async loadWorkspaceMembers(dataSource: DataSourceEntity) {
const workspaceDataSource =
await this.typeORMService.connectToDataSource(dataSource);
return await workspaceDataSource?.query(
`
SELECT *
FROM ${dataSource.schema}."workspaceMember" AS s
INNER JOIN core.user AS u
ON s."userId" = u.id
`,
);
}
async deleteUser(userId: string): Promise<User> {
const user = await this.userRepository.findOne({
where: {
id: userId,
},
relations: ['defaultWorkspace'],
});
assert(user, 'User not found');
const dataSourceMetadata =
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
user.defaultWorkspace.id,
);
const workspaceDataSource =
await this.typeORMService.connectToDataSource(dataSourceMetadata);
await workspaceDataSource?.query(
`DELETE FROM ${dataSourceMetadata.schema}."workspaceMember" WHERE "userId" = '${userId}'`,
);
await this.userWorkspaceRepository.delete({ userId });
await this.userRepository.delete(user.id);
return user;
}
}

View File

@ -0,0 +1,38 @@
import {
AutoResolverOpts,
ReadResolverOpts,
PagingStrategies,
} from '@ptc-org/nestjs-query-graphql';
import { User } from 'src/engine/core-modules/user/user.entity';
import { JwtAuthGuard } from 'src/engine/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,99 @@
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/engine/core-modules/refresh-token/refresh-token.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
@Entity({ name: 'user', schema: 'core' })
@ObjectType('User')
export class User {
@IDField(() => ID)
@PrimaryGeneratedColumn('uuid')
id: string;
@Field()
@Column({ default: '' })
firstName: string;
@Field()
@Column({ default: '' })
lastName: string;
@Field()
@Column()
email: string;
@Field({ nullable: true })
@Column({ nullable: true })
defaultAvatarUrl: 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, {
onDelete: 'SET NULL',
})
defaultWorkspace: Workspace;
@Field()
@Column()
defaultWorkspaceId: string;
@Field({ nullable: true })
@Column({ nullable: true })
passwordResetToken: string;
@Field({ nullable: true })
@Column({ nullable: true })
passwordResetTokenExpiresAt: Date;
@OneToMany(() => RefreshToken, (refreshToken) => refreshToken.user, {
cascade: true,
})
refreshTokens: RefreshToken[];
@Field(() => WorkspaceMember, { nullable: true })
workspaceMember: WorkspaceMember;
@Field(() => [UserWorkspace])
@OneToMany(() => UserWorkspace, (userWorkspace) => userWorkspace.user)
workspaces: UserWorkspace[];
}

View File

@ -0,0 +1,36 @@
/* eslint-disable no-restricted-imports */
import { Module } from '@nestjs/common';
import { NestjsQueryGraphQLModule } from '@ptc-org/nestjs-query-graphql';
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { User } from 'src/engine/core-modules/user/user.entity';
import { UserResolver } from 'src/engine/core-modules/user/user.resolver';
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
import { userAutoResolverOpts } from './user.auto-resolver-opts';
import { UserService } from './services/user.service';
@Module({
imports: [
NestjsQueryGraphQLModule.forFeature({
imports: [
NestjsQueryTypeOrmModule.forFeature([User, UserWorkspace], 'core'),
TypeORMModule,
],
resolvers: userAutoResolverOpts,
}),
DataSourceModule,
FileUploadModule,
UserWorkspaceModule,
],
exports: [UserService],
providers: [UserService, UserResolver, TypeORMService],
})
export class UserModule {}

View File

@ -0,0 +1,118 @@
import {
Resolver,
Query,
Args,
Parent,
ResolveField,
Mutation,
} from '@nestjs/graphql';
import { UseGuards } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import crypto from 'crypto';
import { FileUpload, GraphQLUpload } from 'graphql-upload';
import { Repository } from 'typeorm';
import { SupportDriver } from 'src/engine/integrations/environment/interfaces/support.interface';
import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { streamToBuffer } from 'src/utils/stream-to-buffer';
import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service';
import { assert } from 'src/utils/assert';
import { DemoEnvGuard } from 'src/engine/guards/demo.env.guard';
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard';
import { User } from 'src/engine/core-modules/user/user.entity';
import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto';
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
import { UserService } from './services/user.service';
const getHMACKey = (email?: string, key?: string | null) => {
if (!email || !key) return null;
const hmac = crypto.createHmac('sha256', key);
return hmac.update(email).digest('hex');
};
@UseGuards(JwtAuthGuard)
@Resolver(() => User)
export class UserResolver {
constructor(
@InjectRepository(User, 'core')
private readonly userRepository: Repository<User>,
private readonly userService: UserService,
private readonly userWorkspaceService: UserWorkspaceService,
private readonly environmentService: EnvironmentService,
private readonly fileUploadService: FileUploadService,
) {}
@Query(() => User)
async currentUser(@AuthUser() { id }: User): Promise<User> {
const user = await this.userRepository.findOne({
where: {
id,
},
relations: ['defaultWorkspace', 'workspaces', 'workspaces.workspace'],
});
assert(user, 'User not found');
return user;
}
@ResolveField(() => WorkspaceMember, {
nullable: true,
})
async workspaceMember(
@Parent() user: User,
): Promise<WorkspaceMember | undefined> {
return this.userService.loadWorkspaceMember(user);
}
@ResolveField(() => String, {
nullable: true,
})
supportUserHash(@Parent() parent: User): string | null {
if (this.environmentService.get('SUPPORT_DRIVER') !== SupportDriver.Front) {
return null;
}
const key = this.environmentService.get('SUPPORT_FRONT_HMAC_KEY');
return getHMACKey(parent.email, key);
}
@Mutation(() => String)
async uploadProfilePicture(
@AuthUser() { id }: User,
@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;
const { paths } = await this.fileUploadService.uploadImage({
file: buffer,
filename,
mimeType: mimetype,
fileFolder,
});
return paths[0];
}
@UseGuards(DemoEnvGuard)
@Mutation(() => User)
async deleteUser(@AuthUser() { id: userId }: User) {
// Proceed with user deletion
return this.userService.deleteUser(userId);
}
}