feat: add default value capability (#2544)
* feat: add default value capability * feat: update seeds with default value
This commit is contained in:
@ -2,17 +2,21 @@ import { Field, HideField, InputType } from '@nestjs/graphql';
|
||||
|
||||
import { BeforeCreateOne } from '@ptc-org/nestjs-query-graphql';
|
||||
import {
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
} from 'class-validator';
|
||||
import { GraphQLJSONObject } from 'graphql-type-json';
|
||||
|
||||
import { FieldMetadataTargetColumnMap } from 'src/tenant/schema-builder/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 { 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';
|
||||
|
||||
@InputType()
|
||||
@BeforeCreateOne(BeforeCreateOneField)
|
||||
@ -21,10 +25,12 @@ export class CreateFieldInput {
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
name: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
label: string;
|
||||
|
||||
@IsEnum(FieldMetadataType)
|
||||
@IsNotEmpty()
|
||||
@Field(() => FieldMetadataType)
|
||||
@ -38,11 +44,22 @@ export class CreateFieldInput {
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
description?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
icon?: string;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
isNullable: boolean;
|
||||
|
||||
@IsDefaultValue({ message: 'Invalid default value for the specified type' })
|
||||
@IsOptional()
|
||||
@Field(() => GraphQLJSONObject, { nullable: true })
|
||||
defaultValue: FieldMetadataDefaultValue;
|
||||
|
||||
@HideField()
|
||||
targetColumnMap: FieldMetadataTargetColumnMap;
|
||||
|
||||
|
||||
@ -11,7 +11,8 @@ import {
|
||||
} from 'typeorm';
|
||||
|
||||
import { FieldMetadataInterface } from 'src/tenant/schema-builder/interfaces/field-metadata.interface';
|
||||
import { FieldMetadataTargetColumnMap } from 'src/tenant/schema-builder/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 { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity';
|
||||
import { RelationMetadataEntity } from 'src/metadata/relation-metadata/relation-metadata.entity';
|
||||
@ -62,6 +63,9 @@ export class FieldMetadataEntity implements FieldMetadataInterface {
|
||||
@Column({ nullable: false, type: 'jsonb' })
|
||||
targetColumnMap: FieldMetadataTargetColumnMap;
|
||||
|
||||
@Column({ nullable: true, type: 'jsonb' })
|
||||
defaultValue: FieldMetadataDefaultValue;
|
||||
|
||||
@Column({ nullable: true, type: 'text' })
|
||||
description: string;
|
||||
|
||||
|
||||
@ -10,16 +10,14 @@ import { Repository } from 'typeorm';
|
||||
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
|
||||
import { DeleteOneOptions } from '@ptc-org/nestjs-query-core';
|
||||
|
||||
import {
|
||||
convertFieldMetadataToColumnActions,
|
||||
generateTargetColumnMap,
|
||||
} from 'src/metadata/field-metadata/utils/field-metadata.util';
|
||||
import { TenantMigrationRunnerService } from 'src/tenant-migration-runner/tenant-migration-runner.service';
|
||||
import { TenantMigrationService } from 'src/metadata/tenant-migration/tenant-migration.service';
|
||||
import { ObjectMetadataService } from 'src/metadata/object-metadata/object-metadata.service';
|
||||
import { FieldMetadataDTO } from 'src/metadata/field-metadata/dtos/field-metadata.dto';
|
||||
import { CreateFieldInput } from 'src/metadata/field-metadata/dtos/create-field.input';
|
||||
import { TenantMigrationTableAction } from 'src/metadata/tenant-migration/tenant-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 { FieldMetadataEntity } from './field-metadata.entity';
|
||||
|
||||
|
||||
@ -0,0 +1,85 @@
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
|
||||
export interface FieldMetadataDefaultValueString {
|
||||
value: string;
|
||||
}
|
||||
export interface FieldMetadataDefaultValueNumber {
|
||||
value: number;
|
||||
}
|
||||
export interface FieldMetadataDefaultValueBoolean {
|
||||
value: boolean;
|
||||
}
|
||||
export interface FieldMetadataDefaultValueDate {
|
||||
value: Date;
|
||||
}
|
||||
|
||||
type FieldMetadataScalarDefaultValue =
|
||||
| FieldMetadataDefaultValueString
|
||||
| FieldMetadataDefaultValueNumber
|
||||
| FieldMetadataDefaultValueBoolean
|
||||
| FieldMetadataDefaultValueDate;
|
||||
|
||||
export type FieldMetadataDynamicDefaultValue =
|
||||
| { type: 'uuid' }
|
||||
| { type: 'now' };
|
||||
|
||||
interface FieldMetadataDefaultValueUrl {
|
||||
text: string;
|
||||
link: string;
|
||||
}
|
||||
|
||||
interface FieldMetadataDefaultValueMoney {
|
||||
amount: number;
|
||||
currency: string;
|
||||
}
|
||||
|
||||
type AllFieldMetadataDefaultValueTypes =
|
||||
| FieldMetadataScalarDefaultValue
|
||||
| FieldMetadataDynamicDefaultValue
|
||||
| FieldMetadataDefaultValueUrl
|
||||
| FieldMetadataDefaultValueMoney;
|
||||
|
||||
type FieldMetadataDefaultValueMapping = {
|
||||
[FieldMetadataType.UUID]: FieldMetadataDefaultValueString;
|
||||
[FieldMetadataType.TEXT]: FieldMetadataDefaultValueString;
|
||||
[FieldMetadataType.PHONE]: FieldMetadataDefaultValueString;
|
||||
[FieldMetadataType.EMAIL]: FieldMetadataDefaultValueString;
|
||||
[FieldMetadataType.DATE]: FieldMetadataDefaultValueDate;
|
||||
[FieldMetadataType.BOOLEAN]: FieldMetadataDefaultValueBoolean;
|
||||
[FieldMetadataType.NUMBER]: FieldMetadataDefaultValueNumber;
|
||||
[FieldMetadataType.PROBABILITY]: FieldMetadataDefaultValueNumber;
|
||||
[FieldMetadataType.ENUM]: FieldMetadataDefaultValueString;
|
||||
[FieldMetadataType.URL]: FieldMetadataDefaultValueUrl;
|
||||
[FieldMetadataType.MONEY]: FieldMetadataDefaultValueMoney;
|
||||
};
|
||||
|
||||
type DefaultValueByFieldMetadata<T extends FieldMetadataType | 'default'> = [
|
||||
T,
|
||||
] extends [keyof FieldMetadataDefaultValueMapping]
|
||||
? FieldMetadataDefaultValueMapping[T] | null
|
||||
: T extends 'default'
|
||||
? AllFieldMetadataDefaultValueTypes | null
|
||||
: never;
|
||||
|
||||
export type FieldMetadataDefaultValue<
|
||||
T extends FieldMetadataType | 'default' = 'default',
|
||||
> = DefaultValueByFieldMetadata<T>;
|
||||
|
||||
type FieldMetadataDefaultValueExtractNestedType<T> = T extends {
|
||||
value: infer U;
|
||||
}
|
||||
? U
|
||||
: T extends object
|
||||
? T[keyof T]
|
||||
: never;
|
||||
|
||||
type FieldMetadataDefaultValueExtractedTypes = {
|
||||
[K in keyof FieldMetadataDefaultValueMapping]: FieldMetadataDefaultValueExtractNestedType<
|
||||
FieldMetadataDefaultValueMapping[K]
|
||||
>;
|
||||
};
|
||||
|
||||
export type FieldMetadataDefaultSerializableValue =
|
||||
| FieldMetadataDefaultValueExtractedTypes[keyof FieldMetadataDefaultValueExtractedTypes]
|
||||
| FieldMetadataDynamicDefaultValue
|
||||
| null;
|
||||
@ -0,0 +1,35 @@
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
|
||||
export interface FieldMetadataTargetColumnMapValue {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface FieldMetadataTargetColumnMapUrl {
|
||||
text: string;
|
||||
link: string;
|
||||
}
|
||||
|
||||
export interface FieldMetadataTargetColumnMapMoney {
|
||||
amount: string;
|
||||
currency: string;
|
||||
}
|
||||
|
||||
type AllFieldMetadataTypes = {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
type FieldMetadataTypeMapping = {
|
||||
[FieldMetadataType.URL]: FieldMetadataTargetColumnMapUrl;
|
||||
[FieldMetadataType.MONEY]: FieldMetadataTargetColumnMapMoney;
|
||||
};
|
||||
|
||||
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',
|
||||
> = TypeByFieldMetadata<T>;
|
||||
@ -0,0 +1,67 @@
|
||||
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 URL field metadata to column actions', () => {
|
||||
const fieldMetadata = {
|
||||
type: FieldMetadataType.URL,
|
||||
targetColumnMap: { text: 'url_text', link: 'url_link' },
|
||||
defaultValue: { text: 'http://example.com', link: 'Example' },
|
||||
} as any;
|
||||
const columnActions = convertFieldMetadataToColumnActions(fieldMetadata);
|
||||
expect(columnActions).toEqual([
|
||||
{
|
||||
action: 'CREATE',
|
||||
columnName: 'url_text',
|
||||
columnType: 'varchar',
|
||||
defaultValue: "'http://example.com'",
|
||||
},
|
||||
{
|
||||
action: 'CREATE',
|
||||
columnName: 'url_link',
|
||||
columnType: 'varchar',
|
||||
defaultValue: "'Example'",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should convert MONEY field metadata to column actions', () => {
|
||||
const fieldMetadata = {
|
||||
type: FieldMetadataType.MONEY,
|
||||
targetColumnMap: { amount: 'money_amount', currency: 'money_currency' },
|
||||
defaultValue: { amount: 100, currency: 'USD' },
|
||||
} as any;
|
||||
const columnActions = convertFieldMetadataToColumnActions(fieldMetadata);
|
||||
expect(columnActions).toEqual([
|
||||
{
|
||||
action: 'CREATE',
|
||||
columnName: 'money_amount',
|
||||
columnType: 'integer',
|
||||
defaultValue: 100,
|
||||
},
|
||||
{
|
||||
action: 'CREATE',
|
||||
columnName: 'money_currency',
|
||||
columnType: 'varchar',
|
||||
defaultValue: "'USD'",
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@ -1,9 +1,7 @@
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
generateTargetColumnMap,
|
||||
convertFieldMetadataToColumnActions,
|
||||
} from './field-metadata.util';
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import { generateTargetColumnMap } from 'src/metadata/field-metadata/utils/generate-target-column-map.util';
|
||||
|
||||
describe('generateTargetColumnMap', () => {
|
||||
it('should generate a target column map for a given type', () => {
|
||||
@ -35,23 +33,6 @@ describe('generateTargetColumnMap', () => {
|
||||
it('should throw an error for an unknown type', () => {
|
||||
expect(() =>
|
||||
generateTargetColumnMap('invalid' as FieldMetadataType, false, 'name'),
|
||||
).toThrowError('Unknown type invalid');
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertFieldMetadataToColumnActions', () => {
|
||||
it('should convert field metadata to column actions', () => {
|
||||
const fieldMetadata = {
|
||||
type: FieldMetadataType.TEXT,
|
||||
targetColumnMap: { value: 'name' },
|
||||
} as any;
|
||||
const columnActions = convertFieldMetadataToColumnActions(fieldMetadata);
|
||||
expect(columnActions).toEqual([
|
||||
{
|
||||
action: 'CREATE',
|
||||
columnName: 'name',
|
||||
columnType: 'text',
|
||||
},
|
||||
]);
|
||||
).toThrow(BadRequestException);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,43 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
|
||||
import { serializeDefaultValue } from 'src/metadata/field-metadata/utils/serialize-default-value';
|
||||
|
||||
describe('serializeDefaultValue', () => {
|
||||
it('should return null for undefined defaultValue', () => {
|
||||
expect(serializeDefaultValue()).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle uuid dynamic default value', () => {
|
||||
expect(serializeDefaultValue({ type: 'uuid' })).toBe('uuid_generate_v4()');
|
||||
});
|
||||
|
||||
it('should handle now dynamic default value', () => {
|
||||
expect(serializeDefaultValue({ type: 'now' })).toBe('now()');
|
||||
});
|
||||
|
||||
it('should throw BadRequestException for invalid dynamic default value type', () => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error Just for testing purposes
|
||||
expect(() => serializeDefaultValue({ type: 'invalid' })).toThrow(
|
||||
BadRequestException,
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle string static default value', () => {
|
||||
expect(serializeDefaultValue('test')).toBe("'test'");
|
||||
});
|
||||
|
||||
it('should handle number static default value', () => {
|
||||
expect(serializeDefaultValue(123)).toBe(123);
|
||||
});
|
||||
|
||||
it('should handle boolean static default value', () => {
|
||||
expect(serializeDefaultValue(true)).toBe(true);
|
||||
expect(serializeDefaultValue(false)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle Date static default value', () => {
|
||||
const date = new Date('2023-01-01');
|
||||
expect(serializeDefaultValue(date)).toBe(`'${date.toISOString()}'`);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,178 @@
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import { validateDefaultValueBasedOnType } from 'src/metadata/field-metadata/utils/validate-default-value-based-on-type.util';
|
||||
|
||||
describe('validateDefaultValueBasedOnType', () => {
|
||||
it('should return true for null defaultValue', () => {
|
||||
expect(validateDefaultValueBasedOnType(null, FieldMetadataType.TEXT)).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
// Dynamic default values
|
||||
it('should validate uuid dynamic default value for UUID type', () => {
|
||||
expect(
|
||||
validateDefaultValueBasedOnType({ type: 'uuid' }, FieldMetadataType.UUID),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should validate now dynamic default value for DATE type', () => {
|
||||
expect(
|
||||
validateDefaultValueBasedOnType({ type: 'now' }, FieldMetadataType.DATE),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for mismatched dynamic default value', () => {
|
||||
expect(
|
||||
validateDefaultValueBasedOnType({ type: 'now' }, FieldMetadataType.UUID),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
// Static default values
|
||||
it('should validate string default value for TEXT type', () => {
|
||||
expect(
|
||||
validateDefaultValueBasedOnType(
|
||||
{ value: 'test' },
|
||||
FieldMetadataType.TEXT,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for invalid string default value for TEXT type', () => {
|
||||
expect(
|
||||
validateDefaultValueBasedOnType({ value: 123 }, FieldMetadataType.TEXT),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should validate string default value for PHONE type', () => {
|
||||
expect(
|
||||
validateDefaultValueBasedOnType(
|
||||
{ value: '+123456789' },
|
||||
FieldMetadataType.PHONE,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for invalid string default value for PHONE type', () => {
|
||||
expect(
|
||||
validateDefaultValueBasedOnType({ value: 123 }, FieldMetadataType.PHONE),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should validate string default value for EMAIL type', () => {
|
||||
expect(
|
||||
validateDefaultValueBasedOnType(
|
||||
{ value: 'test@example.com' },
|
||||
FieldMetadataType.EMAIL,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for invalid string default value for EMAIL type', () => {
|
||||
expect(
|
||||
validateDefaultValueBasedOnType({ value: 123 }, FieldMetadataType.EMAIL),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should validate number default value for NUMBER type', () => {
|
||||
expect(
|
||||
validateDefaultValueBasedOnType({ value: 100 }, FieldMetadataType.NUMBER),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for invalid number default value for NUMBER type', () => {
|
||||
expect(
|
||||
validateDefaultValueBasedOnType(
|
||||
{ value: '100' },
|
||||
FieldMetadataType.NUMBER,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should validate number default value for PROBABILITY type', () => {
|
||||
expect(
|
||||
validateDefaultValueBasedOnType(
|
||||
{ value: 0.5 },
|
||||
FieldMetadataType.PROBABILITY,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for invalid number default value for PROBABILITY type', () => {
|
||||
expect(
|
||||
validateDefaultValueBasedOnType(
|
||||
{ value: '50%' },
|
||||
FieldMetadataType.PROBABILITY,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should validate boolean default value for BOOLEAN type', () => {
|
||||
expect(
|
||||
validateDefaultValueBasedOnType(
|
||||
{ value: true },
|
||||
FieldMetadataType.BOOLEAN,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for invalid boolean default value for BOOLEAN type', () => {
|
||||
expect(
|
||||
validateDefaultValueBasedOnType(
|
||||
{ value: 'true' },
|
||||
FieldMetadataType.BOOLEAN,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
// URL type
|
||||
it('should validate URL default value', () => {
|
||||
expect(
|
||||
validateDefaultValueBasedOnType(
|
||||
{ text: 'http://example.com', link: 'Example' },
|
||||
FieldMetadataType.URL,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for invalid URL default value', () => {
|
||||
expect(
|
||||
validateDefaultValueBasedOnType(
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error Just for testing purposes
|
||||
{ text: 123, link: {} },
|
||||
FieldMetadataType.URL,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
// MONEY type
|
||||
it('should validate MONEY default value', () => {
|
||||
expect(
|
||||
validateDefaultValueBasedOnType(
|
||||
{ amount: 100, currency: 'USD' },
|
||||
FieldMetadataType.MONEY,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for invalid MONEY default value', () => {
|
||||
expect(
|
||||
validateDefaultValueBasedOnType(
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error Just for testing purposes
|
||||
{ amount: '100', currency: 'USD' },
|
||||
FieldMetadataType.MONEY,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
// Unknown type
|
||||
it('should return false for unknown type', () => {
|
||||
expect(
|
||||
validateDefaultValueBasedOnType(
|
||||
{ value: 'test' },
|
||||
'unknown' as FieldMetadataType,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@ -1,4 +1,4 @@
|
||||
import { FieldMetadataTargetColumnMap } from 'src/tenant/schema-builder/interfaces/field-metadata-target-column-map.interface';
|
||||
import { FieldMetadataDefaultValue } from 'src/metadata/field-metadata/interfaces/field-metadata-default-value.interface';
|
||||
|
||||
import {
|
||||
FieldMetadataEntity,
|
||||
@ -8,119 +8,121 @@ import {
|
||||
TenantMigrationColumnAction,
|
||||
TenantMigrationColumnActionType,
|
||||
} from 'src/metadata/tenant-migration/tenant-migration.entity';
|
||||
|
||||
/**
|
||||
* Generate a target column map for a given type, this is used to map the field to the correct column(s) in the database.
|
||||
* This is used to support fields that map to multiple columns in the database.
|
||||
*
|
||||
* @param type string
|
||||
* @returns FieldMetadataTargetColumnMap
|
||||
*/
|
||||
export function generateTargetColumnMap(
|
||||
type: FieldMetadataType,
|
||||
isCustomField: boolean,
|
||||
fieldName: string,
|
||||
): FieldMetadataTargetColumnMap {
|
||||
const columnName = isCustomField ? `_${fieldName}` : fieldName;
|
||||
|
||||
switch (type) {
|
||||
case FieldMetadataType.TEXT:
|
||||
case FieldMetadataType.PHONE:
|
||||
case FieldMetadataType.EMAIL:
|
||||
case FieldMetadataType.NUMBER:
|
||||
case FieldMetadataType.PROBABILITY:
|
||||
case FieldMetadataType.BOOLEAN:
|
||||
case FieldMetadataType.DATE:
|
||||
return {
|
||||
value: columnName,
|
||||
};
|
||||
case FieldMetadataType.URL:
|
||||
return {
|
||||
text: `${columnName}_text`,
|
||||
link: `${columnName}_link`,
|
||||
};
|
||||
case FieldMetadataType.MONEY:
|
||||
return {
|
||||
amount: `${columnName}_amount`,
|
||||
currency: `${columnName}_currency`,
|
||||
};
|
||||
default:
|
||||
throw new Error(`Unknown type ${type}`);
|
||||
}
|
||||
}
|
||||
import { serializeDefaultValue } from 'src/metadata/field-metadata/utils/serialize-default-value';
|
||||
|
||||
export function convertFieldMetadataToColumnActions(
|
||||
fieldMetadata: FieldMetadataEntity,
|
||||
): TenantMigrationColumnAction[] {
|
||||
switch (fieldMetadata.type) {
|
||||
case FieldMetadataType.TEXT:
|
||||
case FieldMetadataType.TEXT: {
|
||||
const defaultValue =
|
||||
fieldMetadata.defaultValue as FieldMetadataDefaultValue<FieldMetadataType.TEXT>;
|
||||
|
||||
return [
|
||||
{
|
||||
action: TenantMigrationColumnActionType.CREATE,
|
||||
columnName: fieldMetadata.targetColumnMap.value,
|
||||
columnType: 'text',
|
||||
defaultValue: serializeDefaultValue(defaultValue?.value),
|
||||
},
|
||||
];
|
||||
}
|
||||
case FieldMetadataType.PHONE:
|
||||
case FieldMetadataType.EMAIL:
|
||||
case FieldMetadataType.EMAIL: {
|
||||
const defaultValue =
|
||||
fieldMetadata.defaultValue as FieldMetadataDefaultValue<
|
||||
FieldMetadataType.PHONE | FieldMetadataType.EMAIL
|
||||
>;
|
||||
|
||||
return [
|
||||
{
|
||||
action: TenantMigrationColumnActionType.CREATE,
|
||||
columnName: fieldMetadata.targetColumnMap.value,
|
||||
columnType: 'varchar',
|
||||
defaultValue: serializeDefaultValue(defaultValue?.value),
|
||||
},
|
||||
];
|
||||
}
|
||||
case FieldMetadataType.NUMBER:
|
||||
case FieldMetadataType.PROBABILITY:
|
||||
case FieldMetadataType.PROBABILITY: {
|
||||
const defaultValue =
|
||||
fieldMetadata.defaultValue as FieldMetadataDefaultValue<
|
||||
FieldMetadataType.NUMBER | FieldMetadataType.PROBABILITY
|
||||
>;
|
||||
|
||||
return [
|
||||
{
|
||||
action: TenantMigrationColumnActionType.CREATE,
|
||||
columnName: fieldMetadata.targetColumnMap.value,
|
||||
columnType: 'float',
|
||||
defaultValue: serializeDefaultValue(defaultValue?.value),
|
||||
},
|
||||
];
|
||||
case FieldMetadataType.BOOLEAN:
|
||||
}
|
||||
case FieldMetadataType.BOOLEAN: {
|
||||
const defaultValue =
|
||||
fieldMetadata.defaultValue as FieldMetadataDefaultValue<FieldMetadataType.BOOLEAN>;
|
||||
|
||||
return [
|
||||
{
|
||||
action: TenantMigrationColumnActionType.CREATE,
|
||||
columnName: fieldMetadata.targetColumnMap.value,
|
||||
columnType: 'boolean',
|
||||
defaultValue: serializeDefaultValue(defaultValue?.value),
|
||||
},
|
||||
];
|
||||
case FieldMetadataType.DATE:
|
||||
}
|
||||
case FieldMetadataType.DATE: {
|
||||
const defaultValue =
|
||||
fieldMetadata.defaultValue as FieldMetadataDefaultValue<FieldMetadataType.DATE>;
|
||||
|
||||
return [
|
||||
{
|
||||
action: TenantMigrationColumnActionType.CREATE,
|
||||
columnName: fieldMetadata.targetColumnMap.value,
|
||||
columnType: 'timestamp',
|
||||
defaultValue: serializeDefaultValue(defaultValue?.value),
|
||||
},
|
||||
];
|
||||
case FieldMetadataType.URL:
|
||||
}
|
||||
case FieldMetadataType.URL: {
|
||||
const defaultValue =
|
||||
fieldMetadata.defaultValue as FieldMetadataDefaultValue<FieldMetadataType.URL>;
|
||||
|
||||
return [
|
||||
{
|
||||
action: TenantMigrationColumnActionType.CREATE,
|
||||
columnName: fieldMetadata.targetColumnMap.text,
|
||||
columnType: 'varchar',
|
||||
defaultValue: serializeDefaultValue(defaultValue?.text),
|
||||
},
|
||||
{
|
||||
action: TenantMigrationColumnActionType.CREATE,
|
||||
columnName: fieldMetadata.targetColumnMap.link,
|
||||
columnType: 'varchar',
|
||||
defaultValue: serializeDefaultValue(defaultValue?.link),
|
||||
},
|
||||
];
|
||||
case FieldMetadataType.MONEY:
|
||||
}
|
||||
case FieldMetadataType.MONEY: {
|
||||
const defaultValue =
|
||||
fieldMetadata.defaultValue as FieldMetadataDefaultValue<FieldMetadataType.MONEY>;
|
||||
|
||||
return [
|
||||
{
|
||||
action: TenantMigrationColumnActionType.CREATE,
|
||||
columnName: fieldMetadata.targetColumnMap.amount,
|
||||
columnType: 'integer',
|
||||
defaultValue: serializeDefaultValue(defaultValue?.amount),
|
||||
},
|
||||
{
|
||||
action: TenantMigrationColumnActionType.CREATE,
|
||||
columnName: fieldMetadata.targetColumnMap.currency,
|
||||
columnType: 'varchar',
|
||||
defaultValue: serializeDefaultValue(defaultValue?.currency),
|
||||
},
|
||||
];
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown type ${fieldMetadata.type}`);
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
|
||||
import { FieldMetadataTargetColumnMap } from 'src/metadata/field-metadata/interfaces/field-metadata-target-column-map.interface';
|
||||
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
|
||||
/**
|
||||
* Generate a target column map for a given type, this is used to map the field to the correct column(s) in the database.
|
||||
* This is used to support fields that map to multiple columns in the database.
|
||||
*
|
||||
* @param type string
|
||||
* @returns FieldMetadataTargetColumnMap
|
||||
*/
|
||||
export function generateTargetColumnMap(
|
||||
type: FieldMetadataType,
|
||||
isCustomField: boolean,
|
||||
fieldName: string,
|
||||
): FieldMetadataTargetColumnMap {
|
||||
const columnName = isCustomField ? `_${fieldName}` : fieldName;
|
||||
|
||||
switch (type) {
|
||||
case FieldMetadataType.TEXT:
|
||||
case FieldMetadataType.PHONE:
|
||||
case FieldMetadataType.EMAIL:
|
||||
case FieldMetadataType.NUMBER:
|
||||
case FieldMetadataType.PROBABILITY:
|
||||
case FieldMetadataType.BOOLEAN:
|
||||
case FieldMetadataType.DATE:
|
||||
return {
|
||||
value: columnName,
|
||||
};
|
||||
case FieldMetadataType.URL:
|
||||
return {
|
||||
text: `${columnName}_text`,
|
||||
link: `${columnName}_link`,
|
||||
};
|
||||
case FieldMetadataType.MONEY:
|
||||
return {
|
||||
amount: `${columnName}_amount`,
|
||||
currency: `${columnName}_currency`,
|
||||
};
|
||||
default:
|
||||
throw new BadRequestException(`Unknown type ${type}`);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
|
||||
import { FieldMetadataDefaultSerializableValue } from 'src/metadata/field-metadata/interfaces/field-metadata-default-value.interface';
|
||||
|
||||
export const serializeDefaultValue = (
|
||||
defaultValue?: FieldMetadataDefaultSerializableValue,
|
||||
) => {
|
||||
if (defaultValue === undefined || defaultValue === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Dynamic default values
|
||||
if (typeof defaultValue === 'object' && 'type' in defaultValue) {
|
||||
switch (defaultValue.type) {
|
||||
case 'uuid':
|
||||
return 'uuid_generate_v4()';
|
||||
case 'now':
|
||||
return 'now()';
|
||||
default:
|
||||
throw new BadRequestException('Invalid dynamic default value type');
|
||||
}
|
||||
}
|
||||
|
||||
// Static default values
|
||||
if (typeof defaultValue === 'string') {
|
||||
return `'${defaultValue}'`;
|
||||
}
|
||||
|
||||
if (typeof defaultValue === 'number') {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
if (typeof defaultValue === 'boolean') {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
if (defaultValue instanceof Date) {
|
||||
return `'${defaultValue.toISOString()}'`;
|
||||
}
|
||||
|
||||
if (typeof defaultValue === 'object') {
|
||||
return `'${JSON.stringify(defaultValue)}'`;
|
||||
}
|
||||
|
||||
throw new BadRequestException('Invalid default value');
|
||||
};
|
||||
@ -0,0 +1,78 @@
|
||||
import { FieldMetadataDefaultValue } from 'src/metadata/field-metadata/interfaces/field-metadata-default-value.interface';
|
||||
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
|
||||
export const validateDefaultValueBasedOnType = (
|
||||
defaultValue: FieldMetadataDefaultValue,
|
||||
type: FieldMetadataType,
|
||||
): boolean => {
|
||||
if (defaultValue === null) return true;
|
||||
|
||||
// Dynamic default values
|
||||
if (typeof defaultValue === 'object' && 'type' in defaultValue) {
|
||||
if (type === FieldMetadataType.UUID && defaultValue.type === 'uuid') {
|
||||
return true;
|
||||
}
|
||||
if (type === FieldMetadataType.DATE && defaultValue.type === 'now') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Static default values
|
||||
switch (type) {
|
||||
case FieldMetadataType.TEXT:
|
||||
case FieldMetadataType.PHONE:
|
||||
case FieldMetadataType.EMAIL:
|
||||
case FieldMetadataType.ENUM:
|
||||
return (
|
||||
typeof defaultValue === 'object' &&
|
||||
'value' in defaultValue &&
|
||||
typeof defaultValue.value === 'string'
|
||||
);
|
||||
|
||||
case FieldMetadataType.NUMBER:
|
||||
case FieldMetadataType.PROBABILITY:
|
||||
return (
|
||||
typeof defaultValue === 'object' &&
|
||||
'value' in defaultValue &&
|
||||
typeof defaultValue.value === 'number'
|
||||
);
|
||||
|
||||
case FieldMetadataType.BOOLEAN:
|
||||
return (
|
||||
typeof defaultValue === 'object' &&
|
||||
'value' in defaultValue &&
|
||||
typeof defaultValue.value === 'boolean'
|
||||
);
|
||||
|
||||
case FieldMetadataType.DATE:
|
||||
return (
|
||||
typeof defaultValue === 'object' &&
|
||||
'value' in defaultValue &&
|
||||
defaultValue.value instanceof Date
|
||||
);
|
||||
|
||||
case FieldMetadataType.URL:
|
||||
return (
|
||||
typeof defaultValue === 'object' &&
|
||||
'text' in defaultValue &&
|
||||
typeof defaultValue.text === 'string' &&
|
||||
'link' in defaultValue &&
|
||||
typeof defaultValue.link === 'string'
|
||||
);
|
||||
|
||||
case FieldMetadataType.MONEY:
|
||||
return (
|
||||
typeof defaultValue === 'object' &&
|
||||
'amount' in defaultValue &&
|
||||
typeof defaultValue.amount === 'number' &&
|
||||
'currency' in defaultValue &&
|
||||
typeof defaultValue.currency === 'string'
|
||||
);
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,27 @@
|
||||
import {
|
||||
registerDecorator,
|
||||
ValidationOptions,
|
||||
ValidationArguments,
|
||||
} from 'class-validator';
|
||||
|
||||
import { validateDefaultValueBasedOnType } from 'src/metadata/field-metadata/utils/validate-default-value-based-on-type.util';
|
||||
|
||||
export const IsDefaultValue = (validationOptions?: ValidationOptions) => {
|
||||
return function (object: any, propertyName: string) {
|
||||
registerDecorator({
|
||||
name: 'isDefaultValue',
|
||||
target: object.constructor,
|
||||
propertyName: propertyName,
|
||||
constraints: [],
|
||||
options: validationOptions,
|
||||
validator: {
|
||||
validate(value: any, args: ValidationArguments) {
|
||||
// Extract type value from the object
|
||||
const type = (args.object as any)['type'];
|
||||
|
||||
return validateDefaultValueBasedOnType(value, type);
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
};
|
||||
@ -14,6 +14,7 @@ export type TenantMigrationColumnCreate = {
|
||||
action: TenantMigrationColumnActionType.CREATE;
|
||||
columnName: string;
|
||||
columnType: string;
|
||||
defaultValue?: any;
|
||||
};
|
||||
|
||||
export type TenantMigrationColumnRelation = {
|
||||
|
||||
Reference in New Issue
Block a user