CreateFieldInput transpilation to FlatFieldMetadata, FlatFieldMetadata validation (#13493)
# Introduction Following https://github.com/twentyhq/twenty/pull/13420 What has been done: - `CreateFieldInput` transpilation to `FlatFieldMetadata` - `FlatFieldMetadata` validator service - A lof of transpilation utils from `input` to `flatObject` or `flatField` - Created dedicated v2 api metadata services - Introducing `inferDeletionFromMissingObjectFieldIndex` in the builder, to avoid diffing every object and field of the current workspace we allow only generating create/update migration operations, usefull when passing by the api metadata ## We still need to in another PR: - Implement a strong unit test coverage and critical functions and services - Finalize flat field metadata validation exception for `options` `defaultValue` `settings` and `relations` - Finalize `flatObjectMetadata` validation and v2 service refactor - Plug the new service when feature flag is enabled
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
export class AggregateError extends Error {
|
||||
constructor(
|
||||
public readonly errors: Error[],
|
||||
message = 'Multiple errors occurred',
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'AggregateError';
|
||||
}
|
||||
}
|
||||
-6
@@ -118,9 +118,6 @@ export class FieldMetadataDTO<T extends FieldMetadataType = FieldMetadataType> {
|
||||
@Field({ nullable: true })
|
||||
isUnique?: boolean;
|
||||
|
||||
// TODO: This validator was not used anymore, and it is since graphql error hadling refactoring
|
||||
// it is adding extra load on the database, we are still validing inputs on field update and create
|
||||
// @Validate(IsFieldMetadataDefaultValue)
|
||||
@IsOptional()
|
||||
@Field(() => GraphQLJSON, { nullable: true })
|
||||
defaultValue?: FieldMetadataDefaultValue<T>;
|
||||
@@ -128,9 +125,6 @@ export class FieldMetadataDTO<T extends FieldMetadataType = FieldMetadataType> {
|
||||
@Transform(({ value }) =>
|
||||
transformEnumValue(value as FieldMetadataDefaultOption[]),
|
||||
)
|
||||
// TODO: This validator was not used anymore, and it is since graphql error hadling refactoring
|
||||
// it is adding extra load on the database, we are still validing inputs on field update and create
|
||||
// @Validate(IsFieldMetadataOptions)
|
||||
@IsOptional()
|
||||
@Field(() => GraphQLJSON, { nullable: true })
|
||||
options?: FieldMetadataOptions<T>;
|
||||
|
||||
+1
@@ -21,4 +21,5 @@ export enum FieldMetadataExceptionCode {
|
||||
FIELD_METADATA_RELATION_NOT_ENABLED = 'FIELD_METADATA_RELATION_NOT_ENABLED',
|
||||
FIELD_METADATA_RELATION_MALFORMED = 'FIELD_METADATA_RELATION_MALFORMED',
|
||||
LABEL_IDENTIFIER_FIELD_METADATA_ID_NOT_FOUND = 'LABEL_IDENTIFIER_FIELD_METADATA_ID_NOT_FOUND',
|
||||
UNCOVERED_FIELD_METADATA_TYPE_VALIDATION = 'UNCOVERED_FIELD_METADATA_TYPE_VALIDATION',
|
||||
}
|
||||
|
||||
+15
-42
@@ -1,8 +1,7 @@
|
||||
import { Injectable, ValidationError } from '@nestjs/common';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
import { IsEnum, IsString, IsUUID, validateOrReject } from 'class-validator';
|
||||
import { IsEnum, IsString, IsUUID } from 'class-validator';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
@@ -20,6 +19,7 @@ import {
|
||||
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
|
||||
import { computeRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-relation-field-join-column-name.util';
|
||||
import { prepareCustomFieldMetadataForCreation } from 'src/engine/metadata-modules/field-metadata/utils/prepare-field-metadata-for-creation.util';
|
||||
import { validateRelationCreationPayloadOrThrow } from 'src/engine/metadata-modules/field-metadata/utils/validate-relation-creation-payload.util';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-on-delete-action.type';
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
@@ -156,19 +156,17 @@ export class FieldMetadataRelationService {
|
||||
.relationCreationPayload,
|
||||
)
|
||||
) {
|
||||
validateFieldNameAvailabilityOrThrow(
|
||||
`${fieldMetadataInput.name}Id`,
|
||||
validateFieldNameAvailabilityOrThrow({
|
||||
name: `${fieldMetadataInput.name}Id`,
|
||||
objectMetadata,
|
||||
);
|
||||
});
|
||||
|
||||
const relationCreationPayload = (
|
||||
fieldMetadataInput as unknown as CreateFieldInput
|
||||
).relationCreationPayload;
|
||||
|
||||
if (isDefined(relationCreationPayload)) {
|
||||
await this.validateRelationCreationPayloadOrThrow(
|
||||
relationCreationPayload,
|
||||
);
|
||||
await validateRelationCreationPayloadOrThrow(relationCreationPayload);
|
||||
const computedMetadataNameFromLabel = computeMetadataNameFromLabel(
|
||||
relationCreationPayload.targetFieldLabel,
|
||||
);
|
||||
@@ -187,15 +185,15 @@ export class FieldMetadataRelationService {
|
||||
);
|
||||
}
|
||||
|
||||
validateFieldNameAvailabilityOrThrow(
|
||||
computedMetadataNameFromLabel,
|
||||
objectMetadataTarget,
|
||||
);
|
||||
validateFieldNameAvailabilityOrThrow({
|
||||
name: computedMetadataNameFromLabel,
|
||||
objectMetadata: objectMetadataTarget,
|
||||
});
|
||||
|
||||
validateFieldNameAvailabilityOrThrow(
|
||||
`${computedMetadataNameFromLabel}Id`,
|
||||
objectMetadataTarget,
|
||||
);
|
||||
validateFieldNameAvailabilityOrThrow({
|
||||
name: `${computedMetadataNameFromLabel}Id`,
|
||||
objectMetadata: objectMetadataTarget,
|
||||
});
|
||||
|
||||
if (
|
||||
computedMetadataNameFromLabel === fieldMetadataInput.name &&
|
||||
@@ -215,31 +213,6 @@ export class FieldMetadataRelationService {
|
||||
return fieldMetadataInput;
|
||||
}
|
||||
|
||||
private async validateRelationCreationPayloadOrThrow(
|
||||
relationCreationPayload: RelationCreationPayloadValidation,
|
||||
) {
|
||||
try {
|
||||
const relationCreationPayloadInstance = plainToInstance(
|
||||
RelationCreationPayloadValidation,
|
||||
relationCreationPayload,
|
||||
);
|
||||
|
||||
await validateOrReject(relationCreationPayloadInstance);
|
||||
} catch (error) {
|
||||
const errorMessages = Array.isArray(error)
|
||||
? error
|
||||
.map((err: ValidationError) => Object.values(err.constraints ?? {}))
|
||||
.flat()
|
||||
.join(', ')
|
||||
: error.message;
|
||||
|
||||
throw new FieldMetadataException(
|
||||
`Relation creation payload is invalid: ${errorMessages}`,
|
||||
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async findCachedFieldMetadataRelation(
|
||||
fieldMetadataItems: Array<
|
||||
Pick<
|
||||
|
||||
+3
-3
@@ -151,10 +151,10 @@ export class FieldMetadataValidationService {
|
||||
}
|
||||
|
||||
try {
|
||||
validateFieldNameAvailabilityOrThrow(
|
||||
fieldMetadataInput.name,
|
||||
validateFieldNameAvailabilityOrThrow({
|
||||
name: fieldMetadataInput.name,
|
||||
objectMetadata,
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof InvalidMetadataException) {
|
||||
throw new FieldMetadataException(
|
||||
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { AggregateError } from 'src/engine/core-modules/error/aggregate-error';
|
||||
import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { FlatFieldMetadataValidatorService } from 'src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-validator.service';
|
||||
import { fromCreateFieldInputToFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-create-field-input-to-flat-field-metadata.util';
|
||||
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { fromObjectMetadataMapsToFlatObjectMetadatas } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-object-metadata-maps-to-flat-object-metadatas.util';
|
||||
import { mergeTwoFlatObjectMetadatas } from 'src/engine/metadata-modules/flat-object-metadata/utils/merge-two-flat-object-metadatas.util';
|
||||
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
|
||||
import { WorkspaceMigrationBuilderV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-builder-v2.service';
|
||||
import { WorkspaceMigrationRunnerV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-migration-runner-v2.service';
|
||||
|
||||
@Injectable()
|
||||
export class FieldMetadataServiceV2 extends TypeOrmQueryService<FieldMetadataEntity> {
|
||||
constructor(
|
||||
@InjectRepository(FieldMetadataEntity, 'core')
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService,
|
||||
private readonly workspaceMigrationBuilderV2: WorkspaceMigrationBuilderV2Service,
|
||||
private readonly flatFieldMetadataValidatorService: FlatFieldMetadataValidatorService,
|
||||
private readonly workspaceMigrationRunnerV2Service: WorkspaceMigrationRunnerV2Service,
|
||||
) {
|
||||
super(fieldMetadataRepository);
|
||||
}
|
||||
|
||||
async createMany(
|
||||
fieldMetadataInputs: CreateFieldInput[],
|
||||
): Promise<FieldMetadataEntity[]> {
|
||||
if (!fieldMetadataInputs.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const workspaceId = fieldMetadataInputs[0].workspaceId;
|
||||
|
||||
const { objectMetadataMaps } =
|
||||
await this.workspaceMetadataCacheService.getExistingOrRecomputeMetadataMaps(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const existingFlatObjectMetadatas =
|
||||
fromObjectMetadataMapsToFlatObjectMetadatas(objectMetadataMaps);
|
||||
|
||||
let flatObjectMetadatasWithNewFields: FlatObjectMetadata[] = [];
|
||||
|
||||
for (const fieldMetadataInput of fieldMetadataInputs) {
|
||||
const createdFlatFieldsMetadataAndParentFlatObjectMetadata =
|
||||
await fromCreateFieldInputToFlatFieldMetadata({
|
||||
existingFlatObjectMetadatas,
|
||||
rawCreateFieldInput: fieldMetadataInput,
|
||||
});
|
||||
|
||||
const createdFlatFieldMetadataValidationResult = (
|
||||
await Promise.all(
|
||||
createdFlatFieldsMetadataAndParentFlatObjectMetadata.map(
|
||||
({ flatFieldMetadata: flatFieldMetadataToValidate }) =>
|
||||
this.flatFieldMetadataValidatorService.validateOneFlatFieldMetadata(
|
||||
{
|
||||
existingFlatObjectMetadatas,
|
||||
flatFieldMetadataToValidate,
|
||||
workspaceId,
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
).filter(isDefined);
|
||||
|
||||
if (createdFlatFieldMetadataValidationResult.length > 0) {
|
||||
const errors = createdFlatFieldMetadataValidationResult.map(
|
||||
(validationResult) => validationResult.error,
|
||||
);
|
||||
|
||||
throw new AggregateError(
|
||||
errors,
|
||||
'Multiple validation errors occurred while creating field',
|
||||
);
|
||||
}
|
||||
|
||||
const updatedFlatObjectMetadatas =
|
||||
createdFlatFieldsMetadataAndParentFlatObjectMetadata.map<FlatObjectMetadata>(
|
||||
({ flatFieldMetadata, parentFlatObjectMetadata }) => {
|
||||
return {
|
||||
...parentFlatObjectMetadata,
|
||||
flatFieldMetadatas: [
|
||||
...parentFlatObjectMetadata.flatFieldMetadatas,
|
||||
flatFieldMetadata,
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
flatObjectMetadatasWithNewFields = mergeTwoFlatObjectMetadatas({
|
||||
destFlatObjectMetadatas: flatObjectMetadatasWithNewFields,
|
||||
toMergeFlatObjectMetadatas: updatedFlatObjectMetadatas,
|
||||
});
|
||||
}
|
||||
|
||||
const workspaceMigration = this.workspaceMigrationBuilderV2.build({
|
||||
objectMetadataFromToInputs: {
|
||||
from: existingFlatObjectMetadatas,
|
||||
to: flatObjectMetadatasWithNewFields,
|
||||
},
|
||||
inferDeletionFromMissingObjectFieldIndex: false,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
await this.workspaceMigrationRunnerV2Service.run(workspaceMigration);
|
||||
|
||||
// const recomputedCache =
|
||||
// await this.workspaceMetadataCacheService.getExistingOrRecomputeMetadataMaps(
|
||||
// { workspaceId },
|
||||
// );
|
||||
|
||||
return []; //TODO to retrieve from cache or directly from find
|
||||
}
|
||||
}
|
||||
+8
-8
@@ -223,10 +223,10 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
existingFieldMetadata.isLabelSyncedWithName;
|
||||
|
||||
if (isLabelSyncedWithName) {
|
||||
validateNameAndLabelAreSyncOrThrow(
|
||||
fieldMetadataForUpdate.label ?? existingFieldMetadata.label,
|
||||
fieldMetadataForUpdate.name ?? existingFieldMetadata.name,
|
||||
);
|
||||
validateNameAndLabelAreSyncOrThrow({
|
||||
label: fieldMetadataForUpdate.label ?? existingFieldMetadata.label,
|
||||
name: fieldMetadataForUpdate.name ?? existingFieldMetadata.name,
|
||||
});
|
||||
}
|
||||
|
||||
await fieldMetadataRepository.update(id, fieldMetadataForUpdate);
|
||||
@@ -696,10 +696,10 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
}
|
||||
|
||||
if (fieldMetadataInput.isLabelSyncedWithName === true) {
|
||||
validateNameAndLabelAreSyncOrThrow(
|
||||
fieldMetadataInput.label,
|
||||
fieldMetadataInput.name,
|
||||
);
|
||||
validateNameAndLabelAreSyncOrThrow({
|
||||
label: fieldMetadataInput.label,
|
||||
name: fieldMetadataInput.name,
|
||||
});
|
||||
}
|
||||
|
||||
const fieldMetadataForCreate =
|
||||
|
||||
+1
@@ -29,6 +29,7 @@ export const fieldMetadataGraphqlApiExceptionHandler = (error: Error) => {
|
||||
case FieldMetadataExceptionCode.INTERNAL_SERVER_ERROR:
|
||||
case FieldMetadataExceptionCode.FIELD_METADATA_RELATION_NOT_ENABLED:
|
||||
case FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED:
|
||||
case FieldMetadataExceptionCode.UNCOVERED_FIELD_METADATA_TYPE_VALIDATION:
|
||||
case FieldMetadataExceptionCode.LABEL_IDENTIFIER_FIELD_METADATA_ID_NOT_FOUND:
|
||||
throw error;
|
||||
default: {
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
import { validateOrReject, ValidationError } from 'class-validator';
|
||||
|
||||
import {
|
||||
FieldMetadataException,
|
||||
FieldMetadataExceptionCode,
|
||||
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
|
||||
import { RelationCreationPayloadValidation } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-relation.service';
|
||||
|
||||
export const validateRelationCreationPayloadOrThrow = async (
|
||||
relationCreationPayload: RelationCreationPayloadValidation,
|
||||
) => {
|
||||
try {
|
||||
const relationCreationPayloadInstance = plainToInstance(
|
||||
RelationCreationPayloadValidation,
|
||||
relationCreationPayload,
|
||||
);
|
||||
|
||||
await validateOrReject(relationCreationPayloadInstance);
|
||||
} catch (error) {
|
||||
const errorMessages = Array.isArray(error)
|
||||
? error
|
||||
.map((err: ValidationError) => Object.values(err.constraints ?? {}))
|
||||
.flat()
|
||||
.join(', ')
|
||||
: error.message;
|
||||
|
||||
throw new FieldMetadataException(
|
||||
`Relation creation payload is invalid: ${errorMessages}`,
|
||||
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
);
|
||||
}
|
||||
};
|
||||
-4
@@ -13,13 +13,10 @@ type FlatFieldMetadataOverrides<
|
||||
export const getFlatFieldMetadataMock = <T extends FieldMetadataType>(
|
||||
overrides: FlatFieldMetadataOverrides<T>,
|
||||
): FlatFieldMetadata => {
|
||||
const createdAt = faker.date.anytime();
|
||||
|
||||
return {
|
||||
defaultValue: null,
|
||||
options: null,
|
||||
settings: null,
|
||||
createdAt,
|
||||
description: 'default flat field metadata description',
|
||||
icon: 'icon',
|
||||
id: faker.string.uuid(),
|
||||
@@ -33,7 +30,6 @@ export const getFlatFieldMetadataMock = <T extends FieldMetadataType>(
|
||||
isSystem: false,
|
||||
standardId: null,
|
||||
standardOverrides: null,
|
||||
updatedAt: createdAt,
|
||||
workspaceId: faker.string.uuid(),
|
||||
flatRelationTargetFieldMetadata: null,
|
||||
relationTargetFieldMetadataId: null,
|
||||
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
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 { 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';
|
||||
import {
|
||||
ObjectMetadataException,
|
||||
ObjectMetadataExceptionCode,
|
||||
} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
|
||||
import {
|
||||
InvalidMetadataException,
|
||||
InvalidMetadataExceptionCode,
|
||||
} from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception';
|
||||
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 = {
|
||||
existingFlatObjectMetadatas: FlatObjectMetadata[];
|
||||
othersFlatObjectMetadataToValidate?: FlatObjectMetadata[];
|
||||
flatFieldMetadataToValidate: FlatFieldMetadata;
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class FlatFieldMetadataValidatorService {
|
||||
constructor(private readonly featureFlagService: FeatureFlagService) {}
|
||||
|
||||
async validateOneFlatFieldMetadata({
|
||||
existingFlatObjectMetadatas,
|
||||
flatFieldMetadataToValidate,
|
||||
othersFlatObjectMetadataToValidate,
|
||||
workspaceId,
|
||||
}: ValidateOneFieldMetadataArgs): Promise<
|
||||
FailedFlatFieldMetadataValidation | undefined
|
||||
> {
|
||||
const allFlatObjectMetadata = [
|
||||
...existingFlatObjectMetadatas,
|
||||
...(othersFlatObjectMetadataToValidate ?? []),
|
||||
];
|
||||
const parentFlatObjectMetadata = allFlatObjectMetadata.find(
|
||||
(existingFlatObjectMetadata) =>
|
||||
existingFlatObjectMetadata.id ===
|
||||
flatFieldMetadataToValidate.objectMetadataId, // Question: Should we comparing unique identifier here ?
|
||||
);
|
||||
|
||||
if (!isDefined(parentFlatObjectMetadata)) {
|
||||
return {
|
||||
status: 'fail',
|
||||
error: new FieldMetadataException(
|
||||
isDefined(othersFlatObjectMetadataToValidate)
|
||||
? 'Object metadata does not exist in both existing and about to be created object metadatas'
|
||||
: 'Object metadata does not exist',
|
||||
FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
if (parentFlatObjectMetadata.isRemote === true) {
|
||||
return {
|
||||
status: 'fail',
|
||||
error: new ObjectMetadataException(
|
||||
'Remote objects are read-only',
|
||||
ObjectMetadataExceptionCode.OBJECT_MUTATION_NOT_ALLOWED,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
if (flatFieldMetadataToValidate.isLabelSyncedWithName) {
|
||||
const computedName = computeMetadataNameFromLabel(
|
||||
flatFieldMetadataToValidate.label,
|
||||
);
|
||||
|
||||
if (flatFieldMetadataToValidate.name !== computedName) {
|
||||
return {
|
||||
status: 'fail',
|
||||
error: 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(
|
||||
error.message,
|
||||
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
{
|
||||
userFriendlyMessage: error.userFriendlyMessage,
|
||||
},
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
const failedNameAvailabilityValidation =
|
||||
validateFlatFieldMetadataNameAvailability({
|
||||
name: flatFieldMetadataToValidate.name,
|
||||
objectMetadata: parentFlatObjectMetadata,
|
||||
});
|
||||
|
||||
if (isDefined(failedNameAvailabilityValidation)) {
|
||||
return failedNameAvailabilityValidation;
|
||||
}
|
||||
|
||||
// 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,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { FieldMetadataException } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
|
||||
import { ObjectMetadataException } from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
|
||||
import { InvalidMetadataException } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception';
|
||||
|
||||
export type FailedFlatFieldMetadataValidation = {
|
||||
status: 'fail';
|
||||
error:
|
||||
| FieldMetadataException
|
||||
| ObjectMetadataException
|
||||
| InvalidMetadataException;
|
||||
};
|
||||
+4
-1
@@ -16,7 +16,10 @@ export type FieldMetadataEntityRelationProperties =
|
||||
(typeof fieldMetadataRelationProperties)[number];
|
||||
|
||||
export type FlatFieldMetadata<T extends FieldMetadataType = FieldMetadataType> =
|
||||
Omit<FieldMetadataEntity<T>, FieldMetadataEntityRelationProperties> & {
|
||||
Omit<
|
||||
FieldMetadataEntity<T>,
|
||||
FieldMetadataEntityRelationProperties | 'createdAt' | 'updatedAt'
|
||||
> & {
|
||||
uniqueIdentifier: string;
|
||||
flatRelationTargetFieldMetadata: AssignTypeIfIsRelationFieldMetadataType<
|
||||
Omit<
|
||||
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import {
|
||||
assertUnreachable,
|
||||
isDefined,
|
||||
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties,
|
||||
} from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface';
|
||||
|
||||
import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
|
||||
import {
|
||||
FieldMetadataException,
|
||||
FieldMetadataExceptionCode,
|
||||
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
|
||||
import { generateRatingOptions } from 'src/engine/metadata-modules/field-metadata/utils/generate-rating-optionts.util';
|
||||
import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { fromRelationCreateFieldInputToFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-relation-create-field-input-to-flat-field-metadata.util';
|
||||
import { getDefaultFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/get-default-flat-field-metadata-from-create-field-input.util';
|
||||
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
|
||||
type FromCreateFieldInputToFlatObjectMetadata = {
|
||||
rawCreateFieldInput: CreateFieldInput;
|
||||
existingFlatObjectMetadatas: FlatObjectMetadata[];
|
||||
};
|
||||
export type FlatFieldMetadataAndParentFlatObjectMetadata<
|
||||
T extends FieldMetadataType = FieldMetadataType,
|
||||
> = {
|
||||
flatFieldMetadata: FlatFieldMetadata<T>;
|
||||
parentFlatObjectMetadata: FlatObjectMetadata;
|
||||
};
|
||||
|
||||
export const fromCreateFieldInputToFlatFieldMetadata = async ({
|
||||
existingFlatObjectMetadatas,
|
||||
rawCreateFieldInput,
|
||||
}: FromCreateFieldInputToFlatObjectMetadata): Promise<
|
||||
FlatFieldMetadataAndParentFlatObjectMetadata[]
|
||||
> => {
|
||||
if (rawCreateFieldInput.isRemoteCreation) {
|
||||
throw new FieldMetadataException(
|
||||
"Remote fields aren't supported",
|
||||
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
);
|
||||
}
|
||||
const createFieldInput =
|
||||
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
|
||||
rawCreateFieldInput,
|
||||
['description', 'icon', 'label', 'name', 'objectMetadataId', 'type'],
|
||||
);
|
||||
const parentFlatObjectMetadata = existingFlatObjectMetadatas.find(
|
||||
(existingFlatObjectMetadata) =>
|
||||
existingFlatObjectMetadata.id === createFieldInput.objectMetadataId,
|
||||
);
|
||||
|
||||
if (!isDefined(parentFlatObjectMetadata)) {
|
||||
throw new FieldMetadataException(
|
||||
'Provided object metadata id does not exist',
|
||||
FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
|
||||
{
|
||||
userFriendlyMessage:
|
||||
'Created field metadata, parent object metadata not found',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const fieldMetadataId = v4();
|
||||
const commonFlatFieldMetadata = getDefaultFlatFieldMetadata({
|
||||
createFieldInput,
|
||||
fieldMetadataId,
|
||||
});
|
||||
|
||||
switch (createFieldInput.type) {
|
||||
case FieldMetadataType.MORPH_RELATION: {
|
||||
throw new UserInputError(
|
||||
'Morph relation feature is not migrated to workspace migration v2 yet',
|
||||
);
|
||||
}
|
||||
case FieldMetadataType.RELATION: {
|
||||
return fromRelationCreateFieldInputToFlatFieldMetadata({
|
||||
existingFlatObjectMetadatas,
|
||||
sourceParentFlatObjectMetadata: parentFlatObjectMetadata,
|
||||
createFieldInput,
|
||||
});
|
||||
}
|
||||
case FieldMetadataType.RATING: {
|
||||
return [
|
||||
{
|
||||
flatFieldMetadata: {
|
||||
...commonFlatFieldMetadata,
|
||||
type: createFieldInput.type,
|
||||
settings: null,
|
||||
defaultValue: commonFlatFieldMetadata.defaultValue as string, // Could this be improved ?
|
||||
options: generateRatingOptions(),
|
||||
} satisfies FlatFieldMetadata<typeof createFieldInput.type>,
|
||||
parentFlatObjectMetadata,
|
||||
},
|
||||
];
|
||||
}
|
||||
case FieldMetadataType.SELECT:
|
||||
case FieldMetadataType.MULTI_SELECT: {
|
||||
const options = (createFieldInput?.options ?? []).map<
|
||||
FieldMetadataOptions<typeof createFieldInput.type>[number]
|
||||
>((option) => ({
|
||||
...trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
|
||||
option as FieldMetadataOptions<typeof createFieldInput.type>[number],
|
||||
['label', 'value', 'id', 'color'],
|
||||
),
|
||||
}));
|
||||
|
||||
return [
|
||||
{
|
||||
flatFieldMetadata: {
|
||||
...commonFlatFieldMetadata,
|
||||
type: createFieldInput.type,
|
||||
options,
|
||||
defaultValue: commonFlatFieldMetadata.defaultValue as string, // Could this be improved ?
|
||||
settings: null,
|
||||
} satisfies FlatFieldMetadata<typeof createFieldInput.type>,
|
||||
parentFlatObjectMetadata,
|
||||
},
|
||||
];
|
||||
}
|
||||
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.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 [
|
||||
{
|
||||
flatFieldMetadata: {
|
||||
...commonFlatFieldMetadata,
|
||||
type: createFieldInput.type,
|
||||
},
|
||||
parentFlatObjectMetadata,
|
||||
},
|
||||
];
|
||||
}
|
||||
default: {
|
||||
assertUnreachable(createFieldInput.type, 'Encountered an uncovered');
|
||||
}
|
||||
}
|
||||
};
|
||||
+18
-6
@@ -3,6 +3,7 @@ import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils';
|
||||
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import {
|
||||
FieldMetadataEntityRelationProperties,
|
||||
FlatFieldMetadata,
|
||||
fieldMetadataRelationProperties,
|
||||
} from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
@@ -26,11 +27,6 @@ export const fromFieldMetadataEntityToFlatFieldMetadata = <
|
||||
);
|
||||
}
|
||||
|
||||
const fieldMetadataWithoutRelations = removePropertiesFromRecord(
|
||||
fieldMetadataEntity,
|
||||
fieldMetadataRelationProperties,
|
||||
);
|
||||
|
||||
if (
|
||||
isFieldMetadataEntityOfType(
|
||||
fieldMetadataEntity,
|
||||
@@ -41,12 +37,20 @@ export const fromFieldMetadataEntityToFlatFieldMetadata = <
|
||||
FieldMetadataType.MORPH_RELATION,
|
||||
)
|
||||
) {
|
||||
const fieldMetadataWithoutRelations = removePropertiesFromRecord<
|
||||
FieldMetadataEntity<
|
||||
FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION
|
||||
>,
|
||||
FieldMetadataEntityRelationProperties
|
||||
>(fieldMetadataEntity, fieldMetadataRelationProperties);
|
||||
|
||||
const newDepth = isDefined(_depth) ? _depth + 1 : 1;
|
||||
const flatRelationTargetFieldMetadata =
|
||||
fromFieldMetadataEntityToFlatFieldMetadata(
|
||||
fieldMetadataEntity.relationTargetFieldMetadata,
|
||||
newDepth,
|
||||
);
|
||||
|
||||
const flatObjectTargetFieldMetadata =
|
||||
fromObjectMetadataEntityToFlatObjectMetadata(
|
||||
fieldMetadataEntity.relationTargetObjectMetadata,
|
||||
@@ -63,9 +67,17 @@ export const fromFieldMetadataEntityToFlatFieldMetadata = <
|
||||
fieldMetadataWithoutRelations.id,
|
||||
flatRelationTargetFieldMetadata,
|
||||
flatRelationTargetObjectMetadata,
|
||||
} as FlatFieldMetadata<FieldMetadataType.RELATION>;
|
||||
type: fieldMetadataEntity.type,
|
||||
} satisfies FlatFieldMetadata<
|
||||
FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION
|
||||
>;
|
||||
}
|
||||
|
||||
const fieldMetadataWithoutRelations = removePropertiesFromRecord(
|
||||
fieldMetadataEntity,
|
||||
fieldMetadataRelationProperties,
|
||||
);
|
||||
|
||||
return {
|
||||
...fieldMetadataWithoutRelations,
|
||||
uniqueIdentifier:
|
||||
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
|
||||
import {
|
||||
FieldMetadataException,
|
||||
FieldMetadataExceptionCode,
|
||||
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
|
||||
import { validateRelationCreationPayloadOrThrow } from 'src/engine/metadata-modules/field-metadata/utils/validate-relation-creation-payload.util';
|
||||
import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { FlatFieldMetadataAndParentFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-create-field-input-to-flat-field-metadata.util';
|
||||
import { getDefaultFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/get-default-flat-field-metadata-from-create-field-input.util';
|
||||
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { computeMetadataNameFromLabel } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util';
|
||||
|
||||
type FromRelationCreateFieldInputToFlatFieldMetadataArgs = {
|
||||
createFieldInput: CreateFieldInput;
|
||||
existingFlatObjectMetadatas: FlatObjectMetadata[];
|
||||
sourceParentFlatObjectMetadata: FlatObjectMetadata;
|
||||
};
|
||||
export const fromRelationCreateFieldInputToFlatFieldMetadata = async ({
|
||||
existingFlatObjectMetadatas,
|
||||
sourceParentFlatObjectMetadata,
|
||||
createFieldInput,
|
||||
}: FromRelationCreateFieldInputToFlatFieldMetadataArgs): Promise<
|
||||
FlatFieldMetadataAndParentFlatObjectMetadata[]
|
||||
> => {
|
||||
const { relationCreationPayload } = createFieldInput;
|
||||
|
||||
if (!isDefined(relationCreationPayload)) {
|
||||
throw new FieldMetadataException(
|
||||
`Relation creation payload is required`,
|
||||
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
);
|
||||
}
|
||||
await validateRelationCreationPayloadOrThrow(relationCreationPayload);
|
||||
|
||||
const targetParentFlatObjectMetadata = existingFlatObjectMetadatas.find(
|
||||
(existingFlatObject) =>
|
||||
existingFlatObject.id === relationCreationPayload.targetObjectMetadataId,
|
||||
);
|
||||
|
||||
if (!isDefined(targetParentFlatObjectMetadata)) {
|
||||
throw new FieldMetadataException(
|
||||
`Object metadata relation target not found for relation creation payload`,
|
||||
FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED,
|
||||
);
|
||||
}
|
||||
|
||||
const targetRelationTargetFieldMetadataId = v4();
|
||||
const sourceRelationTargetFieldMetadataId = v4();
|
||||
const sourceFlatFieldMetadata: Omit<
|
||||
FlatFieldMetadata<FieldMetadataType.RELATION>,
|
||||
'flatRelationTargetFieldMetadata'
|
||||
> = {
|
||||
...getDefaultFlatFieldMetadata({
|
||||
createFieldInput,
|
||||
fieldMetadataId: sourceRelationTargetFieldMetadataId,
|
||||
}),
|
||||
type: FieldMetadataType.RELATION,
|
||||
defaultValue: null,
|
||||
settings: null,
|
||||
options: null,
|
||||
relationTargetFieldMetadataId: targetRelationTargetFieldMetadataId, // Note: this won't work until we enable deferred transaction
|
||||
relationTargetObjectMetadataId: targetParentFlatObjectMetadata.id,
|
||||
flatRelationTargetObjectMetadata: targetParentFlatObjectMetadata,
|
||||
};
|
||||
|
||||
const targetFlatFieldMetadata: FlatFieldMetadata<FieldMetadataType.RELATION> =
|
||||
{
|
||||
...getDefaultFlatFieldMetadata({
|
||||
createFieldInput: {
|
||||
icon: relationCreationPayload.targetFieldIcon,
|
||||
label: relationCreationPayload.targetFieldLabel,
|
||||
name: `${computeMetadataNameFromLabel(
|
||||
relationCreationPayload.targetFieldLabel,
|
||||
)}Id`,
|
||||
objectMetadataId: targetParentFlatObjectMetadata.id,
|
||||
type: FieldMetadataType.RELATION,
|
||||
workspaceId: createFieldInput.workspaceId,
|
||||
},
|
||||
fieldMetadataId: targetRelationTargetFieldMetadataId,
|
||||
}),
|
||||
type: FieldMetadataType.RELATION,
|
||||
defaultValue: null,
|
||||
settings: null,
|
||||
options: null,
|
||||
relationTargetFieldMetadataId: sourceRelationTargetFieldMetadataId,
|
||||
relationTargetObjectMetadataId: sourceParentFlatObjectMetadata.id,
|
||||
flatRelationTargetFieldMetadata: sourceFlatFieldMetadata,
|
||||
flatRelationTargetObjectMetadata: sourceParentFlatObjectMetadata,
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
flatFieldMetadata: {
|
||||
...sourceFlatFieldMetadata,
|
||||
flatRelationTargetFieldMetadata: targetFlatFieldMetadata,
|
||||
},
|
||||
parentFlatObjectMetadata: sourceParentFlatObjectMetadata,
|
||||
},
|
||||
{
|
||||
flatFieldMetadata: targetFlatFieldMetadata,
|
||||
parentFlatObjectMetadata: targetParentFlatObjectMetadata,
|
||||
},
|
||||
] satisfies FlatFieldMetadataAndParentFlatObjectMetadata<FieldMetadataType.RELATION>[];
|
||||
};
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
import { sanitizeObjectStringFields } from 'twenty-shared/utils';
|
||||
|
||||
import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
|
||||
import { generateNullable } from 'src/engine/metadata-modules/field-metadata/utils/generate-nullable';
|
||||
import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
|
||||
type GetDefaultFlatFieldMetadataArgs = {
|
||||
fieldMetadataId: string;
|
||||
createFieldInput: CreateFieldInput;
|
||||
};
|
||||
export const getDefaultFlatFieldMetadata = ({
|
||||
createFieldInput,
|
||||
fieldMetadataId,
|
||||
}: GetDefaultFlatFieldMetadataArgs) => {
|
||||
const { defaultValue, settings } = sanitizeObjectStringFields(
|
||||
createFieldInput,
|
||||
['defaultValue', 'settings'],
|
||||
);
|
||||
|
||||
return {
|
||||
description: createFieldInput.description ?? null,
|
||||
id: fieldMetadataId,
|
||||
icon: createFieldInput.icon ?? null,
|
||||
isActive: true,
|
||||
isCustom: true,
|
||||
isLabelSyncedWithName: createFieldInput.isLabelSyncedWithName ?? false,
|
||||
isNullable: generateNullable(
|
||||
createFieldInput.type,
|
||||
createFieldInput.isNullable,
|
||||
createFieldInput.isRemoteCreation,
|
||||
),
|
||||
isSystem: false,
|
||||
isUnique: createFieldInput.isUnique ?? null,
|
||||
label: createFieldInput.label ?? null,
|
||||
name: createFieldInput.name ?? null,
|
||||
objectMetadataId: createFieldInput.objectMetadataId,
|
||||
relationTargetFieldMetadataId: null,
|
||||
relationTargetObjectMetadataId: null,
|
||||
standardId: null,
|
||||
standardOverrides: null,
|
||||
type: createFieldInput.type,
|
||||
uniqueIdentifier: fieldMetadataId,
|
||||
workspaceId: createFieldInput.workspaceId,
|
||||
flatRelationTargetFieldMetadata: null,
|
||||
flatRelationTargetObjectMetadata: null,
|
||||
options: null,
|
||||
defaultValue: defaultValue ?? null,
|
||||
settings: settings ?? null,
|
||||
} as const satisfies FlatFieldMetadata;
|
||||
};
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
|
||||
import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { FailedFlatFieldMetadataValidation } from 'src/engine/metadata-modules/flat-field-metadata/types/failed-flat-field-metadata-validation.type';
|
||||
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import {
|
||||
InvalidMetadataException,
|
||||
InvalidMetadataExceptionCode,
|
||||
} from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception';
|
||||
|
||||
const getReservedCompositeFieldNames = (
|
||||
flatObjectMetadata: FlatObjectMetadata,
|
||||
): string[] => {
|
||||
return flatObjectMetadata.flatFieldMetadatas.flatMap((flatFieldMetadata) => {
|
||||
if (isCompositeFieldMetadataType(flatFieldMetadata.type)) {
|
||||
const base = flatFieldMetadata.name;
|
||||
const compositeType = compositeTypeDefinitions.get(
|
||||
flatFieldMetadata.type,
|
||||
);
|
||||
|
||||
if (!isDefined(compositeType)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return compositeType.properties.map((property) =>
|
||||
computeCompositeColumnName(base, property),
|
||||
);
|
||||
}
|
||||
|
||||
return [];
|
||||
});
|
||||
};
|
||||
|
||||
export const validateFlatFieldMetadataNameAvailability = ({
|
||||
name,
|
||||
objectMetadata,
|
||||
}: {
|
||||
name: string;
|
||||
objectMetadata: FlatObjectMetadata;
|
||||
}): FailedFlatFieldMetadataValidation | undefined => {
|
||||
const reservedCompositeFieldsNames =
|
||||
getReservedCompositeFieldNames(objectMetadata);
|
||||
|
||||
if (
|
||||
objectMetadata.flatFieldMetadatas.some(
|
||||
(field) =>
|
||||
field.name === name ||
|
||||
(field.type === FieldMetadataType.RELATION && // Question: Should we also look for MORPH_RELATION field types ?
|
||||
`${field.name}Id` === name),
|
||||
)
|
||||
) {
|
||||
return {
|
||||
status: 'fail',
|
||||
error: new InvalidMetadataException(
|
||||
`Name "${name}" is not available as it is already used by another field`,
|
||||
InvalidMetadataExceptionCode.NOT_AVAILABLE,
|
||||
{
|
||||
userFriendlyMessage: t`This name is not available as it is already used by another field`,
|
||||
},
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
if (reservedCompositeFieldsNames.includes(name)) {
|
||||
return {
|
||||
status: 'fail',
|
||||
error: new InvalidMetadataException(
|
||||
`Name "${name}" is not available`,
|
||||
InvalidMetadataExceptionCode.RESERVED_KEYWORD,
|
||||
{
|
||||
userFriendlyMessage: t`This name is not available.`,
|
||||
},
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
-6
@@ -9,15 +9,10 @@ type FlatObjectMetadataOverrides = Required<
|
||||
export const getFlatObjectMetadataMock = (
|
||||
overrides: FlatObjectMetadataOverrides,
|
||||
): FlatObjectMetadata => {
|
||||
const createdAt = faker.date.anytime();
|
||||
|
||||
return {
|
||||
flatFieldMetadatas: [],
|
||||
flatIndexMetadatas: [],
|
||||
createdAt,
|
||||
dataSourceId: faker.string.uuid(),
|
||||
description: 'default flat object metadata description',
|
||||
duplicateCriteria: [],
|
||||
icon: 'icon',
|
||||
id: faker.string.uuid(),
|
||||
imageIdentifierFieldMetadataId: faker.string.uuid(),
|
||||
@@ -37,7 +32,6 @@ export const getFlatObjectMetadataMock = (
|
||||
standardId: null,
|
||||
standardOverrides: null,
|
||||
targetTableName: '',
|
||||
updatedAt: createdAt,
|
||||
workspaceId: faker.string.uuid(),
|
||||
...overrides,
|
||||
};
|
||||
|
||||
+5
-1
@@ -20,7 +20,11 @@ type ObjectMetadataRelationProperties = ExtractRecordTypeOrmRelationProperties<
|
||||
|
||||
export type FlatObjectMetadata = Omit<
|
||||
ObjectMetadataEntity,
|
||||
ObjectMetadataRelationProperties
|
||||
| ObjectMetadataRelationProperties
|
||||
| 'dataSourceId'
|
||||
| 'createdAt'
|
||||
| 'updatedAt'
|
||||
| 'duplicateCriteria'
|
||||
> & {
|
||||
uniqueIdentifier: string;
|
||||
flatIndexMetadatas: FlatIndexMetadata[];
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ export type FlatObjectMetadataPropertiesToCompare =
|
||||
(typeof flatObjectMetadataPropertiesToCompare)[number];
|
||||
|
||||
/**
|
||||
* This comparator handles update on colliding uniqueIdentifier flatObjectdMetadata
|
||||
* This comparator handles update on colliding uniqueIdentifier flatObjectMetadata
|
||||
*/
|
||||
export const compareTwoFlatObjectMetadata = ({
|
||||
from,
|
||||
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
import { trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input';
|
||||
import {
|
||||
ObjectMetadataException,
|
||||
ObjectMetadataExceptionCode,
|
||||
} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
|
||||
import { buildDefaultFlatFieldMetadataForCustomObject } from 'src/engine/metadata-modules/object-metadata/utils/build-default-fields-for-custom-object.util';
|
||||
|
||||
export const fromCreateObjectInputToFlatObjectMetadata = (
|
||||
rawCreateObjectInput: CreateObjectInput,
|
||||
): FlatObjectMetadata => {
|
||||
if (rawCreateObjectInput.isRemote) {
|
||||
throw new ObjectMetadataException(
|
||||
'Remote objects are not supported',
|
||||
ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
const createObjectInput =
|
||||
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
|
||||
rawCreateObjectInput,
|
||||
[
|
||||
'description',
|
||||
'icon',
|
||||
'labelPlural',
|
||||
'labelSingular',
|
||||
'namePlural',
|
||||
'nameSingular',
|
||||
'shortcut',
|
||||
],
|
||||
);
|
||||
|
||||
const objectMetadataId = v4();
|
||||
const baseCustomFlatFieldMetadatas =
|
||||
buildDefaultFlatFieldMetadataForCustomObject({
|
||||
objectMetadataId,
|
||||
workspaceId: createObjectInput.workspaceId,
|
||||
});
|
||||
|
||||
return {
|
||||
description: createObjectInput.description ?? null,
|
||||
flatFieldMetadatas: Object.values(baseCustomFlatFieldMetadatas),
|
||||
flatIndexMetadatas: [],
|
||||
icon: createObjectInput.icon ?? null,
|
||||
id: objectMetadataId,
|
||||
imageIdentifierFieldMetadataId: null,
|
||||
isActive: true,
|
||||
isAuditLogged: true,
|
||||
isCustom: true,
|
||||
isLabelSyncedWithName: createObjectInput.isLabelSyncedWithName ?? false,
|
||||
isRemote: false,
|
||||
isSearchable: true,
|
||||
isSystem: false,
|
||||
labelIdentifierFieldMetadataId: baseCustomFlatFieldMetadatas.nameField.id,
|
||||
labelPlural: createObjectInput.labelPlural ?? null,
|
||||
labelSingular: createObjectInput.labelSingular ?? null,
|
||||
namePlural: createObjectInput.namePlural ?? null,
|
||||
nameSingular: createObjectInput.nameSingular ?? null,
|
||||
shortcut: createObjectInput.shortcut ?? null,
|
||||
standardId: null,
|
||||
standardOverrides: null,
|
||||
uniqueIdentifier: objectMetadataId,
|
||||
targetTableName: 'DEPRECATED',
|
||||
workspaceId: createObjectInput.workspaceId,
|
||||
};
|
||||
};
|
||||
+1
-1
@@ -8,7 +8,7 @@ import { mergeTwoFlatFieldMetadatas } from 'src/engine/metadata-modules/flat-fie
|
||||
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { ToMerge } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/to-merge.type';
|
||||
|
||||
export const mergeTwoFlatFieldObjectMetadatas = ({
|
||||
export const mergeTwoFlatObjectMetadatas = ({
|
||||
destFlatObjectMetadatas,
|
||||
toMergeFlatObjectMetadatas,
|
||||
}: ToMerge<FlatObjectMetadata[], 'object'>): FlatObjectMetadata[] => {
|
||||
|
||||
-67
@@ -4,16 +4,11 @@ import { BeforeCreateOne } from '@ptc-org/nestjs-query-graphql';
|
||||
import { IsBoolean, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
import GraphQLJSON from 'graphql-type-json';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
|
||||
|
||||
import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { IsValidMetadataName } from 'src/engine/decorators/metadata/is-valid-metadata-name.decorator';
|
||||
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { BeforeCreateOneObject } from 'src/engine/metadata-modules/object-metadata/hooks/before-create-one-object.hook';
|
||||
import { buildDefaultFlatFieldMetadataForCustomObject } from 'src/engine/metadata-modules/object-metadata/utils/build-default-fields-for-custom-object.util';
|
||||
|
||||
@InputType()
|
||||
@BeforeCreateOne(BeforeCreateOneObject)
|
||||
@@ -79,65 +74,3 @@ export class CreateObjectInput {
|
||||
@Field({ nullable: true }) // Not nullable to me
|
||||
isLabelSyncedWithName?: boolean;
|
||||
}
|
||||
|
||||
export const fromCreateObjectInputToFlatObjectMetadata = (
|
||||
rawCreateObjectInput: CreateObjectInput,
|
||||
): FlatObjectMetadata => {
|
||||
if (rawCreateObjectInput.isRemote) {
|
||||
throw new UserInputError('Remote objects are not supported yet');
|
||||
}
|
||||
|
||||
const createObjectInput =
|
||||
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
|
||||
rawCreateObjectInput,
|
||||
[
|
||||
'description',
|
||||
'icon',
|
||||
'labelPlural',
|
||||
'labelSingular',
|
||||
'namePlural',
|
||||
'nameSingular',
|
||||
'shortcut',
|
||||
],
|
||||
);
|
||||
|
||||
const objectMetadataId = v4();
|
||||
const createdAt = new Date();
|
||||
const baseCustomFlatFieldMetadatas =
|
||||
buildDefaultFlatFieldMetadataForCustomObject({
|
||||
createdAt,
|
||||
objectMetadataId,
|
||||
workspaceId: createObjectInput.workspaceId,
|
||||
});
|
||||
|
||||
return {
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
dataSourceId: createObjectInput.dataSourceId, // TODO is it enough ?
|
||||
description: createObjectInput.description ?? null,
|
||||
duplicateCriteria: [], // TODO is it enough ?
|
||||
flatFieldMetadatas: Object.values(baseCustomFlatFieldMetadatas),
|
||||
flatIndexMetadatas: [], // TODO is it enough ?
|
||||
icon: createObjectInput.icon ?? null,
|
||||
id: objectMetadataId,
|
||||
imageIdentifierFieldMetadataId: null,
|
||||
isActive: true,
|
||||
isAuditLogged: true,
|
||||
isCustom: true,
|
||||
isLabelSyncedWithName: createObjectInput.isLabelSyncedWithName ?? false,
|
||||
isRemote: false,
|
||||
isSearchable: true,
|
||||
isSystem: false,
|
||||
labelIdentifierFieldMetadataId: baseCustomFlatFieldMetadatas.nameField.id,
|
||||
labelPlural: createObjectInput.labelPlural ?? null,
|
||||
labelSingular: createObjectInput.labelSingular ?? null,
|
||||
namePlural: createObjectInput.namePlural ?? null,
|
||||
nameSingular: createObjectInput.nameSingular ?? null,
|
||||
shortcut: createObjectInput.shortcut ?? null,
|
||||
standardId: null,
|
||||
standardOverrides: null,
|
||||
uniqueIdentifier: objectMetadataId,
|
||||
targetTableName: 'DEPRECATED',
|
||||
workspaceId: createObjectInput.workspaceId,
|
||||
};
|
||||
};
|
||||
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { fromCreateObjectInputToFlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-create-object-input-to-flat-object-metadata.util';
|
||||
import { fromObjectMetadataMapsToFlatObjectMetadatas } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-object-metadata-maps-to-flat-object-metadatas.util';
|
||||
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
|
||||
import { WorkspaceMigrationBuilderV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-builder-v2.service';
|
||||
import { WorkspaceMigrationRunnerV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-migration-runner-v2.service';
|
||||
|
||||
import { ObjectMetadataEntity } from './object-metadata.entity';
|
||||
|
||||
import { CreateObjectInput } from './dtos/create-object.input';
|
||||
|
||||
@Injectable()
|
||||
export class ObjectMetadataServiceV2 extends TypeOrmQueryService<ObjectMetadataEntity> {
|
||||
constructor(
|
||||
@InjectRepository(ObjectMetadataEntity, 'core')
|
||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||
|
||||
private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService,
|
||||
private readonly workspaceMigrationBuilderV2: WorkspaceMigrationBuilderV2Service,
|
||||
private readonly workspaceMigrationRunnerV2Service: WorkspaceMigrationRunnerV2Service,
|
||||
) {
|
||||
super(objectMetadataRepository);
|
||||
}
|
||||
|
||||
override async createOne(
|
||||
objectMetadataInput: CreateObjectInput,
|
||||
): Promise<ObjectMetadataEntity> {
|
||||
const { objectMetadataMaps } =
|
||||
await this.workspaceMetadataCacheService.getExistingOrRecomputeMetadataMaps(
|
||||
{
|
||||
workspaceId: objectMetadataInput.workspaceId,
|
||||
},
|
||||
);
|
||||
|
||||
const createdRawFlatObjectMetadata =
|
||||
fromCreateObjectInputToFlatObjectMetadata(objectMetadataInput);
|
||||
const existingFlatObjectMetadatas =
|
||||
fromObjectMetadataMapsToFlatObjectMetadatas(objectMetadataMaps);
|
||||
// @ts-expect-error TODO implement validateFlatObjectMetadataData
|
||||
const createdFlatObjectMetadata = validateFlatObjectMetadataData({
|
||||
existing:
|
||||
// Here we assume that EVERYTHING is in cache and up to date, this is very critical, also race condition prone :thinking:
|
||||
fromObjectMetadataMapsToFlatObjectMetadatas(objectMetadataMaps),
|
||||
toValidate: [createdRawFlatObjectMetadata],
|
||||
});
|
||||
|
||||
const workspaceMigration = this.workspaceMigrationBuilderV2.build({
|
||||
objectMetadataFromToInputs: {
|
||||
from: existingFlatObjectMetadatas,
|
||||
to: [createdFlatObjectMetadata],
|
||||
},
|
||||
inferDeletionFromMissingObjectFieldIndex: false,
|
||||
workspaceId: objectMetadataInput.workspaceId,
|
||||
});
|
||||
|
||||
await this.workspaceMigrationRunnerV2Service.run(workspaceMigration);
|
||||
|
||||
return createdFlatObjectMetadata; // TODO retrieve from cache
|
||||
}
|
||||
}
|
||||
+23
-28
@@ -17,8 +17,8 @@ import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/featu
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { fromCreateObjectInputToFlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-create-object-input-to-flat-object-metadata.util';
|
||||
import { fromObjectMetadataMapsToFlatObjectMetadatas } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-object-metadata-maps-to-flat-object-metadatas.util';
|
||||
import { mergeTwoFlatFieldObjectMetadatas } from 'src/engine/metadata-modules/flat-object-metadata/utils/merge-two-flat-object-metadatas.util';
|
||||
import { IndexMetadataService } from 'src/engine/metadata-modules/index-metadata/index-metadata.service';
|
||||
import { DeleteOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/delete-object.input';
|
||||
import {
|
||||
@@ -57,10 +57,7 @@ import { isSearchableFieldType } from 'src/engine/workspace-manager/workspace-sy
|
||||
|
||||
import { ObjectMetadataEntity } from './object-metadata.entity';
|
||||
|
||||
import {
|
||||
CreateObjectInput,
|
||||
fromCreateObjectInputToFlatObjectMetadata,
|
||||
} from './dtos/create-object.input';
|
||||
import { CreateObjectInput } from './dtos/create-object.input';
|
||||
|
||||
@Injectable()
|
||||
export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEntity> {
|
||||
@@ -147,18 +144,16 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
toValidate: [createdRawFlatObjectMetadata],
|
||||
});
|
||||
|
||||
const workpsaceMigration = this.workspaceMigrationBuilderV2.build({
|
||||
const workspaceMigration = this.workspaceMigrationBuilderV2.build({
|
||||
objectMetadataFromToInputs: {
|
||||
from: existingFlatObjectMetadatas,
|
||||
to: mergeTwoFlatFieldObjectMetadatas({
|
||||
destFlatObjectMetadatas: existingFlatObjectMetadatas,
|
||||
toMergeFlatObjectMetadatas: [createdFlatObjectMetadata],
|
||||
}),
|
||||
to: [createdFlatObjectMetadata],
|
||||
},
|
||||
workspaceId: objectMetadataInput.workspaceId, // Where does this comes from ?
|
||||
inferDeletionFromMissingObjectFieldIndex: false,
|
||||
workspaceId: objectMetadataInput.workspaceId,
|
||||
});
|
||||
|
||||
await this.workspaceMigrationRunnerV2Service.run(workpsaceMigration);
|
||||
await this.workspaceMigrationRunnerV2Service.run(workspaceMigration);
|
||||
|
||||
// What to return exactly ? We now won't have access to the entity directly
|
||||
// We could still retrieve it afterwards using a find on object metadata id or return a flat now
|
||||
@@ -193,14 +188,14 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
});
|
||||
|
||||
if (objectMetadataInput.isLabelSyncedWithName === true) {
|
||||
validateNameAndLabelAreSyncOrThrow(
|
||||
objectMetadataInput.labelSingular,
|
||||
objectMetadataInput.nameSingular,
|
||||
);
|
||||
validateNameAndLabelAreSyncOrThrow(
|
||||
objectMetadataInput.labelPlural,
|
||||
objectMetadataInput.namePlural,
|
||||
);
|
||||
validateNameAndLabelAreSyncOrThrow({
|
||||
label: objectMetadataInput.labelSingular,
|
||||
name: objectMetadataInput.nameSingular,
|
||||
});
|
||||
validateNameAndLabelAreSyncOrThrow({
|
||||
label: objectMetadataInput.labelPlural,
|
||||
name: objectMetadataInput.namePlural,
|
||||
});
|
||||
}
|
||||
|
||||
validatesNoOtherObjectWithSameNameExistsOrThrows({
|
||||
@@ -358,14 +353,14 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
objectMetadataMaps,
|
||||
});
|
||||
if (existingObjectMetadataCombinedWithUpdateInput.isLabelSyncedWithName) {
|
||||
validateNameAndLabelAreSyncOrThrow(
|
||||
existingObjectMetadataCombinedWithUpdateInput.labelSingular,
|
||||
existingObjectMetadataCombinedWithUpdateInput.nameSingular,
|
||||
);
|
||||
validateNameAndLabelAreSyncOrThrow(
|
||||
existingObjectMetadataCombinedWithUpdateInput.labelPlural,
|
||||
existingObjectMetadataCombinedWithUpdateInput.namePlural,
|
||||
);
|
||||
validateNameAndLabelAreSyncOrThrow({
|
||||
label: existingObjectMetadataCombinedWithUpdateInput.labelSingular,
|
||||
name: existingObjectMetadataCombinedWithUpdateInput.nameSingular,
|
||||
});
|
||||
validateNameAndLabelAreSyncOrThrow({
|
||||
label: existingObjectMetadataCombinedWithUpdateInput.labelPlural,
|
||||
name: existingObjectMetadataCombinedWithUpdateInput.namePlural,
|
||||
});
|
||||
}
|
||||
if (
|
||||
isDefined(inputPayload.nameSingular) ||
|
||||
|
||||
+2
-18
@@ -117,25 +117,21 @@ export const buildDefaultFieldsForCustomObject = (
|
||||
];
|
||||
|
||||
type BuildDefaultFlatFieldMetadataForCustomObjectArgs = {
|
||||
createdAt: Date;
|
||||
workspaceId: string;
|
||||
objectMetadataId: string;
|
||||
};
|
||||
|
||||
export const buildDefaultFlatFieldMetadataForCustomObject = ({
|
||||
createdAt,
|
||||
workspaceId,
|
||||
objectMetadataId,
|
||||
}: BuildDefaultFlatFieldMetadataForCustomObjectArgs) => {
|
||||
const idField: FlatFieldMetadata<FieldMetadataType.UUID> = {
|
||||
type: FieldMetadataType.UUID,
|
||||
createdAt,
|
||||
id: v4(),
|
||||
isLabelSyncedWithName: false, // TO CHECK
|
||||
isUnique: true, // Was false before but unsure if normal ?
|
||||
isLabelSyncedWithName: false,
|
||||
isUnique: true,
|
||||
objectMetadataId,
|
||||
uniqueIdentifier: BASE_OBJECT_STANDARD_FIELD_IDS.id,
|
||||
updatedAt: createdAt,
|
||||
workspaceId,
|
||||
standardId: BASE_OBJECT_STANDARD_FIELD_IDS.id,
|
||||
name: 'id',
|
||||
@@ -159,13 +155,11 @@ export const buildDefaultFlatFieldMetadataForCustomObject = ({
|
||||
|
||||
const nameField: FlatFieldMetadata<FieldMetadataType.TEXT> = {
|
||||
type: FieldMetadataType.TEXT,
|
||||
createdAt,
|
||||
id: v4(),
|
||||
isLabelSyncedWithName: false,
|
||||
isUnique: false,
|
||||
objectMetadataId,
|
||||
uniqueIdentifier: CUSTOM_OBJECT_STANDARD_FIELD_IDS.name,
|
||||
updatedAt: createdAt,
|
||||
workspaceId,
|
||||
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.name,
|
||||
name: 'name',
|
||||
@@ -189,13 +183,11 @@ export const buildDefaultFlatFieldMetadataForCustomObject = ({
|
||||
|
||||
const createdAtField: FlatFieldMetadata<FieldMetadataType.DATE_TIME> = {
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
createdAt,
|
||||
id: v4(),
|
||||
isLabelSyncedWithName: false,
|
||||
isUnique: false,
|
||||
objectMetadataId,
|
||||
uniqueIdentifier: BASE_OBJECT_STANDARD_FIELD_IDS.createdAt,
|
||||
updatedAt: createdAt,
|
||||
workspaceId,
|
||||
standardId: BASE_OBJECT_STANDARD_FIELD_IDS.createdAt,
|
||||
name: 'createdAt',
|
||||
@@ -219,13 +211,11 @@ export const buildDefaultFlatFieldMetadataForCustomObject = ({
|
||||
|
||||
const updatedAtField: FlatFieldMetadata<FieldMetadataType.DATE_TIME> = {
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
createdAt,
|
||||
id: v4(),
|
||||
isLabelSyncedWithName: false,
|
||||
isUnique: false,
|
||||
objectMetadataId,
|
||||
uniqueIdentifier: BASE_OBJECT_STANDARD_FIELD_IDS.updatedAt,
|
||||
updatedAt: createdAt,
|
||||
workspaceId,
|
||||
standardId: BASE_OBJECT_STANDARD_FIELD_IDS.updatedAt,
|
||||
name: 'updatedAt',
|
||||
@@ -249,13 +239,11 @@ export const buildDefaultFlatFieldMetadataForCustomObject = ({
|
||||
|
||||
const deletedAtField: FlatFieldMetadata<FieldMetadataType.DATE_TIME> = {
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
createdAt,
|
||||
id: v4(),
|
||||
isLabelSyncedWithName: false,
|
||||
isUnique: false,
|
||||
objectMetadataId,
|
||||
uniqueIdentifier: BASE_OBJECT_STANDARD_FIELD_IDS.deletedAt,
|
||||
updatedAt: createdAt,
|
||||
workspaceId,
|
||||
standardId: BASE_OBJECT_STANDARD_FIELD_IDS.deletedAt,
|
||||
name: 'deletedAt',
|
||||
@@ -279,13 +267,11 @@ export const buildDefaultFlatFieldMetadataForCustomObject = ({
|
||||
|
||||
const createdByField: FlatFieldMetadata<FieldMetadataType.ACTOR> = {
|
||||
type: FieldMetadataType.ACTOR,
|
||||
createdAt,
|
||||
id: v4(),
|
||||
isLabelSyncedWithName: false,
|
||||
isUnique: false,
|
||||
objectMetadataId,
|
||||
uniqueIdentifier: CUSTOM_OBJECT_STANDARD_FIELD_IDS.createdBy,
|
||||
updatedAt: createdAt,
|
||||
workspaceId,
|
||||
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.createdBy,
|
||||
name: 'createdBy',
|
||||
@@ -309,13 +295,11 @@ export const buildDefaultFlatFieldMetadataForCustomObject = ({
|
||||
|
||||
const positionField: FlatFieldMetadata<FieldMetadataType.POSITION> = {
|
||||
type: FieldMetadataType.POSITION,
|
||||
createdAt,
|
||||
id: v4(),
|
||||
isLabelSyncedWithName: false,
|
||||
isUnique: false,
|
||||
objectMetadataId,
|
||||
uniqueIdentifier: CUSTOM_OBJECT_STANDARD_FIELD_IDS.position,
|
||||
updatedAt: createdAt,
|
||||
workspaceId,
|
||||
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.position,
|
||||
name: 'position',
|
||||
|
||||
+8
-8
@@ -62,17 +62,17 @@ describe('validateFieldNameAvailabilityOrThrow', () => {
|
||||
({ context: { input, shouldNotThrow } }) => {
|
||||
if (shouldNotThrow) {
|
||||
expect(() =>
|
||||
validateFieldNameAvailabilityOrThrow(
|
||||
input,
|
||||
objectMetadataMapItemMock,
|
||||
),
|
||||
validateFieldNameAvailabilityOrThrow({
|
||||
name: input,
|
||||
objectMetadata: objectMetadataMapItemMock,
|
||||
}),
|
||||
).not.toThrow();
|
||||
} else {
|
||||
expect(() =>
|
||||
validateFieldNameAvailabilityOrThrow(
|
||||
input,
|
||||
objectMetadataMapItemMock,
|
||||
),
|
||||
validateFieldNameAvailabilityOrThrow({
|
||||
name: input,
|
||||
objectMetadata: objectMetadataMapItemMock,
|
||||
}),
|
||||
).toThrowErrorMatchingSnapshot();
|
||||
}
|
||||
},
|
||||
|
||||
+8
-4
@@ -31,10 +31,14 @@ const getReservedCompositeFieldNames = (
|
||||
return reservedCompositeFieldsNames;
|
||||
};
|
||||
|
||||
export const validateFieldNameAvailabilityOrThrow = (
|
||||
name: string,
|
||||
objectMetadata: ObjectMetadataItemWithFieldMaps,
|
||||
) => {
|
||||
type ValidateFieldNameAvailabilityOrThrowArgs = {
|
||||
name: string;
|
||||
objectMetadata: ObjectMetadataItemWithFieldMaps;
|
||||
};
|
||||
export const validateFieldNameAvailabilityOrThrow = ({
|
||||
name,
|
||||
objectMetadata,
|
||||
}: ValidateFieldNameAvailabilityOrThrowArgs) => {
|
||||
const reservedCompositeFieldsNames =
|
||||
getReservedCompositeFieldNames(objectMetadata);
|
||||
|
||||
|
||||
+7
-4
@@ -7,10 +7,13 @@ import {
|
||||
InvalidMetadataExceptionCode,
|
||||
} from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception';
|
||||
|
||||
export const validateNameAndLabelAreSyncOrThrow = (
|
||||
label: string,
|
||||
name: string,
|
||||
) => {
|
||||
export const validateNameAndLabelAreSyncOrThrow = ({
|
||||
label,
|
||||
name,
|
||||
}: {
|
||||
label: string;
|
||||
name: string;
|
||||
}) => {
|
||||
const computedName = computeMetadataNameFromLabel(label);
|
||||
|
||||
if (name !== computedName) {
|
||||
|
||||
-82
@@ -6,7 +6,6 @@ exports[`Workspace migration builder field actions test suite It should build a
|
||||
[
|
||||
{
|
||||
"flatFieldMetadata": {
|
||||
"createdAt": Any<ClockDate>,
|
||||
"defaultValue": null,
|
||||
"description": "default flat field metadata description",
|
||||
"flatRelationTargetFieldMetadata": null,
|
||||
@@ -30,14 +29,10 @@ exports[`Workspace migration builder field actions test suite It should build a
|
||||
"standardOverrides": null,
|
||||
"type": "TEXT",
|
||||
"uniqueIdentifier": "field-metadata-unique-identifier-1",
|
||||
"updatedAt": Any<ClockDate>,
|
||||
"workspaceId": Any<String>,
|
||||
},
|
||||
"flatObjectMetadataWithoutFields": {
|
||||
"createdAt": Any<ClockDate>,
|
||||
"dataSourceId": Any<String>,
|
||||
"description": "default flat object metadata description",
|
||||
"duplicateCriteria": [],
|
||||
"icon": "icon",
|
||||
"id": Any<String>,
|
||||
"imageIdentifierFieldMetadataId": Any<String>,
|
||||
@@ -58,7 +53,6 @@ exports[`Workspace migration builder field actions test suite It should build a
|
||||
"standardOverrides": null,
|
||||
"targetTableName": "",
|
||||
"uniqueIdentifier": "object-metadata-unique-identifier-1",
|
||||
"updatedAt": Any<ClockDate>,
|
||||
"workspaceId": Any<String>,
|
||||
},
|
||||
"type": "delete_field",
|
||||
@@ -70,7 +64,6 @@ exports[`Workspace migration builder field actions test suite It should build an
|
||||
[
|
||||
{
|
||||
"flatFieldMetadata": {
|
||||
"createdAt": Any<ClockDate>,
|
||||
"defaultValue": null,
|
||||
"description": "default flat field metadata description",
|
||||
"flatRelationTargetFieldMetadata": null,
|
||||
@@ -94,14 +87,10 @@ exports[`Workspace migration builder field actions test suite It should build an
|
||||
"standardOverrides": null,
|
||||
"type": "TEXT",
|
||||
"uniqueIdentifier": "field-metadata-unique-identifier-1",
|
||||
"updatedAt": Any<ClockDate>,
|
||||
"workspaceId": Any<String>,
|
||||
},
|
||||
"flatObjectMetadataWithoutFields": {
|
||||
"createdAt": Any<ClockDate>,
|
||||
"dataSourceId": Any<String>,
|
||||
"description": "default flat object metadata description",
|
||||
"duplicateCriteria": [],
|
||||
"icon": "icon",
|
||||
"id": Any<String>,
|
||||
"imageIdentifierFieldMetadataId": Any<String>,
|
||||
@@ -122,7 +111,6 @@ exports[`Workspace migration builder field actions test suite It should build an
|
||||
"standardOverrides": null,
|
||||
"targetTableName": "",
|
||||
"uniqueIdentifier": "object-metadata-unique-identifier-1",
|
||||
"updatedAt": Any<ClockDate>,
|
||||
"workspaceId": Any<String>,
|
||||
},
|
||||
"type": "create_field",
|
||||
@@ -134,11 +122,9 @@ exports[`Workspace migration builder field actions test suite It should build an
|
||||
[
|
||||
{
|
||||
"flatFieldMetadata": {
|
||||
"createdAt": Any<ClockDate>,
|
||||
"defaultValue": null,
|
||||
"description": "default flat field metadata description",
|
||||
"flatRelationTargetFieldMetadata": {
|
||||
"createdAt": Any<ClockDate>,
|
||||
"defaultValue": null,
|
||||
"description": "default flat field metadata description",
|
||||
"flatRelationTargetFieldMetadata": null,
|
||||
@@ -162,14 +148,10 @@ exports[`Workspace migration builder field actions test suite It should build an
|
||||
"standardOverrides": null,
|
||||
"type": "RELATION",
|
||||
"uniqueIdentifier": "field-metadata-unique-identifier-2",
|
||||
"updatedAt": Any<ClockDate>,
|
||||
"workspaceId": Any<String>,
|
||||
},
|
||||
"flatRelationTargetObjectMetadata": {
|
||||
"createdAt": Any<ClockDate>,
|
||||
"dataSourceId": Any<String>,
|
||||
"description": "default flat object metadata description",
|
||||
"duplicateCriteria": [],
|
||||
"flatFieldMetadatas": [],
|
||||
"flatIndexMetadatas": [],
|
||||
"icon": "icon",
|
||||
@@ -192,7 +174,6 @@ exports[`Workspace migration builder field actions test suite It should build an
|
||||
"standardOverrides": null,
|
||||
"targetTableName": "",
|
||||
"uniqueIdentifier": "object-metadata-unique-identifier-2",
|
||||
"updatedAt": Any<ClockDate>,
|
||||
"workspaceId": Any<String>,
|
||||
},
|
||||
"icon": "icon",
|
||||
@@ -214,14 +195,10 @@ exports[`Workspace migration builder field actions test suite It should build an
|
||||
"standardOverrides": null,
|
||||
"type": "RELATION",
|
||||
"uniqueIdentifier": "field-metadata-unique-identifier-1",
|
||||
"updatedAt": Any<ClockDate>,
|
||||
"workspaceId": Any<String>,
|
||||
},
|
||||
"flatObjectMetadataWithoutFields": {
|
||||
"createdAt": Any<ClockDate>,
|
||||
"dataSourceId": Any<String>,
|
||||
"description": "default flat object metadata description",
|
||||
"duplicateCriteria": [],
|
||||
"icon": "icon",
|
||||
"id": Any<String>,
|
||||
"imageIdentifierFieldMetadataId": Any<String>,
|
||||
@@ -242,7 +219,6 @@ exports[`Workspace migration builder field actions test suite It should build an
|
||||
"standardOverrides": null,
|
||||
"targetTableName": "",
|
||||
"uniqueIdentifier": "object-metadata-unique-identifier-1",
|
||||
"updatedAt": Any<ClockDate>,
|
||||
"workspaceId": Any<String>,
|
||||
},
|
||||
"type": "create_field",
|
||||
@@ -254,7 +230,6 @@ exports[`Workspace migration builder field actions test suite It should build an
|
||||
[
|
||||
{
|
||||
"flatFieldMetadata": {
|
||||
"createdAt": Any<ClockDate>,
|
||||
"defaultValue": null,
|
||||
"description": "new description",
|
||||
"flatRelationTargetFieldMetadata": null,
|
||||
@@ -278,14 +253,10 @@ exports[`Workspace migration builder field actions test suite It should build an
|
||||
"standardOverrides": null,
|
||||
"type": "TEXT",
|
||||
"uniqueIdentifier": "field-metadata-unique-identifier-1",
|
||||
"updatedAt": Any<ClockDate>,
|
||||
"workspaceId": Any<String>,
|
||||
},
|
||||
"flatObjectMetadataWithoutFields": {
|
||||
"createdAt": Any<ClockDate>,
|
||||
"dataSourceId": Any<String>,
|
||||
"description": "default flat object metadata description",
|
||||
"duplicateCriteria": [],
|
||||
"icon": "icon",
|
||||
"id": Any<String>,
|
||||
"imageIdentifierFieldMetadataId": Any<String>,
|
||||
@@ -306,7 +277,6 @@ exports[`Workspace migration builder field actions test suite It should build an
|
||||
"standardOverrides": null,
|
||||
"targetTableName": "",
|
||||
"uniqueIdentifier": "object-metadata-unique-identifier-1",
|
||||
"updatedAt": Any<ClockDate>,
|
||||
"workspaceId": Any<String>,
|
||||
},
|
||||
"type": "update_field",
|
||||
@@ -341,7 +311,6 @@ exports[`Workspace migration builder field actions test suite It should build an
|
||||
[
|
||||
{
|
||||
"flatFieldMetadata": {
|
||||
"createdAt": Any<ClockDate>,
|
||||
"defaultValue": null,
|
||||
"description": "new description",
|
||||
"flatRelationTargetFieldMetadata": null,
|
||||
@@ -365,14 +334,10 @@ exports[`Workspace migration builder field actions test suite It should build an
|
||||
"standardOverrides": null,
|
||||
"type": "RELATION",
|
||||
"uniqueIdentifier": "field-metadata-unique-identifier-1",
|
||||
"updatedAt": Any<ClockDate>,
|
||||
"workspaceId": Any<String>,
|
||||
},
|
||||
"flatObjectMetadataWithoutFields": {
|
||||
"createdAt": Any<ClockDate>,
|
||||
"dataSourceId": Any<String>,
|
||||
"description": "default flat object metadata description",
|
||||
"duplicateCriteria": [],
|
||||
"icon": "icon",
|
||||
"id": Any<String>,
|
||||
"imageIdentifierFieldMetadataId": Any<String>,
|
||||
@@ -393,7 +358,6 @@ exports[`Workspace migration builder field actions test suite It should build an
|
||||
"standardOverrides": null,
|
||||
"targetTableName": "",
|
||||
"uniqueIdentifier": "object-metadata-unique-identifier-1",
|
||||
"updatedAt": Any<ClockDate>,
|
||||
"workspaceId": Any<String>,
|
||||
},
|
||||
"type": "update_field",
|
||||
@@ -511,10 +475,7 @@ exports[`Workspace migration builder object actions test suite It should build a
|
||||
{
|
||||
"createFieldActions": [],
|
||||
"flatObjectMetadataWithoutFields": {
|
||||
"createdAt": Any<ClockDate>,
|
||||
"dataSourceId": Any<String>,
|
||||
"description": "default flat object metadata description",
|
||||
"duplicateCriteria": [],
|
||||
"icon": "icon",
|
||||
"id": Any<String>,
|
||||
"imageIdentifierFieldMetadataId": Any<String>,
|
||||
@@ -535,7 +496,6 @@ exports[`Workspace migration builder object actions test suite It should build a
|
||||
"standardOverrides": null,
|
||||
"targetTableName": "",
|
||||
"uniqueIdentifier": "pomme",
|
||||
"updatedAt": Any<ClockDate>,
|
||||
"workspaceId": Any<String>,
|
||||
},
|
||||
"type": "create_object",
|
||||
@@ -549,7 +509,6 @@ exports[`Workspace migration builder object actions test suite It should build a
|
||||
"createFieldActions": [
|
||||
{
|
||||
"flatFieldMetadata": {
|
||||
"createdAt": Any<ClockDate>,
|
||||
"defaultValue": null,
|
||||
"description": "default flat field metadata description",
|
||||
"flatRelationTargetFieldMetadata": null,
|
||||
@@ -573,14 +532,10 @@ exports[`Workspace migration builder object actions test suite It should build a
|
||||
"standardOverrides": null,
|
||||
"type": "TEXT",
|
||||
"uniqueIdentifier": "field_0",
|
||||
"updatedAt": Any<ClockDate>,
|
||||
"workspaceId": Any<String>,
|
||||
},
|
||||
"flatObjectMetadataWithoutFields": {
|
||||
"createdAt": Any<ClockDate>,
|
||||
"dataSourceId": Any<String>,
|
||||
"description": "default flat object metadata description",
|
||||
"duplicateCriteria": [],
|
||||
"icon": "icon",
|
||||
"id": Any<String>,
|
||||
"imageIdentifierFieldMetadataId": Any<String>,
|
||||
@@ -601,14 +556,12 @@ exports[`Workspace migration builder object actions test suite It should build a
|
||||
"standardOverrides": null,
|
||||
"targetTableName": "",
|
||||
"uniqueIdentifier": "pomme",
|
||||
"updatedAt": Any<ClockDate>,
|
||||
"workspaceId": Any<String>,
|
||||
},
|
||||
"type": "create_field",
|
||||
},
|
||||
{
|
||||
"flatFieldMetadata": {
|
||||
"createdAt": Any<ClockDate>,
|
||||
"defaultValue": null,
|
||||
"description": "default flat field metadata description",
|
||||
"flatRelationTargetFieldMetadata": null,
|
||||
@@ -632,14 +585,10 @@ exports[`Workspace migration builder object actions test suite It should build a
|
||||
"standardOverrides": null,
|
||||
"type": "TEXT",
|
||||
"uniqueIdentifier": "field_1",
|
||||
"updatedAt": Any<ClockDate>,
|
||||
"workspaceId": Any<String>,
|
||||
},
|
||||
"flatObjectMetadataWithoutFields": {
|
||||
"createdAt": Any<ClockDate>,
|
||||
"dataSourceId": Any<String>,
|
||||
"description": "default flat object metadata description",
|
||||
"duplicateCriteria": [],
|
||||
"icon": "icon",
|
||||
"id": Any<String>,
|
||||
"imageIdentifierFieldMetadataId": Any<String>,
|
||||
@@ -660,14 +609,12 @@ exports[`Workspace migration builder object actions test suite It should build a
|
||||
"standardOverrides": null,
|
||||
"targetTableName": "",
|
||||
"uniqueIdentifier": "pomme",
|
||||
"updatedAt": Any<ClockDate>,
|
||||
"workspaceId": Any<String>,
|
||||
},
|
||||
"type": "create_field",
|
||||
},
|
||||
{
|
||||
"flatFieldMetadata": {
|
||||
"createdAt": Any<ClockDate>,
|
||||
"defaultValue": null,
|
||||
"description": "default flat field metadata description",
|
||||
"flatRelationTargetFieldMetadata": null,
|
||||
@@ -691,14 +638,10 @@ exports[`Workspace migration builder object actions test suite It should build a
|
||||
"standardOverrides": null,
|
||||
"type": "TEXT",
|
||||
"uniqueIdentifier": "field_2",
|
||||
"updatedAt": Any<ClockDate>,
|
||||
"workspaceId": Any<String>,
|
||||
},
|
||||
"flatObjectMetadataWithoutFields": {
|
||||
"createdAt": Any<ClockDate>,
|
||||
"dataSourceId": Any<String>,
|
||||
"description": "default flat object metadata description",
|
||||
"duplicateCriteria": [],
|
||||
"icon": "icon",
|
||||
"id": Any<String>,
|
||||
"imageIdentifierFieldMetadataId": Any<String>,
|
||||
@@ -719,14 +662,12 @@ exports[`Workspace migration builder object actions test suite It should build a
|
||||
"standardOverrides": null,
|
||||
"targetTableName": "",
|
||||
"uniqueIdentifier": "pomme",
|
||||
"updatedAt": Any<ClockDate>,
|
||||
"workspaceId": Any<String>,
|
||||
},
|
||||
"type": "create_field",
|
||||
},
|
||||
{
|
||||
"flatFieldMetadata": {
|
||||
"createdAt": Any<ClockDate>,
|
||||
"defaultValue": null,
|
||||
"description": "default flat field metadata description",
|
||||
"flatRelationTargetFieldMetadata": null,
|
||||
@@ -750,14 +691,10 @@ exports[`Workspace migration builder object actions test suite It should build a
|
||||
"standardOverrides": null,
|
||||
"type": "TEXT",
|
||||
"uniqueIdentifier": "field_3",
|
||||
"updatedAt": Any<ClockDate>,
|
||||
"workspaceId": Any<String>,
|
||||
},
|
||||
"flatObjectMetadataWithoutFields": {
|
||||
"createdAt": Any<ClockDate>,
|
||||
"dataSourceId": Any<String>,
|
||||
"description": "default flat object metadata description",
|
||||
"duplicateCriteria": [],
|
||||
"icon": "icon",
|
||||
"id": Any<String>,
|
||||
"imageIdentifierFieldMetadataId": Any<String>,
|
||||
@@ -778,14 +715,12 @@ exports[`Workspace migration builder object actions test suite It should build a
|
||||
"standardOverrides": null,
|
||||
"targetTableName": "",
|
||||
"uniqueIdentifier": "pomme",
|
||||
"updatedAt": Any<ClockDate>,
|
||||
"workspaceId": Any<String>,
|
||||
},
|
||||
"type": "create_field",
|
||||
},
|
||||
{
|
||||
"flatFieldMetadata": {
|
||||
"createdAt": Any<ClockDate>,
|
||||
"defaultValue": null,
|
||||
"description": "default flat field metadata description",
|
||||
"flatRelationTargetFieldMetadata": null,
|
||||
@@ -809,14 +744,10 @@ exports[`Workspace migration builder object actions test suite It should build a
|
||||
"standardOverrides": null,
|
||||
"type": "TEXT",
|
||||
"uniqueIdentifier": "field_4",
|
||||
"updatedAt": Any<ClockDate>,
|
||||
"workspaceId": Any<String>,
|
||||
},
|
||||
"flatObjectMetadataWithoutFields": {
|
||||
"createdAt": Any<ClockDate>,
|
||||
"dataSourceId": Any<String>,
|
||||
"description": "default flat object metadata description",
|
||||
"duplicateCriteria": [],
|
||||
"icon": "icon",
|
||||
"id": Any<String>,
|
||||
"imageIdentifierFieldMetadataId": Any<String>,
|
||||
@@ -837,17 +768,13 @@ exports[`Workspace migration builder object actions test suite It should build a
|
||||
"standardOverrides": null,
|
||||
"targetTableName": "",
|
||||
"uniqueIdentifier": "pomme",
|
||||
"updatedAt": Any<ClockDate>,
|
||||
"workspaceId": Any<String>,
|
||||
},
|
||||
"type": "create_field",
|
||||
},
|
||||
],
|
||||
"flatObjectMetadataWithoutFields": {
|
||||
"createdAt": Any<ClockDate>,
|
||||
"dataSourceId": Any<String>,
|
||||
"description": "default flat object metadata description",
|
||||
"duplicateCriteria": [],
|
||||
"icon": "icon",
|
||||
"id": Any<String>,
|
||||
"imageIdentifierFieldMetadataId": Any<String>,
|
||||
@@ -868,7 +795,6 @@ exports[`Workspace migration builder object actions test suite It should build a
|
||||
"standardOverrides": null,
|
||||
"targetTableName": "",
|
||||
"uniqueIdentifier": "pomme",
|
||||
"updatedAt": Any<ClockDate>,
|
||||
"workspaceId": Any<String>,
|
||||
},
|
||||
"type": "create_object",
|
||||
@@ -897,10 +823,7 @@ exports[`Workspace migration builder object actions test suite It should build a
|
||||
[
|
||||
{
|
||||
"flatObjectMetadataWithoutFields": {
|
||||
"createdAt": Any<ClockDate>,
|
||||
"dataSourceId": Any<String>,
|
||||
"description": "default flat object metadata description",
|
||||
"duplicateCriteria": [],
|
||||
"icon": "icon",
|
||||
"id": Any<String>,
|
||||
"imageIdentifierFieldMetadataId": Any<String>,
|
||||
@@ -921,7 +844,6 @@ exports[`Workspace migration builder object actions test suite It should build a
|
||||
"standardOverrides": null,
|
||||
"targetTableName": "",
|
||||
"uniqueIdentifier": "pomme",
|
||||
"updatedAt": Any<ClockDate>,
|
||||
"workspaceId": Any<String>,
|
||||
},
|
||||
"type": "delete_object",
|
||||
@@ -933,10 +855,7 @@ exports[`Workspace migration builder object actions test suite It should build a
|
||||
[
|
||||
{
|
||||
"flatObjectMetadataWithoutFields": {
|
||||
"createdAt": Any<ClockDate>,
|
||||
"dataSourceId": Any<String>,
|
||||
"description": "default flat object metadata description",
|
||||
"duplicateCriteria": [],
|
||||
"icon": "icon",
|
||||
"id": Any<String>,
|
||||
"imageIdentifierFieldMetadataId": Any<String>,
|
||||
@@ -957,7 +876,6 @@ exports[`Workspace migration builder object actions test suite It should build a
|
||||
"standardOverrides": null,
|
||||
"targetTableName": "",
|
||||
"uniqueIdentifier": "pomme",
|
||||
"updatedAt": Any<ClockDate>,
|
||||
"workspaceId": Any<String>,
|
||||
},
|
||||
"type": "update_object",
|
||||
|
||||
+22
-16
@@ -19,9 +19,11 @@ export class WorkspaceMigrationBuilderV2Service {
|
||||
build({
|
||||
objectMetadataFromToInputs,
|
||||
workspaceId,
|
||||
inferDeletionFromMissingObjectFieldIndex = true,
|
||||
}: {
|
||||
objectMetadataFromToInputs: FromTo<FlatObjectMetadata[]>;
|
||||
workspaceId: string;
|
||||
inferDeletionFromMissingObjectFieldIndex?: boolean;
|
||||
}): WorkspaceMigrationV2 {
|
||||
const {
|
||||
created: createdObjectMetadata,
|
||||
@@ -44,32 +46,36 @@ export class WorkspaceMigrationBuilderV2Service {
|
||||
);
|
||||
|
||||
const deletedObjectWorkspaceMigrationDeleteFieldActions =
|
||||
deletedObjectMetadata.flatMap((flatObjectMetadata) =>
|
||||
flatObjectMetadata.flatFieldMetadatas.map((flatFieldMetadata) =>
|
||||
getWorkspaceMigrationV2FieldDeleteAction({
|
||||
flatFieldMetadata,
|
||||
flatObjectMetadata,
|
||||
}),
|
||||
),
|
||||
);
|
||||
inferDeletionFromMissingObjectFieldIndex
|
||||
? deletedObjectMetadata.flatMap((flatObjectMetadata) =>
|
||||
flatObjectMetadata.flatFieldMetadatas.map((flatFieldMetadata) =>
|
||||
getWorkspaceMigrationV2FieldDeleteAction({
|
||||
flatFieldMetadata,
|
||||
flatObjectMetadata,
|
||||
}),
|
||||
),
|
||||
)
|
||||
: [];
|
||||
|
||||
const updatedObjectMetadataDeletedCreatedUpdatedFieldMatrix =
|
||||
const objectMetadataDeletedCreatedUpdatedFields =
|
||||
computeUpdatedObjectMetadataDeletedCreatedUpdatedFieldMatrix(
|
||||
updatedObjectMetadata,
|
||||
);
|
||||
|
||||
const fieldWorkspaceMigrationActions =
|
||||
buildWorkspaceMigrationV2FieldActions(
|
||||
updatedObjectMetadataDeletedCreatedUpdatedFieldMatrix,
|
||||
);
|
||||
buildWorkspaceMigrationV2FieldActions({
|
||||
inferDeletionFromMissingObjectFieldIndex,
|
||||
objectMetadataDeletedCreatedUpdatedFields,
|
||||
});
|
||||
|
||||
const updatedObjectMetadataIndexDeletedCreatedUpdatedMatrix =
|
||||
const objectMetadataDeletedCreatedUpdatedIndex =
|
||||
computeUpdatedObjectMetadataDeletedCreatedUpdatedIndexMatrix(
|
||||
updatedObjectMetadata,
|
||||
);
|
||||
const indexWorkspaceMigrationActions = buildWorkspaceMigrationIndexActions(
|
||||
updatedObjectMetadataIndexDeletedCreatedUpdatedMatrix,
|
||||
);
|
||||
const indexWorkspaceMigrationActions = buildWorkspaceMigrationIndexActions({
|
||||
objectMetadataDeletedCreatedUpdatedIndex,
|
||||
inferDeletionFromMissingObjectFieldIndex,
|
||||
});
|
||||
|
||||
return {
|
||||
workspaceId,
|
||||
|
||||
+16
-9
@@ -10,9 +10,14 @@ import {
|
||||
getWorkspaceMigrationV2FieldDeleteAction,
|
||||
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/get-workspace-migration-v2-field-actions';
|
||||
|
||||
export const buildWorkspaceMigrationV2FieldActions = (
|
||||
objectMetadataDeletedCreatedUpdatedFields: UpdatedObjectMetadataDeletedCreatedUpdatedFieldMatrix[],
|
||||
): WorkspaceMigrationFieldActionV2[] => {
|
||||
type BuildWorkspaceMigrationV2FieldActionsArgs = {
|
||||
inferDeletionFromMissingObjectFieldIndex: boolean;
|
||||
objectMetadataDeletedCreatedUpdatedFields: UpdatedObjectMetadataDeletedCreatedUpdatedFieldMatrix[];
|
||||
};
|
||||
export const buildWorkspaceMigrationV2FieldActions = ({
|
||||
inferDeletionFromMissingObjectFieldIndex,
|
||||
objectMetadataDeletedCreatedUpdatedFields,
|
||||
}: BuildWorkspaceMigrationV2FieldActionsArgs): WorkspaceMigrationFieldActionV2[] => {
|
||||
let allUpdatedObjectMetadataFieldActions: WorkspaceMigrationFieldActionV2[] =
|
||||
[];
|
||||
|
||||
@@ -53,12 +58,14 @@ export const buildWorkspaceMigrationV2FieldActions = (
|
||||
}),
|
||||
);
|
||||
|
||||
const deleteFieldAction = deletedFieldMetadata.map((flatFieldMetadata) =>
|
||||
getWorkspaceMigrationV2FieldDeleteAction({
|
||||
flatFieldMetadata,
|
||||
flatObjectMetadata,
|
||||
}),
|
||||
);
|
||||
const deleteFieldAction = inferDeletionFromMissingObjectFieldIndex
|
||||
? deletedFieldMetadata.map((flatFieldMetadata) =>
|
||||
getWorkspaceMigrationV2FieldDeleteAction({
|
||||
flatFieldMetadata,
|
||||
flatObjectMetadata,
|
||||
}),
|
||||
)
|
||||
: [];
|
||||
|
||||
allUpdatedObjectMetadataFieldActions =
|
||||
allUpdatedObjectMetadataFieldActions.concat([
|
||||
|
||||
+11
-6
@@ -6,9 +6,14 @@ import {
|
||||
getWorkspaceMigrationV2DeleteIndexAction,
|
||||
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/get-workspace-migration-v2-index-actions';
|
||||
|
||||
export const buildWorkspaceMigrationIndexActions = (
|
||||
objectMetadataDeletedCreatedUpdatedIndex: UpdatedObjectMetadataDeletedCreatedUpdatedIndexMatrix[],
|
||||
): WorkspaceMigrationIndexActionV2[] => {
|
||||
type BuildWorkspaceMigrationIndexActionsArgs = {
|
||||
objectMetadataDeletedCreatedUpdatedIndex: UpdatedObjectMetadataDeletedCreatedUpdatedIndexMatrix[];
|
||||
inferDeletionFromMissingObjectFieldIndex: boolean;
|
||||
};
|
||||
export const buildWorkspaceMigrationIndexActions = ({
|
||||
inferDeletionFromMissingObjectFieldIndex,
|
||||
objectMetadataDeletedCreatedUpdatedIndex,
|
||||
}: BuildWorkspaceMigrationIndexActionsArgs): WorkspaceMigrationIndexActionV2[] => {
|
||||
let allUpdatedObjectMetadataIndexActions: WorkspaceMigrationIndexActionV2[] =
|
||||
[];
|
||||
|
||||
@@ -36,9 +41,9 @@ export const buildWorkspaceMigrationIndexActions = (
|
||||
const createIndexActions = createdIndexMetadata.map(
|
||||
getWorkspaceMigrationV2CreateIndexAction,
|
||||
);
|
||||
const deleteIndexActions = deletedIndexMetadata.map(
|
||||
getWorkspaceMigrationV2DeleteIndexAction,
|
||||
);
|
||||
const deleteIndexActions = inferDeletionFromMissingObjectFieldIndex
|
||||
? deletedIndexMetadata.map(getWorkspaceMigrationV2DeleteIndexAction)
|
||||
: [];
|
||||
|
||||
allUpdatedObjectMetadataIndexActions =
|
||||
allUpdatedObjectMetadataIndexActions.concat([
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
import { EachTestingContext } from '@/testing/types/EachTestingContext.type';
|
||||
import { sanitizeObjectStringFields } from '../sanitizeObjectStringFields';
|
||||
|
||||
type TestObject = {
|
||||
name?: string;
|
||||
age?: number | null;
|
||||
city?: string | undefined;
|
||||
description?: string;
|
||||
user?: {
|
||||
name: string;
|
||||
contact: {
|
||||
email: string;
|
||||
};
|
||||
};
|
||||
tags?: string[];
|
||||
items?: Array<{ name: string }>;
|
||||
mixedArray?: Array<string | number | null | { text: string }>;
|
||||
};
|
||||
|
||||
type SanitizeTestCase = EachTestingContext<{
|
||||
input: {
|
||||
obj: TestObject;
|
||||
keys: (keyof TestObject)[];
|
||||
};
|
||||
expected: object;
|
||||
}>;
|
||||
|
||||
describe('sanitizeObjectStringFields', () => {
|
||||
const testCases: SanitizeTestCase[] = [
|
||||
{
|
||||
title: 'should handle basic string properties and trim whitespaces',
|
||||
context: {
|
||||
input: {
|
||||
obj: { name: ' John Doe ', age: 30 },
|
||||
keys: ['name', 'age'],
|
||||
},
|
||||
expected: { name: 'John Doe', age: 30 },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should handle nested objects',
|
||||
context: {
|
||||
input: {
|
||||
obj: {
|
||||
user: {
|
||||
name: ' Jane Smith ',
|
||||
contact: { email: ' jane@example.com ' },
|
||||
},
|
||||
},
|
||||
keys: ['user'],
|
||||
},
|
||||
expected: {
|
||||
user: {
|
||||
name: 'Jane Smith',
|
||||
contact: { email: 'jane@example.com' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should skip undefined and null values',
|
||||
context: {
|
||||
input: {
|
||||
obj: { name: ' John ', age: null, city: undefined },
|
||||
keys: ['name', 'age', 'city'],
|
||||
},
|
||||
expected: { name: 'John', age: null, city: undefined },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should handle empty object',
|
||||
context: {
|
||||
input: {
|
||||
obj: {},
|
||||
keys: ['name', 'age'],
|
||||
},
|
||||
expected: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should handle object with no matching keys',
|
||||
context: {
|
||||
input: {
|
||||
obj: { name: 'John', age: 30 },
|
||||
keys: ['city', 'name'],
|
||||
},
|
||||
expected: { name: 'John', age: 30 },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should handle string with multiple spaces, tabs and newlines',
|
||||
context: {
|
||||
input: {
|
||||
obj: { description: 'This is\t\ta\n\ntest string' },
|
||||
keys: ['description'],
|
||||
},
|
||||
expected: { description: 'This is a test string' },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should handle array of strings within object',
|
||||
context: {
|
||||
input: {
|
||||
obj: { tags: [' tag1 ', ' tag2 ', 'test string '] },
|
||||
keys: ['tags'],
|
||||
},
|
||||
expected: { tags: ['tag1', 'tag2', 'test string'] },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should handle array of objects within object',
|
||||
context: {
|
||||
input: {
|
||||
obj: {
|
||||
items: [{ name: ' John Doe ' }, { name: ' Jane Smith ' }],
|
||||
},
|
||||
keys: ['items'],
|
||||
},
|
||||
expected: {
|
||||
items: [{ name: 'John Doe' }, { name: 'Jane Smith' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should handle nested arrays within object properties',
|
||||
context: {
|
||||
input: {
|
||||
obj: {
|
||||
tags: [' tag1 ', ' tag2 '],
|
||||
user: {
|
||||
name: ' John Doe ',
|
||||
contact: { email: ' john@example.com ' },
|
||||
},
|
||||
},
|
||||
keys: ['tags', 'user'],
|
||||
},
|
||||
expected: {
|
||||
tags: ['tag1', 'tag2'],
|
||||
user: {
|
||||
name: 'John Doe',
|
||||
contact: { email: 'john@example.com' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should handle mixed content array within object',
|
||||
context: {
|
||||
input: {
|
||||
obj: {
|
||||
mixedArray: [
|
||||
' string ',
|
||||
123,
|
||||
null,
|
||||
{ text: ' nested text ' },
|
||||
],
|
||||
},
|
||||
keys: ['mixedArray'],
|
||||
},
|
||||
expected: {
|
||||
mixedArray: ['string', 123, null, { text: 'nested text' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
test.each(testCases)(
|
||||
'$title',
|
||||
({
|
||||
context: {
|
||||
input: { obj, keys },
|
||||
expected,
|
||||
},
|
||||
}) => {
|
||||
const result = sanitizeObjectStringFields(obj, keys);
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
},
|
||||
);
|
||||
});
|
||||
+26
-8
@@ -1,10 +1,11 @@
|
||||
import { eachTestingContextFilter } from '@/testing';
|
||||
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>;
|
||||
extract?: boolean;
|
||||
}>;
|
||||
|
||||
describe('trim-and-remove-duplicated-whitespaces-from-object-string-properties', () => {
|
||||
@@ -93,14 +94,31 @@ describe('trim-and-remove-duplicated-whitespaces-from-object-string-properties',
|
||||
expected: { name: ' John Doe ', description: 'this is a test' },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should trim only provided keys fields and extract keys',
|
||||
context: {
|
||||
input: {
|
||||
name: ' John Doe ',
|
||||
description: ' this is a test ',
|
||||
},
|
||||
keys: ['description'],
|
||||
expected: { description: 'this is a test' },
|
||||
extract: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
test.each(testCases)('$title', ({ context: { input, keys, expected } }) => {
|
||||
const result = trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
|
||||
input,
|
||||
keys,
|
||||
);
|
||||
test.each(eachTestingContextFilter(testCases))(
|
||||
'$title',
|
||||
({ context: { input, keys, expected, extract } }) => {
|
||||
const result =
|
||||
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
|
||||
input,
|
||||
keys,
|
||||
extract,
|
||||
);
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
expect(result).toEqual(expected);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export const assertUnreachable = (x: never, errorMessage?: string): never => {
|
||||
export const assertUnreachable = (_x: never, errorMessage?: string): never => {
|
||||
throw new Error(errorMessage ?? "Didn't expect to get here.");
|
||||
};
|
||||
|
||||
@@ -8,18 +8,18 @@ export const fromArrayToUniqueKeyRecord = <T extends object>({
|
||||
array: T[];
|
||||
uniqueKey: StringPropertyKeys<T>;
|
||||
}) => {
|
||||
return array.reduce<Record<string, T>>((acc, occurence) => {
|
||||
const currentUniqueKey = occurence[uniqueKey] as string;
|
||||
return array.reduce<Record<string, T>>((acc, occurrence) => {
|
||||
const currentUniqueKey = occurrence[uniqueKey] as string;
|
||||
|
||||
if (isDefined(acc[currentUniqueKey])) {
|
||||
throw new Error(
|
||||
`Should never occur, flat array contains twice the same unique key ${occurence[uniqueKey]}`,
|
||||
`Should never occur, flat array contains twice the same unique key ${occurrence[uniqueKey]}`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...acc,
|
||||
[currentUniqueKey]: occurence,
|
||||
[currentUniqueKey]: occurrence,
|
||||
};
|
||||
}, {});
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ 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';
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { trimAndRemoveDuplicatedWhitespacesFromString } from '@/utils/trim-and-remove-duplicated-whitespaces-from-string';
|
||||
// TODO rename with extract meaning
|
||||
export const sanitizeObjectStringFields = <
|
||||
T extends object,
|
||||
TKeys extends (keyof T)[],
|
||||
>(
|
||||
obj: T,
|
||||
keys: TKeys,
|
||||
maxDepth: number = 10,
|
||||
): {
|
||||
[P in TKeys[number]]: T[P];
|
||||
} => {
|
||||
const processValue = (value: unknown, currentDepth: number): unknown => {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (currentDepth >= maxDepth) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => processValue(item, currentDepth));
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
const obj = value as Record<string, unknown>;
|
||||
const objKeys = Object.keys(obj);
|
||||
return objKeys.reduce(
|
||||
(acc, key) => ({
|
||||
...acc,
|
||||
[key]: processValue(obj[key], currentDepth + 1),
|
||||
}),
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
return trimAndRemoveDuplicatedWhitespacesFromString(value);
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
return keys.reduce((acc, key) => {
|
||||
const value = processValue(obj[key], 0);
|
||||
|
||||
if (value === undefined) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
return {
|
||||
...acc,
|
||||
[key]: value,
|
||||
};
|
||||
}, {} as T);
|
||||
};
|
||||
+29
-17
@@ -8,24 +8,36 @@ export type StringPropertyKeys<T> = {
|
||||
: never;
|
||||
}[OnlyStringPropertiesKey<T>];
|
||||
|
||||
export const trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties = <T>(
|
||||
export const trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties = <
|
||||
T,
|
||||
TKeys extends StringPropertyKeys<T>[],
|
||||
TExtract extends boolean = false,
|
||||
>(
|
||||
obj: T,
|
||||
keys: StringPropertyKeys<T>[],
|
||||
) => {
|
||||
return keys.reduce((acc, key) => {
|
||||
const occurrence = acc[key];
|
||||
|
||||
if (
|
||||
occurrence === undefined ||
|
||||
typeof occurrence !== 'string' ||
|
||||
occurrence === null
|
||||
) {
|
||||
return acc;
|
||||
keys: TKeys,
|
||||
extractKeys?: TExtract,
|
||||
): TExtract extends true
|
||||
? {
|
||||
[P in TKeys[number]]: T[P];
|
||||
}
|
||||
: T => {
|
||||
return keys.reduce(
|
||||
(acc, key) => {
|
||||
const occurrence = obj[key];
|
||||
|
||||
return {
|
||||
...acc,
|
||||
[key]: trimAndRemoveDuplicatedWhitespacesFromString(occurrence),
|
||||
};
|
||||
}, obj);
|
||||
if (
|
||||
occurrence === undefined ||
|
||||
typeof occurrence !== 'string' ||
|
||||
occurrence === null
|
||||
) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
return {
|
||||
...acc,
|
||||
[key]: trimAndRemoveDuplicatedWhitespacesFromString(occurrence),
|
||||
};
|
||||
},
|
||||
extractKeys ? ({} as T) : obj,
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user