feat: add object/field create/update resolvers (#1963)
* feat: add object/field create/update resolvers * fix tests
This commit is contained in:
@ -0,0 +1,32 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
@InputType()
|
||||
export class CreateObjectInput {
|
||||
// Deprecated
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
displayName: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
displayNameSingular?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
displayNamePlural?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
description?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
icon?: string;
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import { IsBoolean, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
@InputType()
|
||||
export class UpdateObjectInput {
|
||||
// Deprecated
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
displayName: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
displayNameSingular?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
displayNamePlural?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
description?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
icon?: string;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
isActive?: boolean;
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
BeforeCreateOneHook,
|
||||
CreateOneInputType,
|
||||
} from '@ptc-org/nestjs-query-graphql';
|
||||
|
||||
import { DataSourceMetadataService } from 'src/metadata/data-source-metadata/data-source-metadata.service';
|
||||
import { ObjectMetadata } from 'src/metadata/object-metadata/object-metadata.entity';
|
||||
|
||||
@Injectable()
|
||||
export class BeforeCreateOneObject<T extends ObjectMetadata>
|
||||
implements BeforeCreateOneHook<T, any>
|
||||
{
|
||||
constructor(readonly dataSourceMetadataService: DataSourceMetadataService) {}
|
||||
|
||||
async run(
|
||||
instance: CreateOneInputType<T>,
|
||||
context: any,
|
||||
): Promise<CreateOneInputType<T>> {
|
||||
const workspaceId = context?.req?.user?.workspace?.id;
|
||||
|
||||
if (!workspaceId) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
const lastDataSourceMetadata =
|
||||
await this.dataSourceMetadataService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
instance.input.dataSourceId = lastDataSourceMetadata.id;
|
||||
instance.input.targetTableName = instance.input.displayName;
|
||||
instance.input.workspaceId = workspaceId;
|
||||
instance.input.isActive = false;
|
||||
instance.input.isCustom = true;
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
import { SortDirection } from '@ptc-org/nestjs-query-core';
|
||||
import {
|
||||
AutoResolverOpts,
|
||||
PagingStrategies,
|
||||
ReadResolverOpts,
|
||||
} from '@ptc-org/nestjs-query-graphql';
|
||||
|
||||
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
|
||||
|
||||
import { ObjectMetadata } from './object-metadata.entity';
|
||||
|
||||
import { ObjectMetadataService } from './services/object-metadata.service';
|
||||
import { CreateObjectInput } from './dtos/create-object.input';
|
||||
import { UpdateObjectInput } from './dtos/update-object.input';
|
||||
|
||||
export const objectMetadataAutoResolverOpts: AutoResolverOpts<
|
||||
any,
|
||||
any,
|
||||
unknown,
|
||||
unknown,
|
||||
ReadResolverOpts<any>,
|
||||
PagingStrategies
|
||||
>[] = [
|
||||
{
|
||||
EntityClass: ObjectMetadata,
|
||||
DTOClass: ObjectMetadata,
|
||||
CreateDTOClass: CreateObjectInput,
|
||||
UpdateDTOClass: UpdateObjectInput,
|
||||
ServiceClass: ObjectMetadataService,
|
||||
enableTotalCount: true,
|
||||
pagingStrategy: PagingStrategies.CURSOR,
|
||||
read: {
|
||||
defaultSort: [{ field: 'id', direction: SortDirection.DESC }],
|
||||
},
|
||||
create: {
|
||||
many: { disabled: true },
|
||||
},
|
||||
update: {
|
||||
many: { disabled: true },
|
||||
},
|
||||
delete: { disabled: true },
|
||||
guards: [JwtAuthGuard],
|
||||
},
|
||||
];
|
||||
@ -10,6 +10,7 @@ import {
|
||||
} from 'typeorm';
|
||||
import {
|
||||
Authorize,
|
||||
BeforeCreateOne,
|
||||
CursorConnection,
|
||||
IDField,
|
||||
QueryOptions,
|
||||
@ -17,20 +18,23 @@ import {
|
||||
|
||||
import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
|
||||
import { BeforeCreateOneObject } from './hooks/before-create-one-object.hook';
|
||||
|
||||
@Entity('object_metadata')
|
||||
@ObjectType('object')
|
||||
@BeforeCreateOne(BeforeCreateOneObject)
|
||||
@Authorize({
|
||||
authorize: (context: any) => ({
|
||||
workspaceId: { eq: context?.req?.user?.workspace?.id },
|
||||
}),
|
||||
})
|
||||
@QueryOptions({
|
||||
defaultResultSize: 10,
|
||||
maxResultsSize: 100,
|
||||
disableFilter: true,
|
||||
disableSort: true,
|
||||
})
|
||||
@Authorize({
|
||||
authorize: (context: any) => ({
|
||||
workspaceId: { eq: context?.req?.user?.workspace?.id },
|
||||
}),
|
||||
})
|
||||
@CursorConnection('fields', () => FieldMetadata)
|
||||
@Entity('object_metadata')
|
||||
export class ObjectMetadata {
|
||||
@IDField(() => ID)
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
@ -44,19 +48,19 @@ export class ObjectMetadata {
|
||||
@Column({ nullable: false, name: 'display_name' })
|
||||
displayName: string;
|
||||
|
||||
@Field()
|
||||
@Field({ nullable: true })
|
||||
@Column({ nullable: true, name: 'display_name_singular' })
|
||||
displayNameSingular: string;
|
||||
|
||||
@Field()
|
||||
@Field({ nullable: true })
|
||||
@Column({ nullable: true, name: 'display_name_plural' })
|
||||
displayNamePlural: string;
|
||||
|
||||
@Field()
|
||||
@Field({ nullable: true })
|
||||
@Column({ nullable: true, name: 'description', type: 'text' })
|
||||
description: string;
|
||||
|
||||
@Field()
|
||||
@Field({ nullable: true })
|
||||
@Column({ nullable: true, name: 'icon' })
|
||||
icon: string;
|
||||
|
||||
|
||||
@ -1,34 +1,28 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
NestjsQueryGraphQLModule,
|
||||
PagingStrategies,
|
||||
} from '@ptc-org/nestjs-query-graphql';
|
||||
import { NestjsQueryGraphQLModule } from '@ptc-org/nestjs-query-graphql';
|
||||
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
|
||||
|
||||
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
|
||||
import { DataSourceMetadataModule } from 'src/metadata/data-source-metadata/data-source-metadata.module';
|
||||
import { MigrationRunnerModule } from 'src/metadata/migration-runner/migration-runner.module';
|
||||
import { TenantMigrationModule } from 'src/metadata/tenant-migration/tenant-migration.module';
|
||||
|
||||
import { ObjectMetadataService } from './object-metadata.service';
|
||||
import { ObjectMetadata } from './object-metadata.entity';
|
||||
import { objectMetadataAutoResolverOpts } from './object-metadata.auto-resolver-opts';
|
||||
|
||||
import { ObjectMetadataService } from './services/object-metadata.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
NestjsQueryGraphQLModule.forFeature({
|
||||
imports: [
|
||||
NestjsQueryTypeOrmModule.forFeature([ObjectMetadata], 'metadata'),
|
||||
DataSourceMetadataModule,
|
||||
TenantMigrationModule,
|
||||
MigrationRunnerModule,
|
||||
],
|
||||
resolvers: [
|
||||
{
|
||||
EntityClass: ObjectMetadata,
|
||||
DTOClass: ObjectMetadata,
|
||||
enableTotalCount: true,
|
||||
pagingStrategy: PagingStrategies.CURSOR,
|
||||
create: { disabled: true },
|
||||
update: { disabled: true },
|
||||
delete: { disabled: true },
|
||||
guards: [JwtAuthGuard],
|
||||
},
|
||||
],
|
||||
services: [ObjectMetadataService],
|
||||
resolvers: objectMetadataAutoResolverOpts,
|
||||
}),
|
||||
],
|
||||
providers: [ObjectMetadataService],
|
||||
|
||||
@ -1,28 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { ObjectMetadata } from './object-metadata.entity';
|
||||
|
||||
@Injectable()
|
||||
export class ObjectMetadataService {
|
||||
constructor(
|
||||
@InjectRepository(ObjectMetadata, 'metadata')
|
||||
private readonly fieldMetadataRepository: Repository<ObjectMetadata>,
|
||||
) {}
|
||||
|
||||
public async getObjectMetadataFromDataSourceId(dataSourceId: string) {
|
||||
return this.fieldMetadataRepository.find({
|
||||
where: { dataSourceId },
|
||||
relations: ['fields'],
|
||||
});
|
||||
}
|
||||
|
||||
public async getObjectMetadataFromId(objectMetadataId: string) {
|
||||
return this.fieldMetadataRepository.findOne({
|
||||
where: { id: objectMetadataId },
|
||||
relations: ['fields'],
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,11 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
import { ObjectMetadata } from 'src/metadata/object-metadata/object-metadata.entity';
|
||||
import { MigrationRunnerService } from 'src/metadata/migration-runner/migration-runner.service';
|
||||
import { TenantMigrationService } from 'src/metadata/tenant-migration/tenant-migration.service';
|
||||
|
||||
import { ObjectMetadataService } from './object-metadata.service';
|
||||
import { ObjectMetadata } from './object-metadata.entity';
|
||||
|
||||
describe('ObjectMetadataService', () => {
|
||||
let service: ObjectMetadataService;
|
||||
@ -15,6 +18,14 @@ describe('ObjectMetadataService', () => {
|
||||
provide: getRepositoryToken(ObjectMetadata, 'metadata'),
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: TenantMigrationService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: MigrationRunnerService,
|
||||
useValue: {},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
@ -0,0 +1,70 @@
|
||||
import { ConflictException, Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
|
||||
|
||||
import { TenantMigrationService } from 'src/metadata/tenant-migration/tenant-migration.service';
|
||||
import { TenantMigrationTableChange } from 'src/metadata/tenant-migration/tenant-migration.entity';
|
||||
import { MigrationRunnerService } from 'src/metadata/migration-runner/migration-runner.service';
|
||||
import { ObjectMetadata } from 'src/metadata/object-metadata/object-metadata.entity';
|
||||
|
||||
@Injectable()
|
||||
export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadata> {
|
||||
constructor(
|
||||
@InjectRepository(ObjectMetadata, 'metadata')
|
||||
private readonly objectMetadataRepository: Repository<ObjectMetadata>,
|
||||
|
||||
private readonly tenantMigrationService: TenantMigrationService,
|
||||
private readonly migrationRunnerService: MigrationRunnerService,
|
||||
) {
|
||||
super(objectMetadataRepository);
|
||||
}
|
||||
|
||||
override async createOne(record: ObjectMetadata): Promise<ObjectMetadata> {
|
||||
const objectAlreadyExists = await this.objectMetadataRepository.findOne({
|
||||
where: {
|
||||
displayName: record.displayName, // deprecated, use singular and plural
|
||||
workspaceId: record.workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (objectAlreadyExists) {
|
||||
throw new ConflictException('Object already exists');
|
||||
}
|
||||
|
||||
const createdObjectMetadata = await super.createOne(record);
|
||||
|
||||
await this.tenantMigrationService.createMigration(
|
||||
createdObjectMetadata.workspaceId,
|
||||
[
|
||||
{
|
||||
name: createdObjectMetadata.targetTableName,
|
||||
change: 'create',
|
||||
} satisfies TenantMigrationTableChange,
|
||||
],
|
||||
);
|
||||
|
||||
await this.migrationRunnerService.executeMigrationFromPendingMigrations(
|
||||
createdObjectMetadata.workspaceId,
|
||||
);
|
||||
|
||||
return createdObjectMetadata;
|
||||
}
|
||||
|
||||
public async getObjectMetadataFromDataSourceId(dataSourceId: string) {
|
||||
return this.objectMetadataRepository.find({
|
||||
where: { dataSourceId },
|
||||
relations: ['fields'],
|
||||
});
|
||||
}
|
||||
|
||||
public async findOneWithinWorkspace(
|
||||
objectMetadataId: string,
|
||||
workspaceId: string,
|
||||
) {
|
||||
return this.objectMetadataRepository.findOne({
|
||||
where: { id: objectMetadataId, workspaceId },
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user