feat: add default value capability (#2544)

* feat: add default value capability

* feat: update seeds with default value
This commit is contained in:
Jérémy M
2023-11-16 18:25:11 +01:00
committed by GitHub
parent e8a1d0d6d5
commit e9827486c0
42 changed files with 1016 additions and 213 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,6 +14,7 @@ export type TenantMigrationColumnCreate = {
action: TenantMigrationColumnActionType.CREATE;
columnName: string;
columnType: string;
defaultValue?: any;
};
export type TenantMigrationColumnRelation = {