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

@ -0,0 +1,35 @@
import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/object-metadata.interface';
import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/field-metadata.interface';
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
export const currencyObjectDefinition = {
id: FieldMetadataType.CURRENCY.toString(),
nameSingular: 'currency',
namePlural: 'currency',
labelSingular: 'Currency',
labelPlural: 'Currency',
targetTableName: '',
fields: [
{
id: 'amountMicros',
type: FieldMetadataType.NUMERIC,
objectMetadataId: FieldMetadataType.CURRENCY.toString(),
name: 'amountMicros',
label: 'AmountMicros',
targetColumnMap: { value: 'amountMicros' },
isNullable: true,
} satisfies FieldMetadataInterface,
{
id: 'currencyCode',
type: FieldMetadataType.TEXT,
objectMetadataId: FieldMetadataType.CURRENCY.toString(),
name: 'currencyCode',
label: 'Currency Code',
targetColumnMap: { value: 'currencyCode' },
isNullable: true,
} satisfies FieldMetadataInterface,
],
fromRelations: [],
toRelations: [],
} satisfies ObjectMetadataInterface;

View File

@ -0,0 +1,35 @@
import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/object-metadata.interface';
import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/field-metadata.interface';
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
export const fullNameObjectDefinition = {
id: FieldMetadataType.FULL_NAME.toString(),
nameSingular: 'fullName',
namePlural: 'fullName',
labelSingular: 'FullName',
labelPlural: 'FullName',
targetTableName: '',
fields: [
{
id: 'firstName',
type: FieldMetadataType.TEXT,
objectMetadataId: FieldMetadataType.FULL_NAME.toString(),
name: 'firstName',
label: 'First Name',
targetColumnMap: { value: 'firstName' },
isNullable: true,
} satisfies FieldMetadataInterface,
{
id: 'lastName',
type: FieldMetadataType.TEXT,
objectMetadataId: FieldMetadataType.FULL_NAME.toString(),
name: 'lastName',
label: 'Last Name',
targetColumnMap: { value: 'lastName' },
isNullable: true,
} satisfies FieldMetadataInterface,
],
fromRelations: [],
toRelations: [],
} satisfies ObjectMetadataInterface;

View File

@ -0,0 +1,35 @@
import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/object-metadata.interface';
import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/field-metadata.interface';
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
export const linkObjectDefinition = {
id: FieldMetadataType.LINK.toString(),
nameSingular: 'link',
namePlural: 'link',
labelSingular: 'Link',
labelPlural: 'Link',
targetTableName: '',
fields: [
{
id: 'label',
type: FieldMetadataType.TEXT,
objectMetadataId: FieldMetadataType.LINK.toString(),
name: 'label',
label: 'Label',
targetColumnMap: { value: 'label' },
isNullable: true,
} satisfies FieldMetadataInterface,
{
id: 'url',
type: FieldMetadataType.TEXT,
objectMetadataId: FieldMetadataType.LINK.toString(),
name: 'url',
label: 'Url',
targetColumnMap: { value: 'url' },
isNullable: true,
} satisfies FieldMetadataInterface,
],
fromRelations: [],
toRelations: [],
} satisfies ObjectMetadataInterface;

View File

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

View File

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

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 { IsBoolean, IsOptional, IsString } from 'class-validator';
import {
IsArray,
IsBoolean,
IsOptional,
IsString,
ValidateNested,
} from 'class-validator';
import GraphQLJSON from 'graphql-type-json';
import { Type } from 'class-transformer';
import { FieldMetadataDefaultValue } from 'src/metadata/field-metadata/interfaces/field-metadata-default-value.interface';
import { FieldMetadataOptions } from 'src/metadata/field-metadata/interfaces/field-metadata-options.interface';
import { BeforeUpdateOneField } from 'src/metadata/field-metadata/hooks/before-update-one-field.hook';
import { FieldMetadataComplexOptions } from 'src/metadata/field-metadata/dtos/options.input';
@InputType()
@BeforeUpdateOne(BeforeUpdateOneField)
@ -32,4 +44,19 @@ export class UpdateFieldInput {
@IsOptional()
@Field({ nullable: true })
isActive?: boolean;
// TODO: Add validation for this but we don't have the type actually
@IsOptional()
@Field(() => GraphQLJSON, { nullable: true })
defaultValue?: FieldMetadataDefaultValue;
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => FieldMetadataComplexOptions)
@Field(() => GraphQLJSON, { nullable: true })
options?: FieldMetadataOptions;
@HideField()
workspaceId: string;
}

