FlatFieldMetadataType hashmap arch and enum validation (#13502)
# Introduction
- FlatFieldMetadataType validators hashmap
- Do not fail fast on validation but aggregate errors
- Implemented `enum` validation
- Plugged the new v2 dynamic call in the field metadata service v2
## What's next:
- Implem integration tests to make things run 🙃
- migrate existing settings valdiation
- Finish the create object metadata service
- Handle update input transpilation and validation
## Open question
Should we implement, not covered validation ?, adding strictness now or
never.
This will be required by the import anw
## Discovered issue with cache
Currently the cache is not accurately typed, `fieldsById` map are not
storing any relations.
Which means the current transpilation tools are hitting undefined at
runtime
In the best of the world we will refactor the cache to be storing
`FlatObjectMetadata` and `FlatFieldMetadata` so we don't even have to
transpile them for validation and so on
But it would require to refactor the loaders that returns the cache to
the front on hit as FieldMetadataEntity, so we might land on a lighter
solution to rather add a new `getExistingFlatCache` that handles the
transpilation itself
About to do that in an other PR to be discussed with Coco
This commit is contained in:
+129
@@ -0,0 +1,129 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import {
|
||||
FieldMetadataException,
|
||||
FieldMetadataExceptionCode,
|
||||
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
|
||||
import { FlatFieldMetadataTypeValidator } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-type-validator.type';
|
||||
import { isEnumValidateOneFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-enum-validate-one-field-metadata-args.util';
|
||||
import { validateEnumSelectFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/validators/validate-enum-flat-field-metadata.validator';
|
||||
import { validateRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/validators/validate-relation-flat-field-metadata.validator';
|
||||
@Injectable()
|
||||
export class FlatFieldMetadataTypeValidatorService {
|
||||
constructor(private readonly featureFlagService: FeatureFlagService) {}
|
||||
|
||||
public readonly FIELD_METADATA_TYPE_VALIDATOR_HASHMAP: FlatFieldMetadataTypeValidator =
|
||||
{
|
||||
ACTOR: async (_args) => {
|
||||
return [];
|
||||
},
|
||||
ADDRESS: async (_args) => {
|
||||
return [];
|
||||
},
|
||||
ARRAY: async (_args) => {
|
||||
return [];
|
||||
},
|
||||
BOOLEAN: async (_args) => {
|
||||
return [];
|
||||
},
|
||||
CURRENCY: async (_args) => {
|
||||
return [];
|
||||
},
|
||||
DATE: async (_args) => {
|
||||
return [];
|
||||
},
|
||||
DATE_TIME: async (_args) => {
|
||||
return [];
|
||||
},
|
||||
EMAILS: async (_args) => {
|
||||
return [];
|
||||
},
|
||||
FULL_NAME: async (_args) => {
|
||||
return [];
|
||||
},
|
||||
LINKS: async (_args) => {
|
||||
return [];
|
||||
},
|
||||
MORPH_RELATION: async ({ workspaceId }) => {
|
||||
const isMorphRelationEnabled =
|
||||
await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_MORPH_RELATION_ENABLED,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (!isMorphRelationEnabled) {
|
||||
return [
|
||||
new FieldMetadataException(
|
||||
'Morph relation feature is disabled',
|
||||
FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
},
|
||||
MULTI_SELECT: (args) => {
|
||||
if (!isEnumValidateOneFieldMetadata(args)) {
|
||||
throw new FieldMetadataException(
|
||||
'Should never occur, invaliad enum field metadata type',
|
||||
FieldMetadataExceptionCode.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
return validateEnumSelectFlatFieldMetadata(args);
|
||||
},
|
||||
NUMBER: async (_args) => {
|
||||
return [];
|
||||
},
|
||||
NUMERIC: async (_args) => {
|
||||
return [];
|
||||
},
|
||||
PHONES: async (_args) => {
|
||||
return [];
|
||||
},
|
||||
POSITION: async (_args) => {
|
||||
return [];
|
||||
},
|
||||
RATING: (args) => {
|
||||
if (!isEnumValidateOneFieldMetadata(args)) {
|
||||
throw new FieldMetadataException(
|
||||
'Should never occur, invaliad enum field metadata type',
|
||||
FieldMetadataExceptionCode.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
return validateEnumSelectFlatFieldMetadata(args);
|
||||
},
|
||||
RAW_JSON: async (_args) => {
|
||||
return [];
|
||||
},
|
||||
RELATION: validateRelationFlatFieldMetadata,
|
||||
RICH_TEXT: async (_args) => {
|
||||
return [];
|
||||
},
|
||||
RICH_TEXT_V2: async (_args) => {
|
||||
return [];
|
||||
},
|
||||
SELECT: (args) => {
|
||||
if (!isEnumValidateOneFieldMetadata(args)) {
|
||||
throw new FieldMetadataException(
|
||||
'Should never occur, invaliad enum field metadata type',
|
||||
FieldMetadataExceptionCode.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
return validateEnumSelectFlatFieldMetadata(args);
|
||||
},
|
||||
TEXT: async (_args) => {
|
||||
return [];
|
||||
},
|
||||
TS_VECTOR: async (_args) => {
|
||||
return [];
|
||||
},
|
||||
UUID: async (_args) => {
|
||||
return [];
|
||||
},
|
||||
};
|
||||
}
|
||||
+67
-102
@@ -1,16 +1,14 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { Expect } from 'twenty-shared/testing';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import {
|
||||
FieldMetadataException,
|
||||
FieldMetadataExceptionCode,
|
||||
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
|
||||
import { FailedFlatFieldMetadataValidation } from 'src/engine/metadata-modules/flat-field-metadata/types/failed-flat-field-metadata-validation.type';
|
||||
import { FlatFieldMetadataTypeValidatorService } from 'src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service';
|
||||
import { FailedFlatFieldMetadataValidationExceptions } from 'src/engine/metadata-modules/flat-field-metadata/types/failed-flat-field-metadata-validation.type';
|
||||
import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { validateFlatFieldMetadataNameAvailability } from 'src/engine/metadata-modules/flat-field-metadata/validators/validate-flat-field-metadata-name-availability.validator';
|
||||
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
@@ -25,25 +23,32 @@ import {
|
||||
import { validateMetadataNameOrThrow } from 'src/engine/metadata-modules/utils/validate-metadata-name.utils';
|
||||
import { computeMetadataNameFromLabel } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util';
|
||||
|
||||
type ValidateOneFieldMetadataArgs = {
|
||||
export type ValidateOneFieldMetadataArgs<
|
||||
T extends FieldMetadataType = FieldMetadataType,
|
||||
> = {
|
||||
existingFlatObjectMetadatas: FlatObjectMetadata[];
|
||||
othersFlatObjectMetadataToValidate?: FlatObjectMetadata[];
|
||||
flatFieldMetadataToValidate: FlatFieldMetadata;
|
||||
flatFieldMetadataToValidate: FlatFieldMetadata<T>;
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class FlatFieldMetadataValidatorService {
|
||||
constructor(private readonly featureFlagService: FeatureFlagService) {}
|
||||
constructor(
|
||||
private readonly flatFieldMetadataTypeValidatorService: FlatFieldMetadataTypeValidatorService,
|
||||
) {}
|
||||
|
||||
async validateOneFlatFieldMetadata({
|
||||
async validateOneFlatFieldMetadata<
|
||||
T extends FieldMetadataType = FieldMetadataType,
|
||||
>({
|
||||
existingFlatObjectMetadatas,
|
||||
flatFieldMetadataToValidate,
|
||||
othersFlatObjectMetadataToValidate,
|
||||
workspaceId,
|
||||
}: ValidateOneFieldMetadataArgs): Promise<
|
||||
FailedFlatFieldMetadataValidation | undefined
|
||||
}: ValidateOneFieldMetadataArgs<T>): Promise<
|
||||
FailedFlatFieldMetadataValidationExceptions[]
|
||||
> {
|
||||
const errors: FailedFlatFieldMetadataValidationExceptions[] = [];
|
||||
const allFlatObjectMetadata = [
|
||||
...existingFlatObjectMetadatas,
|
||||
...(othersFlatObjectMetadataToValidate ?? []),
|
||||
@@ -55,25 +60,33 @@ export class FlatFieldMetadataValidatorService {
|
||||
);
|
||||
|
||||
if (!isDefined(parentFlatObjectMetadata)) {
|
||||
return {
|
||||
status: 'fail',
|
||||
error: new FieldMetadataException(
|
||||
errors.push(
|
||||
new FieldMetadataException(
|
||||
isDefined(othersFlatObjectMetadataToValidate)
|
||||
? 'Object metadata does not exist in both existing and about to be created object metadatas'
|
||||
: 'Object metadata does not exist',
|
||||
? 'Object metadata not found in both existing and about to be created object metadatas'
|
||||
: 'Object metadata not found',
|
||||
FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
|
||||
),
|
||||
};
|
||||
}
|
||||
);
|
||||
} else {
|
||||
if (parentFlatObjectMetadata.isRemote === true) {
|
||||
errors.push(
|
||||
new ObjectMetadataException(
|
||||
'Remote objects are read-only',
|
||||
ObjectMetadataExceptionCode.OBJECT_MUTATION_NOT_ALLOWED,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (parentFlatObjectMetadata.isRemote === true) {
|
||||
return {
|
||||
status: 'fail',
|
||||
error: new ObjectMetadataException(
|
||||
'Remote objects are read-only',
|
||||
ObjectMetadataExceptionCode.OBJECT_MUTATION_NOT_ALLOWED,
|
||||
),
|
||||
};
|
||||
const failedNameAvailabilityValidation =
|
||||
validateFlatFieldMetadataNameAvailability({
|
||||
name: flatFieldMetadataToValidate.name,
|
||||
objectMetadata: parentFlatObjectMetadata,
|
||||
});
|
||||
|
||||
if (isDefined(failedNameAvailabilityValidation)) {
|
||||
errors.push(failedNameAvailabilityValidation);
|
||||
}
|
||||
}
|
||||
|
||||
if (flatFieldMetadataToValidate.isLabelSyncedWithName) {
|
||||
@@ -82,102 +95,54 @@ export class FlatFieldMetadataValidatorService {
|
||||
);
|
||||
|
||||
if (flatFieldMetadataToValidate.name !== computedName) {
|
||||
return {
|
||||
status: 'fail',
|
||||
error: new InvalidMetadataException(
|
||||
errors.push(
|
||||
new InvalidMetadataException(
|
||||
`Name is not synced with label. Expected name: "${computedName}", got ${flatFieldMetadataToValidate.name}`,
|
||||
InvalidMetadataExceptionCode.NAME_NOT_SYNCED_WITH_LABEL,
|
||||
),
|
||||
};
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
validateMetadataNameOrThrow(flatFieldMetadataToValidate.name);
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 'fail',
|
||||
error: new FieldMetadataException(
|
||||
errors.push(
|
||||
new FieldMetadataException(
|
||||
error.message,
|
||||
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
{
|
||||
userFriendlyMessage: error.userFriendlyMessage,
|
||||
},
|
||||
),
|
||||
};
|
||||
);
|
||||
}
|
||||
|
||||
const failedNameAvailabilityValidation =
|
||||
validateFlatFieldMetadataNameAvailability({
|
||||
name: flatFieldMetadataToValidate.name,
|
||||
objectMetadata: parentFlatObjectMetadata,
|
||||
});
|
||||
const fieldMetadataTypeValidator =
|
||||
this.flatFieldMetadataTypeValidatorService
|
||||
.FIELD_METADATA_TYPE_VALIDATOR_HASHMAP[
|
||||
flatFieldMetadataToValidate.type
|
||||
];
|
||||
|
||||
if (isDefined(failedNameAvailabilityValidation)) {
|
||||
return failedNameAvailabilityValidation;
|
||||
if (!isDefined(fieldMetadataTypeValidator)) {
|
||||
errors.push(
|
||||
new FieldMetadataException(
|
||||
'Unsupported field metadata type',
|
||||
FieldMetadataExceptionCode.UNCOVERED_FIELD_METADATA_TYPE_VALIDATION,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
const fieldMetadataTypeValidatorExceptions =
|
||||
await fieldMetadataTypeValidator({
|
||||
existingFlatObjectMetadatas,
|
||||
flatFieldMetadataToValidate,
|
||||
workspaceId,
|
||||
othersFlatObjectMetadataToValidate,
|
||||
});
|
||||
|
||||
errors.push(...fieldMetadataTypeValidatorExceptions);
|
||||
}
|
||||
|
||||
// We should validate each default value and settings and options
|
||||
// We should also handle relation and stuff
|
||||
switch (flatFieldMetadataToValidate.type) {
|
||||
case FieldMetadataType.MORPH_RELATION: {
|
||||
const isMorphRelationEnabled =
|
||||
await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_MORPH_RELATION_ENABLED,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (!isMorphRelationEnabled) {
|
||||
return {
|
||||
status: 'fail',
|
||||
error: new FieldMetadataException(
|
||||
'Morph relation feature is disabled',
|
||||
FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
case FieldMetadataType.RELATION:
|
||||
case FieldMetadataType.UUID:
|
||||
case FieldMetadataType.TEXT:
|
||||
case FieldMetadataType.PHONES:
|
||||
case FieldMetadataType.EMAILS:
|
||||
case FieldMetadataType.DATE_TIME:
|
||||
case FieldMetadataType.DATE:
|
||||
case FieldMetadataType.BOOLEAN:
|
||||
case FieldMetadataType.NUMBER:
|
||||
case FieldMetadataType.NUMERIC:
|
||||
case FieldMetadataType.LINKS:
|
||||
case FieldMetadataType.CURRENCY:
|
||||
case FieldMetadataType.FULL_NAME:
|
||||
case FieldMetadataType.RATING:
|
||||
case FieldMetadataType.SELECT:
|
||||
case FieldMetadataType.MULTI_SELECT:
|
||||
case FieldMetadataType.POSITION:
|
||||
case FieldMetadataType.ADDRESS:
|
||||
case FieldMetadataType.RAW_JSON:
|
||||
case FieldMetadataType.RICH_TEXT:
|
||||
case FieldMetadataType.RICH_TEXT_V2:
|
||||
case FieldMetadataType.ACTOR:
|
||||
case FieldMetadataType.ARRAY:
|
||||
case FieldMetadataType.TS_VECTOR: {
|
||||
return;
|
||||
}
|
||||
default: {
|
||||
const _staticTypeCheck: Expect<
|
||||
typeof flatFieldMetadataToValidate.type extends never ? true : false
|
||||
> = true;
|
||||
|
||||
return {
|
||||
status: 'fail',
|
||||
error: new FieldMetadataException(
|
||||
'Unsupported field metadata type',
|
||||
FieldMetadataExceptionCode.UNCOVERED_FIELD_METADATA_TYPE_VALIDATION,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user