feat: Adding support for new FieldMetadataType with Postgres enums (#2674)

* feat: add enum type (RATING, SELECT, MULTI_SELECT)

feat: wip enum type

feat: try to alter enum

feat: wip enum

feat: wip enum

feat: schema-builder can handle enum

fix: return default value in field metadata response

* fix: create fieldMedata with options

* fix: lint issues

* fix: rename abstract factory

* feat: drop `PHONE` and `EMAIL` fieldMetadata types

* feat: drop `VARCHAR` fieldMetadata type and rely on `TEXT`

* Revert "feat: drop `PHONE` and `EMAIL` fieldMetadata types"

This reverts commit 3857539f7d42f17c81f6ab92a6db950140b3c8e5.
This commit is contained in:
Jérémy M
2023-11-30 15:24:26 +01:00
committed by GitHub
parent c2131a29b8
commit 6e6f0af26e
92 changed files with 1371 additions and 484 deletions

View File

@ -3,7 +3,7 @@ ARG PG_MAIN_VERSION=14
FROM postgres:${PG_MAIN_VERSION} as postgres FROM postgres:${PG_MAIN_VERSION} as postgres
ARG PG_MAIN_VERSION ARG PG_MAIN_VERSION
ARG PG_GRAPHQL_VERSION=1.3.0 ARG PG_GRAPHQL_VERSION=1.4.2
ARG TARGETARCH ARG TARGETARCH
RUN set -eux; \ RUN set -eux; \

View File

@ -0,0 +1,18 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddEnumOptions1700663879152 implements MigrationInterface {
name = 'AddEnumOptions1700663879152'
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
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"`);
}
}

View File

@ -35,7 +35,12 @@ export class LocalMemoryDriver<T> implements MemoryStorageDriver<T> {
return null; return null;
} }
const data = this.storage.get(compositeKey)!; const data = this.storage.get(compositeKey);
if (!data) {
return null;
}
const deserializeData = this.serializer.deserialize(data); const deserializeData = this.serializer.deserialize(data);
return deserializeData; return deserializeData;

View File

