Object metadata API create one using workspace migration v2 (#13420)
# Introduction In this PR we create basic transpilation methods and utils to handle input to flat, entity to flat, object maps to flat. In order to transpile everything into a common validation that will be implemented in another PR ## FieldMetadataEntity typing Added `never | null` to fields that should never be in order to ease general abstracted method to pass null, as anw it's what is in the database ## Todo - ~~Create a feature flag~~ - Integration test for object creation through metadata api + pg col introspection and snapshoting
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
export type FromTo<T> = {
|
||||
from: T;
|
||||
to: T;
|
||||
};
|
||||
@@ -10,6 +10,7 @@
|
||||
export type { ConfigVariableValue } from './ConfigVariableValue';
|
||||
export { ConnectedAccountProvider } from './ConnectedAccountProvider';
|
||||
export { FieldMetadataType } from './FieldMetadataType';
|
||||
export type { FromTo } from './FromToType';
|
||||
export type { IsExactly } from './IsExactly';
|
||||
export type { NullablePartial } from './NullablePartial';
|
||||
export type { ObjectRecordsPermissions } from './ObjectRecordsPermissions';
|
||||
|
||||
@@ -0,0 +1,283 @@
|
||||
import { EachTestingContext } from '@/testing/types/EachTestingContext.type';
|
||||
import { deepMerge } from '@/utils';
|
||||
|
||||
type DeepMergeTestCase<T extends object> = {
|
||||
source: Required<T>;
|
||||
target: Required<T>;
|
||||
expected: T;
|
||||
};
|
||||
|
||||
describe('deepMerge', () => {
|
||||
describe('primitive values', () => {
|
||||
type PrimitiveValue = { value: string | number | boolean };
|
||||
|
||||
const primitiveTestCases: EachTestingContext<DeepMergeTestCase<PrimitiveValue>>[] = [
|
||||
{
|
||||
title: 'should override string values',
|
||||
context: {
|
||||
source: { value: 'hello' },
|
||||
target: { value: 'world' },
|
||||
expected: { value: 'world' },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should override number values',
|
||||
context: {
|
||||
source: { value: 42 },
|
||||
target: { value: 24 },
|
||||
expected: { value: 24 },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should override boolean values',
|
||||
context: {
|
||||
source: { value: true },
|
||||
target: { value: false },
|
||||
expected: { value: false },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
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>>[] = [
|
||||
{
|
||||
title: 'should preserve null values from target',
|
||||
context: {
|
||||
source: { value: 'hello' },
|
||||
target: { value: null },
|
||||
expected: { value: null },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should ignore undefined values from target',
|
||||
context: {
|
||||
source: { value: 'hello' },
|
||||
target: { value: undefined as unknown as null },
|
||||
expected: { value: 'hello' },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should override null values from source',
|
||||
context: {
|
||||
source: { value: null },
|
||||
target: { value: 'world' },
|
||||
expected: { value: 'world' },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
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>> = {
|
||||
title: 'should handle mixed null and undefined values',
|
||||
context: {
|
||||
source: { a: 1, b: 2 },
|
||||
target: { a: null, b: 2 },
|
||||
expected: { a: null, b: 2 },
|
||||
},
|
||||
};
|
||||
|
||||
it(mixedNullTestCase.title, () => {
|
||||
const { source, target, expected } = mixedNullTestCase.context;
|
||||
expect(deepMerge(source, target)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
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] },
|
||||
},
|
||||
},
|
||||
{
|
||||
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 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);
|
||||
});
|
||||
});
|
||||
|
||||
describe('nested objects', () => {
|
||||
type NestedValue = {
|
||||
nested: {
|
||||
a: number;
|
||||
b: number;
|
||||
deep?: {
|
||||
a: number;
|
||||
b: number;
|
||||
};
|
||||
arr: Array<number>;
|
||||
obj: {
|
||||
a: number;
|
||||
b: number;
|
||||
};
|
||||
} | null;
|
||||
};
|
||||
|
||||
const nestedTestCases: EachTestingContext<DeepMergeTestCase<NestedValue>>[] = [
|
||||
{
|
||||
title: 'should merge nested objects',
|
||||
context: {
|
||||
source: { nested: { a: 1, b: 2, arr: [], obj: { a: 1, b: 2 } } },
|
||||
target: { nested: { a: 1, b: 3, arr: [], obj: { a: 1, b: 2 } } },
|
||||
expected: { nested: { a: 1, b: 3, arr: [], obj: { a: 1, b: 2 } } },
|
||||
},
|
||||
},
|
||||
{
|
||||
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 } } },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should handle null nested object in target',
|
||||
context: {
|
||||
source: { nested: { a: 1, b: 2, arr: [], obj: { a: 1, b: 2 } } },
|
||||
target: { nested: null },
|
||||
expected: { nested: null },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should handle complex nested structures',
|
||||
context: {
|
||||
source: {
|
||||
nested: {
|
||||
a: 1,
|
||||
b: 2,
|
||||
arr: [1, 2],
|
||||
obj: { a: 1, b: 2 },
|
||||
},
|
||||
},
|
||||
target: {
|
||||
nested: {
|
||||
a: 1,
|
||||
b: 2,
|
||||
arr: [3, 4],
|
||||
obj: { a: 1, b: 3 },
|
||||
},
|
||||
},
|
||||
expected: {
|
||||
nested: {
|
||||
a: 1,
|
||||
b: 2,
|
||||
arr: [1, 2, 3, 4],
|
||||
obj: { a: 1, b: 3 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
it.each(nestedTestCases)('$title', ({ context: { source, target, expected } }) => {
|
||||
expect(deepMerge(source, target)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
type EdgeValue = {
|
||||
a: number | Date | RegExp;
|
||||
b: number | Date | RegExp;
|
||||
};
|
||||
|
||||
const edgeTestCases: EachTestingContext<DeepMergeTestCase<EdgeValue>>[] = [
|
||||
{
|
||||
title: 'should handle Date objects by replacing them',
|
||||
context: {
|
||||
source: { a: new Date('2023-01-01'), b: new Date('2023-01-01') },
|
||||
target: { a: new Date('2023-12-31'), b: new Date('2023-12-31') },
|
||||
expected: { a: new Date('2023-12-31'), b: new Date('2023-12-31') },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should handle RegExp objects by replacing them',
|
||||
context: {
|
||||
source: { a: /test1/, b: /test1/ },
|
||||
target: { a: /test2/, b: /test2/ },
|
||||
expected: { a: /test2/, b: /test2/ },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should handle mixed Date and RegExp objects',
|
||||
context: {
|
||||
source: { a: new Date('2023-01-01'), b: /test1/ },
|
||||
target: { a: new Date('2023-12-31'), b: /test2/ },
|
||||
expected: { a: new Date('2023-12-31'), b: /test2/ },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
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>> = {
|
||||
title: 'should handle Date objects in nested structures',
|
||||
context: {
|
||||
source: {
|
||||
a: { value: new Date('2023-01-01') },
|
||||
b: { value: new Date('2023-01-01') },
|
||||
},
|
||||
target: {
|
||||
a: { value: new Date('2023-12-31') },
|
||||
b: { value: new Date('2023-12-31') },
|
||||
},
|
||||
expected: {
|
||||
a: { value: new Date('2023-12-31') },
|
||||
b: { value: new Date('2023-12-31') },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it(nestedDateTestCase.title, () => {
|
||||
const { source, target, expected } = nestedDateTestCase.context;
|
||||
expect(deepMerge(source, target)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
import { EachTestingContext } from '@/testing/types/EachTestingContext.type';
|
||||
import { trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties } from '../trim-and-remove-duplicated-whitespaces-from-object-string-properties';
|
||||
|
||||
type SanitizeObjectStringPropertiesTestCase = EachTestingContext<{
|
||||
input: Record<string, any>;
|
||||
keys: string[];
|
||||
expected: Record<string, any>;
|
||||
}>;
|
||||
|
||||
describe('trim-and-remove-duplicated-whitespaces-from-object-string-properties', () => {
|
||||
const testCases: SanitizeObjectStringPropertiesTestCase[] = [
|
||||
{
|
||||
title: 'should sanitize single string property',
|
||||
context: {
|
||||
input: { name: ' John Doe ' },
|
||||
keys: ['name'],
|
||||
expected: { name: 'John Doe' },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should sanitize multiple string properties',
|
||||
context: {
|
||||
input: {
|
||||
firstName: ' John ',
|
||||
lastName: ' Doe ',
|
||||
email: ' john.doe@example.com ',
|
||||
},
|
||||
keys: ['firstName', 'lastName', 'email'],
|
||||
expected: {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
email: 'john.doe@example.com',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should preserve undefined properties',
|
||||
context: {
|
||||
input: { name: ' John Doe ' },
|
||||
keys: ['name', 'age'],
|
||||
expected: { name: 'John Doe' },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should handle null properties',
|
||||
context: {
|
||||
input: { name: ' John Doe ', description: null },
|
||||
keys: ['name', 'description'],
|
||||
expected: { name: 'John Doe', description: null },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should not modify non-string properties',
|
||||
context: {
|
||||
input: { name: ' John Doe ', age: 30, active: true },
|
||||
// In real life passing age would raise an TypeScript error
|
||||
keys: ['name', 'age', 'active'],
|
||||
expected: { name: 'John Doe', age: 30, active: true },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should handle empty string',
|
||||
context: {
|
||||
input: { name: ' ' },
|
||||
keys: ['name'],
|
||||
expected: { name: '' },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should handle object with no properties to sanitize',
|
||||
context: {
|
||||
input: { age: 30, active: true },
|
||||
keys: ['name'],
|
||||
expected: { age: 30, active: true },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should handle nested whitespace',
|
||||
context: {
|
||||
input: { description: ' This is a test ' },
|
||||
keys: ['description'],
|
||||
expected: { description: 'This is a test' },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should trim only provided keys fields',
|
||||
context: {
|
||||
input: {
|
||||
name: ' John Doe ',
|
||||
description: ' this is a test ',
|
||||
},
|
||||
keys: ['description'],
|
||||
expected: { name: ' John Doe ', description: 'this is a test' },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
test.each(testCases)('$title', ({ context: { input, keys, expected } }) => {
|
||||
const result = trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
|
||||
input,
|
||||
keys,
|
||||
);
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
import { EachTestingContext } from '@/testing/types/EachTestingContext.type';
|
||||
import { trimAndRemoveDuplicatedWhitespacesFromString } from '../trim-and-remove-duplicated-whitespaces-from-string';
|
||||
|
||||
type TrimAndRemoveWhitespacesTestCase = EachTestingContext<{
|
||||
input: string;
|
||||
expected: string;
|
||||
}>;
|
||||
|
||||
describe('trim-and-remove-duplicated-whitespaces-from-string', () => {
|
||||
const testCases: TrimAndRemoveWhitespacesTestCase[] = [
|
||||
{
|
||||
title: 'should trim and remove duplicated whitespaces from a string',
|
||||
context: {
|
||||
input: ' Hello World ',
|
||||
expected: 'Hello World',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should handle string with multiple spaces between words',
|
||||
context: {
|
||||
input: 'This is a test',
|
||||
expected: 'This is a test',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should handle string with only whitespaces',
|
||||
context: {
|
||||
input: ' ',
|
||||
expected: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should handle empty string',
|
||||
context: {
|
||||
input: '',
|
||||
expected: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should handle string with tabs and newlines',
|
||||
context: {
|
||||
input: 'Hello\t\t\tWorld\n\n Test',
|
||||
expected: 'Hello World Test',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should handle string with leading and trailing spaces',
|
||||
context: {
|
||||
input: ' Leading and trailing spaces ',
|
||||
expected: 'Leading and trailing spaces',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should preserve single spaces between words',
|
||||
context: {
|
||||
input: 'This is already properly spaced',
|
||||
expected: 'This is already properly spaced',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
test.each(testCases)('$title', ({ context: { input, expected } }) => {
|
||||
const result = trimAndRemoveDuplicatedWhitespacesFromString(input);
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Deep merges two objects or arrays recursively
|
||||
* - Objects are merged by combining their properties
|
||||
* - Arrays are merged by concatenating them
|
||||
* - Primitive values from target override source
|
||||
* - 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
|
||||
*/
|
||||
export const deepMerge = <T extends object>(
|
||||
source: Required<T>,
|
||||
target: Required<T>,
|
||||
): T => {
|
||||
// Handle null/undefined cases
|
||||
if (!source) return target as T;
|
||||
if (!target) return source;
|
||||
|
||||
// Create a new object to avoid mutations
|
||||
const output = { ...source };
|
||||
|
||||
// Iterate through all keys in target
|
||||
Object.keys(target).forEach((key) => {
|
||||
const sourceValue = source[key as keyof T];
|
||||
const targetValue = target[key as keyof T];
|
||||
|
||||
// Skip undefined values in target
|
||||
if (targetValue === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle null values - explicitly assign them
|
||||
if (targetValue === null) {
|
||||
output[key as keyof T] = null as T[keyof T];
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle arrays - concatenate them
|
||||
if (Array.isArray(sourceValue) && Array.isArray(targetValue)) {
|
||||
output[key as keyof T] = [...sourceValue, ...targetValue] as T[keyof T];
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle Date and RegExp objects - treat them as primitives
|
||||
if (
|
||||
targetValue instanceof Date ||
|
||||
targetValue instanceof RegExp ||
|
||||
sourceValue instanceof Date ||
|
||||
sourceValue instanceof RegExp
|
||||
) {
|
||||
output[key as keyof T] = targetValue as T[keyof T];
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle nested objects - recurse
|
||||
if (
|
||||
sourceValue &&
|
||||
targetValue &&
|
||||
typeof sourceValue === 'object' &&
|
||||
typeof targetValue === 'object' &&
|
||||
!Array.isArray(sourceValue) &&
|
||||
!Array.isArray(targetValue)
|
||||
) {
|
||||
output[key as keyof T] = deepMerge(
|
||||
sourceValue as object,
|
||||
targetValue as object,
|
||||
) as T[keyof T];
|
||||
return;
|
||||
}
|
||||
|
||||
// For primitives
|
||||
output[key as keyof T] = targetValue as T[keyof T];
|
||||
});
|
||||
|
||||
return output;
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import { StringPropertyKeys } from '@/utils/trim-and-remove-duplicated-whitespaces-from-object-string-properties';
|
||||
import { isDefined } from '@/utils/validation';
|
||||
|
||||
export const fromArrayToUniqueKeyRecord = <T extends object>({
|
||||
array,
|
||||
uniqueKey,
|
||||
}: {
|
||||
array: T[];
|
||||
uniqueKey: StringPropertyKeys<T>;
|
||||
}) => {
|
||||
return array.reduce<Record<string, T>>((acc, occurence) => {
|
||||
const currentUniqueKey = occurence[uniqueKey] as string;
|
||||
|
||||
if (isDefined(acc[currentUniqueKey])) {
|
||||
throw new Error(
|
||||
`Should never occur, flat array contains twice the same unique key ${occurence[uniqueKey]}`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...acc,
|
||||
[currentUniqueKey]: occurence,
|
||||
};
|
||||
}, {});
|
||||
};
|
||||
@@ -8,7 +8,9 @@
|
||||
*/
|
||||
|
||||
export { assertUnreachable } from './assertUnreachable';
|
||||
export { deepMerge } from './deepMerge';
|
||||
export { isFieldMetadataDateKind } from './fieldMetadata/isFieldMetadataDateKind';
|
||||
export { fromArrayToUniqueKeyRecord } from './from-array-to-unique-key-record.util';
|
||||
export { getURLSafely } from './getURLSafely';
|
||||
export { getImageAbsoluteURI } from './image/getImageAbsoluteURI';
|
||||
export {
|
||||
@@ -17,10 +19,14 @@ export {
|
||||
} from './image/getLogoUrlFromDomainName';
|
||||
export { getUniqueConstraintsFields } from './indexMetadata/getUniqueConstraintsFields';
|
||||
export { parseJson } from './parseJson';
|
||||
export { removePropertiesFromRecord } from './removePropertiesFromRecord';
|
||||
export { removeUndefinedFields } from './removeUndefinedFields';
|
||||
export { getGenericOperationName } from './sentry/getGenericOperationName';
|
||||
export { getHumanReadableNameFromCode } from './sentry/getHumanReadableNameFromCode';
|
||||
export { capitalize } from './strings/capitalize';
|
||||
export type { StringPropertyKeys } from './trim-and-remove-duplicated-whitespaces-from-object-string-properties';
|
||||
export { trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties } from './trim-and-remove-duplicated-whitespaces-from-object-string-properties';
|
||||
export { trimAndRemoveDuplicatedWhitespacesFromString } from './trim-and-remove-duplicated-whitespaces-from-string';
|
||||
export { absoluteUrlSchema } from './url/absoluteUrlSchema';
|
||||
export { buildSignedPath } from './url/buildSignedPath';
|
||||
export { getAbsoluteUrl } from './url/getAbsoluteUrl';
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
export const removePropertiesFromRecord = <T, K extends keyof T>(
|
||||
record: T,
|
||||
keysToRemove: K[],
|
||||
): Omit<T, K> => {
|
||||
const result = { ...record };
|
||||
|
||||
for (const key of keysToRemove) {
|
||||
delete result[key];
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import { trimAndRemoveDuplicatedWhitespacesFromString } from '@/utils/trim-and-remove-duplicated-whitespaces-from-string';
|
||||
|
||||
type OnlyStringPropertiesKey<T> = Extract<keyof T, string>;
|
||||
|
||||
export type StringPropertyKeys<T> = {
|
||||
[K in OnlyStringPropertiesKey<T>]: T[K] extends string | undefined
|
||||
? K
|
||||
: never;
|
||||
}[OnlyStringPropertiesKey<T>];
|
||||
|
||||
export const trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties = <T>(
|
||||
obj: T,
|
||||
keys: StringPropertyKeys<T>[],
|
||||
) => {
|
||||
return keys.reduce((acc, key) => {
|
||||
const occurrence = acc[key];
|
||||
|
||||
if (
|
||||
occurrence === undefined ||
|
||||
typeof occurrence !== 'string' ||
|
||||
occurrence === null
|
||||
) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
return {
|
||||
...acc,
|
||||
[key]: trimAndRemoveDuplicatedWhitespacesFromString(occurrence),
|
||||
};
|
||||
}, obj);
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
const MULTIPLE_WHITESPACE_REGEX = /\s+/g;
|
||||
|
||||
export const trimAndRemoveDuplicatedWhitespacesFromString = (str: string) =>
|
||||
str.trim().replace(MULTIPLE_WHITESPACE_REGEX, ' ');
|
||||
Reference in New Issue
Block a user