feat: add object/field create/update resolvers (#1963)

* feat: add object/field create/update resolvers

* fix tests
This commit is contained in:
Weiko
2023-10-11 12:03:13 +02:00
committed by GitHub
parent 6a3002ddf9
commit f97228bfac
32 changed files with 657 additions and 429 deletions

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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],
},
];

View File

@ -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;

View File

@ -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],

View File

@ -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'],
});
}
}

View File

@ -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();

View File

@ -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 },
});
}
}