feat: workspace health (#3344)
* feat: wip workspace health * feat: split structure and metadata check * feat: check default value structure health * feat: check targetColumnMap structure health * fix: composite types doesn't have default value properly defined * feat: check default value structure health * feat: check options structure health * fix: verbose option not working properly * fix: word issue * fix: tests * fix: remove console.log * fix: TRUE and FALSE instead of YES and NO * fix: fieldMetadataType instead of type
This commit is contained in:
@ -7,11 +7,14 @@ import { generateTargetColumnMap } from 'src/metadata/field-metadata/utils/gener
|
||||
export const currencyFields = (
|
||||
fieldMetadata?: FieldMetadataInterface,
|
||||
): FieldMetadataInterface[] => {
|
||||
const targetColumnMap = fieldMetadata
|
||||
const inferredFieldMetadata = fieldMetadata as
|
||||
| FieldMetadataInterface<FieldMetadataType.CURRENCY>
|
||||
| undefined;
|
||||
const targetColumnMap = inferredFieldMetadata
|
||||
? generateTargetColumnMap(
|
||||
fieldMetadata.type,
|
||||
fieldMetadata.isCustom ?? false,
|
||||
fieldMetadata.name,
|
||||
inferredFieldMetadata.type,
|
||||
inferredFieldMetadata.isCustom ?? false,
|
||||
inferredFieldMetadata.name,
|
||||
)
|
||||
: {
|
||||
amountMicros: 'amountMicros',
|
||||
@ -29,7 +32,14 @@ export const currencyFields = (
|
||||
value: targetColumnMap.amountMicros,
|
||||
},
|
||||
isNullable: true,
|
||||
} satisfies FieldMetadataInterface,
|
||||
...(inferredFieldMetadata
|
||||
? {
|
||||
defaultValue: {
|
||||
value: inferredFieldMetadata.defaultValue?.amountMicros ?? null,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
} satisfies FieldMetadataInterface<FieldMetadataType.NUMERIC>,
|
||||
{
|
||||
id: 'currencyCode',
|
||||
type: FieldMetadataType.TEXT,
|
||||
@ -40,7 +50,14 @@ export const currencyFields = (
|
||||
value: targetColumnMap.currencyCode,
|
||||
},
|
||||
isNullable: true,
|
||||
} satisfies FieldMetadataInterface,
|
||||
...(inferredFieldMetadata
|
||||
? {
|
||||
defaultValue: {
|
||||
value: inferredFieldMetadata.defaultValue?.currencyCode ?? null,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
} satisfies FieldMetadataInterface<FieldMetadataType.TEXT>,
|
||||
];
|
||||
};
|
||||
|
||||
|
||||
@ -7,11 +7,14 @@ import { generateTargetColumnMap } from 'src/metadata/field-metadata/utils/gener
|
||||
export const fullNameFields = (
|
||||
fieldMetadata?: FieldMetadataInterface,
|
||||
): FieldMetadataInterface[] => {
|
||||
const targetColumnMap = fieldMetadata
|
||||
const inferredFieldMetadata = fieldMetadata as
|
||||
| FieldMetadataInterface<FieldMetadataType.FULL_NAME>
|
||||
| undefined;
|
||||
const targetColumnMap = inferredFieldMetadata
|
||||
? generateTargetColumnMap(
|
||||
fieldMetadata.type,
|
||||
fieldMetadata.isCustom ?? false,
|
||||
fieldMetadata.name,
|
||||
inferredFieldMetadata.type,
|
||||
inferredFieldMetadata.isCustom ?? false,
|
||||
inferredFieldMetadata.name,
|
||||
)
|
||||
: {
|
||||
firstName: 'firstName',
|
||||
@ -29,7 +32,14 @@ export const fullNameFields = (
|
||||
value: targetColumnMap.firstName,
|
||||
},
|
||||
isNullable: true,
|
||||
} satisfies FieldMetadataInterface,
|
||||
...(inferredFieldMetadata
|
||||
? {
|
||||
defaultValue: {
|
||||
value: inferredFieldMetadata.defaultValue?.firstName ?? null,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
} satisfies FieldMetadataInterface<FieldMetadataType.TEXT>,
|
||||
{
|
||||
id: 'lastName',
|
||||
type: FieldMetadataType.TEXT,
|
||||
@ -40,7 +50,14 @@ export const fullNameFields = (
|
||||
value: targetColumnMap.lastName,
|
||||
},
|
||||
isNullable: true,
|
||||
} satisfies FieldMetadataInterface,
|
||||
...(inferredFieldMetadata
|
||||
? {
|
||||
defaultValue: {
|
||||
value: inferredFieldMetadata.defaultValue?.lastName ?? null,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
} satisfies FieldMetadataInterface<FieldMetadataType.TEXT>,
|
||||
];
|
||||
};
|
||||
|
||||
|
||||
@ -0,0 +1,19 @@
|
||||
import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/field-metadata.interface';
|
||||
|
||||
import { currencyFields } from 'src/metadata/field-metadata/composite-types/currency.composite-type';
|
||||
import { fullNameFields } from 'src/metadata/field-metadata/composite-types/full-name.composite-type';
|
||||
import { linkFields } from 'src/metadata/field-metadata/composite-types/link.composite-type';
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
|
||||
export type CompositeFieldsDefinitionFunction = (
|
||||
fieldMetadata?: FieldMetadataInterface,
|
||||
) => FieldMetadataInterface[];
|
||||
|
||||
export const compositeDefinitions = new Map<
|
||||
string,
|
||||
CompositeFieldsDefinitionFunction
|
||||
>([
|
||||
[FieldMetadataType.LINK, linkFields],
|
||||
[FieldMetadataType.CURRENCY, currencyFields],
|
||||
[FieldMetadataType.FULL_NAME, fullNameFields],
|
||||
]);
|
||||
@ -7,11 +7,14 @@ import { generateTargetColumnMap } from 'src/metadata/field-metadata/utils/gener
|
||||
export const linkFields = (
|
||||
fieldMetadata?: FieldMetadataInterface,
|
||||
): FieldMetadataInterface[] => {
|
||||
const targetColumnMap = fieldMetadata
|
||||
const inferredFieldMetadata = fieldMetadata as
|
||||
| FieldMetadataInterface<FieldMetadataType.LINK>
|
||||
| undefined;
|
||||
const targetColumnMap = inferredFieldMetadata
|
||||
? generateTargetColumnMap(
|
||||
fieldMetadata.type,
|
||||
fieldMetadata.isCustom ?? false,
|
||||
fieldMetadata.name,
|
||||
inferredFieldMetadata.type,
|
||||
inferredFieldMetadata.isCustom ?? false,
|
||||
inferredFieldMetadata.name,
|
||||
)
|
||||
: {
|
||||
label: 'label',
|
||||
@ -29,7 +32,14 @@ export const linkFields = (
|
||||
value: targetColumnMap.label,
|
||||
},
|
||||
isNullable: true,
|
||||
} satisfies FieldMetadataInterface,
|
||||
...(inferredFieldMetadata
|
||||
? {
|
||||
defaultValue: {
|
||||
value: inferredFieldMetadata.defaultValue?.label ?? null,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
} satisfies FieldMetadataInterface<FieldMetadataType.TEXT>,
|
||||
{
|
||||
id: 'url',
|
||||
type: FieldMetadataType.TEXT,
|
||||
@ -40,7 +50,14 @@ export const linkFields = (
|
||||
value: targetColumnMap.url,
|
||||
},
|
||||
isNullable: true,
|
||||
} satisfies FieldMetadataInterface,
|
||||
...(inferredFieldMetadata
|
||||
? {
|
||||
defaultValue: {
|
||||
value: inferredFieldMetadata.defaultValue?.url ?? null,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
} satisfies FieldMetadataInterface<FieldMetadataType.TEXT>,
|
||||
];
|
||||
};
|
||||
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import { Transform } from 'class-transformer';
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsDate,
|
||||
IsNotEmpty,
|
||||
IsNumber,
|
||||
IsNumberString,
|
||||
IsString,
|
||||
Matches,
|
||||
ValidateIf,
|
||||
@ -52,8 +54,8 @@ export class FieldMetadataDefaultValueLink {
|
||||
|
||||
export class FieldMetadataDefaultValueCurrency {
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsNumber()
|
||||
amountMicros: number | null;
|
||||
@IsNumberString()
|
||||
amountMicros: string | null;
|
||||
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsString()
|
||||
|
||||
@ -133,7 +133,7 @@ describe('validateDefaultValueForType', () => {
|
||||
it('should validate CURRENCY default value', () => {
|
||||
expect(
|
||||
validateDefaultValueForType(FieldMetadataType.CURRENCY, {
|
||||
amountMicros: 100,
|
||||
amountMicros: '100',
|
||||
currencyCode: 'USD',
|
||||
}),
|
||||
).toBe(true);
|
||||
@ -144,7 +144,7 @@ describe('validateDefaultValueForType', () => {
|
||||
validateDefaultValueForType(
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error Just for testing purposes
|
||||
{ amountMicros: '100', currencyCode: 'USD' },
|
||||
{ amountMicros: 100, currencyCode: 'USD' },
|
||||
FieldMetadataType.CURRENCY,
|
||||
),
|
||||
).toBe(false);
|
||||
|
||||
@ -2,6 +2,8 @@ import { BadRequestException } from '@nestjs/common';
|
||||
|
||||
import { FieldMetadataDefaultSerializableValue } from 'src/metadata/field-metadata/interfaces/field-metadata-default-value.interface';
|
||||
|
||||
import { serializeTypeDefaultValue } from 'src/metadata/field-metadata/utils/serialize-type-default-value.util';
|
||||
|
||||
export const serializeDefaultValue = (
|
||||
defaultValue?: FieldMetadataDefaultSerializableValue,
|
||||
) => {
|
||||
@ -15,14 +17,13 @@ export const serializeDefaultValue = (
|
||||
typeof defaultValue === 'object' &&
|
||||
'type' in defaultValue
|
||||
) {
|
||||
switch (defaultValue.type) {
|
||||
case 'uuid':
|
||||
return 'public.uuid_generate_v4()';
|
||||
case 'now':
|
||||
return 'now()';
|
||||
default:
|
||||
throw new BadRequestException('Invalid dynamic default value type');
|
||||
const serializedTypeDefaultValue = serializeTypeDefaultValue(defaultValue);
|
||||
|
||||
if (!serializedTypeDefaultValue) {
|
||||
throw new BadRequestException('Invalid default value');
|
||||
}
|
||||
|
||||
return serializedTypeDefaultValue;
|
||||
}
|
||||
|
||||
// Static default values
|
||||
|
||||
@ -0,0 +1,18 @@
|
||||
import { FieldMetadataDynamicDefaultValue } from 'src/metadata/field-metadata/interfaces/field-metadata-default-value.interface';
|
||||
|
||||
export const serializeTypeDefaultValue = (
|
||||
defaultValue?: FieldMetadataDynamicDefaultValue,
|
||||
) => {
|
||||
if (!defaultValue?.type) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (defaultValue.type) {
|
||||
case 'uuid':
|
||||
return 'public.uuid_generate_v4()';
|
||||
case 'now':
|
||||
return 'now()';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@ -12,13 +12,7 @@ import {
|
||||
WorkspaceMigrationColumnActionType,
|
||||
} from 'src/metadata/workspace-migration/workspace-migration.entity';
|
||||
import { isCompositeFieldMetadataType } from 'src/metadata/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { fullNameFields } from 'src/metadata/field-metadata/composite-types/full-name.composite-type';
|
||||
import { currencyFields } from 'src/metadata/field-metadata/composite-types/currency.composite-type';
|
||||
import { linkFields } from 'src/metadata/field-metadata/composite-types/link.composite-type';
|
||||
|
||||
type CompositeFieldsDefinitionFunction = (
|
||||
fieldMetadata: FieldMetadataInterface,
|
||||
) => FieldMetadataInterface[];
|
||||
import { compositeDefinitions } from 'src/metadata/field-metadata/composite-types';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceMigrationFactory {
|
||||
@ -30,10 +24,6 @@ export class WorkspaceMigrationFactory {
|
||||
options?: WorkspaceColumnActionOptions;
|
||||
}
|
||||
>;
|
||||
private compositeDefinitions = new Map<
|
||||
string,
|
||||
CompositeFieldsDefinitionFunction
|
||||
>();
|
||||
|
||||
constructor(
|
||||
private readonly basicColumnActionFactory: BasicColumnActionFactory,
|
||||
@ -89,15 +79,6 @@ export class WorkspaceMigrationFactory {
|
||||
{ factory: this.enumColumnActionFactory },
|
||||
],
|
||||
]);
|
||||
|
||||
this.compositeDefinitions = new Map<
|
||||
string,
|
||||
CompositeFieldsDefinitionFunction
|
||||
>([
|
||||
[FieldMetadataType.LINK, linkFields],
|
||||
[FieldMetadataType.CURRENCY, currencyFields],
|
||||
[FieldMetadataType.FULL_NAME, fullNameFields],
|
||||
]);
|
||||
}
|
||||
|
||||
createColumnActions(
|
||||
@ -138,7 +119,7 @@ export class WorkspaceMigrationFactory {
|
||||
|
||||
// If it's a composite field type, we need to create a column action for each of the fields
|
||||
if (isCompositeFieldMetadataType(alteredFieldMetadata.type)) {
|
||||
const fieldMetadataSplitterFunction = this.compositeDefinitions.get(
|
||||
const fieldMetadataSplitterFunction = compositeDefinitions.get(
|
||||
alteredFieldMetadata.type,
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user