From f97228bfacbb7c082c1757572b41a5ee4ad2aa6d Mon Sep 17 00:00:00 2001 From: Weiko Date: Wed, 11 Oct 2023 12:03:13 +0200 Subject: [PATCH] feat: add object/field create/update resolvers (#1963) * feat: add object/field create/update resolvers * fix tests --- .../args/create-custom-field.input.ts | 21 ---- .../field-metadata/dtos/create-field.input.ts | 51 +++++++++ .../field-metadata/dtos/update-field.input.ts | 31 +++++ .../field-metadata.auto-resolver-opts.ts | 44 +++++++ .../field-metadata/field-metadata.entity.ts | 19 ++-- .../field-metadata/field-metadata.module.ts | 30 ++--- .../field-metadata/field-metadata.service.ts | 44 ------- .../field-metadata/field-metadata.util.ts | 50 -------- .../hooks/before-create-one-field.hook.ts | 29 +++++ .../field-metadata.service.spec.ts | 18 ++- .../services/field-metadata.service.ts | 99 ++++++++++++++++ .../utils/field-metadata.util.ts} | 73 +++++++++++- server/src/metadata/metadata.module.ts | 8 +- server/src/metadata/metadata.resolver.spec.ts | 26 ----- server/src/metadata/metadata.resolver.ts | 30 ----- server/src/metadata/metadata.service.spec.ts | 48 -------- server/src/metadata/metadata.service.ts | 107 ------------------ .../migration-runner.module.ts} | 8 +- .../migration-runner.service.spec.ts} | 10 +- .../migration-runner.service.ts} | 14 ++- .../dtos/create-object.input.ts | 32 ++++++ .../dtos/update-object.input.ts | 37 ++++++ .../hooks/before-create-one-object.hook.ts | 39 +++++++ .../object-metadata.auto-resolver-opts.ts | 44 +++++++ .../object-metadata/object-metadata.entity.ts | 24 ++-- .../object-metadata/object-metadata.module.ts | 30 ++--- .../object-metadata.service.ts | 28 ----- .../object-metadata.service.spec.ts | 13 ++- .../services/object-metadata.service.ts | 70 ++++++++++++ .../schema-builder/schema-builder.service.ts | 5 + server/src/tenant/tenant.service.spec.ts | 2 +- server/src/tenant/tenant.service.ts | 2 +- 32 files changed, 657 insertions(+), 429 deletions(-) delete mode 100644 server/src/metadata/args/create-custom-field.input.ts create mode 100644 server/src/metadata/field-metadata/dtos/create-field.input.ts create mode 100644 server/src/metadata/field-metadata/dtos/update-field.input.ts create mode 100644 server/src/metadata/field-metadata/field-metadata.auto-resolver-opts.ts delete mode 100644 server/src/metadata/field-metadata/field-metadata.service.ts delete mode 100644 server/src/metadata/field-metadata/field-metadata.util.ts create mode 100644 server/src/metadata/field-metadata/hooks/before-create-one-field.hook.ts rename server/src/metadata/field-metadata/{ => services}/field-metadata.service.spec.ts (51%) create mode 100644 server/src/metadata/field-metadata/services/field-metadata.service.ts rename server/src/metadata/{metadata.util.ts => field-metadata/utils/field-metadata.util.ts} (50%) delete mode 100644 server/src/metadata/metadata.resolver.spec.ts delete mode 100644 server/src/metadata/metadata.resolver.ts delete mode 100644 server/src/metadata/metadata.service.spec.ts delete mode 100644 server/src/metadata/metadata.service.ts rename server/src/metadata/{migration-generator/migration-generator.module.ts => migration-runner/migration-runner.module.ts} (59%) rename server/src/metadata/{migration-generator/migration-generator.service.spec.ts => migration-runner/migration-runner.service.spec.ts} (69%) rename server/src/metadata/{migration-generator/migration-generator.service.ts => migration-runner/migration-runner.service.ts} (94%) create mode 100644 server/src/metadata/object-metadata/dtos/create-object.input.ts create mode 100644 server/src/metadata/object-metadata/dtos/update-object.input.ts create mode 100644 server/src/metadata/object-metadata/hooks/before-create-one-object.hook.ts create mode 100644 server/src/metadata/object-metadata/object-metadata.auto-resolver-opts.ts delete mode 100644 server/src/metadata/object-metadata/object-metadata.service.ts rename server/src/metadata/object-metadata/{ => services}/object-metadata.service.spec.ts (60%) create mode 100644 server/src/metadata/object-metadata/services/object-metadata.service.ts diff --git a/server/src/metadata/args/create-custom-field.input.ts b/server/src/metadata/args/create-custom-field.input.ts deleted file mode 100644 index 14da4109a..000000000 --- a/server/src/metadata/args/create-custom-field.input.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ArgsType, Field } from '@nestjs/graphql'; - -import { IsNotEmpty, IsString } from 'class-validator'; - -@ArgsType() -export class CreateCustomFieldInput { - @Field(() => String) - @IsNotEmpty() - @IsString() - displayName: string; - - @Field(() => String) - @IsNotEmpty() - @IsString() - type: string; - - @Field(() => String) - @IsNotEmpty() - @IsString() - objectId: string; -} diff --git a/server/src/metadata/field-metadata/dtos/create-field.input.ts b/server/src/metadata/field-metadata/dtos/create-field.input.ts new file mode 100644 index 000000000..7b2eb6683 --- /dev/null +++ b/server/src/metadata/field-metadata/dtos/create-field.input.ts @@ -0,0 +1,51 @@ +import { Field, InputType } from '@nestjs/graphql'; + +import { + IsEnum, + IsNotEmpty, + IsOptional, + IsString, + IsUUID, +} from 'class-validator'; + +@InputType() +export class CreateFieldInput { + @IsString() + @IsNotEmpty() + @Field() + displayName: string; + + // Todo: use a type enum and share with typeorm entity + @IsEnum([ + 'text', + 'phone', + 'email', + 'number', + 'boolean', + 'date', + 'url', + 'money', + ]) + @IsNotEmpty() + @Field() + type: string; + + @IsUUID() + @Field() + objectId: string; + + @IsString() + @IsOptional() + @Field({ nullable: true }) + description?: string; + + @IsString() + @IsOptional() + @Field({ nullable: true }) + icon?: string; + + @IsString() + @IsOptional() + @Field({ nullable: true }) + placeholder?: string; +} diff --git a/server/src/metadata/field-metadata/dtos/update-field.input.ts b/server/src/metadata/field-metadata/dtos/update-field.input.ts new file mode 100644 index 000000000..03e892140 --- /dev/null +++ b/server/src/metadata/field-metadata/dtos/update-field.input.ts @@ -0,0 +1,31 @@ +import { Field, InputType } from '@nestjs/graphql'; + +import { IsBoolean, IsOptional, IsString } from 'class-validator'; + +@InputType() +export class UpdateFieldInput { + @IsString() + @IsOptional() + @Field({ nullable: true }) + displayName: string; + + @IsString() + @IsOptional() + @Field({ nullable: true }) + description?: string; + + @IsString() + @IsOptional() + @Field({ nullable: true }) + icon?: string; + + @IsString() + @IsOptional() + @Field({ nullable: true }) + placeholder?: string; + + @IsBoolean() + @IsOptional() + @Field({ nullable: true }) + isActive?: boolean; +} diff --git a/server/src/metadata/field-metadata/field-metadata.auto-resolver-opts.ts b/server/src/metadata/field-metadata/field-metadata.auto-resolver-opts.ts new file mode 100644 index 000000000..3b657842f --- /dev/null +++ b/server/src/metadata/field-metadata/field-metadata.auto-resolver-opts.ts @@ -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 { FieldMetadata } from './field-metadata.entity'; + +import { FieldMetadataService } from './services/field-metadata.service'; +import { CreateFieldInput } from './dtos/create-field.input'; +import { UpdateFieldInput } from './dtos/update-field.input'; + +export const fieldMetadataAutoResolverOpts: AutoResolverOpts< + any, + any, + unknown, + unknown, + ReadResolverOpts, + PagingStrategies +>[] = [ + { + EntityClass: FieldMetadata, + DTOClass: FieldMetadata, + CreateDTOClass: CreateFieldInput, + UpdateDTOClass: UpdateFieldInput, + ServiceClass: FieldMetadataService, + 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], + }, +]; diff --git a/server/src/metadata/field-metadata/field-metadata.entity.ts b/server/src/metadata/field-metadata/field-metadata.entity.ts index 93b15e4f3..3f48b2cd5 100644 --- a/server/src/metadata/field-metadata/field-metadata.entity.ts +++ b/server/src/metadata/field-metadata/field-metadata.entity.ts @@ -11,29 +11,32 @@ import { } from 'typeorm'; import { Authorize, + BeforeCreateOne, IDField, QueryOptions, } from '@ptc-org/nestjs-query-graphql'; import { ObjectMetadata } from 'src/metadata/object-metadata/object-metadata.entity'; +import { BeforeCreateOneField } from './hooks/before-create-one-field.hook'; + export type FieldMetadataTargetColumnMap = { [key: string]: string; }; - +@Entity('field_metadata') @ObjectType('field') +@BeforeCreateOne(BeforeCreateOneField) +@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 }, - }), -}) -@Entity('field_metadata') export class FieldMetadata { @IDField(() => ID) @PrimaryGeneratedColumn('uuid') @@ -90,9 +93,11 @@ export class FieldMetadata { @JoinColumn({ name: 'object_id' }) object: ObjectMetadata; + @Field() @CreateDateColumn({ name: 'created_at' }) createdAt: Date; + @Field() @UpdateDateColumn({ name: 'updated_at' }) updatedAt: Date; } diff --git a/server/src/metadata/field-metadata/field-metadata.module.ts b/server/src/metadata/field-metadata/field-metadata.module.ts index 6a5662a6a..45b1ae626 100644 --- a/server/src/metadata/field-metadata/field-metadata.module.ts +++ b/server/src/metadata/field-metadata/field-metadata.module.ts @@ -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 { MigrationRunnerModule } from 'src/metadata/migration-runner/migration-runner.module'; +import { TenantMigrationModule } from 'src/metadata/tenant-migration/tenant-migration.module'; +import { ObjectMetadataModule } from 'src/metadata/object-metadata/object-metadata.module'; -import { FieldMetadataService } from './field-metadata.service'; import { FieldMetadata } from './field-metadata.entity'; +import { fieldMetadataAutoResolverOpts } from './field-metadata.auto-resolver-opts'; + +import { FieldMetadataService } from './services/field-metadata.service'; @Module({ imports: [ NestjsQueryGraphQLModule.forFeature({ imports: [ NestjsQueryTypeOrmModule.forFeature([FieldMetadata], 'metadata'), + TenantMigrationModule, + MigrationRunnerModule, + ObjectMetadataModule, ], - resolvers: [ - { - EntityClass: FieldMetadata, - DTOClass: FieldMetadata, - enableTotalCount: true, - pagingStrategy: PagingStrategies.CURSOR, - create: { disabled: true }, - update: { disabled: true }, - delete: { disabled: true }, - guards: [JwtAuthGuard], - }, - ], + services: [FieldMetadataService], + resolvers: fieldMetadataAutoResolverOpts, }), ], providers: [FieldMetadataService], diff --git a/server/src/metadata/field-metadata/field-metadata.service.ts b/server/src/metadata/field-metadata/field-metadata.service.ts deleted file mode 100644 index 87a0d652c..000000000 --- a/server/src/metadata/field-metadata/field-metadata.service.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; - -import { Repository } from 'typeorm'; - -import { FieldMetadata } from './field-metadata.entity'; -import { - generateColumnName, - generateTargetColumnMap, -} from './field-metadata.util'; - -@Injectable() -export class FieldMetadataService { - constructor( - @InjectRepository(FieldMetadata, 'metadata') - private readonly fieldMetadataRepository: Repository, - ) {} - - public async createFieldMetadata( - displayName: string, - type: string, - objectId: string, - workspaceId: string, - ): Promise { - return await this.fieldMetadataRepository.save({ - displayName: displayName, - type, - objectId, - isCustom: true, - targetColumnName: generateColumnName(displayName), // deprecated - workspaceId, - targetColumnMap: generateTargetColumnMap(type), - }); - } - - public async getFieldMetadataByDisplayNameAndObjectId( - name: string, - objectId: string, - ): Promise { - return await this.fieldMetadataRepository.findOne({ - where: { displayName: name, objectId }, - }); - } -} diff --git a/server/src/metadata/field-metadata/field-metadata.util.ts b/server/src/metadata/field-metadata/field-metadata.util.ts deleted file mode 100644 index b5eb8c84f..000000000 --- a/server/src/metadata/field-metadata/field-metadata.util.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { v4 } from 'uuid'; - -import { uuidToBase36 } from 'src/metadata/data-source/data-source.util'; - -import { FieldMetadataTargetColumnMap } from './field-metadata.entity'; - -/** - * Generate a column name from a field name removing unsupported characters. - * - * @param name string - * @returns string - */ -export function generateColumnName(name: string): string { - return name.toLowerCase().replace(/ /g, '_'); -} - -/** - * Generate a target column map for a given type, this is used to map the field to the correct column(s) in the database. - * This is used to support fields that map to multiple columns in the database. - * - * @param type string - * @returns FieldMetadataTargetColumnMap - */ -export function generateTargetColumnMap( - type: string, -): FieldMetadataTargetColumnMap { - switch (type) { - case 'text': - case 'phone': - case 'email': - case 'number': - case 'boolean': - case 'date': - return { - value: uuidToBase36(v4()), - }; - case 'url': - return { - text: uuidToBase36(v4()), - link: uuidToBase36(v4()), - }; - case 'money': - return { - amount: uuidToBase36(v4()), - currency: uuidToBase36(v4()), - }; - default: - throw new Error(`Unknown type ${type}`); - } -} diff --git a/server/src/metadata/field-metadata/hooks/before-create-one-field.hook.ts b/server/src/metadata/field-metadata/hooks/before-create-one-field.hook.ts new file mode 100644 index 000000000..0bc4e7dba --- /dev/null +++ b/server/src/metadata/field-metadata/hooks/before-create-one-field.hook.ts @@ -0,0 +1,29 @@ +import { Injectable, UnauthorizedException } from '@nestjs/common'; + +import { + BeforeCreateOneHook, + CreateOneInputType, +} from '@ptc-org/nestjs-query-graphql'; + +import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity'; + +@Injectable() +export class BeforeCreateOneField + implements BeforeCreateOneHook +{ + async run( + instance: CreateOneInputType, + context: any, + ): Promise> { + const workspaceId = context?.req?.user?.workspace?.id; + + if (!workspaceId) { + throw new UnauthorizedException(); + } + + instance.input.workspaceId = workspaceId; + instance.input.isActive = false; + instance.input.isCustom = true; + return instance; + } +} diff --git a/server/src/metadata/field-metadata/field-metadata.service.spec.ts b/server/src/metadata/field-metadata/services/field-metadata.service.spec.ts similarity index 51% rename from server/src/metadata/field-metadata/field-metadata.service.spec.ts rename to server/src/metadata/field-metadata/services/field-metadata.service.spec.ts index 4cc103d8c..87b62cad3 100644 --- a/server/src/metadata/field-metadata/field-metadata.service.spec.ts +++ b/server/src/metadata/field-metadata/services/field-metadata.service.spec.ts @@ -1,8 +1,12 @@ import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; +import { FieldMetadata } from 'src/metadata/field-metadata/field-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 'src/metadata/object-metadata/services/object-metadata.service'; + import { FieldMetadataService } from './field-metadata.service'; -import { FieldMetadata } from './field-metadata.entity'; describe('FieldMetadataService', () => { let service: FieldMetadataService; @@ -15,6 +19,18 @@ describe('FieldMetadataService', () => { provide: getRepositoryToken(FieldMetadata, 'metadata'), useValue: {}, }, + { + provide: ObjectMetadataService, + useValue: {}, + }, + { + provide: TenantMigrationService, + useValue: {}, + }, + { + provide: MigrationRunnerService, + useValue: {}, + }, ], }).compile(); diff --git a/server/src/metadata/field-metadata/services/field-metadata.service.ts b/server/src/metadata/field-metadata/services/field-metadata.service.ts new file mode 100644 index 000000000..0b5d5465d --- /dev/null +++ b/server/src/metadata/field-metadata/services/field-metadata.service.ts @@ -0,0 +1,99 @@ +import { + ConflictException, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { Repository } from 'typeorm'; +import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; + +import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity'; +import { + convertFieldMetadataToColumnChanges, + convertMetadataTypeToColumnType, + generateColumnName, + generateTargetColumnMap, +} from 'src/metadata/field-metadata/utils/field-metadata.util'; +import { MigrationRunnerService } from 'src/metadata/migration-runner/migration-runner.service'; +import { TenantMigrationService } from 'src/metadata/tenant-migration/tenant-migration.service'; +import { ObjectMetadataService } from 'src/metadata/object-metadata/services/object-metadata.service'; +import { + TenantMigrationColumnChange, + TenantMigrationTableChange, +} from 'src/metadata/tenant-migration/tenant-migration.entity'; + +@Injectable() +export class FieldMetadataService extends TypeOrmQueryService { + constructor( + @InjectRepository(FieldMetadata, 'metadata') + private readonly fieldMetadataRepository: Repository, + + private readonly objectMetadataService: ObjectMetadataService, + private readonly tenantMigrationService: TenantMigrationService, + private readonly migrationRunnerService: MigrationRunnerService, + ) { + super(fieldMetadataRepository); + } + + override async createOne(record: FieldMetadata): Promise { + const objectMetadata = + await this.objectMetadataService.findOneWithinWorkspace( + record.objectId, + record.workspaceId, + ); + + if (!objectMetadata) { + throw new NotFoundException('Object does not exist'); + } + + const fieldAlreadyExists = await this.fieldMetadataRepository.findOne({ + where: { + displayName: record.displayName, + objectId: record.objectId, + workspaceId: record.workspaceId, + }, + }); + + if (fieldAlreadyExists) { + throw new ConflictException('Field already exists'); + } + + const createdFieldMetadata = await super.createOne({ + ...record, + targetColumnName: generateColumnName(record.displayName), // deprecated + targetColumnMap: generateTargetColumnMap(record.type), + }); + + await this.tenantMigrationService.createMigration(record.workspaceId, [ + { + name: objectMetadata.targetTableName, + change: 'alter', + columns: [ + ...convertFieldMetadataToColumnChanges(createdFieldMetadata), + // Deprecated + { + name: createdFieldMetadata.targetColumnName, + type: convertMetadataTypeToColumnType(record.type), + change: 'create', + } satisfies TenantMigrationColumnChange, + ], + } satisfies TenantMigrationTableChange, + ]); + + await this.migrationRunnerService.executeMigrationFromPendingMigrations( + record.workspaceId, + ); + + return createdFieldMetadata; + } + + public async getFieldMetadataByDisplayNameAndObjectId( + name: string, + objectId: string, + ): Promise { + return await this.fieldMetadataRepository.findOne({ + where: { displayName: name, objectId }, + }); + } +} diff --git a/server/src/metadata/metadata.util.ts b/server/src/metadata/field-metadata/utils/field-metadata.util.ts similarity index 50% rename from server/src/metadata/metadata.util.ts rename to server/src/metadata/field-metadata/utils/field-metadata.util.ts index e80d2ffe3..1d4c0d1cf 100644 --- a/server/src/metadata/metadata.util.ts +++ b/server/src/metadata/field-metadata/utils/field-metadata.util.ts @@ -1,6 +1,56 @@ +import { v4 } from 'uuid'; + +import { uuidToBase36 } from 'src/metadata/data-source/data-source.util'; +import { + FieldMetadata, + FieldMetadataTargetColumnMap, +} from 'src/metadata/field-metadata/field-metadata.entity'; import { TenantMigrationColumnChange } from 'src/metadata/tenant-migration/tenant-migration.entity'; -import { FieldMetadata } from './field-metadata/field-metadata.entity'; +/** + * Generate a column name from a field name removing unsupported characters. + * + * @param name string + * @returns string + */ +export function generateColumnName(name: string): string { + return name.toLowerCase().replace(/ /g, '_'); +} + +/** + * Generate a target column map for a given type, this is used to map the field to the correct column(s) in the database. + * This is used to support fields that map to multiple columns in the database. + * + * @param type string + * @returns FieldMetadataTargetColumnMap + */ +export function generateTargetColumnMap( + type: string, +): FieldMetadataTargetColumnMap { + switch (type) { + case 'text': + case 'phone': + case 'email': + case 'number': + case 'boolean': + case 'date': + return { + value: uuidToBase36(v4()), + }; + case 'url': + return { + text: uuidToBase36(v4()), + link: uuidToBase36(v4()), + }; + case 'money': + return { + amount: uuidToBase36(v4()), + currency: uuidToBase36(v4()), + }; + default: + throw new Error(`Unknown type ${type}`); + } +} export function convertFieldMetadataToColumnChanges( fieldMetadata: FieldMetadata, @@ -77,3 +127,24 @@ export function convertFieldMetadataToColumnChanges( throw new Error(`Unknown type ${fieldMetadata.type}`); } } + +// Deprecated with target_column_name deprecation +export function convertMetadataTypeToColumnType(type: string) { + switch (type) { + case 'text': + case 'url': + case 'phone': + case 'email': + return 'text'; + case 'number': + return 'int'; + case 'boolean': + return 'boolean'; + case 'date': + return 'timestamp'; + case 'money': + return 'integer'; + default: + throw new Error('Invalid type'); + } +} diff --git a/server/src/metadata/metadata.module.ts b/server/src/metadata/metadata.module.ts index 26e8da807..6810a7fc1 100644 --- a/server/src/metadata/metadata.module.ts +++ b/server/src/metadata/metadata.module.ts @@ -5,12 +5,10 @@ import { GraphQLModule } from '@nestjs/graphql'; import { YogaDriverConfig, YogaDriver } from '@graphql-yoga/nestjs'; import GraphQLJSON from 'graphql-type-json'; -import { MigrationGeneratorModule } from 'src/metadata/migration-generator/migration-generator.module'; +import { MigrationRunnerModule } from 'src/metadata/migration-runner/migration-runner.module'; import { TenantMigrationModule } from 'src/metadata/tenant-migration/tenant-migration.module'; -import { MetadataService } from './metadata.service'; import { typeORMMetadataModuleOptions } from './metadata.datasource'; -import { MetadataResolver } from './metadata.resolver'; import { DataSourceModule } from './data-source/data-source.module'; import { DataSourceMetadataModule } from './data-source-metadata/data-source-metadata.module'; @@ -41,10 +39,8 @@ const typeORMFactory = async (): Promise => ({ DataSourceMetadataModule, FieldMetadataModule, ObjectMetadataModule, - MigrationGeneratorModule, + MigrationRunnerModule, TenantMigrationModule, ], - providers: [MetadataService, MetadataResolver], - exports: [MetadataService], }) export class MetadataModule {} diff --git a/server/src/metadata/metadata.resolver.spec.ts b/server/src/metadata/metadata.resolver.spec.ts deleted file mode 100644 index 9922cf72a..000000000 --- a/server/src/metadata/metadata.resolver.spec.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; - -import { MetadataResolver } from './metadata.resolver'; -import { MetadataService } from './metadata.service'; - -describe('MetadataResolver', () => { - let resolver: MetadataResolver; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - MetadataResolver, - { - provide: MetadataService, - useValue: {}, - }, - ], - }).compile(); - - resolver = module.get(MetadataResolver); - }); - - it('should be defined', () => { - expect(resolver).toBeDefined(); - }); -}); diff --git a/server/src/metadata/metadata.resolver.ts b/server/src/metadata/metadata.resolver.ts deleted file mode 100644 index 1405fa765..000000000 --- a/server/src/metadata/metadata.resolver.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { UseGuards } from '@nestjs/common'; -import { Args, Mutation, Resolver } from '@nestjs/graphql'; - -import { Workspace } from '@prisma/client'; - -import { JwtAuthGuard } from 'src/guards/jwt.auth.guard'; -import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator'; - -import { MetadataService } from './metadata.service'; - -import { CreateCustomFieldInput } from './args/create-custom-field.input'; - -@UseGuards(JwtAuthGuard) -@Resolver() -export class MetadataResolver { - constructor(private readonly metadataService: MetadataService) {} - - @Mutation(() => String) - async createCustomField( - @Args() createCustomFieldInput: CreateCustomFieldInput, - @AuthWorkspace() workspace: Workspace, - ): Promise { - return this.metadataService.createCustomField( - createCustomFieldInput.displayName, - createCustomFieldInput.objectId, - createCustomFieldInput.type, - workspace.id, - ); - } -} diff --git a/server/src/metadata/metadata.service.spec.ts b/server/src/metadata/metadata.service.spec.ts deleted file mode 100644 index d2ebe7043..000000000 --- a/server/src/metadata/metadata.service.spec.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; - -import { MigrationGeneratorService } from 'src/metadata/migration-generator/migration-generator.service'; -import { TenantMigrationService } from 'src/metadata/tenant-migration/tenant-migration.service'; - -import { MetadataService } from './metadata.service'; - -import { DataSourceService } from './data-source/data-source.service'; -import { ObjectMetadataService } from './object-metadata/object-metadata.service'; -import { FieldMetadataService } from './field-metadata/field-metadata.service'; - -describe('MetadataService', () => { - let service: MetadataService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - MetadataService, - { - provide: DataSourceService, - useValue: {}, - }, - { - provide: MigrationGeneratorService, - useValue: {}, - }, - { - provide: ObjectMetadataService, - useValue: {}, - }, - { - provide: TenantMigrationService, - useValue: {}, - }, - { - provide: FieldMetadataService, - useValue: {}, - }, - ], - }).compile(); - - service = module.get(MetadataService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/server/src/metadata/metadata.service.ts b/server/src/metadata/metadata.service.ts deleted file mode 100644 index 0d4b87d08..000000000 --- a/server/src/metadata/metadata.service.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { MigrationGeneratorService } from 'src/metadata/migration-generator/migration-generator.service'; -import { TenantMigrationService } from 'src/metadata/tenant-migration/tenant-migration.service'; -import { - TenantMigrationColumnChange, - TenantMigrationTableChange, -} from 'src/metadata/tenant-migration/tenant-migration.entity'; - -import { convertFieldMetadataToColumnChanges } from './metadata.util'; - -import { DataSourceService } from './data-source/data-source.service'; -import { FieldMetadataService } from './field-metadata/field-metadata.service'; -import { ObjectMetadataService } from './object-metadata/object-metadata.service'; - -@Injectable() -export class MetadataService { - constructor( - private readonly dataSourceService: DataSourceService, - private readonly migrationGenerator: MigrationGeneratorService, - private readonly objectMetadataService: ObjectMetadataService, - private readonly tenantMigrationService: TenantMigrationService, - private readonly fieldMetadataService: FieldMetadataService, - ) {} - - public async createCustomField( - displayName: string, - objectId: string, - type: string, - workspaceId: string, - ): Promise { - const workspaceDataSource = - await this.dataSourceService.connectToWorkspaceDataSource(workspaceId); - - if (!workspaceDataSource) { - throw new Error('Workspace data source not found'); - } - - const objectMetadata = - await this.objectMetadataService.getObjectMetadataFromId(objectId); - - if (!objectMetadata) { - throw new Error('Object not found'); - } - - const fieldMetadataAlreadyExists = - await this.fieldMetadataService.getFieldMetadataByDisplayNameAndObjectId( - displayName, - objectId, - ); - - if (fieldMetadataAlreadyExists) { - throw new Error('Field already exists'); - } - - const createdFieldMetadata = - await this.fieldMetadataService.createFieldMetadata( - displayName, - type, - objectMetadata.id, - workspaceId, - ); - - await this.tenantMigrationService.createMigration(workspaceId, [ - { - name: objectMetadata.targetTableName, - change: 'alter', - columns: [ - ...convertFieldMetadataToColumnChanges(createdFieldMetadata), - // Deprecated - { - name: createdFieldMetadata.targetColumnName, - type: this.convertMetadataTypeToColumnType(type), - change: 'create', - } satisfies TenantMigrationColumnChange, - ], - } satisfies TenantMigrationTableChange, - ]); - - await this.migrationGenerator.executeMigrationFromPendingMigrations( - workspaceId, - ); - - return createdFieldMetadata.id; - } - - // Deprecated with target_column_name - private convertMetadataTypeToColumnType(type: string) { - switch (type) { - case 'text': - case 'url': - case 'phone': - case 'email': - return 'text'; - case 'number': - return 'int'; - case 'boolean': - return 'boolean'; - case 'date': - return 'timestamp'; - case 'money': - return 'integer'; - default: - throw new Error('Invalid type'); - } - } -} diff --git a/server/src/metadata/migration-generator/migration-generator.module.ts b/server/src/metadata/migration-runner/migration-runner.module.ts similarity index 59% rename from server/src/metadata/migration-generator/migration-generator.module.ts rename to server/src/metadata/migration-runner/migration-runner.module.ts index 90edfb514..c042a7671 100644 --- a/server/src/metadata/migration-generator/migration-generator.module.ts +++ b/server/src/metadata/migration-runner/migration-runner.module.ts @@ -3,11 +3,11 @@ import { Module } from '@nestjs/common'; import { DataSourceModule } from 'src/metadata/data-source/data-source.module'; import { TenantMigrationModule } from 'src/metadata/tenant-migration/tenant-migration.module'; -import { MigrationGeneratorService } from './migration-generator.service'; +import { MigrationRunnerService } from './migration-runner.service'; @Module({ imports: [DataSourceModule, TenantMigrationModule], - exports: [MigrationGeneratorService], - providers: [MigrationGeneratorService], + exports: [MigrationRunnerService], + providers: [MigrationRunnerService], }) -export class MigrationGeneratorModule {} +export class MigrationRunnerModule {} diff --git a/server/src/metadata/migration-generator/migration-generator.service.spec.ts b/server/src/metadata/migration-runner/migration-runner.service.spec.ts similarity index 69% rename from server/src/metadata/migration-generator/migration-generator.service.spec.ts rename to server/src/metadata/migration-runner/migration-runner.service.spec.ts index f6388d6b6..e97caafe2 100644 --- a/server/src/metadata/migration-generator/migration-generator.service.spec.ts +++ b/server/src/metadata/migration-runner/migration-runner.service.spec.ts @@ -3,15 +3,15 @@ import { Test, TestingModule } from '@nestjs/testing'; import { DataSourceService } from 'src/metadata/data-source/data-source.service'; import { TenantMigrationService } from 'src/metadata/tenant-migration/tenant-migration.service'; -import { MigrationGeneratorService } from './migration-generator.service'; +import { MigrationRunnerService } from './migration-runner.service'; -describe('MigrationGeneratorService', () => { - let service: MigrationGeneratorService; +describe('MigrationRunnerService', () => { + let service: MigrationRunnerService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ - MigrationGeneratorService, + MigrationRunnerService, { provide: DataSourceService, useValue: {}, @@ -23,7 +23,7 @@ describe('MigrationGeneratorService', () => { ], }).compile(); - service = module.get(MigrationGeneratorService); + service = module.get(MigrationRunnerService); }); it('should be defined', () => { diff --git a/server/src/metadata/migration-generator/migration-generator.service.ts b/server/src/metadata/migration-runner/migration-runner.service.ts similarity index 94% rename from server/src/metadata/migration-generator/migration-generator.service.ts rename to server/src/metadata/migration-runner/migration-runner.service.ts index 792c86ff7..2bf4cb3df 100644 --- a/server/src/metadata/migration-generator/migration-generator.service.ts +++ b/server/src/metadata/migration-runner/migration-runner.service.ts @@ -10,7 +10,7 @@ import { import { TenantMigrationService } from 'src/metadata/tenant-migration/tenant-migration.service'; @Injectable() -export class MigrationGeneratorService { +export class MigrationRunnerService { constructor( private readonly dataSourceService: DataSourceService, private readonly tenantMigrationService: TenantMigrationService, @@ -113,13 +113,23 @@ export class MigrationGeneratorService { name: 'id', type: 'uuid', isPrimary: true, - default: 'uuid_generate_v4()', + default: 'public.uuid_generate_v4()', }, { name: 'created_at', type: 'timestamp', default: 'now()', }, + { + name: 'updated_at', + type: 'timestamp', + default: 'now()', + }, + { + name: 'deleted_at', + type: 'timestamp', + isNullable: true, + }, ], }), true, diff --git a/server/src/metadata/object-metadata/dtos/create-object.input.ts b/server/src/metadata/object-metadata/dtos/create-object.input.ts new file mode 100644 index 000000000..24f488739 --- /dev/null +++ b/server/src/metadata/object-metadata/dtos/create-object.input.ts @@ -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; +} diff --git a/server/src/metadata/object-metadata/dtos/update-object.input.ts b/server/src/metadata/object-metadata/dtos/update-object.input.ts new file mode 100644 index 000000000..0e2477367 --- /dev/null +++ b/server/src/metadata/object-metadata/dtos/update-object.input.ts @@ -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; +} diff --git a/server/src/metadata/object-metadata/hooks/before-create-one-object.hook.ts b/server/src/metadata/object-metadata/hooks/before-create-one-object.hook.ts new file mode 100644 index 000000000..0892c0f86 --- /dev/null +++ b/server/src/metadata/object-metadata/hooks/before-create-one-object.hook.ts @@ -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 + implements BeforeCreateOneHook +{ + constructor(readonly dataSourceMetadataService: DataSourceMetadataService) {} + + async run( + instance: CreateOneInputType, + context: any, + ): Promise> { + 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; + } +} diff --git a/server/src/metadata/object-metadata/object-metadata.auto-resolver-opts.ts b/server/src/metadata/object-metadata/object-metadata.auto-resolver-opts.ts new file mode 100644 index 000000000..a11fab7e3 --- /dev/null +++ b/server/src/metadata/object-metadata/object-metadata.auto-resolver-opts.ts @@ -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, + 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], + }, +]; diff --git a/server/src/metadata/object-metadata/object-metadata.entity.ts b/server/src/metadata/object-metadata/object-metadata.entity.ts index 5a47aee80..15b8b3d44 100644 --- a/server/src/metadata/object-metadata/object-metadata.entity.ts +++ b/server/src/metadata/object-metadata/object-metadata.entity.ts @@ -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; diff --git a/server/src/metadata/object-metadata/object-metadata.module.ts b/server/src/metadata/object-metadata/object-metadata.module.ts index 6bb06f951..24822972a 100644 --- a/server/src/metadata/object-metadata/object-metadata.module.ts +++ b/server/src/metadata/object-metadata/object-metadata.module.ts @@ -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], diff --git a/server/src/metadata/object-metadata/object-metadata.service.ts b/server/src/metadata/object-metadata/object-metadata.service.ts deleted file mode 100644 index 2d16488ab..000000000 --- a/server/src/metadata/object-metadata/object-metadata.service.ts +++ /dev/null @@ -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, - ) {} - - 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'], - }); - } -} diff --git a/server/src/metadata/object-metadata/object-metadata.service.spec.ts b/server/src/metadata/object-metadata/services/object-metadata.service.spec.ts similarity index 60% rename from server/src/metadata/object-metadata/object-metadata.service.spec.ts rename to server/src/metadata/object-metadata/services/object-metadata.service.spec.ts index 1f563e2c9..8b179110b 100644 --- a/server/src/metadata/object-metadata/object-metadata.service.spec.ts +++ b/server/src/metadata/object-metadata/services/object-metadata.service.spec.ts @@ -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(); diff --git a/server/src/metadata/object-metadata/services/object-metadata.service.ts b/server/src/metadata/object-metadata/services/object-metadata.service.ts new file mode 100644 index 000000000..8d23217f1 --- /dev/null +++ b/server/src/metadata/object-metadata/services/object-metadata.service.ts @@ -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 { + constructor( + @InjectRepository(ObjectMetadata, 'metadata') + private readonly objectMetadataRepository: Repository, + + private readonly tenantMigrationService: TenantMigrationService, + private readonly migrationRunnerService: MigrationRunnerService, + ) { + super(objectMetadataRepository); + } + + override async createOne(record: ObjectMetadata): Promise { + 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 }, + }); + } +} diff --git a/server/src/tenant/schema-builder/schema-builder.service.ts b/server/src/tenant/schema-builder/schema-builder.service.ts index b413686ee..6dbd09166 100644 --- a/server/src/tenant/schema-builder/schema-builder.service.ts +++ b/server/src/tenant/schema-builder/schema-builder.service.ts @@ -156,6 +156,11 @@ export class SchemaBuilderService { const mutationFields: any = {}; for (const objectDefinition of objectMetadata) { + if (objectDefinition.fields.length === 0) { + // A graphql type must define one or more fields + continue; + } + const tableName = objectDefinition?.targetTableName ?? ''; const ObjectType = generateObjectType( objectDefinition.displayName, diff --git a/server/src/tenant/tenant.service.spec.ts b/server/src/tenant/tenant.service.spec.ts index 51f2467a4..5bdc2a4c4 100644 --- a/server/src/tenant/tenant.service.spec.ts +++ b/server/src/tenant/tenant.service.spec.ts @@ -1,7 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { DataSourceMetadataService } from 'src/metadata/data-source-metadata/data-source-metadata.service'; -import { ObjectMetadataService } from 'src/metadata/object-metadata/object-metadata.service'; +import { ObjectMetadataService } from 'src/metadata/object-metadata/services/object-metadata.service'; import { TenantService } from './tenant.service'; diff --git a/server/src/tenant/tenant.service.ts b/server/src/tenant/tenant.service.ts index 92fa67363..ec7ee5825 100644 --- a/server/src/tenant/tenant.service.ts +++ b/server/src/tenant/tenant.service.ts @@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common'; import { GraphQLSchema } from 'graphql'; import { DataSourceMetadataService } from 'src/metadata/data-source-metadata/data-source-metadata.service'; -import { ObjectMetadataService } from 'src/metadata/object-metadata/object-metadata.service'; +import { ObjectMetadataService } from 'src/metadata/object-metadata/services/object-metadata.service'; import { SchemaBuilderService } from './schema-builder/schema-builder.service';