@ -1,5 +1,5 @@
import { ObjectMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/object-metadata.interface'; import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/object-metadata.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 { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';

View File

@ -1,5 +1,5 @@
import { ObjectMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/object-metadata.interface'; import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/object-metadata.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 { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';

View File

@ -1,5 +1,5 @@
import { ObjectMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/object-metadata.interface'; import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/object-metadata.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 { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';

View File

@ -2,21 +2,26 @@ import { Field, HideField, InputType } from '@nestjs/graphql';
import { BeforeCreateOne } from '@ptc-org/nestjs-query-graphql'; import { BeforeCreateOne } from '@ptc-org/nestjs-query-graphql';
import { import {
IsArray,
IsBoolean, IsBoolean,
IsEnum, IsEnum,
IsNotEmpty, IsNotEmpty,
IsOptional, IsOptional,
IsString, IsString,
IsUUID, IsUUID,
ValidateNested,
} from 'class-validator'; } 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 { 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 { 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 { BeforeCreateOneField } from 'src/metadata/field-metadata/hooks/before-create-one-field.hook';
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
import { IsDefaultValue } from 'src/metadata/field-metadata/validators/is-default-value.validator'; import { IsDefaultValue } from 'src/metadata/field-metadata/validators/is-default-value.validator';
import { FieldMetadataComplexOptions } from 'src/metadata/field-metadata/dtos/options.input';
@InputType() @InputType()
@BeforeCreateOne(BeforeCreateOneField) @BeforeCreateOne(BeforeCreateOneField)
@ -53,12 +58,19 @@ export class CreateFieldInput {
@IsBoolean() @IsBoolean()
@IsOptional() @IsOptional()
@Field({ nullable: true }) @Field({ nullable: true })
isNullable: boolean; isNullable?: boolean;
@IsDefaultValue({ message: 'Invalid default value for the specified type' }) @IsDefaultValue({ message: 'Invalid default value for the specified type' })
@IsOptional() @IsOptional()
@Field(() => graphqlTypeJson, { nullable: true }) @Field(() => GraphQLJSON, { nullable: true })
defaultValue: FieldMetadataDefaultValue; defaultValue?: FieldMetadataDefaultValue;
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => FieldMetadataComplexOptions)
@Field(() => GraphQLJSON, { nullable: true })
options?: FieldMetadataOptions;
@HideField() @HideField()
targetColumnMap: FieldMetadataTargetColumnMap; targetColumnMap: FieldMetadataTargetColumnMap;

View File

@ -6,6 +6,7 @@ import {
registerEnumType, registerEnumType,
} from '@nestjs/graphql'; } from '@nestjs/graphql';
import { GraphQLJSON } from 'graphql-type-json';
import { import {
Authorize, Authorize,
BeforeDeleteOne, BeforeDeleteOne,
@ -15,6 +16,9 @@ import {
Relation, Relation,
} from '@ptc-org/nestjs-query-graphql'; } 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 { RelationMetadataDTO } from 'src/metadata/relation-metadata/dtos/relation-metadata.dto';
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
import { BeforeDeleteOneField } from 'src/metadata/field-metadata/hooks/before-delete-one-field.hook'; import { BeforeDeleteOneField } from 'src/metadata/field-metadata/hooks/before-delete-one-field.hook';
@ -76,6 +80,12 @@ export class FieldMetadataDTO {
@Field() @Field()
isNullable: boolean; isNullable: boolean;
@Field(() => GraphQLJSON, { nullable: true })
defaultValue?: FieldMetadataDefaultValue;
@Field(() => GraphQLJSON, { nullable: true })
options?: FieldMetadataOptions;
@HideField() @HideField()
workspaceId: string; workspaceId: string;

View File

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

View File

@ -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 { 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 { BeforeUpdateOneField } from 'src/metadata/field-metadata/hooks/before-update-one-field.hook';
import { FieldMetadataComplexOptions } from 'src/metadata/field-metadata/dtos/options.input';
@InputType() @InputType()
@BeforeUpdateOne(BeforeUpdateOneField) @BeforeUpdateOne(BeforeUpdateOneField)
@ -32,4 +44,19 @@ export class UpdateFieldInput {
@IsOptional() @IsOptional()
@Field({ nullable: true }) @Field({ nullable: true })
isActive?: boolean; 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;
} }

View File

@ -10,9 +10,10 @@ import {
UpdateDateColumn, UpdateDateColumn,
} from 'typeorm'; } 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 { 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 { 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 { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity';
import { RelationMetadataEntity } from 'src/metadata/relation-metadata/relation-metadata.entity'; import { RelationMetadataEntity } from 'src/metadata/relation-metadata/relation-metadata.entity';
@ -27,11 +28,13 @@ export enum FieldMetadataType {
NUMBER = 'NUMBER', NUMBER = 'NUMBER',
NUMERIC = 'NUMERIC', NUMERIC = 'NUMERIC',
PROBABILITY = 'PROBABILITY', PROBABILITY = 'PROBABILITY',
ENUM = 'ENUM',
LINK = 'LINK', LINK = 'LINK',
CURRENCY = 'CURRENCY', CURRENCY = 'CURRENCY',
RELATION = 'RELATION',
FULL_NAME = 'FULL_NAME', FULL_NAME = 'FULL_NAME',
RATING = 'RATING',
SELECT = 'SELECT',
MULTI_SELECT = 'MULTI_SELECT',
RELATION = 'RELATION',
} }
@Entity('fieldMetadata') @Entity('fieldMetadata')
@ -40,7 +43,10 @@ export enum FieldMetadataType {
'objectMetadataId', 'objectMetadataId',
'workspaceId', 'workspaceId',
]) ])
export class FieldMetadataEntity implements FieldMetadataInterface { export class FieldMetadataEntity<
T extends FieldMetadataType | 'default' = 'default',
> implements FieldMetadataInterface<T>
{
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
id: string; id: string;
@ -63,10 +69,10 @@ export class FieldMetadataEntity implements FieldMetadataInterface {
label: string; label: string;
@Column({ nullable: false, type: 'jsonb' }) @Column({ nullable: false, type: 'jsonb' })
targetColumnMap: FieldMetadataTargetColumnMap; targetColumnMap: FieldMetadataTargetColumnMap<T>;
@Column({ nullable: true, type: 'jsonb' }) @Column({ nullable: true, type: 'jsonb' })
defaultValue: FieldMetadataDefaultValue; defaultValue: FieldMetadataDefaultValue<T>;
@Column({ nullable: true, type: 'text' }) @Column({ nullable: true, type: 'text' })
description: string; description: string;
@ -74,8 +80,8 @@ export class FieldMetadataEntity implements FieldMetadataInterface {
@Column({ nullable: true }) @Column({ nullable: true })
icon: string; icon: string;
@Column('text', { nullable: true, array: true }) @Column('jsonb', { nullable: true })
enums: string[]; options: FieldMetadataOptions<T>;
@Column({ default: false }) @Column({ default: false })
isCustom: boolean; isCustom: boolean;

View File

@ -1,10 +1,12 @@
import { import {
BadRequestException,
ConflictException, ConflictException,
Injectable, Injectable,
NotFoundException, NotFoundException,
} from '@nestjs/common'; } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { v4 as uuidV4 } from 'uuid';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-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 { WorkspaceMigrationService } from 'src/metadata/workspace-migration/workspace-migration.service';
import { ObjectMetadataService } from 'src/metadata/object-metadata/object-metadata.service'; import { ObjectMetadataService } from 'src/metadata/object-metadata/object-metadata.service';
import { CreateFieldInput } from 'src/metadata/field-metadata/dtos/create-field.input'; 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 { 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 { TypeORMService } from 'src/database/typeorm/typeorm.service';
import { DataSourceService } from 'src/metadata/data-source/data-source.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'; import { FieldMetadataEntity } from './field-metadata.entity';
@ -27,6 +33,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>, private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
private readonly objectMetadataService: ObjectMetadataService, private readonly objectMetadataService: ObjectMetadataService,
private readonly workspaceMigrationFactory: WorkspaceMigrationFactory,
private readonly workspaceMigrationService: WorkspaceMigrationService, private readonly workspaceMigrationService: WorkspaceMigrationService,
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService, private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
private readonly dataSourceService: DataSourceService, private readonly dataSourceService: DataSourceService,
@ -63,6 +70,12 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
const createdFieldMetadata = await super.createOne({ const createdFieldMetadata = await super.createOne({
...record, ...record,
targetColumnMap: generateTargetColumnMap(record.type, true, record.name), targetColumnMap: generateTargetColumnMap(record.type, true, record.name),
options: record.options
? record.options.map((option) => ({
...option,
id: uuidV4(),
}))
: undefined,
isActive: true, isActive: true,
isCustom: true, isCustom: true,
}); });
@ -73,7 +86,10 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
{ {
name: objectMetadata.targetTableName, name: objectMetadata.targetTableName,
action: 'alter', action: 'alter',
columns: convertFieldMetadataToColumnActions(createdFieldMetadata), columns: this.workspaceMigrationFactory.createColumnActions(
WorkspaceMigrationColumnActionType.CREATE,
createdFieldMetadata,
),
} satisfies WorkspaceMigrationTableAction, } satisfies WorkspaceMigrationTableAction,
], ],
); );
@ -123,6 +139,66 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
return createdFieldMetadata; return createdFieldMetadata;
} }
override async updateOne(
id: string,
record: UpdateFieldInput,
): Promise<FieldMetadataEntity> {
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( public async findOneWithinWorkspace(
fieldMetadataId: string, fieldMetadataId: string,
workspaceId: string, workspaceId: string,

View File

@ -61,6 +61,8 @@ export class BeforeUpdateOneField<T extends UpdateFieldInput>
this.checkIfFieldIsEditable(instance.update, fieldMetadata); this.checkIfFieldIsEditable(instance.update, fieldMetadata);
instance.update.workspaceId = workspaceId;
return instance; return instance;
} }

View File

@ -9,6 +9,11 @@ export interface FieldMetadataDefaultValueNumber {
export interface FieldMetadataDefaultValueBoolean { export interface FieldMetadataDefaultValueBoolean {
value: boolean; value: boolean;
} }
export interface FieldMetadataDefaultValueStringArray {
value: string[];
}
export interface FieldMetadataDefaultValueDateTime { export interface FieldMetadataDefaultValueDateTime {
value: Date; value: Date;
} }
@ -55,10 +60,12 @@ type FieldMetadataDefaultValueMapping = {
[FieldMetadataType.NUMBER]: FieldMetadataDefaultValueNumber; [FieldMetadataType.NUMBER]: FieldMetadataDefaultValueNumber;
[FieldMetadataType.NUMERIC]: FieldMetadataDefaultValueString; [FieldMetadataType.NUMERIC]: FieldMetadataDefaultValueString;
[FieldMetadataType.PROBABILITY]: FieldMetadataDefaultValueNumber; [FieldMetadataType.PROBABILITY]: FieldMetadataDefaultValueNumber;
[FieldMetadataType.ENUM]: FieldMetadataDefaultValueString;
[FieldMetadataType.LINK]: FieldMetadataDefaultValueLink; [FieldMetadataType.LINK]: FieldMetadataDefaultValueLink;
[FieldMetadataType.CURRENCY]: FieldMetadataDefaultValueCurrency; [FieldMetadataType.CURRENCY]: FieldMetadataDefaultValueCurrency;
[FieldMetadataType.FULL_NAME]: FieldMetadataDefaultValueFullName; [FieldMetadataType.FULL_NAME]: FieldMetadataDefaultValueFullName;
[FieldMetadataType.RATING]: FieldMetadataDefaultValueString;
[FieldMetadataType.SELECT]: FieldMetadataDefaultValueString;
[FieldMetadataType.MULTI_SELECT]: FieldMetadataDefaultValueStringArray;
}; };
type DefaultValueByFieldMetadata<T extends FieldMetadataType | 'default'> = [ type DefaultValueByFieldMetadata<T extends FieldMetadataType | 'default'> = [
@ -78,7 +85,9 @@ type FieldMetadataDefaultValueExtractNestedType<T> = T extends {
} }
? U ? U
: T extends object : T extends object
? T[keyof T] ? { [K in keyof T]: T[K] } extends { value: infer V }
? V
: T[keyof T]
: never; : never;
type FieldMetadataDefaultValueExtractedTypes = { type FieldMetadataDefaultValueExtractedTypes = {

View File

@ -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 FieldMetadataType | 'default'> =
T extends keyof FieldMetadataOptionsMapping
? FieldMetadataOptionsMapping[T]
: T extends 'default'
? FieldMetadataDefaultOptions[] | FieldMetadataComplexOptions[]
: never;
export type FieldMetadataOptions<
T extends FieldMetadataType | 'default' = 'default',
> = OptionsByFieldMetadata<T>;

View File

@ -29,12 +29,13 @@ type FieldMetadataTypeMapping = {
[FieldMetadataType.FULL_NAME]: FieldMetadataTargetColumnMapFullName; [FieldMetadataType.FULL_NAME]: FieldMetadataTargetColumnMapFullName;
}; };
type TypeByFieldMetadata<T extends FieldMetadataType | 'default'> = type TypeByFieldMetadata<T extends FieldMetadataType | 'default'> = [
T extends keyof FieldMetadataTypeMapping T,
? FieldMetadataTypeMapping[T] ] extends [keyof FieldMetadataTypeMapping]
: T extends 'default' ? FieldMetadataTypeMapping[T]
? AllFieldMetadataTypes : T extends 'default'
: FieldMetadataTargetColumnMapValue; ? AllFieldMetadataTypes
: FieldMetadataTargetColumnMapValue;
export type FieldMetadataTargetColumnMap< export type FieldMetadataTargetColumnMap<
T extends FieldMetadataType | 'default' = 'default', T extends FieldMetadataType | 'default' = 'default',

View File

@ -1,5 +1,6 @@
import { FieldMetadataTargetColumnMap } from 'src/metadata/field-metadata/interfaces/field-metadata-target-column-map.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 { 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 { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
import { RelationMetadataEntity } from 'src/metadata/relation-metadata/relation-metadata.entity'; import { RelationMetadataEntity } from 'src/metadata/relation-metadata/relation-metadata.entity';
@ -13,6 +14,7 @@ export interface FieldMetadataInterface<
label: string; label: string;
targetColumnMap: FieldMetadataTargetColumnMap<T>; targetColumnMap: FieldMetadataTargetColumnMap<T>;
defaultValue?: FieldMetadataDefaultValue<T>; defaultValue?: FieldMetadataDefaultValue<T>;
options?: FieldMetadataOptions<T>;
objectMetadataId: string; objectMetadataId: string;
description?: string; description?: string;
isNullable?: boolean; isNullable?: boolean;

View File

@ -1,5 +1,5 @@
import { FieldMetadataInterface } from './field-metadata.interface';
import { RelationMetadataInterface } from './relation-metadata.interface'; import { RelationMetadataInterface } from './relation-metadata.interface';
import { FieldMetadataInterface } from './field-metadata.interface';
export interface ObjectMetadataInterface { export interface ObjectMetadataInterface {
id: string; id: string;

View File

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

View File

@ -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<FieldMetadataType.UUID>;
return [
{
action: WorkspaceMigrationColumnActionType.CREATE,
columnName: fieldMetadata.targetColumnMap.value,
columnType: 'uuid',
defaultValue: serializeDefaultValue(defaultValue?.value),
},
];
}
case FieldMetadataType.TEXT: {
const defaultValue =
fieldMetadata.defaultValue as FieldMetadataDefaultValue<FieldMetadataType.TEXT>;
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<FieldMetadataType.NUMERIC>;
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<FieldMetadataType.BOOLEAN>;
return [
{
action: WorkspaceMigrationColumnActionType.CREATE,
columnName: fieldMetadata.targetColumnMap.value,
columnType: 'boolean',
defaultValue: serializeDefaultValue(defaultValue?.value),
},
];
}
case FieldMetadataType.DATE_TIME: {
const defaultValue =
fieldMetadata.defaultValue as FieldMetadataDefaultValue<FieldMetadataType.DATE_TIME>;
return [
{
action: WorkspaceMigrationColumnActionType.CREATE,
columnName: fieldMetadata.targetColumnMap.value,
columnType: 'timestamp',
defaultValue: serializeDefaultValue(defaultValue?.value),
},
];
}
case FieldMetadataType.LINK: {
const defaultValue =
fieldMetadata.defaultValue as FieldMetadataDefaultValue<FieldMetadataType.LINK>;
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<FieldMetadataType.CURRENCY>;
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<FieldMetadataType.FULL_NAME>;
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}`);
}
}

View File

@ -28,6 +28,9 @@ export function generateTargetColumnMap(
case FieldMetadataType.PROBABILITY: case FieldMetadataType.PROBABILITY:
case FieldMetadataType.BOOLEAN: case FieldMetadataType.BOOLEAN:
case FieldMetadataType.DATE_TIME: case FieldMetadataType.DATE_TIME:
case FieldMetadataType.RATING:
case FieldMetadataType.SELECT:
case FieldMetadataType.MULTI_SELECT:
return { return {
value: columnName, value: columnName,
}; };

View File

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

View File

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

View File

@ -10,7 +10,11 @@ export const serializeDefaultValue = (
} }
// Dynamic default values // Dynamic default values
if (typeof defaultValue === 'object' && 'type' in defaultValue) { if (
!Array.isArray(defaultValue) &&
typeof defaultValue === 'object' &&
'type' in defaultValue
) {
switch (defaultValue.type) { switch (defaultValue.type) {
case 'uuid': case 'uuid':
return 'public.uuid_generate_v4()'; return 'public.uuid_generate_v4()';
@ -38,6 +42,10 @@ export const serializeDefaultValue = (
return `'${defaultValue.toISOString()}'`; return `'${defaultValue.toISOString()}'`;
} }
if (Array.isArray(defaultValue)) {
return defaultValue;
}
if (typeof defaultValue === 'object') { if (typeof defaultValue === 'object') {
return `'${JSON.stringify(defaultValue)}'`; return `'${JSON.stringify(defaultValue)}'`;
} }

View File

@ -25,7 +25,8 @@ export const validateDefaultValueBasedOnType = (
case FieldMetadataType.TEXT: case FieldMetadataType.TEXT:
case FieldMetadataType.PHONE: case FieldMetadataType.PHONE:
case FieldMetadataType.EMAIL: case FieldMetadataType.EMAIL:
case FieldMetadataType.ENUM: case FieldMetadataType.RATING:
case FieldMetadataType.SELECT:
case FieldMetadataType.NUMERIC: case FieldMetadataType.NUMERIC:
return ( return (
typeof defaultValue === 'object' && typeof defaultValue === 'object' &&
@ -82,6 +83,12 @@ export const validateDefaultValueBasedOnType = (
typeof defaultValue.lastName === 'string' typeof defaultValue.lastName === 'string'
); );
case FieldMetadataType.MULTI_SELECT:
return (
Array.isArray(defaultValue) &&
defaultValue.every((value) => typeof value === 'string')
);
default: default:
return false; return false;
} }

View File

@ -9,7 +9,7 @@ import {
ManyToOne, ManyToOne,
} from 'typeorm'; } 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 { FieldMetadataEntity } from 'src/metadata/field-metadata/field-metadata.entity';
import { RelationMetadataEntity } from 'src/metadata/relation-metadata/relation-metadata.entity'; import { RelationMetadataEntity } from 'src/metadata/relation-metadata/relation-metadata.entity';

View File

@ -9,7 +9,7 @@ import {
UpdateDateColumn, UpdateDateColumn,
} from 'typeorm'; } 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 { FieldMetadataEntity } from 'src/metadata/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity'; import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity';

View File

@ -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<BasicFieldMetadataType> {
protected readonly logger = new Logger(BasicColumnActionFactory.name);
protected handleCreateAction(
fieldMetadata: FieldMetadataInterface<BasicFieldMetadataType>,
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<BasicFieldMetadataType>,
nextFieldMetadata: FieldMetadataInterface<BasicFieldMetadataType>,
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,
};
}
}

View File

@ -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<T>
{
protected readonly logger = new Logger(ColumnActionAbstractFactory.name);
create(
action:
| WorkspaceMigrationColumnActionType.CREATE
| WorkspaceMigrationColumnActionType.ALTER,
previousFieldMetadata: FieldMetadataInterface<T> | undefined,
nextFieldMetadata: FieldMetadataInterface<T>,
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<T>,
_options?: WorkspaceColumnActionOptions,
): WorkspaceMigrationColumnCreate {
throw new Error('handleCreateAction method not implemented.');
}
protected handleAlterAction(
_previousFieldMetadata: FieldMetadataInterface<T>,
_nextFieldMetadata: FieldMetadataInterface<T>,
_options?: WorkspaceColumnActionOptions,
): WorkspaceMigrationColumnAlter {
throw new Error('handleAlterAction method not implemented.');
}
}

View File

@ -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<EnumFieldMetadataType> {
protected readonly logger = new Logger(EnumColumnActionFactory.name);
protected handleCreateAction(
fieldMetadata: FieldMetadataInterface<EnumFieldMetadataType>,
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<EnumFieldMetadataType>,
nextFieldMetadata: FieldMetadataInterface<EnumFieldMetadataType>,
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,
};
}
}

View File

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

View File

@ -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<T> | undefined,
nextFieldMetadata: FieldMetadataInterface<T>,
options?: WorkspaceColumnActionOptions,
): WorkspaceMigrationColumnAction;
}

View File

@ -0,0 +1,3 @@
export interface WorkspaceColumnActionOptions {
defaultValue?: string;
}

View File

@ -0,0 +1,34 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
export const fieldMetadataTypeToColumnType = <Type extends FieldMetadataType>(
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.`);
}
};

View File

@ -7,13 +7,29 @@ import {
export enum WorkspaceMigrationColumnActionType { export enum WorkspaceMigrationColumnActionType {
CREATE = 'CREATE', CREATE = 'CREATE',
ALTER = 'ALTER',
RELATION = 'RELATION', RELATION = 'RELATION',
} }
export type WorkspaceMigrationEnum = string | { from: string; to: string };
export type WorkspaceMigrationColumnCreate = { export type WorkspaceMigrationColumnCreate = {
action: WorkspaceMigrationColumnActionType.CREATE; action: WorkspaceMigrationColumnActionType.CREATE;
columnName: string; columnName: string;
columnType: 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; defaultValue?: any;
}; };
@ -27,7 +43,11 @@ export type WorkspaceMigrationColumnRelation = {
export type WorkspaceMigrationColumnAction = { export type WorkspaceMigrationColumnAction = {
action: WorkspaceMigrationColumnActionType; action: WorkspaceMigrationColumnActionType;
} & (WorkspaceMigrationColumnCreate | WorkspaceMigrationColumnRelation); } & (
| WorkspaceMigrationColumnCreate
| WorkspaceMigrationColumnAlter
| WorkspaceMigrationColumnRelation
);
export type WorkspaceMigrationTableAction = { export type WorkspaceMigrationTableAction = {
name: string; name: string;

View File

@ -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<any>;
options?: WorkspaceColumnActionOptions;
}
>;
private compositeDefinitions = new Map<string, FieldMetadataInterface[]>();
constructor(
private readonly basicColumnActionFactory: BasicColumnActionFactory,
private readonly enumColumnActionFactory: EnumColumnActionFactory,
) {
this.factoriesMap = new Map<
FieldMetadataType,
{
factory: WorkspaceColumnActionFactory<any>;
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<string, FieldMetadataInterface[]>([
[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,
);
}
}

View File

@ -1,12 +1,19 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm'; 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 { WorkspaceMigrationService } from './workspace-migration.service';
import { WorkspaceMigrationEntity } from './workspace-migration.entity'; import { WorkspaceMigrationEntity } from './workspace-migration.entity';
@Module({ @Module({
imports: [TypeOrmModule.forFeature([WorkspaceMigrationEntity], 'metadata')], imports: [TypeOrmModule.forFeature([WorkspaceMigrationEntity], 'metadata')],
exports: [WorkspaceMigrationService], providers: [
providers: [WorkspaceMigrationService], ...workspaceColumnActionFactories,
WorkspaceMigrationFactory,
WorkspaceMigrationService,
],
exports: [WorkspaceMigrationFactory, WorkspaceMigrationService],
}) })
export class WorkspaceMigrationModule {} export class WorkspaceMigrationModule {}

View File

@ -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 { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity';
import { import {

View File

@ -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 { export enum RelationDirection {
FROM = 'from', FROM = 'from',

View File

@ -1,5 +1,5 @@
import { WorkspaceResolverBuilderMethodNames } from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.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 { camelCase } from 'src/utils/camel-case'; import { camelCase } from 'src/utils/camel-case';
import { pascalCase } from 'src/utils/pascal-case'; import { pascalCase } from 'src/utils/pascal-case';

View File

@ -1,5 +1,7 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; 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; return type === FieldMetadataType.RELATION;
}; };

View File

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

View File

@ -3,6 +3,7 @@ import { Module } from '@nestjs/common';
import { WorkspaceMigrationModule } from 'src/metadata/workspace-migration/workspace-migration.module'; import { WorkspaceMigrationModule } from 'src/metadata/workspace-migration/workspace-migration.module';
import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/workspace-datasource.module'; import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/workspace-datasource.module';
import { WorkspaceCacheVersionModule } from 'src/metadata/workspace-cache-version/workspace-cache-version.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'; import { WorkspaceMigrationRunnerService } from './workspace-migration-runner.service';
@ -12,7 +13,7 @@ import { WorkspaceMigrationRunnerService } from './workspace-migration-runner.se
WorkspaceMigrationModule, WorkspaceMigrationModule,
WorkspaceCacheVersionModule, WorkspaceCacheVersionModule,
], ],
providers: [WorkspaceMigrationRunnerService, WorkspaceMigrationEnumService],
exports: [WorkspaceMigrationRunnerService], exports: [WorkspaceMigrationRunnerService],
providers: [WorkspaceMigrationRunnerService],
}) })
export class WorkspaceMigrationRunnerModule {} export class WorkspaceMigrationRunnerModule {}

View File

@ -16,8 +16,10 @@ import {
WorkspaceMigrationColumnActionType, WorkspaceMigrationColumnActionType,
WorkspaceMigrationColumnCreate, WorkspaceMigrationColumnCreate,
WorkspaceMigrationColumnRelation, WorkspaceMigrationColumnRelation,
WorkspaceMigrationColumnAlter,
} from 'src/metadata/workspace-migration/workspace-migration.entity'; } from 'src/metadata/workspace-migration/workspace-migration.entity';
import { WorkspaceCacheVersionService } from 'src/metadata/workspace-cache-version/workspace-cache-version.service'; 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'; import { customTableDefaultColumns } from './utils/custom-table-default-column.util';
@ -27,6 +29,7 @@ export class WorkspaceMigrationRunnerService {
private readonly workspaceDataSourceService: WorkspaceDataSourceService, private readonly workspaceDataSourceService: WorkspaceDataSourceService,
private readonly workspaceMigrationService: WorkspaceMigrationService, private readonly workspaceMigrationService: WorkspaceMigrationService,
private readonly workspaceCacheVersionService: WorkspaceCacheVersionService, private readonly workspaceCacheVersionService: WorkspaceCacheVersionService,
private readonly workspaceMigrationEnumService: WorkspaceMigrationEnumService,
) {} ) {}
/** /**
@ -168,6 +171,14 @@ export class WorkspaceMigrationRunnerService {
columnMigration, columnMigration,
); );
break; break;
case WorkspaceMigrationColumnActionType.ALTER:
await this.alterColumn(
queryRunner,
schemaName,
tableName,
columnMigration,
);
break;
case WorkspaceMigrationColumnActionType.RELATION: case WorkspaceMigrationColumnActionType.RELATION:
await this.createForeignKey( await this.createForeignKey(
queryRunner, queryRunner,
@ -200,6 +211,7 @@ export class WorkspaceMigrationRunnerService {
`${schemaName}.${tableName}`, `${schemaName}.${tableName}`,
migrationColumn.columnName, migrationColumn.columnName,
); );
if (hasColumn) { if (hasColumn) {
return; return;
} }
@ -210,11 +222,46 @@ export class WorkspaceMigrationRunnerService {
name: migrationColumn.columnName, name: migrationColumn.columnName,
type: migrationColumn.columnType, type: migrationColumn.columnType,
default: migrationColumn.defaultValue, default: migrationColumn.defaultValue,
enum: migrationColumn.enum?.filter(
(value): value is string => typeof value === 'string',
),
isArray: migrationColumn.isArray,
isNullable: true, 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( private async createForeignKey(
queryRunner: QueryRunner, queryRunner: QueryRunner,
schemaName: string, schemaName: string,

View File

@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common'; 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() @Injectable()
export class ArgsAliasFactory { export class ArgsAliasFactory {

View File

@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common'; 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'; import { stringifyWithoutKeyQuote } from 'src/workspace/workspace-query-builder/utils/stringify-without-key-quote.util';

View File

@ -1,6 +1,6 @@
import { ArgsAliasFactory } from './args-alias.factory'; import { ArgsAliasFactory } from './args-alias.factory';
import { ArgsStringFactory } from './args-string.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 { CreateManyQueryFactory } from './create-many-query.factory';
import { DeleteOneQueryFactory } from './delete-one-query.factory'; import { DeleteOneQueryFactory } from './delete-one-query.factory';
import { FieldAliasFacotry } from './field-alias.factory'; import { FieldAliasFacotry } from './field-alias.factory';
@ -14,7 +14,7 @@ import { DeleteManyQueryFactory } from './delete-many-query.factory';
export const workspaceQueryBuilderFactories = [ export const workspaceQueryBuilderFactories = [
ArgsAliasFactory, ArgsAliasFactory,
ArgsStringFactory, ArgsStringFactory,
CompositeFieldAliasFactory, RelationFieldAliasFactory,
CreateManyQueryFactory, CreateManyQueryFactory,
DeleteOneQueryFactory, DeleteOneQueryFactory,
FieldAliasFacotry, FieldAliasFacotry,

View File

@ -1,6 +1,6 @@
import { Injectable, Logger } from '@nestjs/common'; 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() @Injectable()
export class FieldAliasFacotry { export class FieldAliasFacotry {

View File

@ -4,12 +4,12 @@ import { GraphQLResolveInfo } from 'graphql';
import graphqlFields from 'graphql-fields'; import graphqlFields from 'graphql-fields';
import isEmpty from 'lodash.isempty'; 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 { FieldAliasFacotry } from './field-alias.factory';
import { CompositeFieldAliasFactory } from './composite-field-alias.factory'; import { RelationFieldAliasFactory } from './relation-field-alias.factory';
@Injectable() @Injectable()
export class FieldsStringFactory { export class FieldsStringFactory {
@ -17,7 +17,7 @@ export class FieldsStringFactory {
constructor( constructor(
private readonly fieldAliasFactory: FieldAliasFacotry, private readonly fieldAliasFactory: FieldAliasFacotry,
private readonly compositeFieldAliasFactory: CompositeFieldAliasFactory, private readonly relationFieldAliasFactory: RelationFieldAliasFactory,
) {} ) {}
create( create(
@ -52,9 +52,9 @@ export class FieldsStringFactory {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const fieldMetadata = fieldMetadataMap.get(fieldKey)!; const fieldMetadata = fieldMetadataMap.get(fieldKey)!;
// If the field is a composite field, we need to create a special alias // If the field is a relation field, we need to create a special alias
if (isCompositeFieldMetadataType(fieldMetadata.type)) { if (isRelationFieldMetadataType(fieldMetadata.type)) {
const alias = this.compositeFieldAliasFactory.create( const alias = this.relationFieldAliasFactory.create(
fieldKey, fieldKey,
fieldValue, fieldValue,
fieldMetadata, fieldMetadata,

View File

@ -2,10 +2,9 @@ import { forwardRef, Inject, Injectable, Logger } from '@nestjs/common';
import { GraphQLResolveInfo } from 'graphql'; 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 { isRelationFieldMetadataType } from 'src/workspace/utils/is-relation-field-metadata-type.util';
import { isCompositeFieldMetadataType } from 'src/workspace/utils/is-composite-field-metadata-type.util';
import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity'; import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity';
import { import {
deduceRelationDirection, deduceRelationDirection,
@ -17,8 +16,8 @@ import { FieldsStringFactory } from './fields-string.factory';
import { ArgsStringFactory } from './args-string.factory'; import { ArgsStringFactory } from './args-string.factory';
@Injectable() @Injectable()
export class CompositeFieldAliasFactory { export class RelationFieldAliasFactory {
private logger = new Logger(CompositeFieldAliasFactory.name); private logger = new Logger(RelationFieldAliasFactory.name);
constructor( constructor(
@Inject(forwardRef(() => FieldsStringFactory)) @Inject(forwardRef(() => FieldsStringFactory))
@ -32,21 +31,11 @@ export class CompositeFieldAliasFactory {
fieldMetadata: FieldMetadataInterface, fieldMetadata: FieldMetadataInterface,
info: GraphQLResolveInfo, info: GraphQLResolveInfo,
) { ) {
if (!isCompositeFieldMetadataType(fieldMetadata.type)) { if (!isRelationFieldMetadataType(fieldMetadata.type)) {
throw new Error(`Field ${fieldMetadata.name} is not a composite field`); throw new Error(`Field ${fieldMetadata.name} is not a relation field`);
} }
switch (fieldMetadata.type) { return this.createRelationAlias(fieldKey, fieldValue, fieldMetadata, info);
case FieldMetadataType.RELATION:
return this.createRelationAlias(
fieldKey,
fieldValue,
fieldMetadata,
info,
);
}
return null;
} }
private createRelationAlias( private createRelationAlias(

View File

@ -1,6 +1,6 @@
import { GraphQLResolveInfo } from 'graphql'; 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 { export interface WorkspaceQueryBuilderOptions {
targetTableName: string; targetTableName: string;

View File

@ -1,6 +1,6 @@
import { GraphQLResolveInfo } from 'graphql'; 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 { export interface WorkspaceQueryRunnerOptions {
targetTableName: string; targetTableName: string;

View File

@ -2,7 +2,7 @@ import { Injectable, Logger } from '@nestjs/common';
import { IResolvers } from '@graphql-tools/utils'; 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 { getResolverName } from 'src/workspace/utils/get-resolver-name.util';
import { UpdateManyResolverFactory } from 'src/workspace/workspace-resolver-builder/factories/update-many-resolver.factory'; import { UpdateManyResolverFactory } from 'src/workspace/workspace-resolver-builder/factories/update-many-resolver.factory';

View File

@ -3,7 +3,7 @@ import { Injectable, Logger } from '@nestjs/common';
import { GraphQLFieldConfigMap, GraphQLObjectType } from 'graphql'; import { GraphQLFieldConfigMap, GraphQLObjectType } from 'graphql';
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; 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 { pascalCase } from 'src/utils/pascal-case';

View File

@ -3,7 +3,7 @@ import { Injectable, Logger } from '@nestjs/common';
import { GraphQLOutputType } from 'graphql'; import { GraphQLOutputType } from 'graphql';
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; 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 { import {
TypeMapperService, TypeMapperService,

View File

@ -3,7 +3,7 @@ import { Injectable, Logger } from '@nestjs/common';
import { GraphQLFieldConfigMap, GraphQLObjectType } from 'graphql'; import { GraphQLFieldConfigMap, GraphQLObjectType } from 'graphql';
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; 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 { pascalCase } from 'src/utils/pascal-case';

View File

@ -3,7 +3,7 @@ import { Injectable, Logger } from '@nestjs/common';
import { GraphQLOutputType } from 'graphql'; import { GraphQLOutputType } from 'graphql';
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; 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 { import {
TypeMapperService, TypeMapperService,

View File

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

View File

@ -7,13 +7,13 @@ import {
} from 'graphql'; } from 'graphql';
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; 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 { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
import { TypeDefinitionsStorage } from 'src/workspace/workspace-schema-builder/storages/type-definitions.storage'; 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 { 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 { import {
RelationDirection, RelationDirection,
deduceRelationDirection, deduceRelationDirection,
@ -54,7 +54,7 @@ export class ExtendObjectTypeDefinitionFactory {
objectMetadata.id, objectMetadata.id,
kind, kind,
); );
const containsCompositeField = objectContainsCompositeField(objectMetadata); const containsRelationField = objectContainsRelationField(objectMetadata);
if (!gqlType) { if (!gqlType) {
this.logger.error( this.logger.error(
@ -71,7 +71,7 @@ export class ExtendObjectTypeDefinitionFactory {
} }
// Security check to avoid extending an object that does not need to be extended // Security check to avoid extending an object that does not need to be extended
if (!containsCompositeField) { if (!containsRelationField) {
this.logger.error( this.logger.error(
`This object does not need to be extended: ${objectMetadata.id.toString()}`, `This object does not need to be extended: ${objectMetadata.id.toString()}`,
{ {
@ -109,8 +109,8 @@ export class ExtendObjectTypeDefinitionFactory {
const fields: GraphQLFieldConfigMap<any, any> = {}; const fields: GraphQLFieldConfigMap<any, any> = {};
for (const fieldMetadata of objectMetadata.fields) { for (const fieldMetadata of objectMetadata.fields) {
// Ignore non composite fields as they are already defined // Ignore relation fields as they are already defined
if (!isCompositeFieldMetadataType(fieldMetadata.type)) { if (!isRelationFieldMetadataType(fieldMetadata.type)) {
continue; continue;
} }

View File

@ -1,3 +1,5 @@
import { EnumTypeDefinitionFactory } from 'src/workspace/workspace-schema-builder/factories/enum-type-definition.factory';
import { ArgsFactory } from './args.factory'; import { ArgsFactory } from './args.factory';
import { InputTypeFactory } from './input-type.factory'; import { InputTypeFactory } from './input-type.factory';
import { InputTypeDefinitionFactory } from './input-type-definition.factory'; import { InputTypeDefinitionFactory } from './input-type-definition.factory';
@ -24,6 +26,7 @@ export const workspaceSchemaBuilderFactories = [
InputTypeDefinitionFactory, InputTypeDefinitionFactory,
OutputTypeFactory, OutputTypeFactory,
ObjectTypeDefinitionFactory, ObjectTypeDefinitionFactory,
EnumTypeDefinitionFactory,
RelationTypeFactory, RelationTypeFactory,
ExtendObjectTypeDefinitionFactory, ExtendObjectTypeDefinitionFactory,
FilterTypeFactory, FilterTypeFactory,

View File

@ -3,11 +3,11 @@ import { Injectable } from '@nestjs/common';
import { GraphQLInputFieldConfigMap, GraphQLInputObjectType } from 'graphql'; import { GraphQLInputFieldConfigMap, GraphQLInputObjectType } from 'graphql';
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; 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 { pascalCase } from 'src/utils/pascal-case';
import { TypeMapperService } from 'src/workspace/workspace-schema-builder/services/type-mapper.service'; 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 { FilterTypeFactory } from './filter-type.factory';
import { import {
@ -68,8 +68,8 @@ export class FilterTypeDefinitionFactory {
const fields: GraphQLInputFieldConfigMap = {}; const fields: GraphQLInputFieldConfigMap = {};
for (const fieldMetadata of objectMetadata.fields) { for (const fieldMetadata of objectMetadata.fields) {
// Composite field types are generated during extension of object type definition // Relation types are generated during extension of object type definition
if (isCompositeFieldMetadataType(fieldMetadata.type)) { if (isRelationFieldMetadataType(fieldMetadata.type)) {
//continue; //continue;
} }

View File

@ -1,15 +1,23 @@
import { Injectable, Logger } from '@nestjs/common'; 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 { 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 { import {
TypeMapperService, TypeMapperService,
TypeOptions, TypeOptions,
} from 'src/workspace/workspace-schema-builder/services/type-mapper.service'; } from 'src/workspace/workspace-schema-builder/services/type-mapper.service';
import { TypeDefinitionsStorage } from 'src/workspace/workspace-schema-builder/storages/type-definitions.storage'; 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'; import { InputTypeDefinitionKind } from './input-type-definition.factory';
@ -27,34 +35,68 @@ export class FilterTypeFactory {
buildOtions: WorkspaceBuildSchemaOptions, buildOtions: WorkspaceBuildSchemaOptions,
typeOptions: TypeOptions, typeOptions: TypeOptions,
): GraphQLInputType { ): GraphQLInputType {
let filterType = this.typeMapperService.mapToFilterType( const target = isCompositeFieldMetadataType(fieldMetadata.type)
fieldMetadata.type, ? fieldMetadata.type.toString()
buildOtions.dateScalarMode, : fieldMetadata.id;
buildOtions.numberScalarMode, let filterType: GraphQLInputObjectType | GraphQLScalarType | undefined =
); undefined;
if (!filterType) { if (isEnumFieldMetadataType(fieldMetadata.type)) {
filterType = this.typeDefinitionsStorage.getInputTypeByKey( filterType = this.createEnumFilterType(fieldMetadata);
fieldMetadata.type.toString(), } else {
InputTypeDefinitionKind.Filter, filterType = this.typeMapperService.mapToFilterType(
fieldMetadata.type,
buildOtions.dateScalarMode,
buildOtions.numberScalarMode,
); );
if (!filterType) { filterType ??= this.typeDefinitionsStorage.getInputTypeByKey(
this.logger.error( target,
`Could not find a GraphQL type for ${fieldMetadata.type.toString()}`, InputTypeDefinitionKind.Filter,
{ );
fieldMetadata, }
buildOtions,
typeOptions,
},
);
throw new Error( if (!filterType) {
`Could not find a GraphQL type for ${fieldMetadata.type.toString()}`, 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); 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 },
}),
});
}
} }

View File

@ -3,10 +3,11 @@ import { Injectable } from '@nestjs/common';
import { GraphQLInputFieldConfigMap, GraphQLInputObjectType } from 'graphql'; import { GraphQLInputFieldConfigMap, GraphQLInputObjectType } from 'graphql';
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; 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 { 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'; import { InputTypeFactory } from './input-type.factory';
@ -53,14 +54,15 @@ export class InputTypeDefinitionFactory {
const fields: GraphQLInputFieldConfigMap = {}; const fields: GraphQLInputFieldConfigMap = {};
for (const fieldMetadata of objectMetadata.fields) { for (const fieldMetadata of objectMetadata.fields) {
// Composite field types are generated during extension of object type definition // Relation field types are generated during extension of object type definition
if (isCompositeFieldMetadataType(fieldMetadata.type)) { if (isRelationFieldMetadataType(fieldMetadata.type)) {
//continue; //continue;
} }
const type = this.inputTypeFactory.create(fieldMetadata, kind, options, { const type = this.inputTypeFactory.create(fieldMetadata, kind, options, {
nullable: fieldMetadata.isNullable, nullable: fieldMetadata.isNullable,
defaultValue: fieldMetadata.defaultValue, defaultValue: fieldMetadata.defaultValue,
isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT,
}); });
fields[fieldMetadata.name] = { fields[fieldMetadata.name] = {

View File

@ -3,13 +3,14 @@ import { Injectable, Logger } from '@nestjs/common';
import { GraphQLInputType } from 'graphql'; import { GraphQLInputType } from 'graphql';
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; 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 { import {
TypeMapperService, TypeMapperService,
TypeOptions, TypeOptions,
} from 'src/workspace/workspace-schema-builder/services/type-mapper.service'; } from 'src/workspace/workspace-schema-builder/services/type-mapper.service';
import { TypeDefinitionsStorage } from 'src/workspace/workspace-schema-builder/storages/type-definitions.storage'; 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'; import { InputTypeDefinitionKind } from './input-type-definition.factory';
@ -28,6 +29,9 @@ export class InputTypeFactory {
buildOtions: WorkspaceBuildSchemaOptions, buildOtions: WorkspaceBuildSchemaOptions,
typeOptions: TypeOptions, typeOptions: TypeOptions,
): GraphQLInputType { ): GraphQLInputType {
const target = isCompositeFieldMetadataType(fieldMetadata.type)
? fieldMetadata.type.toString()
: fieldMetadata.id;
let inputType: GraphQLInputType | undefined = let inputType: GraphQLInputType | undefined =
this.typeMapperService.mapToScalarType( this.typeMapperService.mapToScalarType(
fieldMetadata.type, fieldMetadata.type,
@ -35,27 +39,19 @@ export class InputTypeFactory {
buildOtions.numberScalarMode, buildOtions.numberScalarMode,
); );
inputType ??= this.typeDefinitionsStorage.getInputTypeByKey(target, kind);
inputType ??= this.typeDefinitionsStorage.getEnumTypeByKey(target);
if (!inputType) { if (!inputType) {
inputType = this.typeDefinitionsStorage.getInputTypeByKey( this.logger.error(`Could not find a GraphQL type for ${target}`, {
fieldMetadata.type.toString(), fieldMetadata,
kind, kind,
); buildOtions,
typeOptions,
});
if (!inputType) { throw new Error(`Could not find a GraphQL type for ${target}`);
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()}`,
);
}
} }
return this.typeMapperService.mapToGqlType(inputType, typeOptions); return this.typeMapperService.mapToGqlType(inputType, typeOptions);

View File

@ -4,7 +4,7 @@ import { GraphQLObjectType } from 'graphql';
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; 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 { 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'; import { ObjectTypeName, RootTypeFactory } from './root-type.factory';

View File

@ -3,10 +3,11 @@ import { Injectable } from '@nestjs/common';
import { GraphQLFieldConfigMap, GraphQLObjectType } from 'graphql'; import { GraphQLFieldConfigMap, GraphQLObjectType } from 'graphql';
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; 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 { 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'; import { OutputTypeFactory } from './output-type.factory';
@ -50,13 +51,14 @@ export class ObjectTypeDefinitionFactory {
const fields: GraphQLFieldConfigMap<any, any> = {}; const fields: GraphQLFieldConfigMap<any, any> = {};
for (const fieldMetadata of objectMetadata.fields) { for (const fieldMetadata of objectMetadata.fields) {
// Composite field types are generated during extension of object type definition // Relation field types are generated during extension of object type definition
if (isCompositeFieldMetadataType(fieldMetadata.type)) { if (isRelationFieldMetadataType(fieldMetadata.type)) {
continue; continue;
} }
const type = this.outputTypeFactory.create(fieldMetadata, kind, options, { const type = this.outputTypeFactory.create(fieldMetadata, kind, options, {
nullable: fieldMetadata.isNullable, nullable: fieldMetadata.isNullable,
isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT,
}); });
fields[fieldMetadata.name] = { fields[fieldMetadata.name] = {

View File

@ -3,10 +3,10 @@ import { Injectable } from '@nestjs/common';
import { GraphQLInputFieldConfigMap, GraphQLInputObjectType } from 'graphql'; import { GraphQLInputFieldConfigMap, GraphQLInputObjectType } from 'graphql';
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; 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 { 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 { import {
InputTypeDefinition, InputTypeDefinition,
@ -44,8 +44,8 @@ export class OrderByTypeDefinitionFactory {
const fields: GraphQLInputFieldConfigMap = {}; const fields: GraphQLInputFieldConfigMap = {};
for (const fieldMetadata of objectMetadata.fields) { for (const fieldMetadata of objectMetadata.fields) {
// Composite field types are generated during extension of object type definition // Relation field types are generated during extension of object type definition
if (isCompositeFieldMetadataType(fieldMetadata.type)) { if (isRelationFieldMetadataType(fieldMetadata.type)) {
continue; continue;
} }

View File

@ -3,13 +3,14 @@ import { Injectable, Logger } from '@nestjs/common';
import { GraphQLInputType } from 'graphql'; import { GraphQLInputType } from 'graphql';
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; 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 { import {
TypeMapperService, TypeMapperService,
TypeOptions, TypeOptions,
} from 'src/workspace/workspace-schema-builder/services/type-mapper.service'; } from 'src/workspace/workspace-schema-builder/services/type-mapper.service';
import { TypeDefinitionsStorage } from 'src/workspace/workspace-schema-builder/storages/type-definitions.storage'; 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'; import { InputTypeDefinitionKind } from './input-type-definition.factory';
@ -27,30 +28,26 @@ export class OrderByTypeFactory {
buildOtions: WorkspaceBuildSchemaOptions, buildOtions: WorkspaceBuildSchemaOptions,
typeOptions: TypeOptions, typeOptions: TypeOptions,
): GraphQLInputType { ): GraphQLInputType {
const target = isCompositeFieldMetadataType(fieldMetadata.type)
? fieldMetadata.type.toString()
: fieldMetadata.id;
let orderByType = this.typeMapperService.mapToOrderByType( let orderByType = this.typeMapperService.mapToOrderByType(
fieldMetadata.type, fieldMetadata.type,
); );
orderByType ??= this.typeDefinitionsStorage.getInputTypeByKey(
target,
InputTypeDefinitionKind.OrderBy,
);
if (!orderByType) { if (!orderByType) {
orderByType = this.typeDefinitionsStorage.getInputTypeByKey( this.logger.error(`Could not find a GraphQL type for ${target}`, {
fieldMetadata.type.toString(), fieldMetadata,
InputTypeDefinitionKind.OrderBy, buildOtions,
); typeOptions,
});
if (!orderByType) { throw new Error(`Could not find a GraphQL type for ${target}`);
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()}`,
);
}
} }
return this.typeMapperService.mapToGqlType(orderByType, typeOptions); return this.typeMapperService.mapToGqlType(orderByType, typeOptions);

View File

@ -3,13 +3,14 @@ import { Injectable, Logger } from '@nestjs/common';
import { GraphQLOutputType } from 'graphql'; import { GraphQLOutputType } from 'graphql';
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; 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 { import {
TypeMapperService, TypeMapperService,
TypeOptions, TypeOptions,
} from 'src/workspace/workspace-schema-builder/services/type-mapper.service'; } from 'src/workspace/workspace-schema-builder/services/type-mapper.service';
import { TypeDefinitionsStorage } from 'src/workspace/workspace-schema-builder/storages/type-definitions.storage'; 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'; import { ObjectTypeDefinitionKind } from './object-type-definition.factory';
@ -28,6 +29,9 @@ export class OutputTypeFactory {
buildOtions: WorkspaceBuildSchemaOptions, buildOtions: WorkspaceBuildSchemaOptions,
typeOptions: TypeOptions, typeOptions: TypeOptions,
): GraphQLOutputType { ): GraphQLOutputType {
const target = isCompositeFieldMetadataType(fieldMetadata.type)
? fieldMetadata.type.toString()
: fieldMetadata.id;
let gqlType: GraphQLOutputType | undefined = let gqlType: GraphQLOutputType | undefined =
this.typeMapperService.mapToScalarType( this.typeMapperService.mapToScalarType(
fieldMetadata.type, fieldMetadata.type,
@ -35,26 +39,18 @@ export class OutputTypeFactory {
buildOtions.numberScalarMode, buildOtions.numberScalarMode,
); );
gqlType ??= this.typeDefinitionsStorage.getObjectTypeByKey(target, kind);
gqlType ??= this.typeDefinitionsStorage.getEnumTypeByKey(target);
if (!gqlType) { if (!gqlType) {
gqlType = this.typeDefinitionsStorage.getObjectTypeByKey( this.logger.error(`Could not find a GraphQL type for ${target}`, {
fieldMetadata.type.toString(), fieldMetadata,
kind, buildOtions,
); typeOptions,
});
if (!gqlType) { throw new Error(`Could not find a GraphQL type for ${target}`);
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()}`,
);
}
} }
return this.typeMapperService.mapToGqlType(gqlType, typeOptions); return this.typeMapperService.mapToGqlType(gqlType, typeOptions);

View File

@ -4,7 +4,7 @@ import { GraphQLObjectType } from 'graphql';
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; 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 { 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'; import { ObjectTypeName, RootTypeFactory } from './root-type.factory';

View File

@ -2,8 +2,8 @@ import { Injectable, Logger } from '@nestjs/common';
import { GraphQLOutputType } from 'graphql'; import { GraphQLOutputType } 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 { 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 { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity';
import { TypeDefinitionsStorage } from 'src/workspace/workspace-schema-builder/storages/type-definitions.storage'; import { TypeDefinitionsStorage } from 'src/workspace/workspace-schema-builder/storages/type-definitions.storage';

View File

@ -4,7 +4,7 @@ import { GraphQLFieldConfigMap, GraphQLObjectType } from 'graphql';
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; 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 { 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 { TypeDefinitionsStorage } from 'src/workspace/workspace-schema-builder/storages/type-definitions.storage';
import { getResolverName } from 'src/workspace/utils/get-resolver-name.util'; import { getResolverName } from 'src/workspace/utils/get-resolver-name.util';

View File

@ -1,6 +1,6 @@
import { GraphQLInputObjectType, GraphQLList, GraphQLNonNull } from 'graphql'; 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'; import { BigFloatScalarType } from 'src/workspace/workspace-schema-builder/graphql-types/scalars';
export const BigFloatFilterType = new GraphQLInputObjectType({ export const BigFloatFilterType = new GraphQLInputObjectType({
@ -13,6 +13,6 @@ export const BigFloatFilterType = new GraphQLInputObjectType({
lt: { type: BigFloatScalarType }, lt: { type: BigFloatScalarType },
lte: { type: BigFloatScalarType }, lte: { type: BigFloatScalarType },
neq: { type: BigFloatScalarType }, neq: { type: BigFloatScalarType },
is: { type: FilterIsNullable }, is: { type: FilterIs },
}, },
}); });

View File

@ -5,7 +5,7 @@ import {
GraphQLInt, GraphQLInt,
} from 'graphql'; } 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({ export const BigIntFilterType = new GraphQLInputObjectType({
name: 'BigIntFilter', name: 'BigIntFilter',
@ -17,6 +17,6 @@ export const BigIntFilterType = new GraphQLInputObjectType({
lt: { type: GraphQLInt }, lt: { type: GraphQLInt },
lte: { type: GraphQLInt }, lte: { type: GraphQLInt },
neq: { type: GraphQLInt }, neq: { type: GraphQLInt },
is: { type: FilterIsNullable }, is: { type: FilterIs },
}, },
}); });

View File

@ -1,11 +1,11 @@
import { GraphQLBoolean, GraphQLInputObjectType } from 'graphql'; 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({ export const BooleanFilterType = new GraphQLInputObjectType({
name: 'BooleanFilter', name: 'BooleanFilter',
fields: { fields: {
eq: { type: GraphQLBoolean }, eq: { type: GraphQLBoolean },
is: { type: FilterIsNullable }, is: { type: FilterIs },
}, },
}); });

View File

@ -1,6 +1,6 @@
import { GraphQLInputObjectType, GraphQLList, GraphQLNonNull } from 'graphql'; 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'; import { DateScalarType } from 'src/workspace/workspace-schema-builder/graphql-types/scalars';
export const DateFilterType = new GraphQLInputObjectType({ export const DateFilterType = new GraphQLInputObjectType({
@ -13,6 +13,6 @@ export const DateFilterType = new GraphQLInputObjectType({
lt: { type: DateScalarType }, lt: { type: DateScalarType },
lte: { type: DateScalarType }, lte: { type: DateScalarType },
neq: { type: DateScalarType }, neq: { type: DateScalarType },
is: { type: FilterIsNullable }, is: { type: FilterIs },
}, },
}); });

View File

@ -1,6 +1,6 @@
import { GraphQLInputObjectType, GraphQLList, GraphQLNonNull } from 'graphql'; 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'; import { DateTimeScalarType } from 'src/workspace/workspace-schema-builder/graphql-types/scalars';
export const DatetimeFilterType = new GraphQLInputObjectType({ export const DatetimeFilterType = new GraphQLInputObjectType({
@ -13,6 +13,6 @@ export const DatetimeFilterType = new GraphQLInputObjectType({
lt: { type: DateTimeScalarType }, lt: { type: DateTimeScalarType },
lte: { type: DateTimeScalarType }, lte: { type: DateTimeScalarType },
neq: { type: DateTimeScalarType }, neq: { type: DateTimeScalarType },
is: { type: FilterIsNullable }, is: { type: FilterIs },
}, },
}); });

View File

@ -1,7 +1,7 @@
import { GraphQLEnumType } from 'graphql'; import { GraphQLEnumType } from 'graphql';
export const FilterIsNullable = new GraphQLEnumType({ export const FilterIs = new GraphQLEnumType({
name: 'FilterIsNullable', name: 'FilterIs',
description: 'This enum to filter by nullability', description: 'This enum to filter by nullability',
values: { values: {
NULL: { NULL: {

View File

@ -5,7 +5,7 @@ import {
GraphQLNonNull, GraphQLNonNull,
} from 'graphql'; } 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({ export const FloatFilterType = new GraphQLInputObjectType({
name: 'FloatFilter', name: 'FloatFilter',
@ -17,6 +17,6 @@ export const FloatFilterType = new GraphQLInputObjectType({
lt: { type: GraphQLFloat }, lt: { type: GraphQLFloat },
lte: { type: GraphQLFloat }, lte: { type: GraphQLFloat },
neq: { type: GraphQLFloat }, neq: { type: GraphQLFloat },
is: { type: FilterIsNullable }, is: { type: FilterIs },
}, },
}); });

View File

@ -5,7 +5,7 @@ import {
GraphQLInt, GraphQLInt,
} from 'graphql'; } 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({ export const IntFilterType = new GraphQLInputObjectType({
name: 'IntFilter', name: 'IntFilter',
@ -17,6 +17,6 @@ export const IntFilterType = new GraphQLInputObjectType({
lt: { type: GraphQLInt }, lt: { type: GraphQLInt },
lte: { type: GraphQLInt }, lte: { type: GraphQLInt },
neq: { type: GraphQLInt }, neq: { type: GraphQLInt },
is: { type: FilterIsNullable }, is: { type: FilterIs },
}, },
}); });

View File

@ -5,7 +5,7 @@ import {
GraphQLString, GraphQLString,
} from 'graphql'; } 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({ export const StringFilterType = new GraphQLInputObjectType({
name: 'StringFilter', name: 'StringFilter',
@ -22,6 +22,6 @@ export const StringFilterType = new GraphQLInputObjectType({
ilike: { type: GraphQLString }, ilike: { type: GraphQLString },
regex: { type: GraphQLString }, regex: { type: GraphQLString },
iregex: { type: GraphQLString }, iregex: { type: GraphQLString },
is: { type: FilterIsNullable }, is: { type: FilterIs },
}, },
}); });

View File

@ -1,6 +1,6 @@
import { GraphQLInputObjectType, GraphQLList, GraphQLNonNull } from 'graphql'; 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'; import { TimeScalarType } from 'src/workspace/workspace-schema-builder/graphql-types/scalars';
export const TimeFilterType = new GraphQLInputObjectType({ export const TimeFilterType = new GraphQLInputObjectType({
@ -13,6 +13,6 @@ export const TimeFilterType = new GraphQLInputObjectType({
lt: { type: TimeScalarType }, lt: { type: TimeScalarType },
lte: { type: TimeScalarType }, lte: { type: TimeScalarType },
neq: { type: TimeScalarType }, neq: { type: TimeScalarType },
is: { type: FilterIsNullable }, is: { type: FilterIs },
}, },
}); });

View File

@ -1,6 +1,6 @@
import { GraphQLInputObjectType, GraphQLList } from 'graphql'; 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'; import { UUIDScalarType } from 'src/workspace/workspace-schema-builder/graphql-types/scalars';
export const UUIDFilterType = new GraphQLInputObjectType({ export const UUIDFilterType = new GraphQLInputObjectType({
@ -9,6 +9,6 @@ export const UUIDFilterType = new GraphQLInputObjectType({
eq: { type: UUIDScalarType }, eq: { type: UUIDScalarType },
in: { type: new GraphQLList(UUIDScalarType) }, in: { type: new GraphQLList(UUIDScalarType) },
neq: { type: UUIDScalarType }, neq: { type: UUIDScalarType },
is: { type: FilterIsNullable }, is: { type: FilterIs },
}, },
}); });

View File

@ -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 { InputTypeDefinitionKind } from 'src/workspace/workspace-schema-builder/factories/input-type-definition.factory';
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
import { ObjectMetadataInterface } from './object-metadata.interface';
export interface ArgMetadata<T = any> { export interface ArgMetadata<T = any> {
kind?: InputTypeDefinitionKind; kind?: InputTypeDefinitionKind;
type?: FieldMetadataType; type?: FieldMetadataType;

View File

@ -1,4 +1,4 @@
import { FieldMetadataInterface } from './field-metadata.interface'; import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/field-metadata.interface';
export interface WorkspaceSchemaBuilderContext { export interface WorkspaceSchemaBuilderContext {
workspaceId: string; workspaceId: string;

View File

@ -75,7 +75,7 @@ export class TypeMapperService {
fieldMetadataType: FieldMetadataType, fieldMetadataType: FieldMetadataType,
dateScalarMode: DateScalarMode = 'isoDate', dateScalarMode: DateScalarMode = 'isoDate',
numberScalarMode: NumberScalarMode = 'float', numberScalarMode: NumberScalarMode = 'float',
): GraphQLInputObjectType | GraphQLScalarType<boolean, boolean> | undefined { ): GraphQLInputObjectType | GraphQLScalarType | undefined {
const dateFilter = const dateFilter =
dateScalarMode === 'timestamp' ? DatetimeFilterType : DateFilterType; dateScalarMode === 'timestamp' ? DatetimeFilterType : DateFilterType;
const numberScalar = const numberScalar =
@ -84,7 +84,7 @@ export class TypeMapperService {
// LINK and CURRENCY are handled in the factories because they are objects // LINK and CURRENCY are handled in the factories because they are objects
const typeFilterMapping = new Map< const typeFilterMapping = new Map<
FieldMetadataType, FieldMetadataType,
GraphQLInputObjectType | GraphQLScalarType<boolean, boolean> GraphQLInputObjectType | GraphQLScalarType
>([ >([
[FieldMetadataType.UUID, UUIDFilterType], [FieldMetadataType.UUID, UUIDFilterType],
[FieldMetadataType.TEXT, StringFilterType], [FieldMetadataType.TEXT, StringFilterType],
@ -115,6 +115,9 @@ export class TypeMapperService {
[FieldMetadataType.NUMBER, OrderByDirectionType], [FieldMetadataType.NUMBER, OrderByDirectionType],
[FieldMetadataType.NUMERIC, OrderByDirectionType], [FieldMetadataType.NUMERIC, OrderByDirectionType],
[FieldMetadataType.PROBABILITY, OrderByDirectionType], [FieldMetadataType.PROBABILITY, OrderByDirectionType],
[FieldMetadataType.RATING, OrderByDirectionType],
[FieldMetadataType.SELECT, OrderByDirectionType],
[FieldMetadataType.MULTI_SELECT, OrderByDirectionType],
]); ]);
return typeOrderByMapping.get(fieldMetadataType); return typeOrderByMapping.get(fieldMetadataType);

View File

@ -1,8 +1,13 @@
import { Injectable, Scope } from '@nestjs/common'; 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 { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
import { EnumTypeDefinition } from 'src/workspace/workspace-schema-builder/factories/enum-type-definition.factory';
import { import {
InputTypeDefinition, InputTypeDefinition,
InputTypeDefinitionKind, InputTypeDefinitionKind,
@ -15,6 +20,7 @@ import {
// Must be scoped on REQUEST level // Must be scoped on REQUEST level
@Injectable({ scope: Scope.REQUEST }) @Injectable({ scope: Scope.REQUEST })
export class TypeDefinitionsStorage { export class TypeDefinitionsStorage {
private readonly enumTypeDefinitions = new Map<string, EnumTypeDefinition>();
private readonly objectTypeDefinitions = new Map< private readonly objectTypeDefinitions = new Map<
string, string,
ObjectTypeDefinition ObjectTypeDefinition
@ -24,6 +30,10 @@ export class TypeDefinitionsStorage {
InputTypeDefinition InputTypeDefinition
>(); >();
addEnumTypes(enumDefs: EnumTypeDefinition[]) {
enumDefs.forEach((item) => this.enumTypeDefinitions.set(item.target, item));
}
addObjectTypes(objectDefs: ObjectTypeDefinition[]) { addObjectTypes(objectDefs: ObjectTypeDefinition[]) {
objectDefs.forEach((item) => objectDefs.forEach((item) =>
this.objectTypeDefinitions.set( this.objectTypeDefinitions.set(
@ -64,6 +74,10 @@ export class TypeDefinitionsStorage {
)?.type; )?.type;
} }
getEnumTypeByKey(target: string): GraphQLEnumType | undefined {
return this.enumTypeDefinitions.get(target)?.type;
}
getAllInputTypeDefinitions(): InputTypeDefinition[] { getAllInputTypeDefinitions(): InputTypeDefinition[] {
return Array.from(this.inputTypeDefinitions.values()); return Array.from(this.inputTypeDefinitions.values());
} }

View File

@ -1,8 +1,14 @@
import { Injectable, Logger } from '@nestjs/common'; 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 { FieldMetadataEntity } from 'src/metadata/field-metadata/field-metadata.entity';
import { customTableDefaultColumns } from 'src/workspace/workspace-migration-runner/utils/custom-table-default-column.util'; 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 { TypeDefinitionsStorage } from './storages/type-definitions.storage';
import { import {
@ -15,16 +21,12 @@ import {
} from './factories/input-type-definition.factory'; } from './factories/input-type-definition.factory';
import { getFieldMetadataType } from './utils/get-field-metadata-type.util'; import { getFieldMetadataType } from './utils/get-field-metadata-type.util';
import { WorkspaceBuildSchemaOptions } from './interfaces/workspace-build-schema-optionts.interface'; 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 { FilterTypeDefinitionFactory } from './factories/filter-type-definition.factory';
import { ConnectionTypeDefinitionFactory } from './factories/connection-type-definition.factory'; import { ConnectionTypeDefinitionFactory } from './factories/connection-type-definition.factory';
import { EdgeTypeDefinitionFactory } from './factories/edge-type-definition.factory'; import { EdgeTypeDefinitionFactory } from './factories/edge-type-definition.factory';
import { OrderByTypeDefinitionFactory } from './factories/order-by-type-definition.factory'; import { OrderByTypeDefinitionFactory } from './factories/order-by-type-definition.factory';
import { ExtendObjectTypeDefinitionFactory } from './factories/extend-object-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 // Create a default field for each custom table default column
const defaultFields = customTableDefaultColumns.map((column) => { const defaultFields = customTableDefaultColumns.map((column) => {
@ -42,6 +44,7 @@ export class TypeDefinitionsGenerator {
constructor( constructor(
private readonly typeDefinitionsStorage: TypeDefinitionsStorage, private readonly typeDefinitionsStorage: TypeDefinitionsStorage,
private readonly objectTypeDefinitionFactory: ObjectTypeDefinitionFactory, private readonly objectTypeDefinitionFactory: ObjectTypeDefinitionFactory,
private readonly enumTypeDefinitionFactory: EnumTypeDefinitionFactory,
private readonly inputTypeDefinitionFactory: InputTypeDefinitionFactory, private readonly inputTypeDefinitionFactory: InputTypeDefinitionFactory,
private readonly filterTypeDefintionFactory: FilterTypeDefinitionFactory, private readonly filterTypeDefintionFactory: FilterTypeDefinitionFactory,
private readonly orderByTypeDefinitionFactory: OrderByTypeDefinitionFactory, private readonly orderByTypeDefinitionFactory: OrderByTypeDefinitionFactory,
@ -74,6 +77,7 @@ export class TypeDefinitionsGenerator {
); );
// Generate static objects first because they can be used in dynamic objects // Generate static objects first because they can be used in dynamic objects
this.generateEnumTypeDefs(staticObjectMetadataCollection, options);
this.generateObjectTypeDefs(staticObjectMetadataCollection, options); this.generateObjectTypeDefs(staticObjectMetadataCollection, options);
this.generateInputTypeDefs(staticObjectMetadataCollection, options); this.generateInputTypeDefs(staticObjectMetadataCollection, options);
} }
@ -89,6 +93,7 @@ export class TypeDefinitionsGenerator {
); );
// Generate dynamic objects // Generate dynamic objects
this.generateEnumTypeDefs(dynamicObjectMetadataCollection, options);
this.generateObjectTypeDefs(dynamicObjectMetadataCollection, options); this.generateObjectTypeDefs(dynamicObjectMetadataCollection, options);
this.generatePaginationTypeDefs(dynamicObjectMetadataCollection, options); this.generatePaginationTypeDefs(dynamicObjectMetadataCollection, options);
this.generateInputTypeDefs(dynamicObjectMetadataCollection, options); this.generateInputTypeDefs(dynamicObjectMetadataCollection, options);
@ -203,13 +208,26 @@ export class TypeDefinitionsGenerator {
this.typeDefinitionsStorage.addInputTypes(inputTypeDefs); 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( private generateExtendedObjectTypeDefs(
objectMetadataCollection: ObjectMetadataInterface[], objectMetadataCollection: ObjectMetadataInterface[],
options: WorkspaceBuildSchemaOptions, options: WorkspaceBuildSchemaOptions,
) { ) {
// Generate extended object type defs only for objects that contain composite fields // Generate extended object type defs only for objects that contain composite fields
const objectMetadataCollectionWithCompositeFields = const objectMetadataCollectionWithCompositeFields =
objectMetadataCollection.filter(objectContainsCompositeField); objectMetadataCollection.filter(objectContainsRelationField);
const objectTypeDefs = objectMetadataCollectionWithCompositeFields.map( const objectTypeDefs = objectMetadataCollectionWithCompositeFields.map(
(objectMetadata) => (objectMetadata) =>
this.extendObjectTypeDefinitionFactory.create(objectMetadata, options), this.extendObjectTypeDefinitionFactory.create(objectMetadata, options),

View File

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

View File

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

View File

@ -3,13 +3,13 @@ import { Injectable, Logger } from '@nestjs/common';
import { GraphQLSchema } from 'graphql'; import { GraphQLSchema } from 'graphql';
import { WorkspaceResolverBuilderMethods } from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; 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 { TypeDefinitionsGenerator } from './type-definitions.generator';
import { WorkspaceBuildSchemaOptions } from './interfaces/workspace-build-schema-optionts.interface'; import { WorkspaceBuildSchemaOptions } from './interfaces/workspace-build-schema-optionts.interface';
import { QueryTypeFactory } from './factories/query-type.factory'; import { QueryTypeFactory } from './factories/query-type.factory';
import { MutationTypeFactory } from './factories/mutation-type.factory'; import { MutationTypeFactory } from './factories/mutation-type.factory';
import { ObjectMetadataInterface } from './interfaces/object-metadata.interface';
import { OrphanedTypesFactory } from './factories/orphaned-types.factory'; import { OrphanedTypesFactory } from './factories/orphaned-types.factory';
@Injectable() @Injectable()