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:
Paul Rastoin
2025-07-31 14:54:39 +02:00
committed by GitHub
parent 7e7c64ac35
commit 14537d74b4
30 changed files with 841 additions and 220 deletions
@@ -11,7 +11,9 @@ describe('deepMerge', () => {
describe('primitive values', () => {
type PrimitiveValue = { value: string | number | boolean };
const primitiveTestCases: EachTestingContext<DeepMergeTestCase<PrimitiveValue>>[] = [
const primitiveTestCases: EachTestingContext<
DeepMergeTestCase<PrimitiveValue>
>[] = [
{
title: 'should override string values',
context: {
@@ -38,15 +40,20 @@ describe('deepMerge', () => {
},
];
it.each(primitiveTestCases)('$title', ({ context: { source, target, expected } }) => {
expect(deepMerge(source, target)).toEqual(expected);
});
it.each(primitiveTestCases)(
'$title',
({ context: { source, target, expected } }) => {
expect(deepMerge(source, target)).toEqual(expected);
},
);
});
describe('null and undefined handling', () => {
type NullableValue = { value: string | null };
const nullTestCases: EachTestingContext<DeepMergeTestCase<NullableValue>>[] = [
const nullTestCases: EachTestingContext<
DeepMergeTestCase<NullableValue>
>[] = [
{
title: 'should preserve null values from target',
context: {
@@ -73,13 +80,18 @@ describe('deepMerge', () => {
},
];
it.each(nullTestCases)('$title', ({ context: { source, target, expected } }) => {
expect(deepMerge(source, target)).toEqual(expected);
});
it.each(nullTestCases)(
'$title',
({ context: { source, target, expected } }) => {
expect(deepMerge(source, target)).toEqual(expected);
},
);
type MixedNullValue = { a: number | null; b: number };
const mixedNullTestCase: EachTestingContext<DeepMergeTestCase<MixedNullValue>> = {
const mixedNullTestCase: EachTestingContext<
DeepMergeTestCase<MixedNullValue>
> = {
title: 'should handle mixed null and undefined values',
context: {
source: { a: 1, b: 2 },
@@ -97,44 +109,48 @@ describe('deepMerge', () => {
describe('array handling', () => {
type ArrayValue = { arr: Array<string | number> | null };
const arrayTestCases: EachTestingContext<DeepMergeTestCase<ArrayValue>>[] = [
{
title: 'should concatenate arrays',
context: {
source: { arr: [1, 2] },
target: { arr: [3, 4] },
expected: { arr: [1, 2, 3, 4] },
const arrayTestCases: EachTestingContext<DeepMergeTestCase<ArrayValue>>[] =
[
{
title: 'should concatenate arrays',
context: {
source: { arr: [1, 2] },
target: { arr: [3, 4] },
expected: { arr: [1, 2, 3, 4] },
},
},
},
{
title: 'should handle empty target array',
context: {
source: { arr: [1, 2] },
target: { arr: [] },
expected: { arr: [1, 2] },
{
title: 'should handle empty target array',
context: {
source: { arr: [1, 2] },
target: { arr: [] },
expected: { arr: [1, 2] },
},
},
},
{
title: 'should handle empty source array',
context: {
source: { arr: [] },
target: { arr: [1, 2] },
expected: { arr: [1, 2] },
{
title: 'should handle empty source array',
context: {
source: { arr: [] },
target: { arr: [1, 2] },
expected: { arr: [1, 2] },
},
},
},
{
title: 'should handle null target array',
context: {
source: { arr: ['a', 'b'] },
target: { arr: null },
expected: { arr: null },
{
title: 'should handle null target array',
context: {
source: { arr: ['a', 'b'] },
target: { arr: null },
expected: { arr: null },
},
},
},
];
];
it.each(arrayTestCases)('$title', ({ context: { source, target, expected } }) => {
expect(deepMerge(source, target)).toEqual(expected);
});
it.each(arrayTestCases)(
'$title',
({ context: { source, target, expected } }) => {
expect(deepMerge(source, target)).toEqual(expected);
},
);
});
describe('nested objects', () => {
@@ -154,7 +170,9 @@ describe('deepMerge', () => {
} | null;
};
const nestedTestCases: EachTestingContext<DeepMergeTestCase<NestedValue>>[] = [
const nestedTestCases: EachTestingContext<
DeepMergeTestCase<NestedValue>
>[] = [
{
title: 'should merge nested objects',
context: {
@@ -166,9 +184,33 @@ describe('deepMerge', () => {
{
title: 'should merge deeply nested objects',
context: {
source: { nested: { a: 1, b: 2, deep: { a: 1, b: 2 }, arr: [], obj: { a: 1, b: 2 } } },
target: { nested: { a: 1, b: 2, deep: { a: 1, b: 3 }, arr: [], obj: { a: 1, b: 2 } } },
expected: { nested: { a: 1, b: 2, deep: { a: 1, b: 3 }, arr: [], obj: { a: 1, b: 2 } } },
source: {
nested: {
a: 1,
b: 2,
deep: { a: 1, b: 2 },
arr: [],
obj: { a: 1, b: 2 },
},
},
target: {
nested: {
a: 1,
b: 2,
deep: { a: 1, b: 3 },
arr: [],
obj: { a: 1, b: 2 },
},
},
expected: {
nested: {
a: 1,
b: 2,
deep: { a: 1, b: 3 },
arr: [],
obj: { a: 1, b: 2 },
},
},
},
},
{
@@ -210,9 +252,12 @@ describe('deepMerge', () => {
},
];
it.each(nestedTestCases)('$title', ({ context: { source, target, expected } }) => {
expect(deepMerge(source, target)).toEqual(expected);
});
it.each(nestedTestCases)(
'$title',
({ context: { source, target, expected } }) => {
expect(deepMerge(source, target)).toEqual(expected);
},
);
});
describe('edge cases', () => {
@@ -248,27 +293,32 @@ describe('deepMerge', () => {
},
];
it.each(edgeTestCases)('$title', ({ context: { source, target, expected } }) => {
expect(deepMerge(source, target)).toEqual(expected);
});
it.each(edgeTestCases)(
'$title',
({ context: { source, target, expected } }) => {
expect(deepMerge(source, target)).toEqual(expected);
},
);
type NestedDateValue = {
a: { value: Date };
b: { value: Date };
};
const nestedDateTestCase: EachTestingContext<DeepMergeTestCase<NestedDateValue>> = {
const nestedDateTestCase: EachTestingContext<
DeepMergeTestCase<NestedDateValue>
> = {
title: 'should handle Date objects in nested structures',
context: {
source: {
source: {
a: { value: new Date('2023-01-01') },
b: { value: new Date('2023-01-01') },
},
target: {
target: {
a: { value: new Date('2023-12-31') },
b: { value: new Date('2023-12-31') },
},
expected: {
expected: {
a: { value: new Date('2023-12-31') },
b: { value: new Date('2023-12-31') },
},
@@ -1,5 +1,6 @@
import { eachTestingContextFilter } from '@/testing';
import { EachTestingContext } from '@/testing/types/EachTestingContext.type';
import { sanitizeObjectStringFields } from '../sanitizeObjectStringFields';
import { extractAndSanitizeObjectStringFields } from '../extractAndSanitizeObjectStringFields';
type TestObject = {
name?: string;
@@ -25,7 +26,7 @@ type SanitizeTestCase = EachTestingContext<{
expected: object;
}>;
describe('sanitizeObjectStringFields', () => {
describe('extractAndSanitizeObjectStringFields', () => {
const testCases: SanitizeTestCase[] = [
{
title: 'should handle basic string properties and trim whitespaces',
@@ -84,6 +85,16 @@ describe('sanitizeObjectStringFields', () => {
obj: { name: 'John', age: 30 },
keys: ['city', 'name'],
},
expected: { name: 'John' },
},
},
{
title: 'should handle object with number field',
context: {
input: {
obj: { name: 'John', age: 30 },
keys: ['age', 'name'],
},
expected: { name: 'John', age: 30 },
},
},
@@ -164,7 +175,7 @@ describe('sanitizeObjectStringFields', () => {
},
];
test.each(testCases)(
test.each(eachTestingContextFilter(testCases))(
'$title',
({
context: {
@@ -172,7 +183,7 @@ describe('sanitizeObjectStringFields', () => {
expected,
},
}) => {
const result = sanitizeObjectStringFields(obj, keys);
const result = extractAndSanitizeObjectStringFields(obj, keys);
expect(result).toEqual(expected);
},
@@ -6,7 +6,7 @@
* - Null values from target are preserved
* - Undefined values from target are ignored
* - Date and RegExp objects are treated as primitives (replaced, not merged)
*
*
* @param source The source object to merge from
* @param target The target object to merge into
* @returns A new merged object
@@ -76,4 +76,4 @@ export const deepMerge = <T extends object>(
});
return output;
};
};
@@ -1,12 +1,12 @@
import { trimAndRemoveDuplicatedWhitespacesFromString } from '@/utils/trim-and-remove-duplicated-whitespaces-from-string';
// TODO rename with extract meaning
export const sanitizeObjectStringFields = <
export const extractAndSanitizeObjectStringFields = <
T extends object,
TKeys extends (keyof T)[],
>(
obj: T,
keys: TKeys,
maxDepth: number = 10,
maxDepth = 10,
): {
[P in TKeys[number]]: T[P];
} => {
+1 -1
View File
@@ -9,6 +9,7 @@
export { assertUnreachable } from './assertUnreachable';
export { deepMerge } from './deepMerge';
export { extractAndSanitizeObjectStringFields } from './extractAndSanitizeObjectStringFields';
export { isFieldMetadataDateKind } from './fieldMetadata/isFieldMetadataDateKind';
export { fromArrayToUniqueKeyRecord } from './from-array-to-unique-key-record.util';
export { getURLSafely } from './getURLSafely';
@@ -21,7 +22,6 @@ export { getUniqueConstraintsFields } from './indexMetadata/getUniqueConstraints
export { parseJson } from './parseJson';
export { removePropertiesFromRecord } from './removePropertiesFromRecord';
export { removeUndefinedFields } from './removeUndefinedFields';
export { sanitizeObjectStringFields } from './sanitizeObjectStringFields';
export { getGenericOperationName } from './sentry/getGenericOperationName';
export { getHumanReadableNameFromCode } from './sentry/getHumanReadableNameFromCode';
export { capitalize } from './strings/capitalize';
@@ -1,4 +1,5 @@
import { isNull, isUndefined } from '@sniptt/guards';
export const isDefined = <T>(value: T | null | undefined): value is T =>
!isUndefined(value) && !isNull(value);
export const isDefined = <T>(
value: T | null | undefined,
): value is NonNullable<T> => !isUndefined(value) && !isNull(value);