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:
Jérémy M
2024-01-11 16:41:25 +01:00
committed by GitHub
parent c8aec95325
commit 5f0c9f67c9
24 changed files with 1010 additions and 52 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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()

View File

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

View File

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

View File

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

View File

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