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:
@ -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;
|
||||
}
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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],
|
||||
},
|
||||
];
|
||||
@ -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[];
|
||||
}
|
||||
@ -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 {}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user