Fix unique standard field (#16371)
Fixes https://github.com/twentyhq/twenty/issues/15925 - update field metadata update logic - uniformize the way index are named - command to migrate v1-named unique index - add integration testing --------- Co-authored-by: prastoin <paul@twenty.com>
This commit is contained in:
Vendored
+1
-1
@@ -67,7 +67,7 @@
|
||||
"--config",
|
||||
"./jest-integration.config.ts",
|
||||
"${relativeFile}",
|
||||
"--silent=false"
|
||||
"--silent=false",
|
||||
],
|
||||
"cwd": "${workspaceFolder}/packages/twenty-server",
|
||||
"console": "integratedTerminal",
|
||||
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { generateDeterministicIndexNameV2 } from 'src/engine/metadata-modules/index-metadata/utils/generate-deterministic-index-name-v2';
|
||||
import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-13:rename-index-name',
|
||||
description: 'Rename indexes to use the new v2 index name format',
|
||||
})
|
||||
export class RenameIndexNameCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
protected readonly logger = new Logger(RenameIndexNameCommand.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
@InjectRepository(IndexMetadataEntity)
|
||||
private readonly indexMetadataRepository: Repository<IndexMetadataEntity>,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
protected readonly workspaceCacheService: WorkspaceCacheService,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
dataSource,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
const isDryRun = options.dryRun || false;
|
||||
|
||||
if (!isDefined(dataSource)) {
|
||||
throw new Error(
|
||||
`Could not find data source for workspace ${workspaceId}, should never occur`,
|
||||
);
|
||||
}
|
||||
|
||||
const schemaName = getWorkspaceSchemaName(workspaceId);
|
||||
|
||||
if (isDryRun) {
|
||||
this.logger.log('Dry run mode: No changes will be applied');
|
||||
}
|
||||
|
||||
const indexes = await this.indexMetadataRepository.find({
|
||||
where: {
|
||||
workspaceId,
|
||||
},
|
||||
relations: ['objectMetadata', 'indexFieldMetadatas.fieldMetadata'],
|
||||
});
|
||||
|
||||
let hasIndexNameChanges = false;
|
||||
|
||||
for (const index of indexes) {
|
||||
const indexNameV2 = generateDeterministicIndexNameV2({
|
||||
flatObjectMetadata: {
|
||||
nameSingular: index.objectMetadata.nameSingular,
|
||||
isCustom: index.objectMetadata.isCustom,
|
||||
},
|
||||
relatedFieldNames: index.indexFieldMetadatas.map(
|
||||
(indexFieldMetadata) => ({
|
||||
name: indexFieldMetadata.fieldMetadata.name,
|
||||
}),
|
||||
),
|
||||
isUnique: index.isUnique,
|
||||
});
|
||||
|
||||
if (indexNameV2 === index.name) {
|
||||
this.logger.log(`Index ${index.name} is V2`);
|
||||
continue;
|
||||
} else {
|
||||
this.logger.log(`Renaming index ${index.name} to ${indexNameV2}`);
|
||||
hasIndexNameChanges = true;
|
||||
if (!isDryRun) {
|
||||
await this.renameIndexOnDatabase(
|
||||
dataSource,
|
||||
schemaName,
|
||||
index.name,
|
||||
indexNameV2,
|
||||
);
|
||||
|
||||
await this.indexMetadataRepository.update(index.id, {
|
||||
name: indexNameV2,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hasIndexNameChanges) {
|
||||
this.logger.log('Invalidating workspace cache');
|
||||
|
||||
if (!isDryRun) {
|
||||
await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
|
||||
'flatFieldMetadataMaps',
|
||||
'flatIndexMaps',
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async renameIndexOnDatabase(
|
||||
dataSource: WorkspaceDataSource,
|
||||
schemaName: string,
|
||||
oldIndexName: string,
|
||||
newIndexName: string,
|
||||
): Promise<void> {
|
||||
await dataSource.query(
|
||||
`ALTER INDEX "${schemaName}"."${oldIndexName}" RENAME TO "${newIndexName}"`,
|
||||
[],
|
||||
undefined,
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
}
|
||||
}
|
||||
+5
@@ -5,12 +5,14 @@ import { BackfillPageLayoutUniversalIdentifiersCommand } from 'src/database/comm
|
||||
import { BackfillViewMainGroupByFieldMetadataIdCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-backfill-view-main-group-by-field-metadata-id.command';
|
||||
import { CleanEmptyStringNullInTextFieldsCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-clean-empty-string-null-in-text-fields.command';
|
||||
import { DeduplicateRoleTargetsCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-deduplicate-role-targets.command';
|
||||
import { RenameIndexNameCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-rename-unique-index.command';
|
||||
import { UpdateRoleTargetsUniqueConstraintMigrationCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-update-role-targets-unique-constraint-migration.command';
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout-tab.entity';
|
||||
import { PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout-widget.entity';
|
||||
@@ -26,6 +28,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
|
||||
WorkspaceEntity,
|
||||
ObjectMetadataEntity,
|
||||
FieldMetadataEntity,
|
||||
IndexMetadataEntity,
|
||||
ViewEntity,
|
||||
ViewGroupEntity,
|
||||
FeatureFlagEntity,
|
||||
@@ -43,6 +46,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
|
||||
BackfillViewMainGroupByFieldMetadataIdCommand,
|
||||
BackfillPageLayoutUniversalIdentifiersCommand,
|
||||
DeduplicateRoleTargetsCommand,
|
||||
RenameIndexNameCommand,
|
||||
UpdateRoleTargetsUniqueConstraintMigrationCommand,
|
||||
],
|
||||
exports: [
|
||||
@@ -50,6 +54,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
|
||||
BackfillViewMainGroupByFieldMetadataIdCommand,
|
||||
BackfillPageLayoutUniversalIdentifiersCommand,
|
||||
DeduplicateRoleTargetsCommand,
|
||||
RenameIndexNameCommand,
|
||||
UpdateRoleTargetsUniqueConstraintMigrationCommand,
|
||||
],
|
||||
})
|
||||
|
||||
+14
-5
@@ -22,6 +22,7 @@ import { fromCreateFieldInputToFlatFieldMetadatasToCreate } from 'src/engine/met
|
||||
import { fromDeleteFieldInputToFlatFieldMetadatasToDelete } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-delete-field-input-to-flat-field-metadatas-to-delete.util';
|
||||
import { fromUpdateFieldInputToFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-update-field-input-to-flat-field-metadata.util';
|
||||
import { throwOnFieldInputTranspilationsError } from 'src/engine/metadata-modules/flat-field-metadata/utils/throw-on-field-input-transpilations-error.util';
|
||||
import { EMPTY_ORCHESTRATOR_FAILURE_REPORT } from 'src/engine/workspace-manager/workspace-migration-v2/constant/empty-orchestrator-failure-report.constant';
|
||||
import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-validate-build-and-run-service';
|
||||
|
||||
@@ -183,13 +184,21 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
});
|
||||
|
||||
if (inputTranspilationResult.status === 'fail') {
|
||||
throw new FieldMetadataException(
|
||||
inputTranspilationResult.error.message,
|
||||
inputTranspilationResult.error.code,
|
||||
throw new WorkspaceMigrationBuilderExceptionV2(
|
||||
{
|
||||
userFriendlyMessage:
|
||||
inputTranspilationResult.error.userFriendlyMessage,
|
||||
report: {
|
||||
...EMPTY_ORCHESTRATOR_FAILURE_REPORT(),
|
||||
fieldMetadata: [
|
||||
{
|
||||
errors: inputTranspilationResult.errors,
|
||||
type: 'update_field',
|
||||
flatEntityMinimalInformation: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
status: 'fail',
|
||||
},
|
||||
'Validation errors occurred while updating field',
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+67
-55
@@ -2,99 +2,111 @@
|
||||
|
||||
exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test suite Failure cases should fail when morphRelationsCreationPayload has different relation types 1`] = `
|
||||
{
|
||||
"error": {
|
||||
"code": "FIELD_METADATA_RELATION_MALFORMED",
|
||||
"message": "Morph relation creation payloads must have the same relation type",
|
||||
"userFriendlyMessage": {
|
||||
"id": Any<String>,
|
||||
"errors": [
|
||||
{
|
||||
"code": "FIELD_METADATA_RELATION_MALFORMED",
|
||||
"message": "Morph relation creation payloads must have the same relation type",
|
||||
"userFriendlyMessage": {
|
||||
"id": Any<String>,
|
||||
"message": "Morph relation creation payloads must have the same relation type",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
"status": "fail",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test suite Failure cases should fail when morphRelationsCreationPayload has invalid relation payload 1`] = `
|
||||
{
|
||||
"error": {
|
||||
"code": "FIELD_METADATA_RELATION_MALFORMED",
|
||||
"message": "Morph relation input transpilation failed",
|
||||
"userFriendlyMessage": {
|
||||
"id": Any<String>,
|
||||
"message": "Invalid morph relation input",
|
||||
},
|
||||
"value": [
|
||||
{
|
||||
"targetObjectMetadataId": Any<String>,
|
||||
"type": "ONE_TO_MANY",
|
||||
"errors": [
|
||||
{
|
||||
"code": "FIELD_METADATA_RELATION_MALFORMED",
|
||||
"message": "Morph relation input transpilation failed",
|
||||
"userFriendlyMessage": {
|
||||
"id": Any<String>,
|
||||
"message": "Invalid morph relation input",
|
||||
},
|
||||
],
|
||||
},
|
||||
"value": [
|
||||
{
|
||||
"targetObjectMetadataId": Any<String>,
|
||||
"type": "ONE_TO_MANY",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
"status": "fail",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test suite Failure cases should fail when morphRelationsCreationPayload has several references to same object metadata 1`] = `
|
||||
{
|
||||
"error": {
|
||||
"code": "FIELD_METADATA_RELATION_MALFORMED",
|
||||
"message": "Morph relation creation payloads must have only relation to the same object metadata",
|
||||
"userFriendlyMessage": {
|
||||
"id": Any<String>,
|
||||
"message": "Morph relation creation payloads must only contain relation to the same object metadata",
|
||||
"errors": [
|
||||
{
|
||||
"code": "FIELD_METADATA_RELATION_MALFORMED",
|
||||
"message": "Morph relation creation payloads must have only relation to the same object metadata",
|
||||
"userFriendlyMessage": {
|
||||
"id": Any<String>,
|
||||
"message": "Morph relation creation payloads must only contain relation to the same object metadata",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
"status": "fail",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test suite Failure cases should fail when morphRelationsCreationPayload is empty array 1`] = `
|
||||
{
|
||||
"error": {
|
||||
"code": "FIELD_METADATA_RELATION_MALFORMED",
|
||||
"message": "Morph relation creation payloads are empty",
|
||||
"userFriendlyMessage": {
|
||||
"id": Any<String>,
|
||||
"message": "At least one relation is require",
|
||||
"errors": [
|
||||
{
|
||||
"code": "FIELD_METADATA_RELATION_MALFORMED",
|
||||
"message": "Morph relation creation payloads are empty",
|
||||
"userFriendlyMessage": {
|
||||
"id": Any<String>,
|
||||
"message": "At least one relation is require",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
"status": "fail",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test suite Failure cases should fail when morphRelationsCreationPayload is missing 1`] = `
|
||||
{
|
||||
"error": {
|
||||
"code": "INVALID_FIELD_INPUT",
|
||||
"message": "Relation creation payload is required",
|
||||
"userFriendlyMessage": {
|
||||
"id": Any<String>,
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_FIELD_INPUT",
|
||||
"message": "Relation creation payload is required",
|
||||
"userFriendlyMessage": {
|
||||
"id": Any<String>,
|
||||
"message": "Relation creation payload is required",
|
||||
},
|
||||
"value": undefined,
|
||||
},
|
||||
"value": undefined,
|
||||
},
|
||||
],
|
||||
"status": "fail",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test suite Failure cases should fail when target object metadata is not found 1`] = `
|
||||
{
|
||||
"error": {
|
||||
"code": "FIELD_METADATA_RELATION_MALFORMED",
|
||||
"message": "Morph relation input transpilation failed",
|
||||
"userFriendlyMessage": {
|
||||
"id": Any<String>,
|
||||
"message": "Invalid morph relation input",
|
||||
},
|
||||
"value": [
|
||||
{
|
||||
"targetFieldIcon": "IconPet",
|
||||
"targetFieldLabel": "Pet",
|
||||
"targetObjectMetadataId": Any<String>,
|
||||
"type": "ONE_TO_MANY",
|
||||
"errors": [
|
||||
{
|
||||
"code": "FIELD_METADATA_RELATION_MALFORMED",
|
||||
"message": "Morph relation input transpilation failed",
|
||||
"userFriendlyMessage": {
|
||||
"id": Any<String>,
|
||||
"message": "Invalid morph relation input",
|
||||
},
|
||||
],
|
||||
},
|
||||
"value": [
|
||||
{
|
||||
"targetFieldIcon": "IconPet",
|
||||
"targetFieldLabel": "Pet",
|
||||
"targetObjectMetadataId": Any<String>,
|
||||
"type": "ONE_TO_MANY",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
"status": "fail",
|
||||
}
|
||||
`;
|
||||
|
||||
+2
@@ -21,5 +21,7 @@ export const FLAT_FIELD_METADATA_EDITABLE_PROPERTIES = {
|
||||
'label',
|
||||
'options',
|
||||
'settings',
|
||||
//TODO : uncomment once universal identifier on standard fields is migrated
|
||||
// 'isUnique', // not editable for standard fields with standard unique constraint
|
||||
],
|
||||
} as const satisfies Record<'standard' | 'custom', (keyof FlatFieldMetadata)[]>;
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { type FlatFieldMetadataValidationError } from 'src/engine/metadata-modul
|
||||
|
||||
export type FailedFieldInputTranspilation = {
|
||||
status: 'fail';
|
||||
error: FlatFieldMetadataValidationError;
|
||||
errors: FlatFieldMetadataValidationError[];
|
||||
};
|
||||
export type SuccessfulFieldInputTranspilation<T> = {
|
||||
status: 'success';
|
||||
|
||||
+19
-13
@@ -42,10 +42,12 @@ export const fromCreateFieldInputToFlatFieldMetadatasToCreate = async ({
|
||||
if (rawCreateFieldInput.isRemoteCreation) {
|
||||
return {
|
||||
status: 'fail',
|
||||
error: {
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message: "Remote fields aren't supported",
|
||||
},
|
||||
errors: [
|
||||
{
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message: "Remote fields aren't supported",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -60,11 +62,13 @@ export const fromCreateFieldInputToFlatFieldMetadatasToCreate = async ({
|
||||
if (!isDefined(parentFlatObjectMetadata)) {
|
||||
return {
|
||||
status: 'fail',
|
||||
error: {
|
||||
code: FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
|
||||
message: 'Provided object metadata id does not exist',
|
||||
userFriendlyMessage: msg`Created field metadata, parent object metadata not found`,
|
||||
},
|
||||
errors: [
|
||||
{
|
||||
code: FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
|
||||
message: 'Provided object metadata id does not exist',
|
||||
userFriendlyMessage: msg`Created field metadata, parent object metadata not found`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -149,10 +153,12 @@ export const fromCreateFieldInputToFlatFieldMetadatasToCreate = async ({
|
||||
case FieldMetadataType.TS_VECTOR: {
|
||||
return {
|
||||
status: 'fail',
|
||||
error: {
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message: 'TS Vector is not supported for field creation',
|
||||
},
|
||||
errors: [
|
||||
{
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message: 'TS Vector is not supported for field creation',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
case FieldMetadataType.UUID:
|
||||
|
||||
+8
-6
@@ -44,12 +44,14 @@ export const fromMorphRelationCreateFieldInputToFlatFieldMetadatas = async ({
|
||||
) {
|
||||
return {
|
||||
status: 'fail',
|
||||
error: {
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message: `Relation creation payload is required`,
|
||||
userFriendlyMessage: msg`Relation creation payload is required`,
|
||||
value: rawMorphCreationPayload,
|
||||
},
|
||||
errors: [
|
||||
{
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message: `Relation creation payload is required`,
|
||||
userFriendlyMessage: msg`Relation creation payload is required`,
|
||||
value: rawMorphCreationPayload,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+8
-6
@@ -37,12 +37,14 @@ export const fromRelationCreateFieldInputToFlatFieldMetadatas = async ({
|
||||
if (!isDefined(rawCreationPayload)) {
|
||||
return {
|
||||
status: 'fail',
|
||||
error: {
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message: `Relation creation payload is required`,
|
||||
userFriendlyMessage: msg`Relation creation payload is required`,
|
||||
value: rawCreationPayload,
|
||||
},
|
||||
errors: [
|
||||
{
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message: `Relation creation payload is required`,
|
||||
userFriendlyMessage: msg`Relation creation payload is required`,
|
||||
value: rawCreationPayload,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+121
-93
@@ -6,13 +6,11 @@ import {
|
||||
} from 'twenty-shared/utils';
|
||||
|
||||
import { type UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input';
|
||||
import {
|
||||
FieldMetadataException,
|
||||
FieldMetadataExceptionCode,
|
||||
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
|
||||
import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
|
||||
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { type FieldInputTranspilationResult } from 'src/engine/metadata-modules/flat-field-metadata/types/field-input-transpilation-result.type';
|
||||
import { type FlatFieldMetadataValidationError } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-validation-error.type';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { computeFlatFieldToUpdateAndRelatedFlatFieldToUpdate } from 'src/engine/metadata-modules/flat-field-metadata/utils/compute-flat-field-to-update-and-related-flat-field-to-update.util';
|
||||
import { computeFlatFieldToUpdateFromMorphRelationUpdatePayload } from 'src/engine/metadata-modules/flat-field-metadata/utils/compute-flat-field-to-update-from-morph-relation-update-payload.util';
|
||||
@@ -66,11 +64,13 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({
|
||||
if (!isDefined(existingFlatFieldMetadataToUpdate)) {
|
||||
return {
|
||||
status: 'fail',
|
||||
error: {
|
||||
code: FieldMetadataExceptionCode.FIELD_METADATA_NOT_FOUND,
|
||||
message: 'Field metadata to update not found',
|
||||
userFriendlyMessage: msg`Field metadata to update not found`,
|
||||
},
|
||||
errors: [
|
||||
{
|
||||
code: FieldMetadataExceptionCode.FIELD_METADATA_NOT_FOUND,
|
||||
message: 'Field metadata to update not found',
|
||||
userFriendlyMessage: msg`Field metadata to update not found`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -80,10 +80,16 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({
|
||||
});
|
||||
|
||||
if (!isDefined(flatObjectMetadata)) {
|
||||
throw new FieldMetadataException(
|
||||
'Field to update object metadata not found',
|
||||
FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
|
||||
);
|
||||
return {
|
||||
status: 'fail',
|
||||
errors: [
|
||||
{
|
||||
code: FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
|
||||
message: 'Field to update object metadata not found',
|
||||
userFriendlyMessage: msg`Field to update object metadata not found`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const { flatFieldMetadataFromTo, relatedFlatFieldMetadatasFromTo } =
|
||||
@@ -112,99 +118,121 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({
|
||||
flatIndexMetadatasToCreate: [],
|
||||
};
|
||||
|
||||
const initialAccumulator: FlatFieldMetadataAndIndexToUpdate = {
|
||||
const initialAccumulator: FlatFieldMetadataAndIndexToUpdate & {
|
||||
errors: FlatFieldMetadataValidationError[];
|
||||
} = {
|
||||
...structuredClone(FLAT_FIELD_METADATA_UPDATE_EMPTY_SIDE_EFFECTS),
|
||||
flatFieldMetadatasToUpdate: [],
|
||||
flatFieldMetadatasToCreate: flatFieldMetadatasToCreate,
|
||||
flatIndexMetadatasToCreate: flatIndexMetadatasToCreate,
|
||||
errors: [],
|
||||
};
|
||||
|
||||
const optimisticiallyUpdatedFlatFieldMetadatas = [
|
||||
const { errors: sideEffectErrors, ...sideEffectFlatEntityOperations } = [
|
||||
flatFieldMetadataFromTo,
|
||||
...relatedFlatFieldMetadatasFromTo,
|
||||
].reduce<FlatFieldMetadataAndIndexToUpdate>(
|
||||
(accumulator, { fromFlatFieldMetadata, toFlatFieldMetadata }) => {
|
||||
const {
|
||||
flatViewGroupsToCreate,
|
||||
flatViewGroupsToDelete,
|
||||
flatViewGroupsToUpdate,
|
||||
flatIndexMetadatasToUpdate,
|
||||
flatViewFiltersToDelete,
|
||||
flatViewFiltersToUpdate,
|
||||
flatIndexMetadatasToCreate,
|
||||
flatIndexMetadatasToDelete,
|
||||
flatViewsToDelete,
|
||||
flatViewFieldsToDelete,
|
||||
flatViewsToUpdate,
|
||||
} = handleFlatFieldMetadataUpdateSideEffect({
|
||||
flatViewFilterMaps,
|
||||
flatViewGroupMaps,
|
||||
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
|
||||
fromFlatFieldMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatIndexMaps,
|
||||
toFlatFieldMetadata,
|
||||
flatViewMaps,
|
||||
flatViewFieldMaps,
|
||||
});
|
||||
].reduce<
|
||||
FlatFieldMetadataAndIndexToUpdate & {
|
||||
errors: FlatFieldMetadataValidationError[];
|
||||
}
|
||||
>((accumulator, { fromFlatFieldMetadata, toFlatFieldMetadata }) => {
|
||||
const sideEffectResult = handleFlatFieldMetadataUpdateSideEffect({
|
||||
flatViewFilterMaps,
|
||||
flatViewGroupMaps,
|
||||
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
|
||||
fromFlatFieldMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatIndexMaps,
|
||||
toFlatFieldMetadata,
|
||||
flatViewMaps,
|
||||
flatViewFieldMaps,
|
||||
workspaceCustomApplicationId,
|
||||
});
|
||||
|
||||
if (sideEffectResult.status === 'fail') {
|
||||
return {
|
||||
flatFieldMetadatasToUpdate: [
|
||||
...accumulator.flatFieldMetadatasToUpdate,
|
||||
toFlatFieldMetadata,
|
||||
],
|
||||
flatIndexMetadatasToUpdate: [
|
||||
...accumulator.flatIndexMetadatasToUpdate,
|
||||
...flatIndexMetadatasToUpdate,
|
||||
],
|
||||
flatFieldMetadatasToCreate: [...accumulator.flatFieldMetadatasToCreate],
|
||||
flatViewFiltersToDelete: [
|
||||
...accumulator.flatViewFiltersToDelete,
|
||||
...flatViewFiltersToDelete,
|
||||
],
|
||||
flatViewFiltersToUpdate: [
|
||||
...accumulator.flatViewFiltersToUpdate,
|
||||
...flatViewFiltersToUpdate,
|
||||
],
|
||||
flatViewGroupsToCreate: [
|
||||
...accumulator.flatViewGroupsToCreate,
|
||||
...flatViewGroupsToCreate,
|
||||
],
|
||||
flatViewGroupsToDelete: [
|
||||
...accumulator.flatViewGroupsToDelete,
|
||||
...flatViewGroupsToDelete,
|
||||
],
|
||||
flatViewGroupsToUpdate: [
|
||||
...accumulator.flatViewGroupsToUpdate,
|
||||
...flatViewGroupsToUpdate,
|
||||
],
|
||||
flatIndexMetadatasToDelete: [
|
||||
...accumulator.flatIndexMetadatasToDelete,
|
||||
...flatIndexMetadatasToDelete,
|
||||
],
|
||||
flatIndexMetadatasToCreate: [
|
||||
...accumulator.flatIndexMetadatasToCreate,
|
||||
...flatIndexMetadatasToCreate,
|
||||
],
|
||||
flatViewsToDelete: [
|
||||
...accumulator.flatViewsToDelete,
|
||||
...flatViewsToDelete,
|
||||
],
|
||||
flatViewFieldsToDelete: [
|
||||
...accumulator.flatViewFieldsToDelete,
|
||||
...flatViewFieldsToDelete,
|
||||
],
|
||||
flatViewsToUpdate: [
|
||||
...accumulator.flatViewsToUpdate,
|
||||
...flatViewsToUpdate,
|
||||
],
|
||||
...accumulator,
|
||||
errors: [...accumulator.errors, ...sideEffectResult.errors],
|
||||
};
|
||||
},
|
||||
initialAccumulator,
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
flatViewGroupsToCreate,
|
||||
flatViewGroupsToDelete,
|
||||
flatViewGroupsToUpdate,
|
||||
flatIndexMetadatasToUpdate,
|
||||
flatViewFiltersToDelete,
|
||||
flatViewFiltersToUpdate,
|
||||
flatIndexMetadatasToCreate,
|
||||
flatIndexMetadatasToDelete,
|
||||
flatViewsToDelete,
|
||||
flatViewFieldsToDelete,
|
||||
flatViewsToUpdate,
|
||||
} = sideEffectResult.result;
|
||||
|
||||
return {
|
||||
flatFieldMetadatasToUpdate: [
|
||||
...accumulator.flatFieldMetadatasToUpdate,
|
||||
toFlatFieldMetadata,
|
||||
],
|
||||
flatIndexMetadatasToUpdate: [
|
||||
...accumulator.flatIndexMetadatasToUpdate,
|
||||
...flatIndexMetadatasToUpdate,
|
||||
],
|
||||
flatFieldMetadatasToCreate: [...accumulator.flatFieldMetadatasToCreate],
|
||||
flatViewFiltersToDelete: [
|
||||
...accumulator.flatViewFiltersToDelete,
|
||||
...flatViewFiltersToDelete,
|
||||
],
|
||||
flatViewFiltersToUpdate: [
|
||||
...accumulator.flatViewFiltersToUpdate,
|
||||
...flatViewFiltersToUpdate,
|
||||
],
|
||||
flatViewGroupsToCreate: [
|
||||
...accumulator.flatViewGroupsToCreate,
|
||||
...flatViewGroupsToCreate,
|
||||
],
|
||||
flatViewGroupsToDelete: [
|
||||
...accumulator.flatViewGroupsToDelete,
|
||||
...flatViewGroupsToDelete,
|
||||
],
|
||||
flatViewGroupsToUpdate: [
|
||||
...accumulator.flatViewGroupsToUpdate,
|
||||
...flatViewGroupsToUpdate,
|
||||
],
|
||||
flatIndexMetadatasToDelete: [
|
||||
...accumulator.flatIndexMetadatasToDelete,
|
||||
...flatIndexMetadatasToDelete,
|
||||
],
|
||||
flatIndexMetadatasToCreate: [
|
||||
...accumulator.flatIndexMetadatasToCreate,
|
||||
...flatIndexMetadatasToCreate,
|
||||
],
|
||||
flatViewsToDelete: [
|
||||
...accumulator.flatViewsToDelete,
|
||||
...flatViewsToDelete,
|
||||
],
|
||||
flatViewFieldsToDelete: [
|
||||
...accumulator.flatViewFieldsToDelete,
|
||||
...flatViewFieldsToDelete,
|
||||
],
|
||||
flatViewsToUpdate: [
|
||||
...accumulator.flatViewsToUpdate,
|
||||
...flatViewsToUpdate,
|
||||
],
|
||||
errors: accumulator.errors,
|
||||
};
|
||||
}, initialAccumulator);
|
||||
|
||||
if (sideEffectErrors.length > 0) {
|
||||
return {
|
||||
status: 'fail',
|
||||
errors: sideEffectErrors,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
result: optimisticiallyUpdatedFlatFieldMetadatas,
|
||||
result: sideEffectFlatEntityOperations,
|
||||
};
|
||||
};
|
||||
|
||||
+22
-8
@@ -1,6 +1,7 @@
|
||||
import { type FromTo } from 'twenty-shared/types';
|
||||
|
||||
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
|
||||
import { type FieldInputTranspilationResult } from 'src/engine/metadata-modules/flat-field-metadata/types/field-input-transpilation-result.type';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { handleEnumFlatFieldMetadataUpdateSideEffects } from 'src/engine/metadata-modules/flat-field-metadata/utils/handle-enum-flat-field-metadata-update-side-effects.util';
|
||||
import {
|
||||
@@ -34,7 +35,9 @@ type HandleFlatFieldMetadataUpdateSideEffectArgs = FromTo<
|
||||
| 'flatViewGroupMaps'
|
||||
| 'flatViewMaps'
|
||||
| 'flatViewFieldMaps'
|
||||
>;
|
||||
> & {
|
||||
workspaceCustomApplicationId: string;
|
||||
};
|
||||
|
||||
export const FLAT_FIELD_METADATA_UPDATE_EMPTY_SIDE_EFFECTS: FlatFieldMetadataUpdateSideEffects =
|
||||
{
|
||||
@@ -61,7 +64,8 @@ export const handleFlatFieldMetadataUpdateSideEffect = ({
|
||||
flatViewGroupMaps,
|
||||
flatViewMaps,
|
||||
flatViewFieldMaps,
|
||||
}: HandleFlatFieldMetadataUpdateSideEffectArgs): FlatFieldMetadataUpdateSideEffects => {
|
||||
workspaceCustomApplicationId,
|
||||
}: HandleFlatFieldMetadataUpdateSideEffectArgs): FieldInputTranspilationResult<FlatFieldMetadataUpdateSideEffects> => {
|
||||
const sideEffectResult = structuredClone(
|
||||
FLAT_FIELD_METADATA_UPDATE_EMPTY_SIDE_EFFECTS,
|
||||
);
|
||||
@@ -114,18 +118,25 @@ export const handleFlatFieldMetadataUpdateSideEffect = ({
|
||||
sideEffectResult.flatViewFiltersToDelete.push(...flatViewFiltersToDelete);
|
||||
}
|
||||
|
||||
const {
|
||||
flatIndexMetadatasToUpdate,
|
||||
flatIndexMetadatasToCreate,
|
||||
flatIndexMetadatasToDelete,
|
||||
} = handleIndexChangesDuringFieldUpdate({
|
||||
const indexChangesSideEffectResult = handleIndexChangesDuringFieldUpdate({
|
||||
fromFlatFieldMetadata,
|
||||
toFlatFieldMetadata,
|
||||
flatIndexMaps,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
workspaceCustomApplicationId,
|
||||
});
|
||||
|
||||
if (indexChangesSideEffectResult.status === 'fail') {
|
||||
return indexChangesSideEffectResult;
|
||||
}
|
||||
|
||||
const {
|
||||
flatIndexMetadatasToUpdate,
|
||||
flatIndexMetadatasToCreate,
|
||||
flatIndexMetadatasToDelete,
|
||||
} = indexChangesSideEffectResult.result;
|
||||
|
||||
sideEffectResult.flatIndexMetadatasToUpdate.push(
|
||||
...flatIndexMetadatasToUpdate,
|
||||
);
|
||||
@@ -136,5 +147,8 @@ export const handleFlatFieldMetadataUpdateSideEffect = ({
|
||||
...flatIndexMetadatasToDelete,
|
||||
);
|
||||
|
||||
return sideEffectResult;
|
||||
return {
|
||||
status: 'success',
|
||||
result: sideEffectResult,
|
||||
};
|
||||
};
|
||||
|
||||
+59
-17
@@ -1,7 +1,11 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { type FromTo } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
|
||||
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { type FieldInputTranspilationResult } from 'src/engine/metadata-modules/flat-field-metadata/types/field-input-transpilation-result.type';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { findFieldRelatedIndexes } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-field-related-index.util';
|
||||
import { generateIndexForFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/generate-index-for-flat-field-metadata.util';
|
||||
@@ -23,7 +27,9 @@ type FromToFlatFieldMetadataAndFlatEntityMaps = FromTo<
|
||||
Pick<
|
||||
AllFlatEntityMaps,
|
||||
'flatObjectMetadataMaps' | 'flatFieldMetadataMaps' | 'flatIndexMaps'
|
||||
>;
|
||||
> & {
|
||||
workspaceCustomApplicationId: string;
|
||||
};
|
||||
const FIELD_METADATA_UPDATE_INDEX_SIDE_EFFECT: FieldMetadataUpdateIndexSideEffect =
|
||||
{
|
||||
flatIndexMetadatasToUpdate: [],
|
||||
@@ -37,14 +43,18 @@ export const handleIndexChangesDuringFieldUpdate = ({
|
||||
flatIndexMaps,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
}: FromToFlatFieldMetadataAndFlatEntityMaps): FieldMetadataUpdateIndexSideEffect => {
|
||||
workspaceCustomApplicationId,
|
||||
}: FromToFlatFieldMetadataAndFlatEntityMaps): FieldInputTranspilationResult<FieldMetadataUpdateIndexSideEffect> => {
|
||||
if (
|
||||
!hasIndexRelevantChanges({
|
||||
fromFlatFieldMetadata,
|
||||
toFlatFieldMetadata,
|
||||
})
|
||||
) {
|
||||
return FIELD_METADATA_UPDATE_INDEX_SIDE_EFFECT;
|
||||
return {
|
||||
status: 'success',
|
||||
result: FIELD_METADATA_UPDATE_INDEX_SIDE_EFFECT,
|
||||
};
|
||||
}
|
||||
|
||||
const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
@@ -71,6 +81,7 @@ export const handleIndexChangesDuringFieldUpdate = ({
|
||||
relatedIndexes,
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
workspaceCustomApplicationId,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -87,9 +98,12 @@ const handleNoExistingIndexes = ({
|
||||
}: {
|
||||
toFlatFieldMetadata: FlatFieldMetadata;
|
||||
flatObjectMetadata: FlatObjectMetadata;
|
||||
}): FieldMetadataUpdateIndexSideEffect => {
|
||||
}): FieldInputTranspilationResult<FieldMetadataUpdateIndexSideEffect> => {
|
||||
if (!toFlatFieldMetadata.isUnique) {
|
||||
return FIELD_METADATA_UPDATE_INDEX_SIDE_EFFECT;
|
||||
return {
|
||||
status: 'success',
|
||||
result: FIELD_METADATA_UPDATE_INDEX_SIDE_EFFECT,
|
||||
};
|
||||
}
|
||||
|
||||
const newIndex = generateIndexForFlatFieldMetadata({
|
||||
@@ -99,8 +113,11 @@ const handleNoExistingIndexes = ({
|
||||
});
|
||||
|
||||
return {
|
||||
...FIELD_METADATA_UPDATE_INDEX_SIDE_EFFECT,
|
||||
flatIndexMetadatasToCreate: [newIndex],
|
||||
status: 'success',
|
||||
result: {
|
||||
...FIELD_METADATA_UPDATE_INDEX_SIDE_EFFECT,
|
||||
flatIndexMetadatasToCreate: [newIndex],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -110,14 +127,14 @@ const handleExistingIndexes = ({
|
||||
relatedIndexes,
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
workspaceCustomApplicationId,
|
||||
}: {
|
||||
relatedIndexes: FlatIndexMetadata[];
|
||||
flatObjectMetadata: FlatObjectMetadata;
|
||||
flatFieldMetadataMaps: AllFlatEntityMaps['flatFieldMetadataMaps'];
|
||||
} & FromTo<
|
||||
FlatFieldMetadata,
|
||||
'flatFieldMetadata'
|
||||
>): FieldMetadataUpdateIndexSideEffect => {
|
||||
} & FromTo<FlatFieldMetadata, 'flatFieldMetadata'> & {
|
||||
workspaceCustomApplicationId: string;
|
||||
}): FieldInputTranspilationResult<FieldMetadataUpdateIndexSideEffect> => {
|
||||
if (
|
||||
toFlatFieldMetadata.isUnique === false &&
|
||||
!isMorphOrRelationFlatFieldMetadata(fromFlatFieldMetadata)
|
||||
@@ -135,11 +152,33 @@ const handleExistingIndexes = ({
|
||||
(index) => index.name === expectedUniqueIndex.name,
|
||||
);
|
||||
|
||||
if (
|
||||
isDefined(uniqueIndexToDelete) &&
|
||||
((isDefined(uniqueIndexToDelete.applicationId) &&
|
||||
uniqueIndexToDelete.applicationId !== workspaceCustomApplicationId) ||
|
||||
!uniqueIndexToDelete.isCustom)
|
||||
) {
|
||||
return {
|
||||
status: 'fail',
|
||||
errors: [
|
||||
{
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message:
|
||||
'Cannot delete unique index that have not been created by the workspace custom application',
|
||||
userFriendlyMessage: msg`Cannot delete unique index that have not been created by the workspace custom application`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...FIELD_METADATA_UPDATE_INDEX_SIDE_EFFECT,
|
||||
flatIndexMetadatasToDelete: uniqueIndexToDelete
|
||||
? [uniqueIndexToDelete]
|
||||
: [],
|
||||
status: 'success',
|
||||
result: {
|
||||
...FIELD_METADATA_UPDATE_INDEX_SIDE_EFFECT,
|
||||
flatIndexMetadatasToDelete: uniqueIndexToDelete
|
||||
? [uniqueIndexToDelete]
|
||||
: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
const updatedIndexes = recomputeIndexOnFlatFieldMetadataNameUpdate({
|
||||
@@ -154,7 +193,10 @@ const handleExistingIndexes = ({
|
||||
});
|
||||
|
||||
return {
|
||||
...FIELD_METADATA_UPDATE_INDEX_SIDE_EFFECT,
|
||||
flatIndexMetadatasToUpdate: updatedIndexes,
|
||||
status: 'success',
|
||||
result: {
|
||||
...FIELD_METADATA_UPDATE_INDEX_SIDE_EFFECT,
|
||||
flatIndexMetadatasToUpdate: updatedIndexes,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ export const throwOnFieldInputTranspilationsError: ThrowOnFieldInputTranspilatio
|
||||
) => {
|
||||
const failedInputTranspilationErrors = inputTranspilationResults.flatMap(
|
||||
(transpilationResult) =>
|
||||
transpilationResult.status === 'fail' ? transpilationResult.error : [],
|
||||
transpilationResult.status === 'fail' ? transpilationResult.errors : [],
|
||||
);
|
||||
|
||||
if (failedInputTranspilationErrors.length > 0) {
|
||||
|
||||
+43
-31
@@ -32,11 +32,13 @@ export const validateMorphRelationCreationPayload = async ({
|
||||
if (morphRelationCreationPayload.length === 0) {
|
||||
return {
|
||||
status: 'fail',
|
||||
error: {
|
||||
code: FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED,
|
||||
message: 'Morph relation creation payloads are empty',
|
||||
userFriendlyMessage: msg`At least one relation is require`,
|
||||
},
|
||||
errors: [
|
||||
{
|
||||
code: FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED,
|
||||
message: 'Morph relation creation payloads are empty',
|
||||
userFriendlyMessage: msg`At least one relation is require`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -51,12 +53,14 @@ export const validateMorphRelationCreationPayload = async ({
|
||||
if (allRelationType.length > 1) {
|
||||
return {
|
||||
status: 'fail',
|
||||
error: {
|
||||
code: FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED,
|
||||
message:
|
||||
'Morph relation creation payloads must have the same relation type',
|
||||
userFriendlyMessage: msg`Morph relation creation payloads must have the same relation type`,
|
||||
},
|
||||
errors: [
|
||||
{
|
||||
code: FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED,
|
||||
message:
|
||||
'Morph relation creation payloads must have the same relation type',
|
||||
userFriendlyMessage: msg`Morph relation creation payloads must have the same relation type`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -70,12 +74,14 @@ export const validateMorphRelationCreationPayload = async ({
|
||||
if (allRelatedObjectMetadataIdsSet.includes(objectMetadataId)) {
|
||||
return {
|
||||
status: 'fail',
|
||||
error: {
|
||||
code: FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED,
|
||||
message:
|
||||
'Morph relation creation payloads must not target source object metadata',
|
||||
userFriendlyMessage: msg`Morph relation creation payloads must only contain relation to other object metadata`,
|
||||
},
|
||||
errors: [
|
||||
{
|
||||
code: FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED,
|
||||
message:
|
||||
'Morph relation creation payloads must not target source object metadata',
|
||||
userFriendlyMessage: msg`Morph relation creation payloads must only contain relation to other object metadata`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -84,12 +90,14 @@ export const validateMorphRelationCreationPayload = async ({
|
||||
) {
|
||||
return {
|
||||
status: 'fail',
|
||||
error: {
|
||||
code: FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED,
|
||||
message:
|
||||
'Morph relation creation payloads must have only relation to the same object metadata',
|
||||
userFriendlyMessage: msg`Morph relation creation payloads must only contain relation to the same object metadata`,
|
||||
},
|
||||
errors: [
|
||||
{
|
||||
code: FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED,
|
||||
message:
|
||||
'Morph relation creation payloads must have only relation to the same object metadata',
|
||||
userFriendlyMessage: msg`Morph relation creation payloads must only contain relation to the same object metadata`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -127,14 +135,18 @@ export const validateMorphRelationCreationPayload = async ({
|
||||
if (relationCreationPayloadReport.failed.length > 0) {
|
||||
return {
|
||||
status: 'fail',
|
||||
error: {
|
||||
code: FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED,
|
||||
message: 'Morph relation input transpilation failed',
|
||||
userFriendlyMessage: msg`Invalid morph relation input`,
|
||||
value: relationCreationPayloadReport.failed
|
||||
.map((failedTranspilation) => failedTranspilation.error.value)
|
||||
.filter(isDefined),
|
||||
},
|
||||
errors: [
|
||||
{
|
||||
code: FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED,
|
||||
message: 'Morph relation input transpilation failed',
|
||||
userFriendlyMessage: msg`Invalid morph relation input`,
|
||||
value: relationCreationPayloadReport.failed
|
||||
.flatMap((failedTranspilation) =>
|
||||
failedTranspilation.errors.map((error) => error.value),
|
||||
)
|
||||
.filter(isDefined),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+16
-12
@@ -39,12 +39,14 @@ export const validateRelationCreationPayload = async ({
|
||||
if (error instanceof FieldMetadataException) {
|
||||
return {
|
||||
status: 'fail',
|
||||
error: {
|
||||
code: FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED,
|
||||
message: `Relation creation payload is invalid`,
|
||||
userFriendlyMessage: msg`Invalid relation creation payload`,
|
||||
value: relationCreationPayload,
|
||||
},
|
||||
errors: [
|
||||
{
|
||||
code: FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED,
|
||||
message: `Relation creation payload is invalid`,
|
||||
userFriendlyMessage: msg`Invalid relation creation payload`,
|
||||
value: relationCreationPayload,
|
||||
},
|
||||
],
|
||||
};
|
||||
} else {
|
||||
throw error;
|
||||
@@ -59,12 +61,14 @@ export const validateRelationCreationPayload = async ({
|
||||
if (!isDefined(targetFlatObjectMetadata)) {
|
||||
return {
|
||||
status: 'fail',
|
||||
error: {
|
||||
code: FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED,
|
||||
message: `Object metadata relation target not found for relation creation payload`,
|
||||
userFriendlyMessage: msg`Object targeted by field to create not found`,
|
||||
value: relationCreationPayload,
|
||||
},
|
||||
errors: [
|
||||
{
|
||||
code: FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED,
|
||||
message: `Object metadata relation target not found for relation creation payload`,
|
||||
userFriendlyMessage: msg`Object targeted by field to create not found`,
|
||||
value: relationCreationPayload,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+13
-5
@@ -1,4 +1,4 @@
|
||||
import { generateDeterministicIndexName } from 'src/engine/metadata-modules/index-metadata/utils/generate-deterministic-index-name';
|
||||
import { generateDeterministicIndexNameV2 } from 'src/engine/metadata-modules/index-metadata/utils/generate-deterministic-index-name-v2';
|
||||
import { type WorkspaceIndexOptions } from 'src/engine/twenty-orm/decorators/workspace-index.decorator';
|
||||
import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage';
|
||||
import { getColumnsForIndex } from 'src/engine/twenty-orm/utils/get-default-columns-for-index.util';
|
||||
@@ -30,10 +30,18 @@ export function WorkspaceFieldIndex(
|
||||
];
|
||||
|
||||
metadataArgsStorage.addIndexes({
|
||||
name: `IDX_${generateDeterministicIndexName([
|
||||
convertClassNameToObjectMetadataName(target.constructor.name),
|
||||
...columns,
|
||||
])}`,
|
||||
name: generateDeterministicIndexNameV2({
|
||||
flatObjectMetadata: {
|
||||
nameSingular: convertClassNameToObjectMetadataName(
|
||||
target.constructor.name,
|
||||
),
|
||||
isCustom: false,
|
||||
},
|
||||
relatedFieldNames: columns.map((column) => ({
|
||||
name: column,
|
||||
})),
|
||||
isUnique: options?.isUnique ?? false,
|
||||
}),
|
||||
columns,
|
||||
target: target.constructor,
|
||||
gate,
|
||||
|
||||
+13
-7
@@ -1,5 +1,5 @@
|
||||
import { type IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
|
||||
import { generateDeterministicIndexName } from 'src/engine/metadata-modules/index-metadata/utils/generate-deterministic-index-name';
|
||||
import { generateDeterministicIndexNameV2 } from 'src/engine/metadata-modules/index-metadata/utils/generate-deterministic-index-name-v2';
|
||||
import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage';
|
||||
import { convertClassNameToObjectMetadataName } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/convert-class-to-object-metadata-name.util';
|
||||
import { TypedReflect } from 'src/utils/typed-reflect';
|
||||
@@ -26,12 +26,18 @@ export function WorkspaceIndex(
|
||||
);
|
||||
|
||||
metadataArgsStorage.addIndexes({
|
||||
name: `IDX_${
|
||||
options?.isUnique ? 'UNIQUE_' : ''
|
||||
}${generateDeterministicIndexName([
|
||||
convertClassNameToObjectMetadataName(target.name),
|
||||
...columns,
|
||||
])}`,
|
||||
name: generateDeterministicIndexNameV2({
|
||||
flatObjectMetadata: {
|
||||
nameSingular: convertClassNameToObjectMetadataName(
|
||||
target.constructor.name,
|
||||
),
|
||||
isCustom: false,
|
||||
},
|
||||
relatedFieldNames: columns.map((column) => ({
|
||||
name: column,
|
||||
})),
|
||||
isUnique: options?.isUnique ?? false,
|
||||
}),
|
||||
columns,
|
||||
target: target,
|
||||
gate,
|
||||
|
||||
+13
-5
@@ -1,4 +1,4 @@
|
||||
import { generateDeterministicIndexName } from 'src/engine/metadata-modules/index-metadata/utils/generate-deterministic-index-name';
|
||||
import { generateDeterministicIndexNameV2 } from 'src/engine/metadata-modules/index-metadata/utils/generate-deterministic-index-name-v2';
|
||||
import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage';
|
||||
import { convertClassNameToObjectMetadataName } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/convert-class-to-object-metadata-name.util';
|
||||
import { TypedReflect } from 'src/utils/typed-reflect';
|
||||
@@ -19,10 +19,18 @@ export function WorkspaceIsUnique(): PropertyDecorator {
|
||||
const columns = [propertyKey.toString()];
|
||||
|
||||
metadataArgsStorage.addIndexes({
|
||||
name: `IDX_${generateDeterministicIndexName([
|
||||
convertClassNameToObjectMetadataName(target.constructor.name),
|
||||
...columns,
|
||||
])}`,
|
||||
name: generateDeterministicIndexNameV2({
|
||||
flatObjectMetadata: {
|
||||
nameSingular: convertClassNameToObjectMetadataName(
|
||||
target.constructor.name,
|
||||
),
|
||||
isCustom: false,
|
||||
},
|
||||
relatedFieldNames: columns.map((column) => ({
|
||||
name: column,
|
||||
})),
|
||||
isUnique: true,
|
||||
}),
|
||||
columns,
|
||||
target: target.constructor,
|
||||
gate,
|
||||
|
||||
+13
-3
@@ -5,12 +5,11 @@ import { type WorkspaceSyncContext } from 'src/engine/workspace-manager/workspac
|
||||
|
||||
import { type IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
|
||||
import { generateDeterministicIndexName } from 'src/engine/metadata-modules/index-metadata/utils/generate-deterministic-index-name';
|
||||
import { generateDeterministicIndexNameV2 } from 'src/engine/metadata-modules/index-metadata/utils/generate-deterministic-index-name-v2';
|
||||
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { type BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
import { CustomWorkspaceEntity } from 'src/engine/twenty-orm/custom.workspace-entity';
|
||||
import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage';
|
||||
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
|
||||
import { isGatedAndNotEnabled } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/is-gate-and-not-enabled.util';
|
||||
|
||||
@Injectable()
|
||||
@@ -124,7 +123,18 @@ export class StandardIndexFactory {
|
||||
const indexMetadata: PartialIndexMetadata = {
|
||||
workspaceId: context.workspaceId,
|
||||
objectMetadataId: customObjectMetadata.id,
|
||||
name: `IDX_${generateDeterministicIndexName([computeTableName(customObjectName, true), ...workspaceIndexMetadataArgs.columns])}`,
|
||||
name: generateDeterministicIndexNameV2({
|
||||
flatObjectMetadata: {
|
||||
nameSingular: customObjectName,
|
||||
isCustom: true,
|
||||
},
|
||||
relatedFieldNames: workspaceIndexMetadataArgs.columns.map(
|
||||
(column) => ({
|
||||
name: column,
|
||||
}),
|
||||
),
|
||||
isUnique: workspaceIndexMetadataArgs.isUnique,
|
||||
}),
|
||||
columns: workspaceIndexMetadataArgs.columns,
|
||||
isCustom: false,
|
||||
isUnique: workspaceIndexMetadataArgs.isUnique,
|
||||
|
||||
+63
@@ -35,3 +35,66 @@ exports[`Standard field metadata update should be ignored when trying to update
|
||||
"name": "ForbiddenError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Standard field with standard unique index update should fail on isUnique change should fail when trying to remove unique constraint on standard field with standard index 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "METADATA_VALIDATION_FAILED",
|
||||
"errors": {
|
||||
"agent": [],
|
||||
"cronTrigger": [],
|
||||
"databaseEventTrigger": [],
|
||||
"fieldMetadata": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_FIELD_INPUT",
|
||||
"message": "Cannot delete unique index that have not been created by the workspace custom application",
|
||||
"userFriendlyMessage": "Cannot delete unique index that have not been created by the workspace custom application",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {},
|
||||
"type": "update_field",
|
||||
},
|
||||
],
|
||||
"index": [],
|
||||
"objectMetadata": [],
|
||||
"pageLayout": [],
|
||||
"pageLayoutTab": [],
|
||||
"pageLayoutWidget": [],
|
||||
"role": [],
|
||||
"roleTarget": [],
|
||||
"routeTrigger": [],
|
||||
"serverlessFunction": [],
|
||||
"view": [],
|
||||
"viewField": [],
|
||||
"viewFilter": [],
|
||||
"viewGroup": [],
|
||||
},
|
||||
"message": "Validation failed for 0 object(s) and 0 field(s)",
|
||||
"summary": {
|
||||
"invalidAgent": 0,
|
||||
"invalidCronTrigger": 0,
|
||||
"invalidDatabaseEventTrigger": 0,
|
||||
"invalidFieldMetadata": 0,
|
||||
"invalidIndex": 0,
|
||||
"invalidObjectMetadata": 0,
|
||||
"invalidPageLayout": 0,
|
||||
"invalidPageLayoutTab": 0,
|
||||
"invalidPageLayoutWidget": 0,
|
||||
"invalidRole": 0,
|
||||
"invalidRoleTarget": 0,
|
||||
"invalidRouteTrigger": 0,
|
||||
"invalidServerlessFunction": 0,
|
||||
"invalidView": 0,
|
||||
"invalidViewField": 0,
|
||||
"invalidViewFilter": 0,
|
||||
"invalidViewGroup": 0,
|
||||
"totalErrors": 0,
|
||||
},
|
||||
"userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)",
|
||||
},
|
||||
"message": "Validation errors occurred while updating field",
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
|
||||
+57
@@ -103,3 +103,60 @@ describe('Standard field metadata update should be ignored', () => {
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// TODO: Enable this test once isUnique set as editable on standard fields
|
||||
xdescribe('Standard field with standard unique index update should fail on isUnique change', () => {
|
||||
let companyDomainNameFieldMetadataId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const { objects } = await findManyObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
filter: {},
|
||||
paging: { first: 100 },
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
nameSingular
|
||||
fieldsList {
|
||||
id
|
||||
name
|
||||
label
|
||||
isCustom
|
||||
isUnique
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
const companyObject = objects.find((o) => o.nameSingular === 'company');
|
||||
|
||||
jestExpectToBeDefined(companyObject);
|
||||
|
||||
const companyDomainNameField = companyObject.fieldsList?.find(
|
||||
(field) => field.name === 'domainName' && !field.isCustom,
|
||||
);
|
||||
|
||||
jestExpectToBeDefined(companyDomainNameField);
|
||||
companyDomainNameFieldMetadataId = companyDomainNameField.id;
|
||||
});
|
||||
|
||||
it('should fail when trying to remove unique constraint on standard field with standard index', async () => {
|
||||
const { errors } = await updateOneFieldMetadata({
|
||||
input: {
|
||||
idToUpdate: companyDomainNameFieldMetadataId,
|
||||
updatePayload: {
|
||||
isUnique: false,
|
||||
},
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
name
|
||||
isUnique
|
||||
`,
|
||||
});
|
||||
|
||||
expectOneNotInternalServerErrorSnapshot({
|
||||
errors,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+127
@@ -1,11 +1,16 @@
|
||||
import { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata.util';
|
||||
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
|
||||
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
|
||||
import { findManyObjectMetadataWithIndexes } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata-with-indexes.util';
|
||||
import { findManyObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata.util';
|
||||
import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
|
||||
import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any';
|
||||
import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test';
|
||||
import {
|
||||
eachTestingContextFilter,
|
||||
type EachTestingContext,
|
||||
} from 'twenty-shared/testing';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto';
|
||||
import { type UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input';
|
||||
@@ -253,3 +258,125 @@ describe('Standard field metadata update should succeed', () => {
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// TODO: Enable this test once isUnique set as editable on standard fields
|
||||
xdescribe('Standard field isUnique update should succeed', () => {
|
||||
let nameFieldMetadata: FieldMetadataDTO | undefined;
|
||||
let customObjectId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
const customObject = {
|
||||
labelSingular: `Test Unique Standard Field`,
|
||||
labelPlural: `Test Unique Standard Fields`,
|
||||
namePlural: `testUniqueStandardField`,
|
||||
nameSingular: `testUniqueStandardFields`,
|
||||
description: 'Test unique standard field for isUnique update',
|
||||
icon: 'IconBox',
|
||||
isLabelSyncedWithName: false,
|
||||
};
|
||||
|
||||
const { data } = await createOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: customObject,
|
||||
gqlFields: `
|
||||
id
|
||||
nameSingular
|
||||
`,
|
||||
});
|
||||
|
||||
customObjectId = data.createOneObject.id;
|
||||
|
||||
const { objects } = await findManyObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
filter: {},
|
||||
paging: { first: 100 },
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
nameSingular
|
||||
fieldsList {
|
||||
id
|
||||
name
|
||||
label
|
||||
isUnique
|
||||
isActive
|
||||
isCustom
|
||||
type
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
const customObjectWithFields = objects.find((o) => o.id === customObjectId);
|
||||
|
||||
jestExpectToBeDefined(customObjectWithFields?.fieldsList);
|
||||
|
||||
nameFieldMetadata = customObjectWithFields.fieldsList?.find(
|
||||
(field) => field.name === 'name',
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await updateOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
idToUpdate: customObjectId,
|
||||
updatePayload: {
|
||||
isActive: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await deleteOneObjectMetadata({
|
||||
input: {
|
||||
idToDelete: customObjectId,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should set isUnique to true on standard field', async () => {
|
||||
jestExpectToBeDefined(nameFieldMetadata);
|
||||
|
||||
const { data } = await updateOneFieldMetadata({
|
||||
input: {
|
||||
idToUpdate: nameFieldMetadata.id,
|
||||
updatePayload: {
|
||||
isUnique: true,
|
||||
},
|
||||
},
|
||||
expectToFail: false,
|
||||
gqlFields: `
|
||||
id
|
||||
name
|
||||
label
|
||||
isUnique
|
||||
isActive
|
||||
isCustom
|
||||
`,
|
||||
});
|
||||
|
||||
expect(data.updateOneField.id).toBe(nameFieldMetadata.id);
|
||||
expect(data.updateOneField.name).toBe('name');
|
||||
expect(data.updateOneField.isUnique).toBe(true);
|
||||
|
||||
const objects = await findManyObjectMetadataWithIndexes({
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const customObject = objects.find((obj) => obj.id === customObjectId);
|
||||
|
||||
jestExpectToBeDefined(customObject);
|
||||
|
||||
const nameFieldIndex = customObject.indexMetadataList.find((index) =>
|
||||
index.indexFieldMetadataList.some(
|
||||
(indexField) =>
|
||||
isDefined(nameFieldMetadata) &&
|
||||
indexField.fieldMetadataId === nameFieldMetadata.id,
|
||||
),
|
||||
);
|
||||
|
||||
jestExpectToBeDefined(nameFieldIndex);
|
||||
expect(nameFieldIndex.isUnique).toBe(true);
|
||||
expect(nameFieldIndex.isCustom).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user