diff --git a/infra/dev/postgres/Dockerfile b/infra/dev/postgres/Dockerfile index 31a688ad3..aa2114caf 100644 --- a/infra/dev/postgres/Dockerfile +++ b/infra/dev/postgres/Dockerfile @@ -3,7 +3,7 @@ ARG PG_MAIN_VERSION=14 FROM postgres:${PG_MAIN_VERSION} as postgres ARG PG_MAIN_VERSION -ARG PG_GRAPHQL_VERSION=1.3.0 +ARG PG_GRAPHQL_VERSION=1.4.2 ARG TARGETARCH RUN set -eux; \ diff --git a/server/src/database/typeorm/metadata/migrations/1700663879152-addEnumOptions.ts b/server/src/database/typeorm/metadata/migrations/1700663879152-addEnumOptions.ts new file mode 100644 index 000000000..0f0774e07 --- /dev/null +++ b/server/src/database/typeorm/metadata/migrations/1700663879152-addEnumOptions.ts @@ -0,0 +1,18 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddEnumOptions1700663879152 implements MigrationInterface { + name = 'AddEnumOptions1700663879152' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "metadata"."fieldMetadata" RENAME COLUMN "enums" TO "options"`); + await queryRunner.query(`ALTER TABLE "metadata"."fieldMetadata" DROP COLUMN "options"`); + await queryRunner.query(`ALTER TABLE "metadata"."fieldMetadata" ADD "options" jsonb`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "metadata"."fieldMetadata" DROP COLUMN "options"`); + await queryRunner.query(`ALTER TABLE "metadata"."fieldMetadata" ADD "options" text array`); + await queryRunner.query(`ALTER TABLE "metadata"."fieldMetadata" RENAME COLUMN "options" TO "enums"`); + } + +} diff --git a/server/src/integrations/memory-storage/drivers/local.driver.ts b/server/src/integrations/memory-storage/drivers/local.driver.ts index f8d1e1601..aed409997 100644 --- a/server/src/integrations/memory-storage/drivers/local.driver.ts +++ b/server/src/integrations/memory-storage/drivers/local.driver.ts @@ -35,7 +35,12 @@ export class LocalMemoryDriver implements MemoryStorageDriver { return null; } - const data = this.storage.get(compositeKey)!; + const data = this.storage.get(compositeKey); + + if (!data) { + return null; + } + const deserializeData = this.serializer.deserialize(data); return deserializeData; diff --git a/server/src/workspace/workspace-schema-builder/object-definitions/currency.object-definition.ts b/server/src/metadata/field-metadata/composite-types/currency.composite-type.ts similarity index 81% rename from server/src/workspace/workspace-schema-builder/object-definitions/currency.object-definition.ts rename to server/src/metadata/field-metadata/composite-types/currency.composite-type.ts index 4b565ca26..4591a0cda 100644 --- a/server/src/workspace/workspace-schema-builder/object-definitions/currency.object-definition.ts +++ b/server/src/metadata/field-metadata/composite-types/currency.composite-type.ts @@ -1,5 +1,5 @@ -import { ObjectMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/object-metadata.interface'; -import { FieldMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/field-metadata.interface'; +import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/object-metadata.interface'; +import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/field-metadata.interface'; import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; diff --git a/server/src/workspace/workspace-schema-builder/object-definitions/full-name.object-definition.ts b/server/src/metadata/field-metadata/composite-types/full-name.composite-type.ts similarity index 80% rename from server/src/workspace/workspace-schema-builder/object-definitions/full-name.object-definition.ts rename to server/src/metadata/field-metadata/composite-types/full-name.composite-type.ts index 8d20d1bba..0a9ac2ad4 100644 --- a/server/src/workspace/workspace-schema-builder/object-definitions/full-name.object-definition.ts +++ b/server/src/metadata/field-metadata/composite-types/full-name.composite-type.ts @@ -1,5 +1,5 @@ -import { ObjectMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/object-metadata.interface'; -import { FieldMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/field-metadata.interface'; +import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/object-metadata.interface'; +import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/field-metadata.interface'; import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; diff --git a/server/src/workspace/workspace-schema-builder/object-definitions/link.object-definition.ts b/server/src/metadata/field-metadata/composite-types/link.composite-type.ts similarity index 79% rename from server/src/workspace/workspace-schema-builder/object-definitions/link.object-definition.ts rename to server/src/metadata/field-metadata/composite-types/link.composite-type.ts index e179c23fc..f3e2b64bd 100644 --- a/server/src/workspace/workspace-schema-builder/object-definitions/link.object-definition.ts +++ b/server/src/metadata/field-metadata/composite-types/link.composite-type.ts @@ -1,5 +1,5 @@ -import { ObjectMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/object-metadata.interface'; -import { FieldMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/field-metadata.interface'; +import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/object-metadata.interface'; +import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/field-metadata.interface'; import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; diff --git a/server/src/metadata/field-metadata/dtos/create-field.input.ts b/server/src/metadata/field-metadata/dtos/create-field.input.ts index 831c5c982..e7787f14b 100644 --- a/server/src/metadata/field-metadata/dtos/create-field.input.ts +++ b/server/src/metadata/field-metadata/dtos/create-field.input.ts @@ -2,21 +2,26 @@ import { Field, HideField, InputType } from '@nestjs/graphql'; import { BeforeCreateOne } from '@ptc-org/nestjs-query-graphql'; import { + IsArray, IsBoolean, IsEnum, IsNotEmpty, IsOptional, IsString, IsUUID, + ValidateNested, } from 'class-validator'; -import graphqlTypeJson from 'graphql-type-json'; +import GraphQLJSON from 'graphql-type-json'; +import { Type } from 'class-transformer'; import { FieldMetadataTargetColumnMap } from 'src/metadata/field-metadata/interfaces/field-metadata-target-column-map.interface'; import { FieldMetadataDefaultValue } from 'src/metadata/field-metadata/interfaces/field-metadata-default-value.interface'; +import { FieldMetadataOptions } from 'src/metadata/field-metadata/interfaces/field-metadata-options.interface'; import { BeforeCreateOneField } from 'src/metadata/field-metadata/hooks/before-create-one-field.hook'; import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; import { IsDefaultValue } from 'src/metadata/field-metadata/validators/is-default-value.validator'; +import { FieldMetadataComplexOptions } from 'src/metadata/field-metadata/dtos/options.input'; @InputType() @BeforeCreateOne(BeforeCreateOneField) @@ -53,12 +58,19 @@ export class CreateFieldInput { @IsBoolean() @IsOptional() @Field({ nullable: true }) - isNullable: boolean; + isNullable?: boolean; @IsDefaultValue({ message: 'Invalid default value for the specified type' }) @IsOptional() - @Field(() => graphqlTypeJson, { nullable: true }) - defaultValue: FieldMetadataDefaultValue; + @Field(() => GraphQLJSON, { nullable: true }) + defaultValue?: FieldMetadataDefaultValue; + + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => FieldMetadataComplexOptions) + @Field(() => GraphQLJSON, { nullable: true }) + options?: FieldMetadataOptions; @HideField() targetColumnMap: FieldMetadataTargetColumnMap; diff --git a/server/src/metadata/field-metadata/dtos/field-metadata.dto.ts b/server/src/metadata/field-metadata/dtos/field-metadata.dto.ts index d659e1e56..2fcb34ecd 100644 --- a/server/src/metadata/field-metadata/dtos/field-metadata.dto.ts +++ b/server/src/metadata/field-metadata/dtos/field-metadata.dto.ts @@ -6,6 +6,7 @@ import { registerEnumType, } from '@nestjs/graphql'; +import { GraphQLJSON } from 'graphql-type-json'; import { Authorize, BeforeDeleteOne, @@ -15,6 +16,9 @@ import { Relation, } from '@ptc-org/nestjs-query-graphql'; +import { FieldMetadataOptions } from 'src/metadata/field-metadata/interfaces/field-metadata-options.interface'; +import { FieldMetadataDefaultValue } from 'src/metadata/field-metadata/interfaces/field-metadata-default-value.interface'; + import { RelationMetadataDTO } from 'src/metadata/relation-metadata/dtos/relation-metadata.dto'; import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; import { BeforeDeleteOneField } from 'src/metadata/field-metadata/hooks/before-delete-one-field.hook'; @@ -76,6 +80,12 @@ export class FieldMetadataDTO { @Field() isNullable: boolean; + @Field(() => GraphQLJSON, { nullable: true }) + defaultValue?: FieldMetadataDefaultValue; + + @Field(() => GraphQLJSON, { nullable: true }) + options?: FieldMetadataOptions; + @HideField() workspaceId: string; diff --git a/server/src/metadata/field-metadata/dtos/options.input.ts b/server/src/metadata/field-metadata/dtos/options.input.ts new file mode 100644 index 000000000..904b99c62 --- /dev/null +++ b/server/src/metadata/field-metadata/dtos/options.input.ts @@ -0,0 +1,22 @@ +import { IsString, IsNumber, IsOptional } from 'class-validator'; + +export class FieldMetadataDefaultOptions { + @IsOptional() + @IsString() + id?: string; + + @IsNumber() + position: number; + + @IsString() + label: string; + + @IsString() + value: string; +} + +export class FieldMetadataComplexOptions extends FieldMetadataDefaultOptions { + @IsOptional() + @IsString() + color: 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 index 9c41551a3..9a1f00f76 100644 --- a/server/src/metadata/field-metadata/dtos/update-field.input.ts +++ b/server/src/metadata/field-metadata/dtos/update-field.input.ts @@ -1,9 +1,21 @@ -import { Field, InputType } from '@nestjs/graphql'; +import { Field, HideField, InputType } from '@nestjs/graphql'; import { BeforeUpdateOne } from '@ptc-org/nestjs-query-graphql'; -import { IsBoolean, IsOptional, IsString } from 'class-validator'; +import { + IsArray, + IsBoolean, + IsOptional, + IsString, + ValidateNested, +} from 'class-validator'; +import GraphQLJSON from 'graphql-type-json'; +import { Type } from 'class-transformer'; + +import { FieldMetadataDefaultValue } from 'src/metadata/field-metadata/interfaces/field-metadata-default-value.interface'; +import { FieldMetadataOptions } from 'src/metadata/field-metadata/interfaces/field-metadata-options.interface'; import { BeforeUpdateOneField } from 'src/metadata/field-metadata/hooks/before-update-one-field.hook'; +import { FieldMetadataComplexOptions } from 'src/metadata/field-metadata/dtos/options.input'; @InputType() @BeforeUpdateOne(BeforeUpdateOneField) @@ -32,4 +44,19 @@ export class UpdateFieldInput { @IsOptional() @Field({ nullable: true }) isActive?: boolean; + + // TODO: Add validation for this but we don't have the type actually + @IsOptional() + @Field(() => GraphQLJSON, { nullable: true }) + defaultValue?: FieldMetadataDefaultValue; + + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => FieldMetadataComplexOptions) + @Field(() => GraphQLJSON, { nullable: true }) + options?: FieldMetadataOptions; + + @HideField() + workspaceId: string; } diff --git a/server/src/metadata/field-metadata/field-metadata.entity.ts b/server/src/metadata/field-metadata/field-metadata.entity.ts index addcc1cba..fbf0ea16b 100644 --- a/server/src/metadata/field-metadata/field-metadata.entity.ts +++ b/server/src/metadata/field-metadata/field-metadata.entity.ts @@ -10,9 +10,10 @@ import { UpdateDateColumn, } from 'typeorm'; -import { FieldMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/field-metadata.interface'; +import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/field-metadata.interface'; import { FieldMetadataTargetColumnMap } from 'src/metadata/field-metadata/interfaces/field-metadata-target-column-map.interface'; import { FieldMetadataDefaultValue } from 'src/metadata/field-metadata/interfaces/field-metadata-default-value.interface'; +import { FieldMetadataOptions } from 'src/metadata/field-metadata/interfaces/field-metadata-options.interface'; import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity'; import { RelationMetadataEntity } from 'src/metadata/relation-metadata/relation-metadata.entity'; @@ -27,11 +28,13 @@ export enum FieldMetadataType { NUMBER = 'NUMBER', NUMERIC = 'NUMERIC', PROBABILITY = 'PROBABILITY', - ENUM = 'ENUM', LINK = 'LINK', CURRENCY = 'CURRENCY', - RELATION = 'RELATION', FULL_NAME = 'FULL_NAME', + RATING = 'RATING', + SELECT = 'SELECT', + MULTI_SELECT = 'MULTI_SELECT', + RELATION = 'RELATION', } @Entity('fieldMetadata') @@ -40,7 +43,10 @@ export enum FieldMetadataType { 'objectMetadataId', 'workspaceId', ]) -export class FieldMetadataEntity implements FieldMetadataInterface { +export class FieldMetadataEntity< + T extends FieldMetadataType | 'default' = 'default', +> implements FieldMetadataInterface +{ @PrimaryGeneratedColumn('uuid') id: string; @@ -63,10 +69,10 @@ export class FieldMetadataEntity implements FieldMetadataInterface { label: string; @Column({ nullable: false, type: 'jsonb' }) - targetColumnMap: FieldMetadataTargetColumnMap; + targetColumnMap: FieldMetadataTargetColumnMap; @Column({ nullable: true, type: 'jsonb' }) - defaultValue: FieldMetadataDefaultValue; + defaultValue: FieldMetadataDefaultValue; @Column({ nullable: true, type: 'text' }) description: string; @@ -74,8 +80,8 @@ export class FieldMetadataEntity implements FieldMetadataInterface { @Column({ nullable: true }) icon: string; - @Column('text', { nullable: true, array: true }) - enums: string[]; + @Column('jsonb', { nullable: true }) + options: FieldMetadataOptions; @Column({ default: false }) isCustom: boolean; diff --git a/server/src/metadata/field-metadata/field-metadata.service.ts b/server/src/metadata/field-metadata/field-metadata.service.ts index f50975a0f..574fb494a 100644 --- a/server/src/metadata/field-metadata/field-metadata.service.ts +++ b/server/src/metadata/field-metadata/field-metadata.service.ts @@ -1,10 +1,12 @@ import { + BadRequestException, ConflictException, Injectable, NotFoundException, } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import { v4 as uuidV4 } from 'uuid'; import { Repository } from 'typeorm'; import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; @@ -12,11 +14,15 @@ import { WorkspaceMigrationRunnerService } from 'src/workspace/workspace-migrati import { WorkspaceMigrationService } from 'src/metadata/workspace-migration/workspace-migration.service'; import { ObjectMetadataService } from 'src/metadata/object-metadata/object-metadata.service'; import { CreateFieldInput } from 'src/metadata/field-metadata/dtos/create-field.input'; -import { WorkspaceMigrationTableAction } from 'src/metadata/workspace-migration/workspace-migration.entity'; +import { + WorkspaceMigrationColumnActionType, + WorkspaceMigrationTableAction, +} from 'src/metadata/workspace-migration/workspace-migration.entity'; import { generateTargetColumnMap } from 'src/metadata/field-metadata/utils/generate-target-column-map.util'; -import { convertFieldMetadataToColumnActions } from 'src/metadata/field-metadata/utils/convert-field-metadata-to-column-action.util'; import { TypeORMService } from 'src/database/typeorm/typeorm.service'; import { DataSourceService } from 'src/metadata/data-source/data-source.service'; +import { UpdateFieldInput } from 'src/metadata/field-metadata/dtos/update-field.input'; +import { WorkspaceMigrationFactory } from 'src/metadata/workspace-migration/workspace-migration.factory'; import { FieldMetadataEntity } from './field-metadata.entity'; @@ -27,6 +33,7 @@ export class FieldMetadataService extends TypeOrmQueryService, private readonly objectMetadataService: ObjectMetadataService, + private readonly workspaceMigrationFactory: WorkspaceMigrationFactory, private readonly workspaceMigrationService: WorkspaceMigrationService, private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService, private readonly dataSourceService: DataSourceService, @@ -63,6 +70,12 @@ export class FieldMetadataService extends TypeOrmQueryService ({ + ...option, + id: uuidV4(), + })) + : undefined, isActive: true, isCustom: true, }); @@ -73,7 +86,10 @@ export class FieldMetadataService extends TypeOrmQueryService { + const existingFieldMetadata = await this.fieldMetadataRepository.findOne({ + where: { + id, + workspaceId: record.workspaceId, + }, + }); + + if (!existingFieldMetadata) { + throw new NotFoundException('Field does not exist'); + } + + const objectMetadata = + await this.objectMetadataService.findOneWithinWorkspace( + existingFieldMetadata?.objectMetadataId, + record.workspaceId, + ); + + if (!objectMetadata) { + throw new NotFoundException('Object does not exist'); + } + + // Check if the id of the options has been provided + if (record.options) { + for (const option of record.options) { + if (!option.id) { + throw new BadRequestException('Option id is required'); + } + } + } + + const updatedFieldMetadata = await super.updateOne(id, record); + + if (record.options || record.defaultValue) { + await this.workspaceMigrationService.createCustomMigration( + existingFieldMetadata.workspaceId, + [ + { + name: objectMetadata.targetTableName, + action: 'alter', + columns: this.workspaceMigrationFactory.createColumnActions( + WorkspaceMigrationColumnActionType.ALTER, + existingFieldMetadata, + updatedFieldMetadata, + ), + } satisfies WorkspaceMigrationTableAction, + ], + ); + + await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations( + updatedFieldMetadata.workspaceId, + ); + } + + return updatedFieldMetadata; + } + public async findOneWithinWorkspace( fieldMetadataId: string, workspaceId: string, diff --git a/server/src/metadata/field-metadata/hooks/before-update-one-field.hook.ts b/server/src/metadata/field-metadata/hooks/before-update-one-field.hook.ts index 53726c79e..d2d39cafe 100644 --- a/server/src/metadata/field-metadata/hooks/before-update-one-field.hook.ts +++ b/server/src/metadata/field-metadata/hooks/before-update-one-field.hook.ts @@ -61,6 +61,8 @@ export class BeforeUpdateOneField this.checkIfFieldIsEditable(instance.update, fieldMetadata); + instance.update.workspaceId = workspaceId; + return instance; } diff --git a/server/src/metadata/field-metadata/interfaces/field-metadata-default-value.interface.ts b/server/src/metadata/field-metadata/interfaces/field-metadata-default-value.interface.ts index 63bb66d0a..6e6e3ab42 100644 --- a/server/src/metadata/field-metadata/interfaces/field-metadata-default-value.interface.ts +++ b/server/src/metadata/field-metadata/interfaces/field-metadata-default-value.interface.ts @@ -9,6 +9,11 @@ export interface FieldMetadataDefaultValueNumber { export interface FieldMetadataDefaultValueBoolean { value: boolean; } + +export interface FieldMetadataDefaultValueStringArray { + value: string[]; +} + export interface FieldMetadataDefaultValueDateTime { value: Date; } @@ -55,10 +60,12 @@ type FieldMetadataDefaultValueMapping = { [FieldMetadataType.NUMBER]: FieldMetadataDefaultValueNumber; [FieldMetadataType.NUMERIC]: FieldMetadataDefaultValueString; [FieldMetadataType.PROBABILITY]: FieldMetadataDefaultValueNumber; - [FieldMetadataType.ENUM]: FieldMetadataDefaultValueString; [FieldMetadataType.LINK]: FieldMetadataDefaultValueLink; [FieldMetadataType.CURRENCY]: FieldMetadataDefaultValueCurrency; [FieldMetadataType.FULL_NAME]: FieldMetadataDefaultValueFullName; + [FieldMetadataType.RATING]: FieldMetadataDefaultValueString; + [FieldMetadataType.SELECT]: FieldMetadataDefaultValueString; + [FieldMetadataType.MULTI_SELECT]: FieldMetadataDefaultValueStringArray; }; type DefaultValueByFieldMetadata = [ @@ -78,7 +85,9 @@ type FieldMetadataDefaultValueExtractNestedType = T extends { } ? U : T extends object - ? T[keyof T] + ? { [K in keyof T]: T[K] } extends { value: infer V } + ? V + : T[keyof T] : never; type FieldMetadataDefaultValueExtractedTypes = { diff --git a/server/src/metadata/field-metadata/interfaces/field-metadata-options.interface.ts b/server/src/metadata/field-metadata/interfaces/field-metadata-options.interface.ts new file mode 100644 index 000000000..c8dc56cf2 --- /dev/null +++ b/server/src/metadata/field-metadata/interfaces/field-metadata-options.interface.ts @@ -0,0 +1,22 @@ +import { + FieldMetadataComplexOptions, + FieldMetadataDefaultOptions, +} from 'src/metadata/field-metadata/dtos/options.input'; +import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; + +type FieldMetadataOptionsMapping = { + [FieldMetadataType.RATING]: FieldMetadataDefaultOptions[]; + [FieldMetadataType.SELECT]: FieldMetadataComplexOptions[]; + [FieldMetadataType.MULTI_SELECT]: FieldMetadataComplexOptions[]; +}; + +type OptionsByFieldMetadata = + T extends keyof FieldMetadataOptionsMapping + ? FieldMetadataOptionsMapping[T] + : T extends 'default' + ? FieldMetadataDefaultOptions[] | FieldMetadataComplexOptions[] + : never; + +export type FieldMetadataOptions< + T extends FieldMetadataType | 'default' = 'default', +> = OptionsByFieldMetadata; diff --git a/server/src/metadata/field-metadata/interfaces/field-metadata-target-column-map.interface.ts b/server/src/metadata/field-metadata/interfaces/field-metadata-target-column-map.interface.ts index 0c7c27640..4a25dd46c 100644 --- a/server/src/metadata/field-metadata/interfaces/field-metadata-target-column-map.interface.ts +++ b/server/src/metadata/field-metadata/interfaces/field-metadata-target-column-map.interface.ts @@ -29,12 +29,13 @@ type FieldMetadataTypeMapping = { [FieldMetadataType.FULL_NAME]: FieldMetadataTargetColumnMapFullName; }; -type TypeByFieldMetadata = - T extends keyof FieldMetadataTypeMapping - ? FieldMetadataTypeMapping[T] - : T extends 'default' - ? AllFieldMetadataTypes - : FieldMetadataTargetColumnMapValue; +type TypeByFieldMetadata = [ + T, +] extends [keyof FieldMetadataTypeMapping] + ? FieldMetadataTypeMapping[T] + : T extends 'default' + ? AllFieldMetadataTypes + : FieldMetadataTargetColumnMapValue; export type FieldMetadataTargetColumnMap< T extends FieldMetadataType | 'default' = 'default', diff --git a/server/src/workspace/workspace-schema-builder/interfaces/field-metadata.interface.ts b/server/src/metadata/field-metadata/interfaces/field-metadata.interface.ts similarity index 85% rename from server/src/workspace/workspace-schema-builder/interfaces/field-metadata.interface.ts rename to server/src/metadata/field-metadata/interfaces/field-metadata.interface.ts index b7234a5b0..23e01227b 100644 --- a/server/src/workspace/workspace-schema-builder/interfaces/field-metadata.interface.ts +++ b/server/src/metadata/field-metadata/interfaces/field-metadata.interface.ts @@ -1,5 +1,6 @@ import { FieldMetadataTargetColumnMap } from 'src/metadata/field-metadata/interfaces/field-metadata-target-column-map.interface'; import { FieldMetadataDefaultValue } from 'src/metadata/field-metadata/interfaces/field-metadata-default-value.interface'; +import { FieldMetadataOptions } from 'src/metadata/field-metadata/interfaces/field-metadata-options.interface'; import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; import { RelationMetadataEntity } from 'src/metadata/relation-metadata/relation-metadata.entity'; @@ -13,6 +14,7 @@ export interface FieldMetadataInterface< label: string; targetColumnMap: FieldMetadataTargetColumnMap; defaultValue?: FieldMetadataDefaultValue; + options?: FieldMetadataOptions; objectMetadataId: string; description?: string; isNullable?: boolean; diff --git a/server/src/workspace/workspace-schema-builder/interfaces/object-metadata.interface.ts b/server/src/metadata/field-metadata/interfaces/object-metadata.interface.ts similarity index 100% rename from server/src/workspace/workspace-schema-builder/interfaces/object-metadata.interface.ts rename to server/src/metadata/field-metadata/interfaces/object-metadata.interface.ts index 1a6bcc339..b79b6c40f 100644 --- a/server/src/workspace/workspace-schema-builder/interfaces/object-metadata.interface.ts +++ b/server/src/metadata/field-metadata/interfaces/object-metadata.interface.ts @@ -1,5 +1,5 @@ -import { FieldMetadataInterface } from './field-metadata.interface'; import { RelationMetadataInterface } from './relation-metadata.interface'; +import { FieldMetadataInterface } from './field-metadata.interface'; export interface ObjectMetadataInterface { id: string; diff --git a/server/src/workspace/workspace-schema-builder/interfaces/relation-metadata.interface.ts b/server/src/metadata/field-metadata/interfaces/relation-metadata.interface.ts similarity index 100% rename from server/src/workspace/workspace-schema-builder/interfaces/relation-metadata.interface.ts rename to server/src/metadata/field-metadata/interfaces/relation-metadata.interface.ts diff --git a/server/src/metadata/field-metadata/utils/__tests__/convert-field-metadata-to-column-action.spec.ts b/server/src/metadata/field-metadata/utils/__tests__/convert-field-metadata-to-column-action.spec.ts deleted file mode 100644 index 70193aeb0..000000000 --- a/server/src/metadata/field-metadata/utils/__tests__/convert-field-metadata-to-column-action.spec.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; -import { convertFieldMetadataToColumnActions } from 'src/metadata/field-metadata/utils/convert-field-metadata-to-column-action.util'; - -describe('convertFieldMetadataToColumnActions', () => { - it('should convert TEXT field metadata to column actions', () => { - const fieldMetadata = { - type: FieldMetadataType.TEXT, - targetColumnMap: { value: 'name' }, - defaultValue: { value: 'default text' }, - } as any; - const columnActions = convertFieldMetadataToColumnActions(fieldMetadata); - expect(columnActions).toEqual([ - { - action: 'CREATE', - columnName: 'name', - columnType: 'text', - defaultValue: "'default text'", - }, - ]); - }); - - it('should convert LINK field metadata to column actions', () => { - const fieldMetadata = { - type: FieldMetadataType.LINK, - targetColumnMap: { label: 'linkLabel', url: 'linkURL' }, - defaultValue: { label: 'http://example.com', url: 'Example' }, - } as any; - const columnActions = convertFieldMetadataToColumnActions(fieldMetadata); - expect(columnActions).toEqual([ - { - action: 'CREATE', - columnName: 'linkLabel', - columnType: 'varchar', - defaultValue: "'http://example.com'", - }, - { - action: 'CREATE', - columnName: 'linkURL', - columnType: 'varchar', - defaultValue: "'Example'", - }, - ]); - }); - - it('should convert CURRENCY field metadata to column actions', () => { - const fieldMetadata = { - type: FieldMetadataType.CURRENCY, - targetColumnMap: { - amountMicros: 'moneyAmountMicros', - currencyCode: 'moneyCurrencyCode', - }, - defaultValue: { amountMicros: 100 * 1_000_000, currencyCode: 'USD' }, - } as any; - const columnActions = convertFieldMetadataToColumnActions(fieldMetadata); - expect(columnActions).toEqual([ - { - action: 'CREATE', - columnName: 'moneyAmountMicros', - columnType: 'numeric', - defaultValue: 100 * 1_000_000, - }, - { - action: 'CREATE', - columnName: 'moneyCurrencyCode', - columnType: 'varchar', - defaultValue: "'USD'", - }, - ]); - }); -}); diff --git a/server/src/metadata/field-metadata/utils/convert-field-metadata-to-column-action.util.ts b/server/src/metadata/field-metadata/utils/convert-field-metadata-to-column-action.util.ts deleted file mode 100644 index e2017a065..000000000 --- a/server/src/metadata/field-metadata/utils/convert-field-metadata-to-column-action.util.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { FieldMetadataDefaultValue } from 'src/metadata/field-metadata/interfaces/field-metadata-default-value.interface'; - -import { - FieldMetadataEntity, - FieldMetadataType, -} from 'src/metadata/field-metadata/field-metadata.entity'; -import { - WorkspaceMigrationColumnAction, - WorkspaceMigrationColumnActionType, -} from 'src/metadata/workspace-migration/workspace-migration.entity'; -import { serializeDefaultValue } from 'src/metadata/field-metadata/utils/serialize-default-value'; - -export function convertFieldMetadataToColumnActions( - fieldMetadata: FieldMetadataEntity, -): WorkspaceMigrationColumnAction[] { - switch (fieldMetadata.type) { - case FieldMetadataType.UUID: { - const defaultValue = - fieldMetadata.defaultValue as FieldMetadataDefaultValue; - - return [ - { - action: WorkspaceMigrationColumnActionType.CREATE, - columnName: fieldMetadata.targetColumnMap.value, - columnType: 'uuid', - defaultValue: serializeDefaultValue(defaultValue?.value), - }, - ]; - } - case FieldMetadataType.TEXT: { - const defaultValue = - fieldMetadata.defaultValue as FieldMetadataDefaultValue; - - return [ - { - action: WorkspaceMigrationColumnActionType.CREATE, - columnName: fieldMetadata.targetColumnMap.value, - columnType: 'text', - defaultValue: serializeDefaultValue(defaultValue?.value ?? ''), - }, - ]; - } - case FieldMetadataType.PHONE: - case FieldMetadataType.EMAIL: { - const defaultValue = - fieldMetadata.defaultValue as FieldMetadataDefaultValue< - FieldMetadataType.PHONE | FieldMetadataType.EMAIL - >; - - return [ - { - action: WorkspaceMigrationColumnActionType.CREATE, - columnName: fieldMetadata.targetColumnMap.value, - columnType: 'varchar', - defaultValue: serializeDefaultValue(defaultValue?.value ?? ''), - }, - ]; - } - case FieldMetadataType.NUMERIC: { - const defaultValue = - fieldMetadata.defaultValue as FieldMetadataDefaultValue; - - return [ - { - action: WorkspaceMigrationColumnActionType.CREATE, - columnName: fieldMetadata.targetColumnMap.value, - columnType: 'numeric', - defaultValue: serializeDefaultValue(defaultValue?.value), - }, - ]; - } - case FieldMetadataType.NUMBER: - case FieldMetadataType.PROBABILITY: { - const defaultValue = - fieldMetadata.defaultValue as FieldMetadataDefaultValue< - FieldMetadataType.NUMBER | FieldMetadataType.PROBABILITY - >; - - return [ - { - action: WorkspaceMigrationColumnActionType.CREATE, - columnName: fieldMetadata.targetColumnMap.value, - columnType: 'float', - defaultValue: serializeDefaultValue(defaultValue?.value), - }, - ]; - } - case FieldMetadataType.BOOLEAN: { - const defaultValue = - fieldMetadata.defaultValue as FieldMetadataDefaultValue; - - return [ - { - action: WorkspaceMigrationColumnActionType.CREATE, - columnName: fieldMetadata.targetColumnMap.value, - columnType: 'boolean', - defaultValue: serializeDefaultValue(defaultValue?.value), - }, - ]; - } - case FieldMetadataType.DATE_TIME: { - const defaultValue = - fieldMetadata.defaultValue as FieldMetadataDefaultValue; - - return [ - { - action: WorkspaceMigrationColumnActionType.CREATE, - columnName: fieldMetadata.targetColumnMap.value, - columnType: 'timestamp', - defaultValue: serializeDefaultValue(defaultValue?.value), - }, - ]; - } - case FieldMetadataType.LINK: { - const defaultValue = - fieldMetadata.defaultValue as FieldMetadataDefaultValue; - - return [ - { - action: WorkspaceMigrationColumnActionType.CREATE, - columnName: fieldMetadata.targetColumnMap.label, - columnType: 'varchar', - defaultValue: serializeDefaultValue(defaultValue?.label ?? ''), - }, - { - action: WorkspaceMigrationColumnActionType.CREATE, - columnName: fieldMetadata.targetColumnMap.url, - columnType: 'varchar', - defaultValue: serializeDefaultValue(defaultValue?.url ?? ''), - }, - ]; - } - - case FieldMetadataType.CURRENCY: { - const defaultValue = - fieldMetadata.defaultValue as FieldMetadataDefaultValue; - - return [ - { - action: WorkspaceMigrationColumnActionType.CREATE, - columnName: fieldMetadata.targetColumnMap.amountMicros, - columnType: 'numeric', - defaultValue: serializeDefaultValue(defaultValue?.amountMicros), - }, - { - action: WorkspaceMigrationColumnActionType.CREATE, - columnName: fieldMetadata.targetColumnMap.currencyCode, - columnType: 'varchar', - defaultValue: serializeDefaultValue(defaultValue?.currencyCode ?? ''), - }, - ]; - } - case FieldMetadataType.FULL_NAME: { - const defaultValue = - fieldMetadata.defaultValue as FieldMetadataDefaultValue; - - return [ - { - action: WorkspaceMigrationColumnActionType.CREATE, - columnName: fieldMetadata.targetColumnMap.firstName, - columnType: 'varchar', - defaultValue: serializeDefaultValue(defaultValue?.firstName ?? ''), - }, - { - action: WorkspaceMigrationColumnActionType.CREATE, - columnName: fieldMetadata.targetColumnMap.lastName, - columnType: 'varchar', - defaultValue: serializeDefaultValue(defaultValue?.lastName ?? ''), - }, - ]; - } - default: - throw new Error(`Unknown type ${fieldMetadata.type}`); - } -} diff --git a/server/src/metadata/field-metadata/utils/generate-target-column-map.util.ts b/server/src/metadata/field-metadata/utils/generate-target-column-map.util.ts index 37c109c5a..5822e5c2d 100644 --- a/server/src/metadata/field-metadata/utils/generate-target-column-map.util.ts +++ b/server/src/metadata/field-metadata/utils/generate-target-column-map.util.ts @@ -28,6 +28,9 @@ export function generateTargetColumnMap( case FieldMetadataType.PROBABILITY: case FieldMetadataType.BOOLEAN: case FieldMetadataType.DATE_TIME: + case FieldMetadataType.RATING: + case FieldMetadataType.SELECT: + case FieldMetadataType.MULTI_SELECT: return { value: columnName, }; diff --git a/server/src/metadata/field-metadata/utils/is-composite-field-metadata-type.util.ts b/server/src/metadata/field-metadata/utils/is-composite-field-metadata-type.util.ts new file mode 100644 index 000000000..bc8dd6a55 --- /dev/null +++ b/server/src/metadata/field-metadata/utils/is-composite-field-metadata-type.util.ts @@ -0,0 +1,14 @@ +import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; + +export const isCompositeFieldMetadataType = ( + type: FieldMetadataType, +): type is + | FieldMetadataType.LINK + | FieldMetadataType.CURRENCY + | FieldMetadataType.FULL_NAME => { + return ( + type === FieldMetadataType.LINK || + type === FieldMetadataType.CURRENCY || + type === FieldMetadataType.FULL_NAME + ); +}; diff --git a/server/src/metadata/field-metadata/utils/is-enum-field-metadata-type.util.ts b/server/src/metadata/field-metadata/utils/is-enum-field-metadata-type.util.ts new file mode 100644 index 000000000..2eae3bad1 --- /dev/null +++ b/server/src/metadata/field-metadata/utils/is-enum-field-metadata-type.util.ts @@ -0,0 +1,14 @@ +import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; + +export const isEnumFieldMetadataType = ( + type: FieldMetadataType, +): type is + | FieldMetadataType.RATING + | FieldMetadataType.SELECT + | FieldMetadataType.MULTI_SELECT => { + return ( + type === FieldMetadataType.RATING || + type === FieldMetadataType.SELECT || + type === FieldMetadataType.MULTI_SELECT + ); +}; diff --git a/server/src/metadata/field-metadata/utils/serialize-default-value.ts b/server/src/metadata/field-metadata/utils/serialize-default-value.ts index ef45af5e0..0fd84064c 100644 --- a/server/src/metadata/field-metadata/utils/serialize-default-value.ts +++ b/server/src/metadata/field-metadata/utils/serialize-default-value.ts @@ -10,7 +10,11 @@ export const serializeDefaultValue = ( } // Dynamic default values - if (typeof defaultValue === 'object' && 'type' in defaultValue) { + if ( + !Array.isArray(defaultValue) && + typeof defaultValue === 'object' && + 'type' in defaultValue + ) { switch (defaultValue.type) { case 'uuid': return 'public.uuid_generate_v4()'; @@ -38,6 +42,10 @@ export const serializeDefaultValue = ( return `'${defaultValue.toISOString()}'`; } + if (Array.isArray(defaultValue)) { + return defaultValue; + } + if (typeof defaultValue === 'object') { return `'${JSON.stringify(defaultValue)}'`; } diff --git a/server/src/metadata/field-metadata/utils/validate-default-value-based-on-type.util.ts b/server/src/metadata/field-metadata/utils/validate-default-value-based-on-type.util.ts index 091e158fd..6bde45e84 100644 --- a/server/src/metadata/field-metadata/utils/validate-default-value-based-on-type.util.ts +++ b/server/src/metadata/field-metadata/utils/validate-default-value-based-on-type.util.ts @@ -25,7 +25,8 @@ export const validateDefaultValueBasedOnType = ( case FieldMetadataType.TEXT: case FieldMetadataType.PHONE: case FieldMetadataType.EMAIL: - case FieldMetadataType.ENUM: + case FieldMetadataType.RATING: + case FieldMetadataType.SELECT: case FieldMetadataType.NUMERIC: return ( typeof defaultValue === 'object' && @@ -82,6 +83,12 @@ export const validateDefaultValueBasedOnType = ( typeof defaultValue.lastName === 'string' ); + case FieldMetadataType.MULTI_SELECT: + return ( + Array.isArray(defaultValue) && + defaultValue.every((value) => typeof value === 'string') + ); + default: return false; } diff --git a/server/src/metadata/object-metadata/object-metadata.entity.ts b/server/src/metadata/object-metadata/object-metadata.entity.ts index 8e5999722..85cb92d5e 100644 --- a/server/src/metadata/object-metadata/object-metadata.entity.ts +++ b/server/src/metadata/object-metadata/object-metadata.entity.ts @@ -9,7 +9,7 @@ import { ManyToOne, } from 'typeorm'; -import { ObjectMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/object-metadata.interface'; +import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/object-metadata.interface'; import { FieldMetadataEntity } from 'src/metadata/field-metadata/field-metadata.entity'; import { RelationMetadataEntity } from 'src/metadata/relation-metadata/relation-metadata.entity'; diff --git a/server/src/metadata/relation-metadata/relation-metadata.entity.ts b/server/src/metadata/relation-metadata/relation-metadata.entity.ts index 6e46b8280..d34098245 100644 --- a/server/src/metadata/relation-metadata/relation-metadata.entity.ts +++ b/server/src/metadata/relation-metadata/relation-metadata.entity.ts @@ -9,7 +9,7 @@ import { UpdateDateColumn, } from 'typeorm'; -import { RelationMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/relation-metadata.interface'; +import { RelationMetadataInterface } from 'src/metadata/field-metadata/interfaces/relation-metadata.interface'; import { FieldMetadataEntity } from 'src/metadata/field-metadata/field-metadata.entity'; import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity'; diff --git a/server/src/metadata/workspace-migration/factories/basic-column-action.factory.ts b/server/src/metadata/workspace-migration/factories/basic-column-action.factory.ts new file mode 100644 index 000000000..704c115d4 --- /dev/null +++ b/server/src/metadata/workspace-migration/factories/basic-column-action.factory.ts @@ -0,0 +1,65 @@ +import { Injectable, Logger } from '@nestjs/common'; + +import { WorkspaceColumnActionOptions } from 'src/metadata/workspace-migration/interfaces/workspace-column-action-options.interface'; +import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/field-metadata.interface'; + +import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; +import { + WorkspaceMigrationColumnActionType, + WorkspaceMigrationColumnAlter, + WorkspaceMigrationColumnCreate, +} from 'src/metadata/workspace-migration/workspace-migration.entity'; +import { serializeDefaultValue } from 'src/metadata/field-metadata/utils/serialize-default-value'; +import { fieldMetadataTypeToColumnType } from 'src/metadata/workspace-migration/utils/field-metadata-type-to-column-type.util'; +import { ColumnActionAbstractFactory } from 'src/metadata/workspace-migration/factories/column-action-abstract.factory'; + +export type BasicFieldMetadataType = + | FieldMetadataType.UUID + | FieldMetadataType.TEXT + | FieldMetadataType.PHONE + | FieldMetadataType.EMAIL + | FieldMetadataType.NUMERIC + | FieldMetadataType.NUMBER + | FieldMetadataType.PROBABILITY + | FieldMetadataType.BOOLEAN + | FieldMetadataType.DATE_TIME; + +@Injectable() +export class BasicColumnActionFactory extends ColumnActionAbstractFactory { + protected readonly logger = new Logger(BasicColumnActionFactory.name); + + protected handleCreateAction( + fieldMetadata: FieldMetadataInterface, + options?: WorkspaceColumnActionOptions, + ): WorkspaceMigrationColumnCreate { + const defaultValue = + fieldMetadata.defaultValue?.value ?? options?.defaultValue; + const serializedDefaultValue = serializeDefaultValue(defaultValue); + + return { + action: WorkspaceMigrationColumnActionType.CREATE, + columnName: fieldMetadata.targetColumnMap.value, + columnType: fieldMetadataTypeToColumnType(fieldMetadata.type), + isNullable: fieldMetadata.isNullable, + defaultValue: serializedDefaultValue, + }; + } + + protected handleAlterAction( + previousFieldMetadata: FieldMetadataInterface, + nextFieldMetadata: FieldMetadataInterface, + options?: WorkspaceColumnActionOptions, + ): WorkspaceMigrationColumnAlter { + const defaultValue = + nextFieldMetadata.defaultValue?.value ?? options?.defaultValue; + const serializedDefaultValue = serializeDefaultValue(defaultValue); + + return { + action: WorkspaceMigrationColumnActionType.ALTER, + columnName: nextFieldMetadata.targetColumnMap.value, + columnType: fieldMetadataTypeToColumnType(nextFieldMetadata.type), + isNullable: nextFieldMetadata.isNullable, + defaultValue: serializedDefaultValue, + }; + } +} diff --git a/server/src/metadata/workspace-migration/factories/column-action-abstract.factory.ts b/server/src/metadata/workspace-migration/factories/column-action-abstract.factory.ts new file mode 100644 index 000000000..38abfb64c --- /dev/null +++ b/server/src/metadata/workspace-migration/factories/column-action-abstract.factory.ts @@ -0,0 +1,66 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { Logger } from '@nestjs/common'; + +import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/field-metadata.interface'; +import { WorkspaceColumnActionOptions } from 'src/metadata/workspace-migration/interfaces/workspace-column-action-options.interface'; +import { WorkspaceColumnActionFactory } from 'src/metadata/workspace-migration/interfaces/workspace-column-action-factory.interface'; + +import { + WorkspaceMigrationColumnActionType, + WorkspaceMigrationColumnAction, + WorkspaceMigrationColumnCreate, + WorkspaceMigrationColumnAlter, +} from 'src/metadata/workspace-migration/workspace-migration.entity'; +import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; + +export class ColumnActionAbstractFactory< + T extends FieldMetadataType | 'default', +> implements WorkspaceColumnActionFactory +{ + protected readonly logger = new Logger(ColumnActionAbstractFactory.name); + + create( + action: + | WorkspaceMigrationColumnActionType.CREATE + | WorkspaceMigrationColumnActionType.ALTER, + previousFieldMetadata: FieldMetadataInterface | undefined, + nextFieldMetadata: FieldMetadataInterface, + options?: WorkspaceColumnActionOptions, + ): WorkspaceMigrationColumnAction { + switch (action) { + case WorkspaceMigrationColumnActionType.CREATE: + return this.handleCreateAction(nextFieldMetadata, options); + case WorkspaceMigrationColumnActionType.ALTER: { + if (!previousFieldMetadata) { + throw new Error('Previous field metadata is required for alter'); + } + + return this.handleAlterAction( + previousFieldMetadata, + nextFieldMetadata, + options, + ); + } + default: { + this.logger.error(`Invalid action: ${action}`); + + throw new Error('[AbstractFactory]: invalid action'); + } + } + } + + protected handleCreateAction( + _fieldMetadata: FieldMetadataInterface, + _options?: WorkspaceColumnActionOptions, + ): WorkspaceMigrationColumnCreate { + throw new Error('handleCreateAction method not implemented.'); + } + + protected handleAlterAction( + _previousFieldMetadata: FieldMetadataInterface, + _nextFieldMetadata: FieldMetadataInterface, + _options?: WorkspaceColumnActionOptions, + ): WorkspaceMigrationColumnAlter { + throw new Error('handleAlterAction method not implemented.'); + } +} diff --git a/server/src/metadata/workspace-migration/factories/enum-column-action.factory.ts b/server/src/metadata/workspace-migration/factories/enum-column-action.factory.ts new file mode 100644 index 000000000..4f5d04cd5 --- /dev/null +++ b/server/src/metadata/workspace-migration/factories/enum-column-action.factory.ts @@ -0,0 +1,85 @@ +import { Injectable, Logger } from '@nestjs/common'; + +import { WorkspaceColumnActionOptions } from 'src/metadata/workspace-migration/interfaces/workspace-column-action-options.interface'; +import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/field-metadata.interface'; + +import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; +import { + WorkspaceMigrationColumnActionType, + WorkspaceMigrationColumnAlter, + WorkspaceMigrationColumnCreate, +} from 'src/metadata/workspace-migration/workspace-migration.entity'; +import { serializeDefaultValue } from 'src/metadata/field-metadata/utils/serialize-default-value'; +import { fieldMetadataTypeToColumnType } from 'src/metadata/workspace-migration/utils/field-metadata-type-to-column-type.util'; +import { ColumnActionAbstractFactory } from 'src/metadata/workspace-migration/factories/column-action-abstract.factory'; + +export type EnumFieldMetadataType = + | FieldMetadataType.RATING + | FieldMetadataType.SELECT + | FieldMetadataType.MULTI_SELECT; + +@Injectable() +export class EnumColumnActionFactory extends ColumnActionAbstractFactory { + protected readonly logger = new Logger(EnumColumnActionFactory.name); + + protected handleCreateAction( + fieldMetadata: FieldMetadataInterface, + options: WorkspaceColumnActionOptions, + ): WorkspaceMigrationColumnCreate { + const defaultValue = + fieldMetadata.defaultValue?.value ?? options?.defaultValue; + const serializedDefaultValue = serializeDefaultValue(defaultValue); + const enumOptions = fieldMetadata.options + ? [...fieldMetadata.options.map((option) => option.value)] + : undefined; + + return { + action: WorkspaceMigrationColumnActionType.CREATE, + columnName: fieldMetadata.targetColumnMap.value, + columnType: fieldMetadataTypeToColumnType(fieldMetadata.type), + enum: enumOptions, + isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT, + isNullable: fieldMetadata.isNullable, + defaultValue: serializedDefaultValue, + }; + } + + protected handleAlterAction( + previousFieldMetadata: FieldMetadataInterface, + nextFieldMetadata: FieldMetadataInterface, + options: WorkspaceColumnActionOptions, + ): WorkspaceMigrationColumnAlter { + const defaultValue = + nextFieldMetadata.defaultValue?.value ?? options?.defaultValue; + const serializedDefaultValue = serializeDefaultValue(defaultValue); + const enumOptions = nextFieldMetadata.options + ? [ + ...nextFieldMetadata.options.map((option) => { + const previousOption = previousFieldMetadata.options?.find( + (previousOption) => previousOption.id === option.id, + ); + + // The id is the same, but the value is different, so we need to alter the enum + if (previousOption && previousOption.value !== option.value) { + return { + from: previousOption.value, + to: option.value, + }; + } + + return option.value; + }), + ] + : undefined; + + return { + action: WorkspaceMigrationColumnActionType.ALTER, + columnName: nextFieldMetadata.targetColumnMap.value, + columnType: fieldMetadataTypeToColumnType(nextFieldMetadata.type), + enum: enumOptions, + isArray: nextFieldMetadata.type === FieldMetadataType.MULTI_SELECT, + isNullable: nextFieldMetadata.isNullable, + defaultValue: serializedDefaultValue, + }; + } +} diff --git a/server/src/metadata/workspace-migration/factories/factories.ts b/server/src/metadata/workspace-migration/factories/factories.ts new file mode 100644 index 000000000..6083cd0eb --- /dev/null +++ b/server/src/metadata/workspace-migration/factories/factories.ts @@ -0,0 +1,7 @@ +import { BasicColumnActionFactory } from 'src/metadata/workspace-migration/factories/basic-column-action.factory'; +import { EnumColumnActionFactory } from 'src/metadata/workspace-migration/factories/enum-column-action.factory'; + +export const workspaceColumnActionFactories = [ + BasicColumnActionFactory, + EnumColumnActionFactory, +]; diff --git a/server/src/metadata/workspace-migration/interfaces/workspace-column-action-factory.interface.ts b/server/src/metadata/workspace-migration/interfaces/workspace-column-action-factory.interface.ts new file mode 100644 index 000000000..71318f161 --- /dev/null +++ b/server/src/metadata/workspace-migration/interfaces/workspace-column-action-factory.interface.ts @@ -0,0 +1,21 @@ +import { WorkspaceColumnActionOptions } from 'src/metadata/workspace-migration/interfaces/workspace-column-action-options.interface'; +import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/field-metadata.interface'; + +import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; +import { + WorkspaceMigrationColumnActionType, + WorkspaceMigrationColumnAction, +} from 'src/metadata/workspace-migration/workspace-migration.entity'; + +export interface WorkspaceColumnActionFactory< + T extends FieldMetadataType | 'default', +> { + create( + action: + | WorkspaceMigrationColumnActionType.CREATE + | WorkspaceMigrationColumnActionType.ALTER, + previousFieldMetadata: FieldMetadataInterface | undefined, + nextFieldMetadata: FieldMetadataInterface, + options?: WorkspaceColumnActionOptions, + ): WorkspaceMigrationColumnAction; +} diff --git a/server/src/metadata/workspace-migration/interfaces/workspace-column-action-options.interface.ts b/server/src/metadata/workspace-migration/interfaces/workspace-column-action-options.interface.ts new file mode 100644 index 000000000..c100117d8 --- /dev/null +++ b/server/src/metadata/workspace-migration/interfaces/workspace-column-action-options.interface.ts @@ -0,0 +1,3 @@ +export interface WorkspaceColumnActionOptions { + defaultValue?: string; +} diff --git a/server/src/metadata/workspace-migration/utils/field-metadata-type-to-column-type.util.ts b/server/src/metadata/workspace-migration/utils/field-metadata-type-to-column-type.util.ts new file mode 100644 index 000000000..85647ae57 --- /dev/null +++ b/server/src/metadata/workspace-migration/utils/field-metadata-type-to-column-type.util.ts @@ -0,0 +1,34 @@ +import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; + +export const fieldMetadataTypeToColumnType = ( + fieldMetadataType: Type, +): string => { + /** + * Composite types are not implemented here, as they are flattened by their composite definitions. + * See src/metadata/field-metadata/composite-types for more information. + */ + switch (fieldMetadataType) { + case FieldMetadataType.UUID: + return 'uuid'; + case FieldMetadataType.TEXT: + return 'text'; + case FieldMetadataType.PHONE: + case FieldMetadataType.EMAIL: + return 'varchar'; + case FieldMetadataType.NUMERIC: + return 'numeric'; + case FieldMetadataType.NUMBER: + case FieldMetadataType.PROBABILITY: + return 'float'; + case FieldMetadataType.BOOLEAN: + return 'boolean'; + case FieldMetadataType.DATE_TIME: + return 'timestamp'; + case FieldMetadataType.RATING: + case FieldMetadataType.SELECT: + case FieldMetadataType.MULTI_SELECT: + return 'enum'; + default: + throw new Error(`Cannot convert ${fieldMetadataType} to column type.`); + } +}; diff --git a/server/src/metadata/workspace-migration/workspace-migration.entity.ts b/server/src/metadata/workspace-migration/workspace-migration.entity.ts index 1e847c935..7ce8accee 100644 --- a/server/src/metadata/workspace-migration/workspace-migration.entity.ts +++ b/server/src/metadata/workspace-migration/workspace-migration.entity.ts @@ -7,13 +7,29 @@ import { export enum WorkspaceMigrationColumnActionType { CREATE = 'CREATE', + ALTER = 'ALTER', RELATION = 'RELATION', } +export type WorkspaceMigrationEnum = string | { from: string; to: string }; + export type WorkspaceMigrationColumnCreate = { action: WorkspaceMigrationColumnActionType.CREATE; columnName: string; columnType: string; + enum?: WorkspaceMigrationEnum[]; + isArray?: boolean; + isNullable?: boolean; + defaultValue?: any; +}; + +export type WorkspaceMigrationColumnAlter = { + action: WorkspaceMigrationColumnActionType.ALTER; + columnName: string; + columnType: string; + enum?: WorkspaceMigrationEnum[]; + isArray?: boolean; + isNullable?: boolean; defaultValue?: any; }; @@ -27,7 +43,11 @@ export type WorkspaceMigrationColumnRelation = { export type WorkspaceMigrationColumnAction = { action: WorkspaceMigrationColumnActionType; -} & (WorkspaceMigrationColumnCreate | WorkspaceMigrationColumnRelation); +} & ( + | WorkspaceMigrationColumnCreate + | WorkspaceMigrationColumnAlter + | WorkspaceMigrationColumnRelation +); export type WorkspaceMigrationTableAction = { name: string; diff --git a/server/src/metadata/workspace-migration/workspace-migration.factory.ts b/server/src/metadata/workspace-migration/workspace-migration.factory.ts new file mode 100644 index 000000000..e8e2f4108 --- /dev/null +++ b/server/src/metadata/workspace-migration/workspace-migration.factory.ts @@ -0,0 +1,186 @@ +import { Injectable, Logger } from '@nestjs/common'; + +import { WorkspaceColumnActionFactory } from 'src/metadata/workspace-migration/interfaces/workspace-column-action-factory.interface'; +import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/field-metadata.interface'; +import { WorkspaceColumnActionOptions } from 'src/metadata/workspace-migration/interfaces/workspace-column-action-options.interface'; + +import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; +import { BasicColumnActionFactory } from 'src/metadata/workspace-migration/factories/basic-column-action.factory'; +import { EnumColumnActionFactory } from 'src/metadata/workspace-migration/factories/enum-column-action.factory'; +import { + WorkspaceMigrationColumnAction, + WorkspaceMigrationColumnActionType, +} from 'src/metadata/workspace-migration/workspace-migration.entity'; +import { isCompositeFieldMetadataType } from 'src/metadata/field-metadata/utils/is-composite-field-metadata-type.util'; +import { linkObjectDefinition } from 'src/metadata/field-metadata/composite-types/link.composite-type'; +import { currencyObjectDefinition } from 'src/metadata/field-metadata/composite-types/currency.composite-type'; +import { fullNameObjectDefinition } from 'src/metadata/field-metadata/composite-types/full-name.composite-type'; + +@Injectable() +export class WorkspaceMigrationFactory { + private readonly logger = new Logger(WorkspaceMigrationFactory.name); + private factoriesMap: Map< + FieldMetadataType, + { + factory: WorkspaceColumnActionFactory; + options?: WorkspaceColumnActionOptions; + } + >; + private compositeDefinitions = new Map(); + + constructor( + private readonly basicColumnActionFactory: BasicColumnActionFactory, + private readonly enumColumnActionFactory: EnumColumnActionFactory, + ) { + this.factoriesMap = new Map< + FieldMetadataType, + { + factory: WorkspaceColumnActionFactory; + options?: WorkspaceColumnActionOptions; + } + >([ + [FieldMetadataType.UUID, { factory: this.basicColumnActionFactory }], + [ + FieldMetadataType.TEXT, + { + factory: this.basicColumnActionFactory, + options: { + defaultValue: '', + }, + }, + ], + [ + FieldMetadataType.PHONE, + { + factory: this.basicColumnActionFactory, + options: { + defaultValue: '', + }, + }, + ], + [ + FieldMetadataType.EMAIL, + { + factory: this.basicColumnActionFactory, + options: { + defaultValue: '', + }, + }, + ], + [FieldMetadataType.NUMERIC, { factory: this.basicColumnActionFactory }], + [FieldMetadataType.NUMBER, { factory: this.basicColumnActionFactory }], + [ + FieldMetadataType.PROBABILITY, + { factory: this.basicColumnActionFactory }, + ], + [FieldMetadataType.BOOLEAN, { factory: this.basicColumnActionFactory }], + [FieldMetadataType.DATE_TIME, { factory: this.basicColumnActionFactory }], + [FieldMetadataType.RATING, { factory: this.enumColumnActionFactory }], + [FieldMetadataType.SELECT, { factory: this.enumColumnActionFactory }], + [ + FieldMetadataType.MULTI_SELECT, + { factory: this.enumColumnActionFactory }, + ], + ]); + + this.compositeDefinitions = new Map([ + [FieldMetadataType.LINK, linkObjectDefinition.fields], + [FieldMetadataType.CURRENCY, currencyObjectDefinition.fields], + [FieldMetadataType.FULL_NAME, fullNameObjectDefinition.fields], + ]); + } + + createColumnActions( + action: WorkspaceMigrationColumnActionType.CREATE, + fieldMetadata: FieldMetadataInterface, + ): WorkspaceMigrationColumnAction[]; + createColumnActions( + action: WorkspaceMigrationColumnActionType.ALTER, + previousFieldMetadata: FieldMetadataInterface, + nextFieldMetadata: FieldMetadataInterface, + ): WorkspaceMigrationColumnAction[]; + createColumnActions( + action: + | WorkspaceMigrationColumnActionType.CREATE + | WorkspaceMigrationColumnActionType.ALTER, + fieldMetadataOrPreviousFieldMetadata: FieldMetadataInterface, + undefinedOrnextFieldMetadata?: FieldMetadataInterface, + ): WorkspaceMigrationColumnAction[] { + const previousFieldMetadata = + action === WorkspaceMigrationColumnActionType.ALTER + ? fieldMetadataOrPreviousFieldMetadata + : undefined; + const nextFieldMetadata = + action === WorkspaceMigrationColumnActionType.CREATE + ? fieldMetadataOrPreviousFieldMetadata + : undefinedOrnextFieldMetadata; + + if (!nextFieldMetadata) { + this.logger.error( + `No field metadata provided for action ${action}`, + fieldMetadataOrPreviousFieldMetadata, + ); + + throw new Error(`No field metadata provided for action ${action}`); + } + + // If it's a composite field type, we need to create a column action for each of the fields + if (isCompositeFieldMetadataType(nextFieldMetadata.type)) { + const fieldMetadataCollection = this.compositeDefinitions.get( + nextFieldMetadata.type, + ); + + if (!fieldMetadataCollection) { + this.logger.error( + `No composite definition found for type ${nextFieldMetadata.type}`, + { + nextFieldMetadata, + }, + ); + + throw new Error( + `No composite definition found for type ${nextFieldMetadata.type}`, + ); + } + + return fieldMetadataCollection.map((fieldMetadata) => + this.createColumnAction(action, fieldMetadata, fieldMetadata), + ); + } + + // Otherwise, we create a single column action + const columnAction = this.createColumnAction( + action, + previousFieldMetadata, + nextFieldMetadata, + ); + + return [columnAction]; + } + + private createColumnAction( + action: + | WorkspaceMigrationColumnActionType.CREATE + | WorkspaceMigrationColumnActionType.ALTER, + previousFieldMetadata: FieldMetadataInterface | undefined, + nextFieldMetadata: FieldMetadataInterface, + ): WorkspaceMigrationColumnAction { + const { factory, options } = + this.factoriesMap.get(nextFieldMetadata.type) ?? {}; + + if (!factory) { + this.logger.error(`No factory found for type ${nextFieldMetadata.type}`, { + nextFieldMetadata, + }); + + throw new Error(`No factory found for type ${nextFieldMetadata.type}`); + } + + return factory.create( + action, + previousFieldMetadata, + nextFieldMetadata, + options, + ); + } +} diff --git a/server/src/metadata/workspace-migration/workspace-migration.module.ts b/server/src/metadata/workspace-migration/workspace-migration.module.ts index a7c1fa863..50661b2e8 100644 --- a/server/src/metadata/workspace-migration/workspace-migration.module.ts +++ b/server/src/metadata/workspace-migration/workspace-migration.module.ts @@ -1,12 +1,19 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { workspaceColumnActionFactories } from 'src/metadata/workspace-migration/factories/factories'; +import { WorkspaceMigrationFactory } from 'src/metadata/workspace-migration/workspace-migration.factory'; + import { WorkspaceMigrationService } from './workspace-migration.service'; import { WorkspaceMigrationEntity } from './workspace-migration.entity'; @Module({ imports: [TypeOrmModule.forFeature([WorkspaceMigrationEntity], 'metadata')], - exports: [WorkspaceMigrationService], - providers: [WorkspaceMigrationService], + providers: [ + ...workspaceColumnActionFactories, + WorkspaceMigrationFactory, + WorkspaceMigrationService, + ], + exports: [WorkspaceMigrationFactory, WorkspaceMigrationService], }) export class WorkspaceMigrationModule {} diff --git a/server/src/workspace/utils/__tests__/deduce-relation-direction.spec.ts b/server/src/workspace/utils/__tests__/deduce-relation-direction.spec.ts index 84ac894b2..e050e15ee 100644 --- a/server/src/workspace/utils/__tests__/deduce-relation-direction.spec.ts +++ b/server/src/workspace/utils/__tests__/deduce-relation-direction.spec.ts @@ -1,4 +1,4 @@ -import { RelationMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/relation-metadata.interface'; +import { RelationMetadataInterface } from 'src/metadata/field-metadata/interfaces/relation-metadata.interface'; import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity'; import { diff --git a/server/src/workspace/utils/deduce-relation-direction.util.ts b/server/src/workspace/utils/deduce-relation-direction.util.ts index 8ae831d41..5d91f90a7 100644 --- a/server/src/workspace/utils/deduce-relation-direction.util.ts +++ b/server/src/workspace/utils/deduce-relation-direction.util.ts @@ -1,4 +1,4 @@ -import { RelationMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/relation-metadata.interface'; +import { RelationMetadataInterface } from 'src/metadata/field-metadata/interfaces/relation-metadata.interface'; export enum RelationDirection { FROM = 'from', diff --git a/server/src/workspace/utils/get-resolver-name.util.ts b/server/src/workspace/utils/get-resolver-name.util.ts index d1a60e790..1b3dad067 100644 --- a/server/src/workspace/utils/get-resolver-name.util.ts +++ b/server/src/workspace/utils/get-resolver-name.util.ts @@ -1,5 +1,5 @@ import { WorkspaceResolverBuilderMethodNames } from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; -import { ObjectMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/object-metadata.interface'; +import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/object-metadata.interface'; import { camelCase } from 'src/utils/camel-case'; import { pascalCase } from 'src/utils/pascal-case'; diff --git a/server/src/workspace/utils/is-composite-field-metadata-type.util.ts b/server/src/workspace/utils/is-relation-field-metadata-type.util.ts similarity index 54% rename from server/src/workspace/utils/is-composite-field-metadata-type.util.ts rename to server/src/workspace/utils/is-relation-field-metadata-type.util.ts index 547fb98c2..f42b593ac 100644 --- a/server/src/workspace/utils/is-composite-field-metadata-type.util.ts +++ b/server/src/workspace/utils/is-relation-field-metadata-type.util.ts @@ -1,5 +1,7 @@ import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; -export const isCompositeFieldMetadataType = (type: FieldMetadataType) => { +export const isRelationFieldMetadataType = ( + type: FieldMetadataType, +): type is FieldMetadataType.RELATION => { return type === FieldMetadataType.RELATION; }; diff --git a/server/src/workspace/workspace-migration-runner/services/workspace-migration-enum.service.ts b/server/src/workspace/workspace-migration-runner/services/workspace-migration-enum.service.ts new file mode 100644 index 000000000..33aef3422 --- /dev/null +++ b/server/src/workspace/workspace-migration-runner/services/workspace-migration-enum.service.ts @@ -0,0 +1,183 @@ +import { Injectable } from '@nestjs/common'; + +import { QueryRunner } from 'typeorm'; + +import { WorkspaceMigrationColumnAlter } from 'src/metadata/workspace-migration/workspace-migration.entity'; + +@Injectable() +export class WorkspaceMigrationEnumService { + async alterEnum( + queryRunner: QueryRunner, + schemaName: string, + tableName: string, + migrationColumn: WorkspaceMigrationColumnAlter, + ) { + const oldEnumTypeName = `${tableName}_${migrationColumn.columnName}_enum`; + const newEnumTypeName = `${tableName}_${migrationColumn.columnName}_enum_new`; + const enumValues = + migrationColumn.enum?.map((enumValue) => { + if (typeof enumValue === 'string') { + return enumValue; + } + + return enumValue.to; + }) ?? []; + + if (!migrationColumn.isNullable && !migrationColumn.defaultValue) { + migrationColumn.defaultValue = migrationColumn.enum?.[0]; + } + + // Create new enum type with new values + await this.createNewEnumType( + newEnumTypeName, + queryRunner, + schemaName, + enumValues, + ); + + // Temporarily change column type to text + await queryRunner.query(` + ALTER TABLE "${schemaName}"."${tableName}" + ALTER COLUMN "${migrationColumn.columnName}" TYPE TEXT + `); + + // Migrate existing values to new values + await this.migrateEnumValues( + queryRunner, + schemaName, + tableName, + migrationColumn, + ); + + // Update existing rows to handle missing values + await this.handleMissingEnumValues( + queryRunner, + schemaName, + tableName, + migrationColumn, + enumValues, + ); + + // Alter column type to new enum + await this.updateColumnToNewEnum( + queryRunner, + schemaName, + tableName, + migrationColumn.columnName, + newEnumTypeName, + ); + + // Drop old enum type + await this.dropOldEnumType(queryRunner, schemaName, oldEnumTypeName); + + // Rename new enum type to old enum type name + await this.renameEnumType( + queryRunner, + schemaName, + oldEnumTypeName, + newEnumTypeName, + ); + } + + private async createNewEnumType( + name: string, + queryRunner: QueryRunner, + schemaName: string, + newValues: string[], + ) { + const enumValues = newValues + .map((value) => `'${value.replace(/'/g, "''")}'`) + .join(', '); + + await queryRunner.query( + `CREATE TYPE "${schemaName}"."${name}" AS ENUM (${enumValues})`, + ); + } + + private async migrateEnumValues( + queryRunner: QueryRunner, + schemaName: string, + tableName: string, + migrationColumn: WorkspaceMigrationColumnAlter, + ) { + if (!migrationColumn.enum) { + return; + } + + for (const enumValue of migrationColumn.enum) { + // Skip string values + if (typeof enumValue === 'string') { + continue; + } + + await queryRunner.query(` + UPDATE "${schemaName}"."${tableName}" + SET "${migrationColumn.columnName}" = '${enumValue.to}' + WHERE "${migrationColumn.columnName}" = '${enumValue.from}' + `); + } + } + + private async handleMissingEnumValues( + queryRunner: QueryRunner, + schemaName: string, + tableName: string, + migrationColumn: WorkspaceMigrationColumnAlter, + enumValues: string[], + ) { + // Set missing values to null or default value + let defaultValue = 'NULL'; + + if (migrationColumn.defaultValue) { + if (Array.isArray(migrationColumn.defaultValue)) { + defaultValue = `ARRAY[${migrationColumn.defaultValue + .map((e) => `'${e}'`) + .join(', ')}]`; + } else { + defaultValue = `'${migrationColumn.defaultValue}'`; + } + } + + await queryRunner.query(` + UPDATE "${schemaName}"."${tableName}" + SET "${migrationColumn.columnName}" = ${defaultValue} + WHERE "${migrationColumn.columnName}" NOT IN (${enumValues + .map((e) => `'${e}'`) + .join(', ')}) + `); + } + + private async updateColumnToNewEnum( + queryRunner: QueryRunner, + schemaName: string, + tableName: string, + columnName: string, + newEnumTypeName: string, + ) { + await queryRunner.query( + `ALTER TABLE "${schemaName}"."${tableName}" ALTER COLUMN "${columnName}" TYPE "${schemaName}"."${newEnumTypeName}" USING ("${columnName}"::text::"${schemaName}"."${newEnumTypeName}")`, + ); + } + + private async dropOldEnumType( + queryRunner: QueryRunner, + schemaName: string, + oldEnumTypeName: string, + ) { + await queryRunner.query( + `DROP TYPE IF EXISTS "${schemaName}"."${oldEnumTypeName}"`, + ); + } + + private async renameEnumType( + queryRunner: QueryRunner, + schemaName: string, + oldEnumTypeName: string, + newEnumTypeName: string, + ) { + await queryRunner.query(` + ALTER TYPE "${schemaName}"."${newEnumTypeName}" + RENAME TO "${oldEnumTypeName}" + `); + } +} diff --git a/server/src/workspace/workspace-migration-runner/workspace-migration-runner.module.ts b/server/src/workspace/workspace-migration-runner/workspace-migration-runner.module.ts index 96d58101b..81ec80f2b 100644 --- a/server/src/workspace/workspace-migration-runner/workspace-migration-runner.module.ts +++ b/server/src/workspace/workspace-migration-runner/workspace-migration-runner.module.ts @@ -3,6 +3,7 @@ import { Module } from '@nestjs/common'; import { WorkspaceMigrationModule } from 'src/metadata/workspace-migration/workspace-migration.module'; import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/workspace-datasource.module'; import { WorkspaceCacheVersionModule } from 'src/metadata/workspace-cache-version/workspace-cache-version.module'; +import { WorkspaceMigrationEnumService } from 'src/workspace/workspace-migration-runner/services/workspace-migration-enum.service'; import { WorkspaceMigrationRunnerService } from './workspace-migration-runner.service'; @@ -12,7 +13,7 @@ import { WorkspaceMigrationRunnerService } from './workspace-migration-runner.se WorkspaceMigrationModule, WorkspaceCacheVersionModule, ], + providers: [WorkspaceMigrationRunnerService, WorkspaceMigrationEnumService], exports: [WorkspaceMigrationRunnerService], - providers: [WorkspaceMigrationRunnerService], }) export class WorkspaceMigrationRunnerModule {} diff --git a/server/src/workspace/workspace-migration-runner/workspace-migration-runner.service.ts b/server/src/workspace/workspace-migration-runner/workspace-migration-runner.service.ts index 5e43bfc6f..b39f985be 100644 --- a/server/src/workspace/workspace-migration-runner/workspace-migration-runner.service.ts +++ b/server/src/workspace/workspace-migration-runner/workspace-migration-runner.service.ts @@ -16,8 +16,10 @@ import { WorkspaceMigrationColumnActionType, WorkspaceMigrationColumnCreate, WorkspaceMigrationColumnRelation, + WorkspaceMigrationColumnAlter, } from 'src/metadata/workspace-migration/workspace-migration.entity'; import { WorkspaceCacheVersionService } from 'src/metadata/workspace-cache-version/workspace-cache-version.service'; +import { WorkspaceMigrationEnumService } from 'src/workspace/workspace-migration-runner/services/workspace-migration-enum.service'; import { customTableDefaultColumns } from './utils/custom-table-default-column.util'; @@ -27,6 +29,7 @@ export class WorkspaceMigrationRunnerService { private readonly workspaceDataSourceService: WorkspaceDataSourceService, private readonly workspaceMigrationService: WorkspaceMigrationService, private readonly workspaceCacheVersionService: WorkspaceCacheVersionService, + private readonly workspaceMigrationEnumService: WorkspaceMigrationEnumService, ) {} /** @@ -168,6 +171,14 @@ export class WorkspaceMigrationRunnerService { columnMigration, ); break; + case WorkspaceMigrationColumnActionType.ALTER: + await this.alterColumn( + queryRunner, + schemaName, + tableName, + columnMigration, + ); + break; case WorkspaceMigrationColumnActionType.RELATION: await this.createForeignKey( queryRunner, @@ -200,6 +211,7 @@ export class WorkspaceMigrationRunnerService { `${schemaName}.${tableName}`, migrationColumn.columnName, ); + if (hasColumn) { return; } @@ -210,11 +222,46 @@ export class WorkspaceMigrationRunnerService { name: migrationColumn.columnName, type: migrationColumn.columnType, default: migrationColumn.defaultValue, + enum: migrationColumn.enum?.filter( + (value): value is string => typeof value === 'string', + ), + isArray: migrationColumn.isArray, isNullable: true, }), ); } + private async alterColumn( + queryRunner: QueryRunner, + schemaName: string, + tableName: string, + migrationColumn: WorkspaceMigrationColumnAlter, + ) { + const enumValues = migrationColumn.enum; + + // TODO: Maybe we can do something better if we can recreate the old `TableColumn` object + if (enumValues) { + // This is returning the old enum values to avoid TypeORM droping the enum type + await this.workspaceMigrationEnumService.alterEnum( + queryRunner, + schemaName, + tableName, + migrationColumn, + ); + } else { + await queryRunner.changeColumn( + `${schemaName}.${tableName}`, + migrationColumn.columnName, + new TableColumn({ + name: migrationColumn.columnName, + type: migrationColumn.columnType, + default: migrationColumn.defaultValue, + isNullable: migrationColumn.isNullable, + }), + ); + } + } + private async createForeignKey( queryRunner: QueryRunner, schemaName: string, diff --git a/server/src/workspace/workspace-query-builder/factories/args-alias.factory.ts b/server/src/workspace/workspace-query-builder/factories/args-alias.factory.ts index 5c3cf4821..ae80ffd22 100644 --- a/server/src/workspace/workspace-query-builder/factories/args-alias.factory.ts +++ b/server/src/workspace/workspace-query-builder/factories/args-alias.factory.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; -import { FieldMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/field-metadata.interface'; +import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/field-metadata.interface'; @Injectable() export class ArgsAliasFactory { diff --git a/server/src/workspace/workspace-query-builder/factories/args-string.factory.ts b/server/src/workspace/workspace-query-builder/factories/args-string.factory.ts index 6aede0941..9e06e4532 100644 --- a/server/src/workspace/workspace-query-builder/factories/args-string.factory.ts +++ b/server/src/workspace/workspace-query-builder/factories/args-string.factory.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; -import { FieldMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/field-metadata.interface'; +import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/field-metadata.interface'; import { stringifyWithoutKeyQuote } from 'src/workspace/workspace-query-builder/utils/stringify-without-key-quote.util'; diff --git a/server/src/workspace/workspace-query-builder/factories/factories.ts b/server/src/workspace/workspace-query-builder/factories/factories.ts index 7790d2d97..b5ec97d49 100644 --- a/server/src/workspace/workspace-query-builder/factories/factories.ts +++ b/server/src/workspace/workspace-query-builder/factories/factories.ts @@ -1,6 +1,6 @@ import { ArgsAliasFactory } from './args-alias.factory'; import { ArgsStringFactory } from './args-string.factory'; -import { CompositeFieldAliasFactory } from './composite-field-alias.factory'; +import { RelationFieldAliasFactory } from './relation-field-alias.factory'; import { CreateManyQueryFactory } from './create-many-query.factory'; import { DeleteOneQueryFactory } from './delete-one-query.factory'; import { FieldAliasFacotry } from './field-alias.factory'; @@ -14,7 +14,7 @@ import { DeleteManyQueryFactory } from './delete-many-query.factory'; export const workspaceQueryBuilderFactories = [ ArgsAliasFactory, ArgsStringFactory, - CompositeFieldAliasFactory, + RelationFieldAliasFactory, CreateManyQueryFactory, DeleteOneQueryFactory, FieldAliasFacotry, diff --git a/server/src/workspace/workspace-query-builder/factories/field-alias.factory.ts b/server/src/workspace/workspace-query-builder/factories/field-alias.factory.ts index ac387b4e2..e00a6f39d 100644 --- a/server/src/workspace/workspace-query-builder/factories/field-alias.factory.ts +++ b/server/src/workspace/workspace-query-builder/factories/field-alias.factory.ts @@ -1,6 +1,6 @@ import { Injectable, Logger } from '@nestjs/common'; -import { FieldMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/field-metadata.interface'; +import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/field-metadata.interface'; @Injectable() export class FieldAliasFacotry { diff --git a/server/src/workspace/workspace-query-builder/factories/fields-string.factory.ts b/server/src/workspace/workspace-query-builder/factories/fields-string.factory.ts index ffe449301..5cfffa686 100644 --- a/server/src/workspace/workspace-query-builder/factories/fields-string.factory.ts +++ b/server/src/workspace/workspace-query-builder/factories/fields-string.factory.ts @@ -4,12 +4,12 @@ import { GraphQLResolveInfo } from 'graphql'; import graphqlFields from 'graphql-fields'; import isEmpty from 'lodash.isempty'; -import { FieldMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/field-metadata.interface'; +import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/field-metadata.interface'; -import { isCompositeFieldMetadataType } from 'src/workspace/utils/is-composite-field-metadata-type.util'; +import { isRelationFieldMetadataType } from 'src/workspace/utils/is-relation-field-metadata-type.util'; import { FieldAliasFacotry } from './field-alias.factory'; -import { CompositeFieldAliasFactory } from './composite-field-alias.factory'; +import { RelationFieldAliasFactory } from './relation-field-alias.factory'; @Injectable() export class FieldsStringFactory { @@ -17,7 +17,7 @@ export class FieldsStringFactory { constructor( private readonly fieldAliasFactory: FieldAliasFacotry, - private readonly compositeFieldAliasFactory: CompositeFieldAliasFactory, + private readonly relationFieldAliasFactory: RelationFieldAliasFactory, ) {} create( @@ -52,9 +52,9 @@ export class FieldsStringFactory { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const fieldMetadata = fieldMetadataMap.get(fieldKey)!; - // If the field is a composite field, we need to create a special alias - if (isCompositeFieldMetadataType(fieldMetadata.type)) { - const alias = this.compositeFieldAliasFactory.create( + // If the field is a relation field, we need to create a special alias + if (isRelationFieldMetadataType(fieldMetadata.type)) { + const alias = this.relationFieldAliasFactory.create( fieldKey, fieldValue, fieldMetadata, diff --git a/server/src/workspace/workspace-query-builder/factories/composite-field-alias.factory.ts b/server/src/workspace/workspace-query-builder/factories/relation-field-alias.factory.ts similarity index 80% rename from server/src/workspace/workspace-query-builder/factories/composite-field-alias.factory.ts rename to server/src/workspace/workspace-query-builder/factories/relation-field-alias.factory.ts index 83ce684d5..79e6c9266 100644 --- a/server/src/workspace/workspace-query-builder/factories/composite-field-alias.factory.ts +++ b/server/src/workspace/workspace-query-builder/factories/relation-field-alias.factory.ts @@ -2,10 +2,9 @@ import { forwardRef, Inject, Injectable, Logger } from '@nestjs/common'; import { GraphQLResolveInfo } from 'graphql'; -import { FieldMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/field-metadata.interface'; +import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/field-metadata.interface'; -import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; -import { isCompositeFieldMetadataType } from 'src/workspace/utils/is-composite-field-metadata-type.util'; +import { isRelationFieldMetadataType } from 'src/workspace/utils/is-relation-field-metadata-type.util'; import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity'; import { deduceRelationDirection, @@ -17,8 +16,8 @@ import { FieldsStringFactory } from './fields-string.factory'; import { ArgsStringFactory } from './args-string.factory'; @Injectable() -export class CompositeFieldAliasFactory { - private logger = new Logger(CompositeFieldAliasFactory.name); +export class RelationFieldAliasFactory { + private logger = new Logger(RelationFieldAliasFactory.name); constructor( @Inject(forwardRef(() => FieldsStringFactory)) @@ -32,21 +31,11 @@ export class CompositeFieldAliasFactory { fieldMetadata: FieldMetadataInterface, info: GraphQLResolveInfo, ) { - if (!isCompositeFieldMetadataType(fieldMetadata.type)) { - throw new Error(`Field ${fieldMetadata.name} is not a composite field`); + if (!isRelationFieldMetadataType(fieldMetadata.type)) { + throw new Error(`Field ${fieldMetadata.name} is not a relation field`); } - switch (fieldMetadata.type) { - case FieldMetadataType.RELATION: - return this.createRelationAlias( - fieldKey, - fieldValue, - fieldMetadata, - info, - ); - } - - return null; + return this.createRelationAlias(fieldKey, fieldValue, fieldMetadata, info); } private createRelationAlias( diff --git a/server/src/workspace/workspace-query-builder/interfaces/workspace-query-builder-options.interface.ts b/server/src/workspace/workspace-query-builder/interfaces/workspace-query-builder-options.interface.ts index 319df6f38..db968548b 100644 --- a/server/src/workspace/workspace-query-builder/interfaces/workspace-query-builder-options.interface.ts +++ b/server/src/workspace/workspace-query-builder/interfaces/workspace-query-builder-options.interface.ts @@ -1,6 +1,6 @@ import { GraphQLResolveInfo } from 'graphql'; -import { FieldMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/field-metadata.interface'; +import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/field-metadata.interface'; export interface WorkspaceQueryBuilderOptions { targetTableName: string; diff --git a/server/src/workspace/workspace-query-runner/interfaces/query-runner-optionts.interface.ts b/server/src/workspace/workspace-query-runner/interfaces/query-runner-optionts.interface.ts index f138a711e..d1f94d54a 100644 --- a/server/src/workspace/workspace-query-runner/interfaces/query-runner-optionts.interface.ts +++ b/server/src/workspace/workspace-query-runner/interfaces/query-runner-optionts.interface.ts @@ -1,6 +1,6 @@ import { GraphQLResolveInfo } from 'graphql'; -import { FieldMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/field-metadata.interface'; +import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/field-metadata.interface'; export interface WorkspaceQueryRunnerOptions { targetTableName: string; diff --git a/server/src/workspace/workspace-resolver-builder/workspace-resolver.factory.ts b/server/src/workspace/workspace-resolver-builder/workspace-resolver.factory.ts index 7b67ce211..e39833024 100644 --- a/server/src/workspace/workspace-resolver-builder/workspace-resolver.factory.ts +++ b/server/src/workspace/workspace-resolver-builder/workspace-resolver.factory.ts @@ -2,7 +2,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { IResolvers } from '@graphql-tools/utils'; -import { ObjectMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/object-metadata.interface'; +import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/object-metadata.interface'; import { getResolverName } from 'src/workspace/utils/get-resolver-name.util'; import { UpdateManyResolverFactory } from 'src/workspace/workspace-resolver-builder/factories/update-many-resolver.factory'; diff --git a/server/src/workspace/workspace-schema-builder/factories/connection-type-definition.factory.ts b/server/src/workspace/workspace-schema-builder/factories/connection-type-definition.factory.ts index 1b46d667d..50e3ccd75 100644 --- a/server/src/workspace/workspace-schema-builder/factories/connection-type-definition.factory.ts +++ b/server/src/workspace/workspace-schema-builder/factories/connection-type-definition.factory.ts @@ -3,7 +3,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { GraphQLFieldConfigMap, GraphQLObjectType } from 'graphql'; import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; -import { ObjectMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/object-metadata.interface'; +import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/object-metadata.interface'; import { pascalCase } from 'src/utils/pascal-case'; diff --git a/server/src/workspace/workspace-schema-builder/factories/connection-type.factory.ts b/server/src/workspace/workspace-schema-builder/factories/connection-type.factory.ts index 7117bcec8..05256efa4 100644 --- a/server/src/workspace/workspace-schema-builder/factories/connection-type.factory.ts +++ b/server/src/workspace/workspace-schema-builder/factories/connection-type.factory.ts @@ -3,7 +3,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { GraphQLOutputType } from 'graphql'; import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; -import { ObjectMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/object-metadata.interface'; +import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/object-metadata.interface'; import { TypeMapperService, diff --git a/server/src/workspace/workspace-schema-builder/factories/edge-type-definition.factory.ts b/server/src/workspace/workspace-schema-builder/factories/edge-type-definition.factory.ts index e6c1bfa4b..5cecd9320 100644 --- a/server/src/workspace/workspace-schema-builder/factories/edge-type-definition.factory.ts +++ b/server/src/workspace/workspace-schema-builder/factories/edge-type-definition.factory.ts @@ -3,7 +3,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { GraphQLFieldConfigMap, GraphQLObjectType } from 'graphql'; import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; -import { ObjectMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/object-metadata.interface'; +import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/object-metadata.interface'; import { pascalCase } from 'src/utils/pascal-case'; diff --git a/server/src/workspace/workspace-schema-builder/factories/edge-type.factory.ts b/server/src/workspace/workspace-schema-builder/factories/edge-type.factory.ts index 5baf5d5d1..003c29ce1 100644 --- a/server/src/workspace/workspace-schema-builder/factories/edge-type.factory.ts +++ b/server/src/workspace/workspace-schema-builder/factories/edge-type.factory.ts @@ -3,7 +3,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { GraphQLOutputType } from 'graphql'; import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; -import { ObjectMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/object-metadata.interface'; +import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/object-metadata.interface'; import { TypeMapperService, diff --git a/server/src/workspace/workspace-schema-builder/factories/enum-type-definition.factory.ts b/server/src/workspace/workspace-schema-builder/factories/enum-type-definition.factory.ts new file mode 100644 index 000000000..34eb23e77 --- /dev/null +++ b/server/src/workspace/workspace-schema-builder/factories/enum-type-definition.factory.ts @@ -0,0 +1,85 @@ +import { Injectable, Logger } from '@nestjs/common'; + +import { GraphQLEnumType } from 'graphql'; + +import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; +import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/object-metadata.interface'; +import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/field-metadata.interface'; + +import { pascalCase } from 'src/utils/pascal-case'; +import { + FieldMetadataComplexOptions, + FieldMetadataDefaultOptions, +} from 'src/metadata/field-metadata/dtos/options.input'; +import { isEnumFieldMetadataType } from 'src/metadata/field-metadata/utils/is-enum-field-metadata-type.util'; + +export interface EnumTypeDefinition { + target: string; + type: GraphQLEnumType; +} + +@Injectable() +export class EnumTypeDefinitionFactory { + private readonly logger = new Logger(EnumTypeDefinitionFactory.name); + + public create( + objectMetadata: ObjectMetadataInterface, + options: WorkspaceBuildSchemaOptions, + ): EnumTypeDefinition[] { + const enumTypeDefinitions: EnumTypeDefinition[] = []; + + for (const fieldMetadata of objectMetadata.fields) { + if (!isEnumFieldMetadataType(fieldMetadata.type)) { + continue; + } + + enumTypeDefinitions.push({ + target: fieldMetadata.id, + type: this.generateEnum( + objectMetadata.nameSingular, + fieldMetadata, + options, + ), + }); + } + + return enumTypeDefinitions; + } + + private generateEnum( + objectName: string, + fieldMetadata: FieldMetadataInterface, + options: WorkspaceBuildSchemaOptions, + ): GraphQLEnumType { + // FixMe: It's a hack until Typescript get fixed on union types for reduce function + // https://github.com/microsoft/TypeScript/issues/36390 + const enumOptions = fieldMetadata.options as Array< + FieldMetadataDefaultOptions | FieldMetadataComplexOptions + >; + + if (!enumOptions) { + this.logger.error( + `Enum options are not defined for ${fieldMetadata.name}`, + { + fieldMetadata, + options, + }, + ); + + throw new Error(`Enum options are not defined for ${fieldMetadata.name}`); + } + + return new GraphQLEnumType({ + name: `${pascalCase(objectName)}${pascalCase(fieldMetadata.name)}Enum`, + description: fieldMetadata.description, + values: enumOptions.reduce((acc, enumOption) => { + acc[enumOption.value] = { + value: enumOption.value, + description: enumOption.label, + }; + + return acc; + }, {} as { [key: string]: { value: string; description: string } }), + }); + } +} diff --git a/server/src/workspace/workspace-schema-builder/factories/extend-object-type-definition.factory.ts b/server/src/workspace/workspace-schema-builder/factories/extend-object-type-definition.factory.ts index ea5b8c35e..d4c852e44 100644 --- a/server/src/workspace/workspace-schema-builder/factories/extend-object-type-definition.factory.ts +++ b/server/src/workspace/workspace-schema-builder/factories/extend-object-type-definition.factory.ts @@ -7,13 +7,13 @@ import { } from 'graphql'; import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; -import { ObjectMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/object-metadata.interface'; +import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/object-metadata.interface'; import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; import { TypeDefinitionsStorage } from 'src/workspace/workspace-schema-builder/storages/type-definitions.storage'; -import { objectContainsCompositeField } from 'src/workspace/workspace-schema-builder/utils/object-contains-composite-field'; +import { objectContainsRelationField } from 'src/workspace/workspace-schema-builder/utils/object-contains-relation-field'; import { getResolverArgs } from 'src/workspace/workspace-schema-builder/utils/get-resolver-args.util'; -import { isCompositeFieldMetadataType } from 'src/workspace/utils/is-composite-field-metadata-type.util'; +import { isRelationFieldMetadataType } from 'src/workspace/utils/is-relation-field-metadata-type.util'; import { RelationDirection, deduceRelationDirection, @@ -54,7 +54,7 @@ export class ExtendObjectTypeDefinitionFactory { objectMetadata.id, kind, ); - const containsCompositeField = objectContainsCompositeField(objectMetadata); + const containsRelationField = objectContainsRelationField(objectMetadata); if (!gqlType) { this.logger.error( @@ -71,7 +71,7 @@ export class ExtendObjectTypeDefinitionFactory { } // Security check to avoid extending an object that does not need to be extended - if (!containsCompositeField) { + if (!containsRelationField) { this.logger.error( `This object does not need to be extended: ${objectMetadata.id.toString()}`, { @@ -109,8 +109,8 @@ export class ExtendObjectTypeDefinitionFactory { const fields: GraphQLFieldConfigMap = {}; for (const fieldMetadata of objectMetadata.fields) { - // Ignore non composite fields as they are already defined - if (!isCompositeFieldMetadataType(fieldMetadata.type)) { + // Ignore relation fields as they are already defined + if (!isRelationFieldMetadataType(fieldMetadata.type)) { continue; } diff --git a/server/src/workspace/workspace-schema-builder/factories/factories.ts b/server/src/workspace/workspace-schema-builder/factories/factories.ts index 6a68b92ed..3d9feba65 100644 --- a/server/src/workspace/workspace-schema-builder/factories/factories.ts +++ b/server/src/workspace/workspace-schema-builder/factories/factories.ts @@ -1,3 +1,5 @@ +import { EnumTypeDefinitionFactory } from 'src/workspace/workspace-schema-builder/factories/enum-type-definition.factory'; + import { ArgsFactory } from './args.factory'; import { InputTypeFactory } from './input-type.factory'; import { InputTypeDefinitionFactory } from './input-type-definition.factory'; @@ -24,6 +26,7 @@ export const workspaceSchemaBuilderFactories = [ InputTypeDefinitionFactory, OutputTypeFactory, ObjectTypeDefinitionFactory, + EnumTypeDefinitionFactory, RelationTypeFactory, ExtendObjectTypeDefinitionFactory, FilterTypeFactory, diff --git a/server/src/workspace/workspace-schema-builder/factories/filter-type-definition.factory.ts b/server/src/workspace/workspace-schema-builder/factories/filter-type-definition.factory.ts index 607d1c096..db62bf9fc 100644 --- a/server/src/workspace/workspace-schema-builder/factories/filter-type-definition.factory.ts +++ b/server/src/workspace/workspace-schema-builder/factories/filter-type-definition.factory.ts @@ -3,11 +3,11 @@ import { Injectable } from '@nestjs/common'; import { GraphQLInputFieldConfigMap, GraphQLInputObjectType } from 'graphql'; import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; -import { ObjectMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/object-metadata.interface'; +import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/object-metadata.interface'; import { pascalCase } from 'src/utils/pascal-case'; import { TypeMapperService } from 'src/workspace/workspace-schema-builder/services/type-mapper.service'; -import { isCompositeFieldMetadataType } from 'src/workspace/utils/is-composite-field-metadata-type.util'; +import { isRelationFieldMetadataType } from 'src/workspace/utils/is-relation-field-metadata-type.util'; import { FilterTypeFactory } from './filter-type.factory'; import { @@ -68,8 +68,8 @@ export class FilterTypeDefinitionFactory { const fields: GraphQLInputFieldConfigMap = {}; for (const fieldMetadata of objectMetadata.fields) { - // Composite field types are generated during extension of object type definition - if (isCompositeFieldMetadataType(fieldMetadata.type)) { + // Relation types are generated during extension of object type definition + if (isRelationFieldMetadataType(fieldMetadata.type)) { //continue; } diff --git a/server/src/workspace/workspace-schema-builder/factories/filter-type.factory.ts b/server/src/workspace/workspace-schema-builder/factories/filter-type.factory.ts index 48b158139..ea73a2192 100644 --- a/server/src/workspace/workspace-schema-builder/factories/filter-type.factory.ts +++ b/server/src/workspace/workspace-schema-builder/factories/filter-type.factory.ts @@ -1,15 +1,23 @@ import { Injectable, Logger } from '@nestjs/common'; -import { GraphQLInputType } from 'graphql'; +import { + GraphQLInputObjectType, + GraphQLInputType, + GraphQLList, + GraphQLScalarType, +} from 'graphql'; import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; -import { FieldMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/field-metadata.interface'; +import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/field-metadata.interface'; import { TypeMapperService, TypeOptions, } from 'src/workspace/workspace-schema-builder/services/type-mapper.service'; import { TypeDefinitionsStorage } from 'src/workspace/workspace-schema-builder/storages/type-definitions.storage'; +import { isCompositeFieldMetadataType } from 'src/metadata/field-metadata/utils/is-composite-field-metadata-type.util'; +import { isEnumFieldMetadataType } from 'src/metadata/field-metadata/utils/is-enum-field-metadata-type.util'; +import { FilterIs } from 'src/workspace/workspace-schema-builder/graphql-types/input/filter-is.input-type'; import { InputTypeDefinitionKind } from './input-type-definition.factory'; @@ -27,34 +35,68 @@ export class FilterTypeFactory { buildOtions: WorkspaceBuildSchemaOptions, typeOptions: TypeOptions, ): GraphQLInputType { - let filterType = this.typeMapperService.mapToFilterType( - fieldMetadata.type, - buildOtions.dateScalarMode, - buildOtions.numberScalarMode, - ); + const target = isCompositeFieldMetadataType(fieldMetadata.type) + ? fieldMetadata.type.toString() + : fieldMetadata.id; + let filterType: GraphQLInputObjectType | GraphQLScalarType | undefined = + undefined; - if (!filterType) { - filterType = this.typeDefinitionsStorage.getInputTypeByKey( - fieldMetadata.type.toString(), - InputTypeDefinitionKind.Filter, + if (isEnumFieldMetadataType(fieldMetadata.type)) { + filterType = this.createEnumFilterType(fieldMetadata); + } else { + filterType = this.typeMapperService.mapToFilterType( + fieldMetadata.type, + buildOtions.dateScalarMode, + buildOtions.numberScalarMode, ); - if (!filterType) { - this.logger.error( - `Could not find a GraphQL type for ${fieldMetadata.type.toString()}`, - { - fieldMetadata, - buildOtions, - typeOptions, - }, - ); + filterType ??= this.typeDefinitionsStorage.getInputTypeByKey( + target, + InputTypeDefinitionKind.Filter, + ); + } - throw new Error( - `Could not find a GraphQL type for ${fieldMetadata.type.toString()}`, - ); - } + if (!filterType) { + this.logger.error(`Could not find a GraphQL type for ${target}`, { + fieldMetadata, + buildOtions, + typeOptions, + }); + + throw new Error(`Could not find a GraphQL type for ${target}`); } return this.typeMapperService.mapToGqlType(filterType, typeOptions); } + + private createEnumFilterType( + fieldMetadata: FieldMetadataInterface, + ): GraphQLInputObjectType { + const enumType = this.typeDefinitionsStorage.getEnumTypeByKey( + fieldMetadata.id, + ); + + if (!enumType) { + this.logger.error( + `Could not find a GraphQL enum type for ${fieldMetadata.id}`, + { + fieldMetadata, + }, + ); + + throw new Error( + `Could not find a GraphQL enum type for ${fieldMetadata.id}`, + ); + } + + return new GraphQLInputObjectType({ + name: `${enumType.name}Filter`, + fields: () => ({ + eq: { type: enumType }, + neq: { type: enumType }, + in: { type: new GraphQLList(enumType) }, + is: { type: FilterIs }, + }), + }); + } } diff --git a/server/src/workspace/workspace-schema-builder/factories/input-type-definition.factory.ts b/server/src/workspace/workspace-schema-builder/factories/input-type-definition.factory.ts index 317960314..e1923e916 100644 --- a/server/src/workspace/workspace-schema-builder/factories/input-type-definition.factory.ts +++ b/server/src/workspace/workspace-schema-builder/factories/input-type-definition.factory.ts @@ -3,10 +3,11 @@ import { Injectable } from '@nestjs/common'; import { GraphQLInputFieldConfigMap, GraphQLInputObjectType } from 'graphql'; import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; -import { ObjectMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/object-metadata.interface'; +import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/object-metadata.interface'; import { pascalCase } from 'src/utils/pascal-case'; -import { isCompositeFieldMetadataType } from 'src/workspace/utils/is-composite-field-metadata-type.util'; +import { isRelationFieldMetadataType } from 'src/workspace/utils/is-relation-field-metadata-type.util'; +import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; import { InputTypeFactory } from './input-type.factory'; @@ -53,14 +54,15 @@ export class InputTypeDefinitionFactory { const fields: GraphQLInputFieldConfigMap = {}; for (const fieldMetadata of objectMetadata.fields) { - // Composite field types are generated during extension of object type definition - if (isCompositeFieldMetadataType(fieldMetadata.type)) { + // Relation field types are generated during extension of object type definition + if (isRelationFieldMetadataType(fieldMetadata.type)) { //continue; } const type = this.inputTypeFactory.create(fieldMetadata, kind, options, { nullable: fieldMetadata.isNullable, defaultValue: fieldMetadata.defaultValue, + isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT, }); fields[fieldMetadata.name] = { diff --git a/server/src/workspace/workspace-schema-builder/factories/input-type.factory.ts b/server/src/workspace/workspace-schema-builder/factories/input-type.factory.ts index 5d75927ca..2bd22552b 100644 --- a/server/src/workspace/workspace-schema-builder/factories/input-type.factory.ts +++ b/server/src/workspace/workspace-schema-builder/factories/input-type.factory.ts @@ -3,13 +3,14 @@ import { Injectable, Logger } from '@nestjs/common'; import { GraphQLInputType } from 'graphql'; import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; -import { FieldMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/field-metadata.interface'; +import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/field-metadata.interface'; import { TypeMapperService, TypeOptions, } from 'src/workspace/workspace-schema-builder/services/type-mapper.service'; import { TypeDefinitionsStorage } from 'src/workspace/workspace-schema-builder/storages/type-definitions.storage'; +import { isCompositeFieldMetadataType } from 'src/metadata/field-metadata/utils/is-composite-field-metadata-type.util'; import { InputTypeDefinitionKind } from './input-type-definition.factory'; @@ -28,6 +29,9 @@ export class InputTypeFactory { buildOtions: WorkspaceBuildSchemaOptions, typeOptions: TypeOptions, ): GraphQLInputType { + const target = isCompositeFieldMetadataType(fieldMetadata.type) + ? fieldMetadata.type.toString() + : fieldMetadata.id; let inputType: GraphQLInputType | undefined = this.typeMapperService.mapToScalarType( fieldMetadata.type, @@ -35,27 +39,19 @@ export class InputTypeFactory { buildOtions.numberScalarMode, ); + inputType ??= this.typeDefinitionsStorage.getInputTypeByKey(target, kind); + + inputType ??= this.typeDefinitionsStorage.getEnumTypeByKey(target); + if (!inputType) { - inputType = this.typeDefinitionsStorage.getInputTypeByKey( - fieldMetadata.type.toString(), + this.logger.error(`Could not find a GraphQL type for ${target}`, { + fieldMetadata, kind, - ); + buildOtions, + typeOptions, + }); - if (!inputType) { - this.logger.error( - `Could not find a GraphQL type for ${fieldMetadata.type.toString()}`, - { - fieldMetadata, - kind, - buildOtions, - typeOptions, - }, - ); - - throw new Error( - `Could not find a GraphQL type for ${fieldMetadata.type.toString()}`, - ); - } + throw new Error(`Could not find a GraphQL type for ${target}`); } return this.typeMapperService.mapToGqlType(inputType, typeOptions); diff --git a/server/src/workspace/workspace-schema-builder/factories/mutation-type.factory.ts b/server/src/workspace/workspace-schema-builder/factories/mutation-type.factory.ts index c736c9ea1..a5940ad9d 100644 --- a/server/src/workspace/workspace-schema-builder/factories/mutation-type.factory.ts +++ b/server/src/workspace/workspace-schema-builder/factories/mutation-type.factory.ts @@ -4,7 +4,7 @@ import { GraphQLObjectType } from 'graphql'; import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; import { WorkspaceResolverBuilderMutationMethodNames } from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; -import { ObjectMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/object-metadata.interface'; +import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/object-metadata.interface'; import { ObjectTypeName, RootTypeFactory } from './root-type.factory'; diff --git a/server/src/workspace/workspace-schema-builder/factories/object-type-definition.factory.ts b/server/src/workspace/workspace-schema-builder/factories/object-type-definition.factory.ts index d420eade0..af30aea5c 100644 --- a/server/src/workspace/workspace-schema-builder/factories/object-type-definition.factory.ts +++ b/server/src/workspace/workspace-schema-builder/factories/object-type-definition.factory.ts @@ -3,10 +3,11 @@ import { Injectable } from '@nestjs/common'; import { GraphQLFieldConfigMap, GraphQLObjectType } from 'graphql'; import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; -import { ObjectMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/object-metadata.interface'; +import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/object-metadata.interface'; import { pascalCase } from 'src/utils/pascal-case'; -import { isCompositeFieldMetadataType } from 'src/workspace/utils/is-composite-field-metadata-type.util'; +import { isRelationFieldMetadataType } from 'src/workspace/utils/is-relation-field-metadata-type.util'; +import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; import { OutputTypeFactory } from './output-type.factory'; @@ -50,13 +51,14 @@ export class ObjectTypeDefinitionFactory { const fields: GraphQLFieldConfigMap = {}; for (const fieldMetadata of objectMetadata.fields) { - // Composite field types are generated during extension of object type definition - if (isCompositeFieldMetadataType(fieldMetadata.type)) { + // Relation field types are generated during extension of object type definition + if (isRelationFieldMetadataType(fieldMetadata.type)) { continue; } const type = this.outputTypeFactory.create(fieldMetadata, kind, options, { nullable: fieldMetadata.isNullable, + isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT, }); fields[fieldMetadata.name] = { diff --git a/server/src/workspace/workspace-schema-builder/factories/order-by-type-definition.factory.ts b/server/src/workspace/workspace-schema-builder/factories/order-by-type-definition.factory.ts index 9d9127b39..29c5bc8e9 100644 --- a/server/src/workspace/workspace-schema-builder/factories/order-by-type-definition.factory.ts +++ b/server/src/workspace/workspace-schema-builder/factories/order-by-type-definition.factory.ts @@ -3,10 +3,10 @@ import { Injectable } from '@nestjs/common'; import { GraphQLInputFieldConfigMap, GraphQLInputObjectType } from 'graphql'; import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; -import { ObjectMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/object-metadata.interface'; +import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/object-metadata.interface'; import { pascalCase } from 'src/utils/pascal-case'; -import { isCompositeFieldMetadataType } from 'src/workspace/utils/is-composite-field-metadata-type.util'; +import { isRelationFieldMetadataType } from 'src/workspace/utils/is-relation-field-metadata-type.util'; import { InputTypeDefinition, @@ -44,8 +44,8 @@ export class OrderByTypeDefinitionFactory { const fields: GraphQLInputFieldConfigMap = {}; for (const fieldMetadata of objectMetadata.fields) { - // Composite field types are generated during extension of object type definition - if (isCompositeFieldMetadataType(fieldMetadata.type)) { + // Relation field types are generated during extension of object type definition + if (isRelationFieldMetadataType(fieldMetadata.type)) { continue; } diff --git a/server/src/workspace/workspace-schema-builder/factories/order-by-type.factory.ts b/server/src/workspace/workspace-schema-builder/factories/order-by-type.factory.ts index 0ee630d97..21eb11460 100644 --- a/server/src/workspace/workspace-schema-builder/factories/order-by-type.factory.ts +++ b/server/src/workspace/workspace-schema-builder/factories/order-by-type.factory.ts @@ -3,13 +3,14 @@ import { Injectable, Logger } from '@nestjs/common'; import { GraphQLInputType } from 'graphql'; import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; -import { FieldMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/field-metadata.interface'; +import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/field-metadata.interface'; import { TypeMapperService, TypeOptions, } from 'src/workspace/workspace-schema-builder/services/type-mapper.service'; import { TypeDefinitionsStorage } from 'src/workspace/workspace-schema-builder/storages/type-definitions.storage'; +import { isCompositeFieldMetadataType } from 'src/metadata/field-metadata/utils/is-composite-field-metadata-type.util'; import { InputTypeDefinitionKind } from './input-type-definition.factory'; @@ -27,30 +28,26 @@ export class OrderByTypeFactory { buildOtions: WorkspaceBuildSchemaOptions, typeOptions: TypeOptions, ): GraphQLInputType { + const target = isCompositeFieldMetadataType(fieldMetadata.type) + ? fieldMetadata.type.toString() + : fieldMetadata.id; let orderByType = this.typeMapperService.mapToOrderByType( fieldMetadata.type, ); + orderByType ??= this.typeDefinitionsStorage.getInputTypeByKey( + target, + InputTypeDefinitionKind.OrderBy, + ); + if (!orderByType) { - orderByType = this.typeDefinitionsStorage.getInputTypeByKey( - fieldMetadata.type.toString(), - InputTypeDefinitionKind.OrderBy, - ); + this.logger.error(`Could not find a GraphQL type for ${target}`, { + fieldMetadata, + buildOtions, + typeOptions, + }); - if (!orderByType) { - this.logger.error( - `Could not find a GraphQL type for ${fieldMetadata.type.toString()}`, - { - fieldMetadata, - buildOtions, - typeOptions, - }, - ); - - throw new Error( - `Could not find a GraphQL type for ${fieldMetadata.type.toString()}`, - ); - } + throw new Error(`Could not find a GraphQL type for ${target}`); } return this.typeMapperService.mapToGqlType(orderByType, typeOptions); diff --git a/server/src/workspace/workspace-schema-builder/factories/output-type.factory.ts b/server/src/workspace/workspace-schema-builder/factories/output-type.factory.ts index 519bcac15..5f2883f07 100644 --- a/server/src/workspace/workspace-schema-builder/factories/output-type.factory.ts +++ b/server/src/workspace/workspace-schema-builder/factories/output-type.factory.ts @@ -3,13 +3,14 @@ import { Injectable, Logger } from '@nestjs/common'; import { GraphQLOutputType } from 'graphql'; import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; -import { FieldMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/field-metadata.interface'; +import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/field-metadata.interface'; import { TypeMapperService, TypeOptions, } from 'src/workspace/workspace-schema-builder/services/type-mapper.service'; import { TypeDefinitionsStorage } from 'src/workspace/workspace-schema-builder/storages/type-definitions.storage'; +import { isCompositeFieldMetadataType } from 'src/metadata/field-metadata/utils/is-composite-field-metadata-type.util'; import { ObjectTypeDefinitionKind } from './object-type-definition.factory'; @@ -28,6 +29,9 @@ export class OutputTypeFactory { buildOtions: WorkspaceBuildSchemaOptions, typeOptions: TypeOptions, ): GraphQLOutputType { + const target = isCompositeFieldMetadataType(fieldMetadata.type) + ? fieldMetadata.type.toString() + : fieldMetadata.id; let gqlType: GraphQLOutputType | undefined = this.typeMapperService.mapToScalarType( fieldMetadata.type, @@ -35,26 +39,18 @@ export class OutputTypeFactory { buildOtions.numberScalarMode, ); + gqlType ??= this.typeDefinitionsStorage.getObjectTypeByKey(target, kind); + + gqlType ??= this.typeDefinitionsStorage.getEnumTypeByKey(target); + if (!gqlType) { - gqlType = this.typeDefinitionsStorage.getObjectTypeByKey( - fieldMetadata.type.toString(), - kind, - ); + this.logger.error(`Could not find a GraphQL type for ${target}`, { + fieldMetadata, + buildOtions, + typeOptions, + }); - if (!gqlType) { - this.logger.error( - `Could not find a GraphQL type for ${fieldMetadata.type.toString()}`, - { - fieldMetadata, - buildOtions, - typeOptions, - }, - ); - - throw new Error( - `Could not find a GraphQL type for ${fieldMetadata.type.toString()}`, - ); - } + throw new Error(`Could not find a GraphQL type for ${target}`); } return this.typeMapperService.mapToGqlType(gqlType, typeOptions); diff --git a/server/src/workspace/workspace-schema-builder/factories/query-type.factory.ts b/server/src/workspace/workspace-schema-builder/factories/query-type.factory.ts index aae048ce6..fec7594b9 100644 --- a/server/src/workspace/workspace-schema-builder/factories/query-type.factory.ts +++ b/server/src/workspace/workspace-schema-builder/factories/query-type.factory.ts @@ -4,7 +4,7 @@ import { GraphQLObjectType } from 'graphql'; import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; import { WorkspaceResolverBuilderQueryMethodNames } from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; -import { ObjectMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/object-metadata.interface'; +import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/object-metadata.interface'; import { ObjectTypeName, RootTypeFactory } from './root-type.factory'; diff --git a/server/src/workspace/workspace-schema-builder/factories/relation-type.factory.ts b/server/src/workspace/workspace-schema-builder/factories/relation-type.factory.ts index e8d12d4dd..266c08ac2 100644 --- a/server/src/workspace/workspace-schema-builder/factories/relation-type.factory.ts +++ b/server/src/workspace/workspace-schema-builder/factories/relation-type.factory.ts @@ -2,8 +2,8 @@ import { Injectable, Logger } from '@nestjs/common'; import { GraphQLOutputType } from 'graphql'; -import { FieldMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/field-metadata.interface'; -import { RelationMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/relation-metadata.interface'; +import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/field-metadata.interface'; +import { RelationMetadataInterface } from 'src/metadata/field-metadata/interfaces/relation-metadata.interface'; import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity'; import { TypeDefinitionsStorage } from 'src/workspace/workspace-schema-builder/storages/type-definitions.storage'; diff --git a/server/src/workspace/workspace-schema-builder/factories/root-type.factory.ts b/server/src/workspace/workspace-schema-builder/factories/root-type.factory.ts index 4f595e7bf..88927e84a 100644 --- a/server/src/workspace/workspace-schema-builder/factories/root-type.factory.ts +++ b/server/src/workspace/workspace-schema-builder/factories/root-type.factory.ts @@ -4,7 +4,7 @@ import { GraphQLFieldConfigMap, GraphQLObjectType } from 'graphql'; import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; import { WorkspaceResolverBuilderMethodNames } from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; -import { ObjectMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/object-metadata.interface'; +import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/object-metadata.interface'; import { TypeDefinitionsStorage } from 'src/workspace/workspace-schema-builder/storages/type-definitions.storage'; import { getResolverName } from 'src/workspace/utils/get-resolver-name.util'; diff --git a/server/src/workspace/workspace-schema-builder/graphql-types/input/big-float-filter.input-type.ts b/server/src/workspace/workspace-schema-builder/graphql-types/input/big-float-filter.input-type.ts index 3ec8d355d..31d68e36b 100644 --- a/server/src/workspace/workspace-schema-builder/graphql-types/input/big-float-filter.input-type.ts +++ b/server/src/workspace/workspace-schema-builder/graphql-types/input/big-float-filter.input-type.ts @@ -1,6 +1,6 @@ import { GraphQLInputObjectType, GraphQLList, GraphQLNonNull } from 'graphql'; -import { FilterIsNullable } from 'src/workspace/workspace-schema-builder/graphql-types/input/filter-is-nullable.input-type'; +import { FilterIs } from 'src/workspace/workspace-schema-builder/graphql-types/input/filter-is.input-type'; import { BigFloatScalarType } from 'src/workspace/workspace-schema-builder/graphql-types/scalars'; export const BigFloatFilterType = new GraphQLInputObjectType({ @@ -13,6 +13,6 @@ export const BigFloatFilterType = new GraphQLInputObjectType({ lt: { type: BigFloatScalarType }, lte: { type: BigFloatScalarType }, neq: { type: BigFloatScalarType }, - is: { type: FilterIsNullable }, + is: { type: FilterIs }, }, }); diff --git a/server/src/workspace/workspace-schema-builder/graphql-types/input/big-int-filter.input-type.ts b/server/src/workspace/workspace-schema-builder/graphql-types/input/big-int-filter.input-type.ts index 3d97c1724..e93eb2c7a 100644 --- a/server/src/workspace/workspace-schema-builder/graphql-types/input/big-int-filter.input-type.ts +++ b/server/src/workspace/workspace-schema-builder/graphql-types/input/big-int-filter.input-type.ts @@ -5,7 +5,7 @@ import { GraphQLInt, } from 'graphql'; -import { FilterIsNullable } from 'src/workspace/workspace-schema-builder/graphql-types/input/filter-is-nullable.input-type'; +import { FilterIs } from 'src/workspace/workspace-schema-builder/graphql-types/input/filter-is.input-type'; export const BigIntFilterType = new GraphQLInputObjectType({ name: 'BigIntFilter', @@ -17,6 +17,6 @@ export const BigIntFilterType = new GraphQLInputObjectType({ lt: { type: GraphQLInt }, lte: { type: GraphQLInt }, neq: { type: GraphQLInt }, - is: { type: FilterIsNullable }, + is: { type: FilterIs }, }, }); diff --git a/server/src/workspace/workspace-schema-builder/graphql-types/input/boolean-filter.input-type.ts b/server/src/workspace/workspace-schema-builder/graphql-types/input/boolean-filter.input-type.ts index c96f05341..6439478b8 100644 --- a/server/src/workspace/workspace-schema-builder/graphql-types/input/boolean-filter.input-type.ts +++ b/server/src/workspace/workspace-schema-builder/graphql-types/input/boolean-filter.input-type.ts @@ -1,11 +1,11 @@ import { GraphQLBoolean, GraphQLInputObjectType } from 'graphql'; -import { FilterIsNullable } from 'src/workspace/workspace-schema-builder/graphql-types/input/filter-is-nullable.input-type'; +import { FilterIs } from 'src/workspace/workspace-schema-builder/graphql-types/input/filter-is.input-type'; export const BooleanFilterType = new GraphQLInputObjectType({ name: 'BooleanFilter', fields: { eq: { type: GraphQLBoolean }, - is: { type: FilterIsNullable }, + is: { type: FilterIs }, }, }); diff --git a/server/src/workspace/workspace-schema-builder/graphql-types/input/date-filter.input-type.ts b/server/src/workspace/workspace-schema-builder/graphql-types/input/date-filter.input-type.ts index 8344f4bee..0ab2ef7be 100644 --- a/server/src/workspace/workspace-schema-builder/graphql-types/input/date-filter.input-type.ts +++ b/server/src/workspace/workspace-schema-builder/graphql-types/input/date-filter.input-type.ts @@ -1,6 +1,6 @@ import { GraphQLInputObjectType, GraphQLList, GraphQLNonNull } from 'graphql'; -import { FilterIsNullable } from 'src/workspace/workspace-schema-builder/graphql-types/input/filter-is-nullable.input-type'; +import { FilterIs } from 'src/workspace/workspace-schema-builder/graphql-types/input/filter-is.input-type'; import { DateScalarType } from 'src/workspace/workspace-schema-builder/graphql-types/scalars'; export const DateFilterType = new GraphQLInputObjectType({ @@ -13,6 +13,6 @@ export const DateFilterType = new GraphQLInputObjectType({ lt: { type: DateScalarType }, lte: { type: DateScalarType }, neq: { type: DateScalarType }, - is: { type: FilterIsNullable }, + is: { type: FilterIs }, }, }); diff --git a/server/src/workspace/workspace-schema-builder/graphql-types/input/date-time-filter.input-type.ts b/server/src/workspace/workspace-schema-builder/graphql-types/input/date-time-filter.input-type.ts index 64580bd35..423640582 100644 --- a/server/src/workspace/workspace-schema-builder/graphql-types/input/date-time-filter.input-type.ts +++ b/server/src/workspace/workspace-schema-builder/graphql-types/input/date-time-filter.input-type.ts @@ -1,6 +1,6 @@ import { GraphQLInputObjectType, GraphQLList, GraphQLNonNull } from 'graphql'; -import { FilterIsNullable } from 'src/workspace/workspace-schema-builder/graphql-types/input/filter-is-nullable.input-type'; +import { FilterIs } from 'src/workspace/workspace-schema-builder/graphql-types/input/filter-is.input-type'; import { DateTimeScalarType } from 'src/workspace/workspace-schema-builder/graphql-types/scalars'; export const DatetimeFilterType = new GraphQLInputObjectType({ @@ -13,6 +13,6 @@ export const DatetimeFilterType = new GraphQLInputObjectType({ lt: { type: DateTimeScalarType }, lte: { type: DateTimeScalarType }, neq: { type: DateTimeScalarType }, - is: { type: FilterIsNullable }, + is: { type: FilterIs }, }, }); diff --git a/server/src/workspace/workspace-schema-builder/graphql-types/input/filter-is-nullable.input-type.ts b/server/src/workspace/workspace-schema-builder/graphql-types/input/filter-is.input-type.ts similarity index 77% rename from server/src/workspace/workspace-schema-builder/graphql-types/input/filter-is-nullable.input-type.ts rename to server/src/workspace/workspace-schema-builder/graphql-types/input/filter-is.input-type.ts index 4acc83e2e..199d084ef 100644 --- a/server/src/workspace/workspace-schema-builder/graphql-types/input/filter-is-nullable.input-type.ts +++ b/server/src/workspace/workspace-schema-builder/graphql-types/input/filter-is.input-type.ts @@ -1,7 +1,7 @@ import { GraphQLEnumType } from 'graphql'; -export const FilterIsNullable = new GraphQLEnumType({ - name: 'FilterIsNullable', +export const FilterIs = new GraphQLEnumType({ + name: 'FilterIs', description: 'This enum to filter by nullability', values: { NULL: { diff --git a/server/src/workspace/workspace-schema-builder/graphql-types/input/float-filter.input-type.ts b/server/src/workspace/workspace-schema-builder/graphql-types/input/float-filter.input-type.ts index ee51bbe33..15346e427 100644 --- a/server/src/workspace/workspace-schema-builder/graphql-types/input/float-filter.input-type.ts +++ b/server/src/workspace/workspace-schema-builder/graphql-types/input/float-filter.input-type.ts @@ -5,7 +5,7 @@ import { GraphQLNonNull, } from 'graphql'; -import { FilterIsNullable } from 'src/workspace/workspace-schema-builder/graphql-types/input/filter-is-nullable.input-type'; +import { FilterIs } from 'src/workspace/workspace-schema-builder/graphql-types/input/filter-is.input-type'; export const FloatFilterType = new GraphQLInputObjectType({ name: 'FloatFilter', @@ -17,6 +17,6 @@ export const FloatFilterType = new GraphQLInputObjectType({ lt: { type: GraphQLFloat }, lte: { type: GraphQLFloat }, neq: { type: GraphQLFloat }, - is: { type: FilterIsNullable }, + is: { type: FilterIs }, }, }); diff --git a/server/src/workspace/workspace-schema-builder/graphql-types/input/int-filter.input-type.ts b/server/src/workspace/workspace-schema-builder/graphql-types/input/int-filter.input-type.ts index 9ff27926d..f8e76f70b 100644 --- a/server/src/workspace/workspace-schema-builder/graphql-types/input/int-filter.input-type.ts +++ b/server/src/workspace/workspace-schema-builder/graphql-types/input/int-filter.input-type.ts @@ -5,7 +5,7 @@ import { GraphQLInt, } from 'graphql'; -import { FilterIsNullable } from 'src/workspace/workspace-schema-builder/graphql-types/input/filter-is-nullable.input-type'; +import { FilterIs } from 'src/workspace/workspace-schema-builder/graphql-types/input/filter-is.input-type'; export const IntFilterType = new GraphQLInputObjectType({ name: 'IntFilter', @@ -17,6 +17,6 @@ export const IntFilterType = new GraphQLInputObjectType({ lt: { type: GraphQLInt }, lte: { type: GraphQLInt }, neq: { type: GraphQLInt }, - is: { type: FilterIsNullable }, + is: { type: FilterIs }, }, }); diff --git a/server/src/workspace/workspace-schema-builder/graphql-types/input/string-filter.input-type.ts b/server/src/workspace/workspace-schema-builder/graphql-types/input/string-filter.input-type.ts index 1c28b5d68..96ffd19cf 100644 --- a/server/src/workspace/workspace-schema-builder/graphql-types/input/string-filter.input-type.ts +++ b/server/src/workspace/workspace-schema-builder/graphql-types/input/string-filter.input-type.ts @@ -5,7 +5,7 @@ import { GraphQLString, } from 'graphql'; -import { FilterIsNullable } from 'src/workspace/workspace-schema-builder/graphql-types/input/filter-is-nullable.input-type'; +import { FilterIs } from 'src/workspace/workspace-schema-builder/graphql-types/input/filter-is.input-type'; export const StringFilterType = new GraphQLInputObjectType({ name: 'StringFilter', @@ -22,6 +22,6 @@ export const StringFilterType = new GraphQLInputObjectType({ ilike: { type: GraphQLString }, regex: { type: GraphQLString }, iregex: { type: GraphQLString }, - is: { type: FilterIsNullable }, + is: { type: FilterIs }, }, }); diff --git a/server/src/workspace/workspace-schema-builder/graphql-types/input/time-filter.input-type.ts b/server/src/workspace/workspace-schema-builder/graphql-types/input/time-filter.input-type.ts index 9a6528121..81a1f895f 100644 --- a/server/src/workspace/workspace-schema-builder/graphql-types/input/time-filter.input-type.ts +++ b/server/src/workspace/workspace-schema-builder/graphql-types/input/time-filter.input-type.ts @@ -1,6 +1,6 @@ import { GraphQLInputObjectType, GraphQLList, GraphQLNonNull } from 'graphql'; -import { FilterIsNullable } from 'src/workspace/workspace-schema-builder/graphql-types/input/filter-is-nullable.input-type'; +import { FilterIs } from 'src/workspace/workspace-schema-builder/graphql-types/input/filter-is.input-type'; import { TimeScalarType } from 'src/workspace/workspace-schema-builder/graphql-types/scalars'; export const TimeFilterType = new GraphQLInputObjectType({ @@ -13,6 +13,6 @@ export const TimeFilterType = new GraphQLInputObjectType({ lt: { type: TimeScalarType }, lte: { type: TimeScalarType }, neq: { type: TimeScalarType }, - is: { type: FilterIsNullable }, + is: { type: FilterIs }, }, }); diff --git a/server/src/workspace/workspace-schema-builder/graphql-types/input/uuid-filter.input-type.ts b/server/src/workspace/workspace-schema-builder/graphql-types/input/uuid-filter.input-type.ts index 4cfba8365..c1ff25832 100644 --- a/server/src/workspace/workspace-schema-builder/graphql-types/input/uuid-filter.input-type.ts +++ b/server/src/workspace/workspace-schema-builder/graphql-types/input/uuid-filter.input-type.ts @@ -1,6 +1,6 @@ import { GraphQLInputObjectType, GraphQLList } from 'graphql'; -import { FilterIsNullable } from 'src/workspace/workspace-schema-builder/graphql-types/input/filter-is-nullable.input-type'; +import { FilterIs } from 'src/workspace/workspace-schema-builder/graphql-types/input/filter-is.input-type'; import { UUIDScalarType } from 'src/workspace/workspace-schema-builder/graphql-types/scalars'; export const UUIDFilterType = new GraphQLInputObjectType({ @@ -9,6 +9,6 @@ export const UUIDFilterType = new GraphQLInputObjectType({ eq: { type: UUIDScalarType }, in: { type: new GraphQLList(UUIDScalarType) }, neq: { type: UUIDScalarType }, - is: { type: FilterIsNullable }, + is: { type: FilterIs }, }, }); diff --git a/server/src/workspace/workspace-schema-builder/interfaces/param-metadata.interface.ts b/server/src/workspace/workspace-schema-builder/interfaces/param-metadata.interface.ts index f8ca6cbc6..2b9f94eb3 100644 --- a/server/src/workspace/workspace-schema-builder/interfaces/param-metadata.interface.ts +++ b/server/src/workspace/workspace-schema-builder/interfaces/param-metadata.interface.ts @@ -1,8 +1,8 @@ +import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/object-metadata.interface'; + import { InputTypeDefinitionKind } from 'src/workspace/workspace-schema-builder/factories/input-type-definition.factory'; import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; -import { ObjectMetadataInterface } from './object-metadata.interface'; - export interface ArgMetadata { kind?: InputTypeDefinitionKind; type?: FieldMetadataType; diff --git a/server/src/workspace/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface.ts b/server/src/workspace/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface.ts index 3136ea75d..c5908bfba 100644 --- a/server/src/workspace/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface.ts +++ b/server/src/workspace/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface.ts @@ -1,4 +1,4 @@ -import { FieldMetadataInterface } from './field-metadata.interface'; +import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/field-metadata.interface'; export interface WorkspaceSchemaBuilderContext { workspaceId: string; diff --git a/server/src/workspace/workspace-schema-builder/services/type-mapper.service.ts b/server/src/workspace/workspace-schema-builder/services/type-mapper.service.ts index 332b48b9e..b54194317 100644 --- a/server/src/workspace/workspace-schema-builder/services/type-mapper.service.ts +++ b/server/src/workspace/workspace-schema-builder/services/type-mapper.service.ts @@ -75,7 +75,7 @@ export class TypeMapperService { fieldMetadataType: FieldMetadataType, dateScalarMode: DateScalarMode = 'isoDate', numberScalarMode: NumberScalarMode = 'float', - ): GraphQLInputObjectType | GraphQLScalarType | undefined { + ): GraphQLInputObjectType | GraphQLScalarType | undefined { const dateFilter = dateScalarMode === 'timestamp' ? DatetimeFilterType : DateFilterType; const numberScalar = @@ -84,7 +84,7 @@ export class TypeMapperService { // LINK and CURRENCY are handled in the factories because they are objects const typeFilterMapping = new Map< FieldMetadataType, - GraphQLInputObjectType | GraphQLScalarType + GraphQLInputObjectType | GraphQLScalarType >([ [FieldMetadataType.UUID, UUIDFilterType], [FieldMetadataType.TEXT, StringFilterType], @@ -115,6 +115,9 @@ export class TypeMapperService { [FieldMetadataType.NUMBER, OrderByDirectionType], [FieldMetadataType.NUMERIC, OrderByDirectionType], [FieldMetadataType.PROBABILITY, OrderByDirectionType], + [FieldMetadataType.RATING, OrderByDirectionType], + [FieldMetadataType.SELECT, OrderByDirectionType], + [FieldMetadataType.MULTI_SELECT, OrderByDirectionType], ]); return typeOrderByMapping.get(fieldMetadataType); diff --git a/server/src/workspace/workspace-schema-builder/storages/type-definitions.storage.ts b/server/src/workspace/workspace-schema-builder/storages/type-definitions.storage.ts index f659baf58..15abbc97a 100644 --- a/server/src/workspace/workspace-schema-builder/storages/type-definitions.storage.ts +++ b/server/src/workspace/workspace-schema-builder/storages/type-definitions.storage.ts @@ -1,8 +1,13 @@ import { Injectable, Scope } from '@nestjs/common'; -import { GraphQLInputObjectType, GraphQLObjectType } from 'graphql'; +import { + GraphQLEnumType, + GraphQLInputObjectType, + GraphQLObjectType, +} from 'graphql'; import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; +import { EnumTypeDefinition } from 'src/workspace/workspace-schema-builder/factories/enum-type-definition.factory'; import { InputTypeDefinition, InputTypeDefinitionKind, @@ -15,6 +20,7 @@ import { // Must be scoped on REQUEST level @Injectable({ scope: Scope.REQUEST }) export class TypeDefinitionsStorage { + private readonly enumTypeDefinitions = new Map(); private readonly objectTypeDefinitions = new Map< string, ObjectTypeDefinition @@ -24,6 +30,10 @@ export class TypeDefinitionsStorage { InputTypeDefinition >(); + addEnumTypes(enumDefs: EnumTypeDefinition[]) { + enumDefs.forEach((item) => this.enumTypeDefinitions.set(item.target, item)); + } + addObjectTypes(objectDefs: ObjectTypeDefinition[]) { objectDefs.forEach((item) => this.objectTypeDefinitions.set( @@ -64,6 +74,10 @@ export class TypeDefinitionsStorage { )?.type; } + getEnumTypeByKey(target: string): GraphQLEnumType | undefined { + return this.enumTypeDefinitions.get(target)?.type; + } + getAllInputTypeDefinitions(): InputTypeDefinition[] { return Array.from(this.inputTypeDefinitions.values()); } diff --git a/server/src/workspace/workspace-schema-builder/type-definitions.generator.ts b/server/src/workspace/workspace-schema-builder/type-definitions.generator.ts index 7177f5502..da56b3665 100644 --- a/server/src/workspace/workspace-schema-builder/type-definitions.generator.ts +++ b/server/src/workspace/workspace-schema-builder/type-definitions.generator.ts @@ -1,8 +1,14 @@ import { Injectable, Logger } from '@nestjs/common'; +import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/object-metadata.interface'; +import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/field-metadata.interface'; + import { FieldMetadataEntity } from 'src/metadata/field-metadata/field-metadata.entity'; import { customTableDefaultColumns } from 'src/workspace/workspace-migration-runner/utils/custom-table-default-column.util'; -import { fullNameObjectDefinition } from 'src/workspace/workspace-schema-builder/object-definitions/full-name.object-definition'; +import { fullNameObjectDefinition } from 'src/metadata/field-metadata/composite-types/full-name.composite-type'; +import { currencyObjectDefinition } from 'src/metadata/field-metadata/composite-types/currency.composite-type'; +import { linkObjectDefinition } from 'src/metadata/field-metadata/composite-types/link.composite-type'; +import { EnumTypeDefinitionFactory } from 'src/workspace/workspace-schema-builder/factories/enum-type-definition.factory'; import { TypeDefinitionsStorage } from './storages/type-definitions.storage'; import { @@ -15,16 +21,12 @@ import { } from './factories/input-type-definition.factory'; import { getFieldMetadataType } from './utils/get-field-metadata-type.util'; import { WorkspaceBuildSchemaOptions } from './interfaces/workspace-build-schema-optionts.interface'; -import { currencyObjectDefinition } from './object-definitions/currency.object-definition'; -import { linkObjectDefinition } from './object-definitions/link.object-definition'; -import { ObjectMetadataInterface } from './interfaces/object-metadata.interface'; -import { FieldMetadataInterface } from './interfaces/field-metadata.interface'; import { FilterTypeDefinitionFactory } from './factories/filter-type-definition.factory'; import { ConnectionTypeDefinitionFactory } from './factories/connection-type-definition.factory'; import { EdgeTypeDefinitionFactory } from './factories/edge-type-definition.factory'; import { OrderByTypeDefinitionFactory } from './factories/order-by-type-definition.factory'; import { ExtendObjectTypeDefinitionFactory } from './factories/extend-object-type-definition.factory'; -import { objectContainsCompositeField } from './utils/object-contains-composite-field'; +import { objectContainsRelationField } from './utils/object-contains-relation-field'; // Create a default field for each custom table default column const defaultFields = customTableDefaultColumns.map((column) => { @@ -42,6 +44,7 @@ export class TypeDefinitionsGenerator { constructor( private readonly typeDefinitionsStorage: TypeDefinitionsStorage, private readonly objectTypeDefinitionFactory: ObjectTypeDefinitionFactory, + private readonly enumTypeDefinitionFactory: EnumTypeDefinitionFactory, private readonly inputTypeDefinitionFactory: InputTypeDefinitionFactory, private readonly filterTypeDefintionFactory: FilterTypeDefinitionFactory, private readonly orderByTypeDefinitionFactory: OrderByTypeDefinitionFactory, @@ -74,6 +77,7 @@ export class TypeDefinitionsGenerator { ); // Generate static objects first because they can be used in dynamic objects + this.generateEnumTypeDefs(staticObjectMetadataCollection, options); this.generateObjectTypeDefs(staticObjectMetadataCollection, options); this.generateInputTypeDefs(staticObjectMetadataCollection, options); } @@ -89,6 +93,7 @@ export class TypeDefinitionsGenerator { ); // Generate dynamic objects + this.generateEnumTypeDefs(dynamicObjectMetadataCollection, options); this.generateObjectTypeDefs(dynamicObjectMetadataCollection, options); this.generatePaginationTypeDefs(dynamicObjectMetadataCollection, options); this.generateInputTypeDefs(dynamicObjectMetadataCollection, options); @@ -203,13 +208,26 @@ export class TypeDefinitionsGenerator { this.typeDefinitionsStorage.addInputTypes(inputTypeDefs); } + private generateEnumTypeDefs( + objectMetadataCollection: ObjectMetadataInterface[], + options: WorkspaceBuildSchemaOptions, + ) { + const enumTypeDefs = objectMetadataCollection + .map((objectMetadata) => + this.enumTypeDefinitionFactory.create(objectMetadata, options), + ) + .flat(); + + this.typeDefinitionsStorage.addEnumTypes(enumTypeDefs); + } + private generateExtendedObjectTypeDefs( objectMetadataCollection: ObjectMetadataInterface[], options: WorkspaceBuildSchemaOptions, ) { // Generate extended object type defs only for objects that contain composite fields const objectMetadataCollectionWithCompositeFields = - objectMetadataCollection.filter(objectContainsCompositeField); + objectMetadataCollection.filter(objectContainsRelationField); const objectTypeDefs = objectMetadataCollectionWithCompositeFields.map( (objectMetadata) => this.extendObjectTypeDefinitionFactory.create(objectMetadata, options), diff --git a/server/src/workspace/workspace-schema-builder/utils/object-contains-composite-field.ts b/server/src/workspace/workspace-schema-builder/utils/object-contains-composite-field.ts deleted file mode 100644 index caf882f35..000000000 --- a/server/src/workspace/workspace-schema-builder/utils/object-contains-composite-field.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ObjectMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/object-metadata.interface'; - -import { isCompositeFieldMetadataType } from 'src/workspace/utils/is-composite-field-metadata-type.util'; - -export const objectContainsCompositeField = ( - objectMetadata: ObjectMetadataInterface, -): boolean => { - return objectMetadata.fields.some((field) => - isCompositeFieldMetadataType(field.type), - ); -}; diff --git a/server/src/workspace/workspace-schema-builder/utils/object-contains-relation-field.ts b/server/src/workspace/workspace-schema-builder/utils/object-contains-relation-field.ts new file mode 100644 index 000000000..0ece0b83b --- /dev/null +++ b/server/src/workspace/workspace-schema-builder/utils/object-contains-relation-field.ts @@ -0,0 +1,11 @@ +import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/object-metadata.interface'; + +import { isRelationFieldMetadataType } from 'src/workspace/utils/is-relation-field-metadata-type.util'; + +export const objectContainsRelationField = ( + objectMetadata: ObjectMetadataInterface, +): boolean => { + return objectMetadata.fields.some((field) => + isRelationFieldMetadataType(field.type), + ); +}; diff --git a/server/src/workspace/workspace-schema-builder/workspace-graphql-schema.factory.ts b/server/src/workspace/workspace-schema-builder/workspace-graphql-schema.factory.ts index 321d6bcb3..252bcf432 100644 --- a/server/src/workspace/workspace-schema-builder/workspace-graphql-schema.factory.ts +++ b/server/src/workspace/workspace-schema-builder/workspace-graphql-schema.factory.ts @@ -3,13 +3,13 @@ import { Injectable, Logger } from '@nestjs/common'; import { GraphQLSchema } from 'graphql'; import { WorkspaceResolverBuilderMethods } from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; +import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/object-metadata.interface'; import { TypeDefinitionsGenerator } from './type-definitions.generator'; import { WorkspaceBuildSchemaOptions } from './interfaces/workspace-build-schema-optionts.interface'; import { QueryTypeFactory } from './factories/query-type.factory'; import { MutationTypeFactory } from './factories/mutation-type.factory'; -import { ObjectMetadataInterface } from './interfaces/object-metadata.interface'; import { OrphanedTypesFactory } from './factories/orphaned-types.factory'; @Injectable()