View File

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

View File

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

View File

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

View File

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

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;
};
type TypeByFieldMetadata<T extends FieldMetadataType | 'default'> =
T extends keyof FieldMetadataTypeMapping
? FieldMetadataTypeMapping[T]
: T extends 'default'
? AllFieldMetadataTypes
: FieldMetadataTargetColumnMapValue;
type TypeByFieldMetadata<T extends FieldMetadataType | 'default'> = [
T,
] extends [keyof FieldMetadataTypeMapping]
? FieldMetadataTypeMapping[T]
: T extends 'default'
? AllFieldMetadataTypes
: FieldMetadataTargetColumnMapValue;
export type FieldMetadataTargetColumnMap<
T extends FieldMetadataType | 'default' = 'default',

View File

@ -0,0 +1,24 @@
import { FieldMetadataTargetColumnMap } from 'src/metadata/field-metadata/interfaces/field-metadata-target-column-map.interface';
import { FieldMetadataDefaultValue } from 'src/metadata/field-metadata/interfaces/field-metadata-default-value.interface';
import { FieldMetadataOptions } from 'src/metadata/field-metadata/interfaces/field-metadata-options.interface';
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
import { RelationMetadataEntity } from 'src/metadata/relation-metadata/relation-metadata.entity';
export interface FieldMetadataInterface<
T extends FieldMetadataType | 'default' = 'default',
> {
id: string;
type: FieldMetadataType;
name: string;
label: string;
targetColumnMap: FieldMetadataTargetColumnMap<T>;
defaultValue?: FieldMetadataDefaultValue<T>;
options?: FieldMetadataOptions<T>;
objectMetadataId: string;
description?: string;
isNullable?: boolean;
fromRelationMetadata?: RelationMetadataEntity;
toRelationMetadata?: RelationMetadataEntity;
isCustom?: boolean;
}

View File

@ -0,0 +1,15 @@
import { RelationMetadataInterface } from './relation-metadata.interface';
import { FieldMetadataInterface } from './field-metadata.interface';
export interface ObjectMetadataInterface {
id: string;
nameSingular: string;
namePlural: string;
labelSingular: string;
labelPlural: string;
description?: string;
targetTableName: string;
fromRelations: RelationMetadataInterface[];
toRelations: RelationMetadataInterface[];
fields: FieldMetadataInterface[];
}

View File

@ -0,0 +1,22 @@
import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity';
import { ObjectMetadataInterface } from './object-metadata.interface';
import { FieldMetadataInterface } from './field-metadata.interface';
export interface RelationMetadataInterface {
id: string;
relationType: RelationMetadataType;
fromObjectMetadataId: string;
fromObjectMetadata: ObjectMetadataInterface;
toObjectMetadataId: string;
toObjectMetadata: ObjectMetadataInterface;
fromFieldMetadataId: string;
fromFieldMetadata: FieldMetadataInterface;
toFieldMetadataId: string;
toFieldMetadata: FieldMetadataInterface;
}

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.BOOLEAN:
case FieldMetadataType.DATE_TIME:
case FieldMetadataType.RATING:
case FieldMetadataType.SELECT:
case FieldMetadataType.MULTI_SELECT:
return {
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
if (typeof defaultValue === 'object' && 'type' in defaultValue) {
if (
!Array.isArray(defaultValue) &&
typeof defaultValue === 'object' &&
'type' in defaultValue
) {
switch (defaultValue.type) {
case 'uuid':
return 'public.uuid_generate_v4()';
@ -38,6 +42,10 @@ export const serializeDefaultValue = (
return `'${defaultValue.toISOString()}'`;
}
if (Array.isArray(defaultValue)) {
return defaultValue;
}
if (typeof defaultValue === 'object') {
return `'${JSON.stringify(defaultValue)}'`;
}

View File

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

View File

@ -9,7 +9,7 @@ import {
ManyToOne,
} from 'typeorm';
import { ObjectMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/object-metadata.interface';
import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/object-metadata.interface';
import { FieldMetadataEntity } from 'src/metadata/field-metadata/field-metadata.entity';
import { RelationMetadataEntity } from 'src/metadata/relation-metadata/relation-metadata.entity';

View File

@ -9,7 +9,7 @@ import {
UpdateDateColumn,
} from 'typeorm';
import { RelationMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/relation-metadata.interface';
import { RelationMetadataInterface } from 'src/metadata/field-metadata/interfaces/relation-metadata.interface';
import { FieldMetadataEntity } from 'src/metadata/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity';

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

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