feat: wip server folder structure (#4573)

* feat: wip server folder structure

* fix: merge

* fix: wrong merge

* fix: remove unused file

* fix: comment

* fix: lint

* fix: merge

* fix: remove console.log

* fix: metadata graphql arguments broken
This commit is contained in:
Jérémy M
2024-03-20 16:23:46 +01:00
committed by GitHub
parent da12710fe9
commit e5c1309e8c
461 changed files with 1396 additions and 1322 deletions

View File

@ -0,0 +1,27 @@
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { generateNullable } from 'src/engine/metadata-modules/field-metadata/utils/generate-nullable';
describe('generateNullable', () => {
it('should generate a nullable value false for TEXT, EMAIL, PHONE no matter what the input is', () => {
expect(generateNullable(FieldMetadataType.TEXT, false)).toEqual(false);
expect(generateNullable(FieldMetadataType.PHONE, false)).toEqual(false);
expect(generateNullable(FieldMetadataType.EMAIL, false)).toEqual(false);
expect(generateNullable(FieldMetadataType.TEXT, true)).toEqual(false);
expect(generateNullable(FieldMetadataType.PHONE, true)).toEqual(false);
expect(generateNullable(FieldMetadataType.EMAIL, true)).toEqual(false);
expect(generateNullable(FieldMetadataType.TEXT)).toEqual(false);
expect(generateNullable(FieldMetadataType.PHONE)).toEqual(false);
expect(generateNullable(FieldMetadataType.EMAIL)).toEqual(false);
});
it('should should return true if no input is given', () => {
expect(generateNullable(FieldMetadataType.DATE_TIME)).toEqual(true);
});
it('should should return the input value if the input value is given', () => {
expect(generateNullable(FieldMetadataType.DATE_TIME, true)).toEqual(true);
expect(generateNullable(FieldMetadataType.DATE_TIME, false)).toEqual(false);
});
});

View File

@ -0,0 +1,41 @@
import { BadRequestException } from '@nestjs/common';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { generateTargetColumnMap } from 'src/engine/metadata-modules/field-metadata/utils/generate-target-column-map.util';
describe('generateTargetColumnMap', () => {
it('should generate a target column map for a given type', () => {
const textMap = generateTargetColumnMap(
FieldMetadataType.TEXT,
false,
'name',
);
expect(textMap).toEqual({ value: 'name' });
const linkMap = generateTargetColumnMap(
FieldMetadataType.LINK,
false,
'website',
);
expect(linkMap).toEqual({ label: 'websiteLabel', url: 'websiteUrl' });
const currencyMap = generateTargetColumnMap(
FieldMetadataType.CURRENCY,
true,
'price',
);
expect(currencyMap).toEqual({
amountMicros: '_priceAmountMicros',
currencyCode: '_priceCurrencyCode',
});
});
it('should throw an error for an unknown type', () => {
expect(() =>
generateTargetColumnMap('invalid' as FieldMetadataType, false, 'name'),
).toThrow(BadRequestException);
});
});

View File

@ -0,0 +1,46 @@
import { BadRequestException } from '@nestjs/common';
import { serializeDefaultValue } from 'src/engine/metadata-modules/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(
'public.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,161 @@
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { validateDefaultValueForType } from 'src/engine/metadata-modules/field-metadata/utils/validate-default-value-for-type.util';
describe('validateDefaultValueForType', () => {
it('should return true for null defaultValue', () => {
expect(validateDefaultValueForType(FieldMetadataType.TEXT, null)).toBe(
true,
);
});
// Dynamic default values
it('should validate uuid dynamic default value for UUID type', () => {
expect(
validateDefaultValueForType(FieldMetadataType.UUID, { type: 'uuid' }),
).toBe(true);
});
it('should validate now dynamic default value for DATE_TIME type', () => {
expect(
validateDefaultValueForType(FieldMetadataType.DATE_TIME, { type: 'now' }),
).toBe(true);
});
it('should return false for mismatched dynamic default value', () => {
expect(
validateDefaultValueForType(FieldMetadataType.UUID, { type: 'now' }),
).toBe(false);
});
// Static default values
it('should validate string default value for TEXT type', () => {
expect(
validateDefaultValueForType(FieldMetadataType.TEXT, { value: 'test' }),
).toBe(true);
});
it('should return false for invalid string default value for TEXT type', () => {
expect(
validateDefaultValueForType(FieldMetadataType.TEXT, { value: 123 }),
).toBe(false);
});
it('should validate string default value for PHONE type', () => {
expect(
validateDefaultValueForType(FieldMetadataType.PHONE, {
value: '+123456789',
}),
).toBe(true);
});
it('should return false for invalid string default value for PHONE type', () => {
expect(
validateDefaultValueForType(FieldMetadataType.PHONE, { value: 123 }),
).toBe(false);
});
it('should validate string default value for EMAIL type', () => {
expect(
validateDefaultValueForType(FieldMetadataType.EMAIL, {
value: 'test@example.com',
}),
).toBe(true);
});
it('should return false for invalid string default value for EMAIL type', () => {
expect(
validateDefaultValueForType(FieldMetadataType.EMAIL, { value: 123 }),
).toBe(false);
});
it('should validate number default value for NUMBER type', () => {
expect(
validateDefaultValueForType(FieldMetadataType.NUMBER, { value: 100 }),
).toBe(true);
});
it('should return false for invalid number default value for NUMBER type', () => {
expect(
validateDefaultValueForType(FieldMetadataType.NUMBER, { value: '100' }),
).toBe(false);
});
it('should validate number default value for PROBABILITY type', () => {
expect(
validateDefaultValueForType(FieldMetadataType.PROBABILITY, {
value: 0.5,
}),
).toBe(true);
});
it('should return false for invalid number default value for PROBABILITY type', () => {
expect(
validateDefaultValueForType(FieldMetadataType.PROBABILITY, {
value: '50%',
}),
).toBe(false);
});
it('should validate boolean default value for BOOLEAN type', () => {
expect(
validateDefaultValueForType(FieldMetadataType.BOOLEAN, { value: true }),
).toBe(true);
});
it('should return false for invalid boolean default value for BOOLEAN type', () => {
expect(
validateDefaultValueForType(FieldMetadataType.BOOLEAN, { value: 'true' }),
).toBe(false);
});
// LINK type
it('should validate LINK default value', () => {
expect(
validateDefaultValueForType(FieldMetadataType.LINK, {
label: 'http://example.com',
url: 'Example',
}),
).toBe(true);
});
it('should return false for invalid LINK default value', () => {
expect(
validateDefaultValueForType(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error Just for testing purposes
{ label: 123, url: {} },
FieldMetadataType.LINK,
),
).toBe(false);
});
// CURRENCY type
it('should validate CURRENCY default value', () => {
expect(
validateDefaultValueForType(FieldMetadataType.CURRENCY, {
amountMicros: '100',
currencyCode: 'USD',
}),
).toBe(true);
});
it('should return false for invalid CURRENCY default value', () => {
expect(
validateDefaultValueForType(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error Just for testing purposes
{ amountMicros: 100, currencyCode: 'USD' },
FieldMetadataType.CURRENCY,
),
).toBe(false);
});
// Unknown type
it('should return false for unknown type', () => {
expect(
validateDefaultValueForType('unknown' as FieldMetadataType, {
value: 'test',
}),
).toBe(false);
});
});

View File

@ -0,0 +1,33 @@
import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
export function generateDefaultValue(
type: FieldMetadataType,
): FieldMetadataDefaultValue {
switch (type) {
case FieldMetadataType.TEXT:
case FieldMetadataType.PHONE:
case FieldMetadataType.EMAIL:
return {
value: '',
};
case FieldMetadataType.FULL_NAME:
return {
firstName: '',
lastName: '',
};
case FieldMetadataType.LINK:
return {
url: '',
label: '',
};
case FieldMetadataType.CURRENCY:
return {
amountMicros: null,
currencyCode: '',
};
default:
return null;
}
}

View File

@ -0,0 +1,15 @@
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
export function generateNullable(
type: FieldMetadataType,
inputNullableValue?: boolean,
): boolean {
switch (type) {
case FieldMetadataType.TEXT:
case FieldMetadataType.PHONE:
case FieldMetadataType.EMAIL:
return false;
default:
return inputNullableValue ?? true;
}
}

View File

@ -0,0 +1,23 @@
import { v4 as uuidV4 } from 'uuid';
import { FieldMetadataDefaultOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
const range = {
start: 1,
end: 5,
};
export function generateRatingOptions(): FieldMetadataDefaultOption[] {
const options: FieldMetadataDefaultOption[] = [];
for (let i = range.start; i <= range.end; i++) {
options.push({
id: uuidV4(),
label: i.toString(),
value: `RATING_${i}`,
position: i - 1,
});
}
return options;
}

View File

@ -0,0 +1,62 @@
import { BadRequestException } from '@nestjs/common';
import { FieldMetadataTargetColumnMap } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-target-column-map.interface';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { createCustomColumnName } from 'src/engine/utils/create-custom-column-name.util';
/**
* 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
? createCustomColumnName(fieldName)
: fieldName;
switch (type) {
case FieldMetadataType.UUID:
case FieldMetadataType.TEXT:
case FieldMetadataType.PHONE:
case FieldMetadataType.EMAIL:
case FieldMetadataType.NUMBER:
case FieldMetadataType.NUMERIC:
case FieldMetadataType.PROBABILITY:
case FieldMetadataType.BOOLEAN:
case FieldMetadataType.DATE_TIME:
case FieldMetadataType.RATING:
case FieldMetadataType.SELECT:
case FieldMetadataType.MULTI_SELECT:
case FieldMetadataType.POSITION:
case FieldMetadataType.JSON:
return {
value: columnName,
};
case FieldMetadataType.LINK:
return {
label: `${columnName}Label`,
url: `${columnName}Url`,
};
case FieldMetadataType.CURRENCY:
return {
amountMicros: `${columnName}AmountMicros`,
currencyCode: `${columnName}CurrencyCode`,
};
case FieldMetadataType.FULL_NAME:
return {
firstName: `${columnName}FirstName`,
lastName: `${columnName}LastName`,
};
case FieldMetadataType.RELATION:
return {};
default:
throw new BadRequestException(`Unknown type ${type}`);
}
}

View File

@ -0,0 +1,14 @@
import { FieldMetadataType } from 'src/engine/metadata-modules/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,16 @@
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
export type EnumFieldMetadataUnionType =
| FieldMetadataType.RATING
| FieldMetadataType.SELECT
| FieldMetadataType.MULTI_SELECT;
export const isEnumFieldMetadataType = (
type: FieldMetadataType,
): type is EnumFieldMetadataUnionType => {
return (
type === FieldMetadataType.RATING ||
type === FieldMetadataType.SELECT ||
type === FieldMetadataType.MULTI_SELECT
);
};

View File

@ -0,0 +1,55 @@
import { BadRequestException } from '@nestjs/common';
import { FieldMetadataDefaultSerializableValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
import { serializeTypeDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-type-default-value.util';
export const serializeDefaultValue = (
defaultValue?: FieldMetadataDefaultSerializableValue,
) => {
if (defaultValue === undefined || defaultValue === null) {
return null;
}
// Dynamic default values
if (
!Array.isArray(defaultValue) &&
typeof defaultValue === 'object' &&
'type' in defaultValue
) {
const serializedTypeDefaultValue = serializeTypeDefaultValue(defaultValue);
if (!serializedTypeDefaultValue) {
throw new BadRequestException('Invalid default value');
}
return serializedTypeDefaultValue;
}
// 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 (Array.isArray(defaultValue)) {
return defaultValue;
}
if (typeof defaultValue === 'object') {
return `'${JSON.stringify(defaultValue)}'`;
}
throw new BadRequestException('Invalid default value');
};

View File

@ -0,0 +1,18 @@
import { FieldMetadataDynamicDefaultValue } from 'src/engine/metadata-modules/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

@ -0,0 +1,72 @@
import { plainToInstance } from 'class-transformer';
import { validateSync } from 'class-validator';
import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import {
FieldMetadataDefaultValueBoolean,
FieldMetadataDefaultValueCurrency,
FieldMetadataDefaultValueDateTime,
FieldMetadataDefaultValueFullName,
FieldMetadataDefaultValueJson,
FieldMetadataDefaultValueLink,
FieldMetadataDefaultValueNumber,
FieldMetadataDefaultValueString,
FieldMetadataDefaultValueStringArray,
FieldMetadataDynamicDefaultValueNow,
FieldMetadataDynamicDefaultValueUuid,
} from 'src/engine/metadata-modules/field-metadata/dtos/default-value.input';
export const defaultValueValidatorsMap = {
[FieldMetadataType.UUID]: [
FieldMetadataDefaultValueString,
FieldMetadataDynamicDefaultValueUuid,
],
[FieldMetadataType.TEXT]: [FieldMetadataDefaultValueString],
[FieldMetadataType.PHONE]: [FieldMetadataDefaultValueString],
[FieldMetadataType.EMAIL]: [FieldMetadataDefaultValueString],
[FieldMetadataType.DATE_TIME]: [
FieldMetadataDefaultValueDateTime,
FieldMetadataDynamicDefaultValueNow,
],
[FieldMetadataType.BOOLEAN]: [FieldMetadataDefaultValueBoolean],
[FieldMetadataType.NUMBER]: [FieldMetadataDefaultValueNumber],
[FieldMetadataType.NUMERIC]: [FieldMetadataDefaultValueString],
[FieldMetadataType.PROBABILITY]: [FieldMetadataDefaultValueNumber],
[FieldMetadataType.LINK]: [FieldMetadataDefaultValueLink],
[FieldMetadataType.CURRENCY]: [FieldMetadataDefaultValueCurrency],
[FieldMetadataType.FULL_NAME]: [FieldMetadataDefaultValueFullName],
[FieldMetadataType.RATING]: [FieldMetadataDefaultValueString],
[FieldMetadataType.SELECT]: [FieldMetadataDefaultValueString],
[FieldMetadataType.MULTI_SELECT]: [FieldMetadataDefaultValueStringArray],
[FieldMetadataType.JSON]: [FieldMetadataDefaultValueJson],
};
export const validateDefaultValueForType = (
type: FieldMetadataType,
defaultValue: FieldMetadataDefaultValue,
): boolean => {
if (defaultValue === null) return true;
const validators = defaultValueValidatorsMap[type];
if (!validators) return false;
const isValid = validators.some((validator) => {
const defaultValueInstance = plainToInstance<
any,
FieldMetadataDefaultValue
>(validator, defaultValue);
return (
validateSync(defaultValueInstance, {
whitelist: true,
forbidNonWhitelisted: true,
forbidUnknownValues: true,
}).length === 0
);
});
return isValid;
};

View File

@ -0,0 +1,60 @@
import { plainToInstance } from 'class-transformer';
import { validateSync } from 'class-validator';
import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import {
FieldMetadataComplexOption,
FieldMetadataDefaultOption,
} from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
import { isEnumFieldMetadataType } from './is-enum-field-metadata-type.util';
export const optionsValidatorsMap = {
// RATING doesn't need to be provided as it's the backend that will generate the options
[FieldMetadataType.SELECT]: [FieldMetadataComplexOption],
[FieldMetadataType.MULTI_SELECT]: [FieldMetadataComplexOption],
};
export const validateOptionsForType = (
type: FieldMetadataType,
options: FieldMetadataOptions,
): boolean => {
if (options === null) return true;
if (!Array.isArray(options)) {
return false;
}
if (!isEnumFieldMetadataType(type)) {
return true;
}
if (type === FieldMetadataType.RATING) {
return true;
}
const validators = optionsValidatorsMap[type];
if (!validators) return false;
const isValid = options.every((option) => {
return validators.some((validator) => {
const optionsInstance = plainToInstance<
any,
FieldMetadataDefaultOption | FieldMetadataComplexOption
>(validator, option);
return (
validateSync(optionsInstance, {
whitelist: true,
forbidNonWhitelisted: true,
forbidUnknownValues: true,
}).length === 0
);
});
});
return isValid;
};