[BREAKING_CHANGE/GRAPHQL/OBJECT_METADATA_CREATE_ONE] Remove object/fields/view-fields v1 implementation (#15823)
# Introduction Remove the v2 feature flag for view-field field-metadata and object-metadata metadata entities ## Some details - Disabled nestjs-query for object metadata creation and explicitly calling it - removed all v1 integration tests files ## Remarks Not remove v2 referencing in both filenaming right now will handle that globally later ## Breaking change Due to object metadata resolver createOne standardization had to rename the input from `CreateObjectInput` to `CreateOneObjectInput`
This commit is contained in:
@@ -852,7 +852,7 @@ export type CreateOneFieldMetadataInput = {
|
||||
};
|
||||
|
||||
export type CreateOneObjectInput = {
|
||||
/** The record to create */
|
||||
/** The object to create */
|
||||
object: CreateObjectInput;
|
||||
};
|
||||
|
||||
|
||||
@@ -800,11 +800,30 @@ export type CreateFieldInput = {
|
||||
type: FieldMetadataType;
|
||||
};
|
||||
|
||||
export type CreateObjectInput = {
|
||||
description?: InputMaybe<Scalars['String']>;
|
||||
icon?: InputMaybe<Scalars['String']>;
|
||||
isLabelSyncedWithName?: InputMaybe<Scalars['Boolean']>;
|
||||
isRemote?: InputMaybe<Scalars['Boolean']>;
|
||||
labelPlural: Scalars['String'];
|
||||
labelSingular: Scalars['String'];
|
||||
namePlural: Scalars['String'];
|
||||
nameSingular: Scalars['String'];
|
||||
primaryKeyColumnType?: InputMaybe<Scalars['String']>;
|
||||
primaryKeyFieldMetadataSettings?: InputMaybe<Scalars['JSON']>;
|
||||
shortcut?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type CreateOneFieldMetadataInput = {
|
||||
/** The record to create */
|
||||
field: CreateFieldInput;
|
||||
};
|
||||
|
||||
export type CreateOneObjectInput = {
|
||||
/** The object to create */
|
||||
object: CreateObjectInput;
|
||||
};
|
||||
|
||||
export type CreatePageLayoutInput = {
|
||||
name: Scalars['String'];
|
||||
objectMetadataId?: InputMaybe<Scalars['UUID']>;
|
||||
@@ -1999,6 +2018,11 @@ export type MutationCreateOneFieldArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateOneObjectArgs = {
|
||||
input: CreateOneObjectInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateOneRoleArgs = {
|
||||
createRoleInput: CreateRoleInput;
|
||||
};
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@ export class MakeSureDashboardNamingAvailableCommand extends ActiveOrSuspendedWo
|
||||
`Updating the dashboard object metadata for workspace ${workspaceId}...`,
|
||||
);
|
||||
|
||||
await this.objectMetadataServiceV2.updateOne({
|
||||
await this.objectMetadataServiceV2.updateOneObject({
|
||||
workspaceId,
|
||||
updateObjectInput: {
|
||||
id: potentialCustomDashboardObjectMetadata.id,
|
||||
|
||||
@@ -7,7 +7,7 @@ import { WorkspaceSchemaFactory } from 'src/engine/api/graphql/workspace-schema.
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
||||
import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service';
|
||||
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
|
||||
@@ -23,7 +23,7 @@ describe('WorkspaceSchemaFactory', () => {
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: ObjectMetadataService,
|
||||
provide: ObjectMetadataServiceV2,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
|
||||
+2
-2
@@ -6,7 +6,7 @@ import { DeleteRecordService } from 'src/engine/core-modules/record-crud/service
|
||||
import { FindRecordsService } from 'src/engine/core-modules/record-crud/services/find-records.service';
|
||||
import { UpdateRecordService } from 'src/engine/core-modules/record-crud/services/update-record.service';
|
||||
import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service';
|
||||
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
||||
import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service';
|
||||
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
@@ -57,7 +57,7 @@ describe('ToolService', () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: ObjectMetadataService,
|
||||
provide: ObjectMetadataServiceV2,
|
||||
useValue: {
|
||||
findManyWithinWorkspace: jest.fn().mockResolvedValue([testObject]),
|
||||
},
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
type ToolHints,
|
||||
type ToolOperation,
|
||||
} from 'src/engine/metadata-modules/ai-router/types/tool-hints.interface';
|
||||
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
||||
import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service';
|
||||
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
|
||||
@@ -29,7 +29,7 @@ export class ToolService {
|
||||
|
||||
constructor(
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
private readonly objectMetadataService: ObjectMetadataService,
|
||||
private readonly objectMetadataService: ObjectMetadataServiceV2,
|
||||
protected readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
|
||||
private readonly createRecordService: CreateRecordService,
|
||||
private readonly updateRecordService: UpdateRecordService,
|
||||
|
||||
+6
-6
@@ -219,7 +219,7 @@ export class ApplicationSyncService {
|
||||
);
|
||||
|
||||
for (const fieldToDelete of fieldsToDelete) {
|
||||
await this.fieldMetadataServiceV2.updateOne({
|
||||
await this.fieldMetadataServiceV2.updateOneField({
|
||||
updateFieldInput: {
|
||||
id: fieldToDelete.id,
|
||||
isActive: false,
|
||||
@@ -256,7 +256,7 @@ export class ApplicationSyncService {
|
||||
isNullable: fieldToSync.isNullable ?? true,
|
||||
};
|
||||
|
||||
await this.fieldMetadataServiceV2.updateOne({
|
||||
await this.fieldMetadataServiceV2.updateOneField({
|
||||
updateFieldInput,
|
||||
workspaceId,
|
||||
});
|
||||
@@ -281,7 +281,7 @@ export class ApplicationSyncService {
|
||||
workspaceId,
|
||||
};
|
||||
|
||||
await this.fieldMetadataServiceV2.createOne({
|
||||
await this.fieldMetadataServiceV2.createOneField({
|
||||
createFieldInput,
|
||||
workspaceId,
|
||||
});
|
||||
@@ -341,7 +341,7 @@ export class ApplicationSyncService {
|
||||
);
|
||||
|
||||
for (const objectToDelete of objectsToDelete) {
|
||||
await this.objectMetadataServiceV2.deleteOne({
|
||||
await this.objectMetadataServiceV2.deleteOneObject({
|
||||
deleteObjectInput: { id: objectToDelete.id },
|
||||
workspaceId,
|
||||
isSystemBuild: true,
|
||||
@@ -372,7 +372,7 @@ export class ApplicationSyncService {
|
||||
},
|
||||
};
|
||||
|
||||
await this.objectMetadataServiceV2.updateOne({
|
||||
await this.objectMetadataServiceV2.updateOneObject({
|
||||
updateObjectInput,
|
||||
workspaceId,
|
||||
});
|
||||
@@ -404,7 +404,7 @@ export class ApplicationSyncService {
|
||||
applicationId,
|
||||
};
|
||||
|
||||
const createdObject = await this.objectMetadataServiceV2.createOne({
|
||||
const createdObject = await this.objectMetadataServiceV2.createOneObject({
|
||||
createObjectInput,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { OpenApiService } from 'src/engine/core-modules/open-api/open-api.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
||||
import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service';
|
||||
|
||||
describe('OpenApiService', () => {
|
||||
let service: OpenApiService;
|
||||
@@ -18,7 +18,7 @@ describe('OpenApiService', () => {
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: ObjectMetadataService,
|
||||
provide: ObjectMetadataServiceV2,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -47,8 +47,8 @@ import {
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception';
|
||||
import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service';
|
||||
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
||||
import { standardObjectMetadataDefinitions } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects';
|
||||
import { shouldExcludeFromWorkspaceApi } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/should-exclude-from-workspace-api.util';
|
||||
import { getServerUrl } from 'src/utils/get-server-url';
|
||||
@@ -58,7 +58,7 @@ export class OpenApiService {
|
||||
constructor(
|
||||
private readonly accessTokenService: AccessTokenService,
|
||||
private readonly twentyConfigService: TwentyConfigService,
|
||||
private readonly objectMetadataService: ObjectMetadataService,
|
||||
private readonly objectMetadataService: ObjectMetadataServiceV2,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
) {}
|
||||
|
||||
|
||||
-215
@@ -1,215 +0,0 @@
|
||||
import { Transform } from 'class-transformer';
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
IsNumber,
|
||||
IsString,
|
||||
} from 'class-validator';
|
||||
|
||||
import { ConfigVariableType } from 'src/engine/core-modules/twenty-config/enums/config-variable-type.enum';
|
||||
import { applyBasicValidators } from 'src/engine/core-modules/twenty-config/utils/apply-basic-validators.util';
|
||||
import { configTransformers } from 'src/engine/core-modules/twenty-config/utils/config-transformers.util';
|
||||
|
||||
jest.mock('class-transformer', () => ({
|
||||
Transform: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('class-validator', () => ({
|
||||
IsBoolean: jest.fn().mockReturnValue(jest.fn()),
|
||||
IsNumber: jest.fn().mockReturnValue(jest.fn()),
|
||||
IsString: jest.fn().mockReturnValue(jest.fn()),
|
||||
IsEnum: jest.fn().mockReturnValue(jest.fn()),
|
||||
IsArray: jest.fn().mockReturnValue(jest.fn()),
|
||||
}));
|
||||
|
||||
jest.mock(
|
||||
'src/engine/core-modules/twenty-config/utils/config-transformers.util',
|
||||
() => ({
|
||||
configTransformers: {
|
||||
boolean: jest.fn(),
|
||||
number: jest.fn(),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
describe('applyBasicValidators', () => {
|
||||
const mockTarget = {};
|
||||
const mockPropertyKey = 'testProperty';
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('boolean type', () => {
|
||||
it('should apply boolean transformers and validators', () => {
|
||||
let capturedTransformFn;
|
||||
|
||||
(Transform as jest.Mock).mockImplementation((transformFn) => {
|
||||
capturedTransformFn = transformFn;
|
||||
|
||||
return jest.fn();
|
||||
});
|
||||
|
||||
applyBasicValidators(
|
||||
ConfigVariableType.BOOLEAN,
|
||||
mockTarget,
|
||||
mockPropertyKey,
|
||||
);
|
||||
|
||||
expect(Transform).toHaveBeenCalled();
|
||||
expect(IsBoolean).toHaveBeenCalled();
|
||||
|
||||
const transformFn = capturedTransformFn;
|
||||
const mockTransformParams = { value: 'true' };
|
||||
|
||||
(configTransformers.boolean as jest.Mock).mockReturnValueOnce(true);
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
const result1 = transformFn(mockTransformParams);
|
||||
|
||||
expect(configTransformers.boolean).toHaveBeenCalledWith('true');
|
||||
expect(result1).toBe(true);
|
||||
|
||||
(configTransformers.boolean as jest.Mock).mockReturnValueOnce(undefined);
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
const result2 = transformFn(mockTransformParams);
|
||||
|
||||
expect(result2).toBe('true');
|
||||
});
|
||||
});
|
||||
|
||||
describe('number type', () => {
|
||||
it('should apply number transformers and validators', () => {
|
||||
let capturedTransformFn;
|
||||
|
||||
(Transform as jest.Mock).mockImplementation((transformFn) => {
|
||||
capturedTransformFn = transformFn;
|
||||
|
||||
return jest.fn();
|
||||
});
|
||||
|
||||
applyBasicValidators(
|
||||
ConfigVariableType.NUMBER,
|
||||
mockTarget,
|
||||
mockPropertyKey,
|
||||
);
|
||||
|
||||
expect(Transform).toHaveBeenCalled();
|
||||
expect(IsNumber).toHaveBeenCalled();
|
||||
|
||||
const transformFn = capturedTransformFn;
|
||||
const mockTransformParams = { value: '42' };
|
||||
|
||||
(configTransformers.number as jest.Mock).mockReturnValueOnce(42);
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
const result1 = transformFn(mockTransformParams);
|
||||
|
||||
expect(configTransformers.number).toHaveBeenCalledWith('42');
|
||||
expect(result1).toBe(42);
|
||||
|
||||
(configTransformers.number as jest.Mock).mockReturnValueOnce(undefined);
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
const result2 = transformFn(mockTransformParams);
|
||||
|
||||
expect(result2).toBe('42');
|
||||
});
|
||||
});
|
||||
|
||||
describe('string type', () => {
|
||||
it('should apply string validator', () => {
|
||||
applyBasicValidators(
|
||||
ConfigVariableType.STRING,
|
||||
mockTarget,
|
||||
mockPropertyKey,
|
||||
);
|
||||
|
||||
expect(IsString).toHaveBeenCalled();
|
||||
expect(Transform).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('enum type', () => {
|
||||
it('should apply enum validator with string array options', () => {
|
||||
const enumOptions = ['option1', 'option2', 'option3'];
|
||||
|
||||
applyBasicValidators(
|
||||
ConfigVariableType.ENUM,
|
||||
mockTarget,
|
||||
mockPropertyKey,
|
||||
enumOptions,
|
||||
);
|
||||
|
||||
expect(IsEnum).toHaveBeenCalledWith(enumOptions);
|
||||
expect(Transform).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should apply enum validator with enum object options', () => {
|
||||
enum TestEnum {
|
||||
Option1 = 'value1',
|
||||
Option2 = 'value2',
|
||||
Option3 = 'value3',
|
||||
}
|
||||
|
||||
jest.mock(
|
||||
'src/engine/core-modules/twenty-config/utils/type-transformers.registry',
|
||||
() => ({
|
||||
typeTransformers: {
|
||||
enum: {
|
||||
getValidators: jest.fn().mockImplementation((options) => {
|
||||
if (options && Object.keys(options).length > 0) {
|
||||
return [IsEnum(options)];
|
||||
}
|
||||
|
||||
return [];
|
||||
}),
|
||||
getTransformers: jest.fn().mockReturnValue([]),
|
||||
},
|
||||
},
|
||||
}),
|
||||
{ virtual: true },
|
||||
);
|
||||
|
||||
applyBasicValidators(
|
||||
ConfigVariableType.ENUM,
|
||||
mockTarget,
|
||||
mockPropertyKey,
|
||||
TestEnum,
|
||||
);
|
||||
|
||||
expect(IsEnum).toHaveBeenCalledWith(TestEnum);
|
||||
expect(Transform).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not apply enum validator without options', () => {
|
||||
applyBasicValidators(
|
||||
ConfigVariableType.ENUM,
|
||||
mockTarget,
|
||||
mockPropertyKey,
|
||||
);
|
||||
|
||||
expect(IsEnum).not.toHaveBeenCalled();
|
||||
expect(Transform).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('array type', () => {
|
||||
it('should apply array validator', () => {
|
||||
applyBasicValidators(
|
||||
ConfigVariableType.ARRAY,
|
||||
mockTarget,
|
||||
mockPropertyKey,
|
||||
);
|
||||
|
||||
expect(IsArray).toHaveBeenCalled();
|
||||
expect(Transform).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('unsupported type', () => {
|
||||
it('should throw error for unsupported types', () => {
|
||||
expect(() => {
|
||||
applyBasicValidators('unsupported' as any, mockTarget, mockPropertyKey);
|
||||
}).toThrow('Unsupported config variable type: unsupported');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -15,9 +15,9 @@ import { AI_TELEMETRY_CONFIG } from 'src/engine/core-modules/ai/constants/ai-tel
|
||||
import { AiModelRegistryService } from 'src/engine/core-modules/ai/services/ai-model-registry.service';
|
||||
import { AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity';
|
||||
import { isWorkflowRelatedObject } from 'src/engine/metadata-modules/agent/utils/is-workflow-related-object.util';
|
||||
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
||||
import { DATA_MANIPULATOR_AGENT } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-agents/agents/data-manipulator-agent';
|
||||
import { HELPER_AGENT } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-agents/agents/helper-agent';
|
||||
import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service';
|
||||
|
||||
import { type ToolHints } from './types/tool-hints.interface';
|
||||
|
||||
@@ -47,7 +47,7 @@ export class AiRouterService {
|
||||
@InjectRepository(AgentEntity)
|
||||
private readonly agentRepository: Repository<AgentEntity>,
|
||||
private readonly aiModelRegistryService: AiModelRegistryService,
|
||||
private readonly objectMetadataService: ObjectMetadataService,
|
||||
private readonly objectMetadataService: ObjectMetadataServiceV2,
|
||||
) {}
|
||||
|
||||
// Routes a user message to the most appropriate agent
|
||||
|
||||
+3
-17
@@ -14,11 +14,9 @@ import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto';
|
||||
import { FieldMetadataResolver } from 'src/engine/metadata-modules/field-metadata/field-metadata.resolver';
|
||||
import { BeforeUpdateOneField } from 'src/engine/metadata-modules/field-metadata/hooks/before-update-one-field.hook';
|
||||
import { FieldMetadataGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/field-metadata/interceptors/field-metadata-graphql-api-exception.interceptor';
|
||||
import { FieldMetadataEnumValidationService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-enum-validation.service';
|
||||
import { FieldMetadataMorphRelationService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-morph-relation.service';
|
||||
import { FieldMetadataRelatedRecordsService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-related-records.service';
|
||||
import { FieldMetadataRelationService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-relation.service';
|
||||
import { FieldMetadataValidationService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-validation.service';
|
||||
import { FieldMetadataServiceV2 } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service-v2';
|
||||
@@ -45,7 +43,6 @@ import { FieldMetadataEntity } from './field-metadata.entity';
|
||||
|
||||
import { CreateFieldInput } from './dtos/create-field.input';
|
||||
import { UpdateFieldInput } from './dtos/update-field.input';
|
||||
import { FieldMetadataService } from './services/field-metadata.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -77,9 +74,7 @@ import { FieldMetadataService } from './services/field-metadata.service';
|
||||
],
|
||||
services: [
|
||||
IsFieldMetadataDefaultValue,
|
||||
FieldMetadataService,
|
||||
FieldMetadataServiceV2,
|
||||
FieldMetadataRelatedRecordsService,
|
||||
FieldMetadataMorphRelationService,
|
||||
FieldMetadataRelationService,
|
||||
FieldMetadataValidationService,
|
||||
@@ -91,20 +86,16 @@ import { FieldMetadataService } from './services/field-metadata.service';
|
||||
DTOClass: FieldMetadataDTO,
|
||||
CreateDTOClass: CreateFieldInput,
|
||||
UpdateDTOClass: UpdateFieldInput,
|
||||
ServiceClass: FieldMetadataService,
|
||||
ServiceClass: FieldMetadataServiceV2,
|
||||
pagingStrategy: PagingStrategies.CURSOR,
|
||||
read: {
|
||||
defaultSort: [{ field: 'id', direction: SortDirection.DESC }],
|
||||
},
|
||||
create: {
|
||||
// Manually created because of the async validation
|
||||
one: { disabled: true },
|
||||
many: { disabled: true },
|
||||
disabled: true,
|
||||
},
|
||||
update: {
|
||||
// Manually created because of the async validation
|
||||
one: { disabled: true },
|
||||
many: { disabled: true },
|
||||
disabled: true,
|
||||
},
|
||||
delete: { disabled: true },
|
||||
guards: [WorkspaceAuthGuard],
|
||||
@@ -116,22 +107,17 @@ import { FieldMetadataService } from './services/field-metadata.service';
|
||||
providers: [
|
||||
IsFieldMetadataDefaultValue,
|
||||
IsFieldMetadataOptions,
|
||||
FieldMetadataService,
|
||||
FieldMetadataServiceV2,
|
||||
FieldMetadataRelationService,
|
||||
FieldMetadataRelatedRecordsService,
|
||||
FieldMetadataMorphRelationService,
|
||||
FieldMetadataValidationService,
|
||||
FieldMetadataEnumValidationService,
|
||||
FieldMetadataResolver,
|
||||
BeforeUpdateOneField,
|
||||
],
|
||||
exports: [
|
||||
FieldMetadataService,
|
||||
FieldMetadataServiceV2,
|
||||
FieldMetadataRelationService,
|
||||
FieldMetadataMorphRelationService,
|
||||
FieldMetadataRelatedRecordsService,
|
||||
FieldMetadataEnumValidationService,
|
||||
FieldMetadataValidationService,
|
||||
],
|
||||
|
||||
+18
-76
@@ -10,14 +10,9 @@ import {
|
||||
|
||||
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 { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
|
||||
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
|
||||
import {
|
||||
ForbiddenError,
|
||||
ValidationError,
|
||||
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { ForbiddenError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
|
||||
import { I18nContext } from 'src/engine/core-modules/i18n/types/i18n-context.type';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
@@ -29,14 +24,10 @@ import { CreateOneFieldMetadataInput } from 'src/engine/metadata-modules/field-m
|
||||
import { DeleteOneFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/delete-field.input';
|
||||
import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto';
|
||||
import { RelationDTO } from 'src/engine/metadata-modules/field-metadata/dtos/relation.dto';
|
||||
import {
|
||||
UpdateOneFieldMetadataInput,
|
||||
type UpdateFieldInput,
|
||||
} from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input';
|
||||
import { BeforeUpdateOneField } from 'src/engine/metadata-modules/field-metadata/hooks/before-update-one-field.hook';
|
||||
import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service';
|
||||
import { UpdateOneFieldMetadataInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input';
|
||||
import { FieldMetadataServiceV2 } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service-v2';
|
||||
import { fieldMetadataGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/field-metadata/utils/field-metadata-graphql-api-exception-handler.util';
|
||||
import { fromFlatFieldMetadataToFieldMetadataDto } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-flat-field-metadata-to-field-metadata-dto.util';
|
||||
import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants';
|
||||
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
|
||||
|
||||
@@ -49,9 +40,6 @@ import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-module
|
||||
)
|
||||
export class FieldMetadataResolver {
|
||||
constructor(
|
||||
private readonly fieldMetadataService: FieldMetadataService,
|
||||
private readonly beforeUpdateOneField: BeforeUpdateOneField<UpdateFieldInput>,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
private readonly fieldMetadataServiceV2: FieldMetadataServiceV2,
|
||||
private readonly i18nService: I18nService,
|
||||
) {}
|
||||
@@ -64,10 +52,13 @@ export class FieldMetadataResolver {
|
||||
@Context() context: I18nContext,
|
||||
) {
|
||||
try {
|
||||
return await this.fieldMetadataService.createOne({
|
||||
...input.field,
|
||||
workspaceId,
|
||||
});
|
||||
const flatFieldMetadata =
|
||||
await this.fieldMetadataServiceV2.createOneField({
|
||||
createFieldInput: input.field,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
return fromFlatFieldMetadataToFieldMetadataDto(flatFieldMetadata);
|
||||
} catch (error) {
|
||||
return fieldMetadataGraphqlApiExceptionHandler(
|
||||
error,
|
||||
@@ -83,29 +74,14 @@ export class FieldMetadataResolver {
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
@Context() context: I18nContext,
|
||||
) {
|
||||
const isWorkspaceMigrationV2Enabled =
|
||||
await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
try {
|
||||
if (isWorkspaceMigrationV2Enabled) {
|
||||
return await this.fieldMetadataServiceV2.updateOne({
|
||||
const flatFieldMetadata =
|
||||
await this.fieldMetadataServiceV2.updateOneField({
|
||||
updateFieldInput: { ...input.update, id: input.id },
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
const updatedInput = (await this.beforeUpdateOneField.run(input, {
|
||||
workspaceId,
|
||||
locale: context.req.locale,
|
||||
})) as UpdateOneFieldMetadataInput;
|
||||
|
||||
return await this.fieldMetadataService.updateOne(updatedInput.id, {
|
||||
...updatedInput.update,
|
||||
workspaceId,
|
||||
});
|
||||
return fromFlatFieldMetadataToFieldMetadataDto(flatFieldMetadata);
|
||||
} catch (error) {
|
||||
fieldMetadataGraphqlApiExceptionHandler(
|
||||
error,
|
||||
@@ -117,7 +93,7 @@ export class FieldMetadataResolver {
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.DATA_MODEL))
|
||||
@Mutation(() => FieldMetadataDTO)
|
||||
async deleteOneField(
|
||||
@Args('input') input: DeleteOneFieldInput,
|
||||
@Args('input') deleteOneFieldInput: DeleteOneFieldInput,
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
@Context() context: I18nContext,
|
||||
) {
|
||||
@@ -126,46 +102,13 @@ export class FieldMetadataResolver {
|
||||
}
|
||||
|
||||
try {
|
||||
const isWorkspaceMigrationV2Enabled =
|
||||
await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (isWorkspaceMigrationV2Enabled) {
|
||||
return await this.fieldMetadataServiceV2.deleteOneField({
|
||||
deleteOneFieldInput: input,
|
||||
const flatFieldMetadata =
|
||||
await this.fieldMetadataServiceV2.deleteOneField({
|
||||
deleteOneFieldInput,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
fieldMetadataGraphqlApiExceptionHandler(
|
||||
error,
|
||||
this.i18nService.getI18nInstance(context.req.locale),
|
||||
);
|
||||
}
|
||||
|
||||
const fieldMetadata =
|
||||
await this.fieldMetadataService.findOneWithinWorkspace(workspaceId, {
|
||||
where: {
|
||||
id: input.id.toString(),
|
||||
},
|
||||
});
|
||||
|
||||
if (!isDefined(fieldMetadata)) {
|
||||
throw new ValidationError('Field does not exist');
|
||||
}
|
||||
|
||||
if (!fieldMetadata.isCustom) {
|
||||
throw new ValidationError("Standard Fields can't be deleted");
|
||||
}
|
||||
|
||||
if (fieldMetadata.isActive) {
|
||||
throw new ValidationError("Active fields can't be deleted");
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.fieldMetadataService.deleteOneField(input, workspaceId);
|
||||
return fromFlatFieldMetadataToFieldMetadataDto(flatFieldMetadata);
|
||||
} catch (error) {
|
||||
fieldMetadataGraphqlApiExceptionHandler(
|
||||
error,
|
||||
@@ -177,7 +120,6 @@ export class FieldMetadataResolver {
|
||||
@ResolveField(() => RelationDTO, { nullable: true })
|
||||
async relation(
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
|
||||
@Parent()
|
||||
{
|
||||
id: fieldMetadataId,
|
||||
|
||||
-684
@@ -1,684 +0,0 @@
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { type UpdateOneInputType } from '@ptc-org/nestjs-query-graphql';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import {
|
||||
ForbiddenError,
|
||||
ValidationError,
|
||||
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { type UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input';
|
||||
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { BeforeUpdateOneField } from 'src/engine/metadata-modules/field-metadata/hooks/before-update-one-field.hook';
|
||||
import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service';
|
||||
import { type IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
||||
import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock';
|
||||
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
|
||||
|
||||
// Create a type that omits id and workspaceId from UpdateFieldInput
|
||||
type UpdateFieldInputForTest = Omit<UpdateFieldInput, 'id' | 'workspaceId'>;
|
||||
|
||||
describe('BeforeUpdateOneField', () => {
|
||||
let hook: BeforeUpdateOneField<UpdateFieldInput>;
|
||||
let fieldMetadataService: FieldMetadataService;
|
||||
let objectMetadataService: ObjectMetadataService;
|
||||
|
||||
const mockWorkspaceId = 'workspace-id';
|
||||
const mockFieldId = 'field-id';
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
BeforeUpdateOneField,
|
||||
{
|
||||
provide: FieldMetadataService,
|
||||
useValue: {
|
||||
findOneWithinWorkspace: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: ObjectMetadataService,
|
||||
useValue: {
|
||||
findOneWithinWorkspace: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: I18nService,
|
||||
useValue: {
|
||||
getI18nInstance: jest.fn().mockReturnValue({
|
||||
_: jest.fn().mockReturnValue('mocked-translation'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
hook =
|
||||
module.get<BeforeUpdateOneField<UpdateFieldInput>>(BeforeUpdateOneField);
|
||||
fieldMetadataService =
|
||||
module.get<FieldMetadataService>(FieldMetadataService);
|
||||
objectMetadataService = module.get<ObjectMetadataService>(
|
||||
ObjectMetadataService,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should throw ForbiddenError if workspaceId is not provided', async () => {
|
||||
const instance: UpdateOneInputType<UpdateFieldInputForTest> = {
|
||||
id: mockFieldId,
|
||||
update: {
|
||||
isActive: true,
|
||||
},
|
||||
};
|
||||
|
||||
await expect(
|
||||
hook.run(instance as UpdateOneInputType<UpdateFieldInput>, {
|
||||
workspaceId: '',
|
||||
locale: undefined,
|
||||
}),
|
||||
).rejects.toThrow(ForbiddenError);
|
||||
});
|
||||
|
||||
it('should throw ValidationError if field does not exist', async () => {
|
||||
const instance: UpdateOneInputType<UpdateFieldInputForTest> = {
|
||||
id: mockFieldId,
|
||||
update: {
|
||||
isActive: true,
|
||||
},
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(fieldMetadataService, 'findOneWithinWorkspace')
|
||||
.mockResolvedValue(null as unknown as FieldMetadataEntity);
|
||||
|
||||
await expect(
|
||||
hook.run(instance as UpdateOneInputType<UpdateFieldInput>, {
|
||||
workspaceId: mockWorkspaceId,
|
||||
locale: undefined,
|
||||
}),
|
||||
).rejects.toThrow(ValidationError);
|
||||
});
|
||||
|
||||
it('should not affect custom fields', async () => {
|
||||
const instance: UpdateOneInputType<UpdateFieldInputForTest> = {
|
||||
id: mockFieldId,
|
||||
update: {
|
||||
isActive: true,
|
||||
name: 'newName', // Custom fields can update all properties
|
||||
},
|
||||
};
|
||||
|
||||
const mockField = getMockFieldMetadataEntity({
|
||||
workspaceId: mockWorkspaceId,
|
||||
objectMetadataId: '20202020-0000-0000-0000-000000000002',
|
||||
id: mockFieldId,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'oldName',
|
||||
label: 'Old Name',
|
||||
isNullable: true,
|
||||
isCustom: true,
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
jest
|
||||
.spyOn(fieldMetadataService, 'findOneWithinWorkspace')
|
||||
.mockResolvedValue(mockField);
|
||||
|
||||
const result = await hook.run(
|
||||
instance as UpdateOneInputType<UpdateFieldInput>,
|
||||
{
|
||||
workspaceId: mockWorkspaceId,
|
||||
locale: undefined,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toEqual(instance);
|
||||
});
|
||||
|
||||
it('should throw ValidationError when trying to update non-updatable fields on standard fields', async () => {
|
||||
const instance: UpdateOneInputType<UpdateFieldInputForTest> = {
|
||||
id: mockFieldId,
|
||||
update: {
|
||||
name: 'newName', // Not allowed for standard fields
|
||||
},
|
||||
};
|
||||
|
||||
const mockField = getMockFieldMetadataEntity({
|
||||
workspaceId: mockWorkspaceId,
|
||||
objectMetadataId: '20202020-0000-0000-0000-000000000002',
|
||||
id: mockFieldId,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'oldName',
|
||||
label: 'Old Name',
|
||||
isNullable: true,
|
||||
isCustom: false,
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
jest
|
||||
.spyOn(fieldMetadataService, 'findOneWithinWorkspace')
|
||||
.mockResolvedValue(mockField);
|
||||
|
||||
await expect(
|
||||
hook.run(instance as UpdateOneInputType<UpdateFieldInput>, {
|
||||
workspaceId: mockWorkspaceId,
|
||||
locale: undefined,
|
||||
}),
|
||||
).rejects.toThrow(ValidationError);
|
||||
});
|
||||
|
||||
it('should handle isActive updates for standard fields', async () => {
|
||||
const instance: UpdateOneInputType<UpdateFieldInputForTest> = {
|
||||
id: mockFieldId,
|
||||
update: {
|
||||
isActive: false,
|
||||
},
|
||||
};
|
||||
|
||||
const mockField = getMockFieldMetadataEntity({
|
||||
workspaceId: mockWorkspaceId,
|
||||
objectMetadataId: '20202020-0000-0000-0000-000000000002',
|
||||
id: mockFieldId,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'oldName',
|
||||
label: 'Old Name',
|
||||
isNullable: true,
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
jest
|
||||
.spyOn(fieldMetadataService, 'findOneWithinWorkspace')
|
||||
.mockResolvedValue(mockField);
|
||||
|
||||
const result = await hook.run(
|
||||
instance as UpdateOneInputType<UpdateFieldInput>,
|
||||
{
|
||||
workspaceId: mockWorkspaceId,
|
||||
locale: undefined,
|
||||
},
|
||||
);
|
||||
|
||||
const expectedResult = {
|
||||
id: mockFieldId,
|
||||
update: {
|
||||
isActive: false,
|
||||
standardOverrides: {},
|
||||
},
|
||||
};
|
||||
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it('should handle isLabelSyncedWithName updates for standard fields', async () => {
|
||||
const instance: UpdateOneInputType<UpdateFieldInputForTest> = {
|
||||
id: mockFieldId,
|
||||
update: {
|
||||
isLabelSyncedWithName: true,
|
||||
},
|
||||
};
|
||||
|
||||
const mockField = getMockFieldMetadataEntity({
|
||||
workspaceId: mockWorkspaceId,
|
||||
objectMetadataId: '20202020-0000-0000-0000-000000000002',
|
||||
id: mockFieldId,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'oldName',
|
||||
label: 'Old Name',
|
||||
isNullable: true,
|
||||
isCustom: false,
|
||||
isLabelSyncedWithName: false,
|
||||
standardOverrides: {
|
||||
label: 'Custom Label',
|
||||
},
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
jest
|
||||
.spyOn(fieldMetadataService, 'findOneWithinWorkspace')
|
||||
.mockResolvedValue(mockField);
|
||||
|
||||
const result = await hook.run(
|
||||
instance as UpdateOneInputType<UpdateFieldInput>,
|
||||
{
|
||||
workspaceId: mockWorkspaceId,
|
||||
locale: undefined,
|
||||
},
|
||||
);
|
||||
|
||||
const expectedResult = {
|
||||
id: mockFieldId,
|
||||
update: {
|
||||
isLabelSyncedWithName: true,
|
||||
standardOverrides: {
|
||||
label: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it('should handle label override when not synced with name', async () => {
|
||||
const instance: UpdateOneInputType<UpdateFieldInputForTest> = {
|
||||
id: mockFieldId,
|
||||
update: {
|
||||
label: 'New Label',
|
||||
},
|
||||
};
|
||||
|
||||
const mockField = getMockFieldMetadataEntity({
|
||||
workspaceId: mockWorkspaceId,
|
||||
objectMetadataId: '20202020-0000-0000-0000-000000000002',
|
||||
id: mockFieldId,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'oldName',
|
||||
label: 'Default Label',
|
||||
isNullable: true,
|
||||
isCustom: false,
|
||||
isLabelSyncedWithName: false,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
jest
|
||||
.spyOn(fieldMetadataService, 'findOneWithinWorkspace')
|
||||
.mockResolvedValue(mockField);
|
||||
|
||||
const result = await hook.run(
|
||||
instance as UpdateOneInputType<UpdateFieldInput>,
|
||||
{
|
||||
workspaceId: mockWorkspaceId,
|
||||
locale: undefined,
|
||||
},
|
||||
);
|
||||
|
||||
const expectedResult = {
|
||||
id: mockFieldId,
|
||||
update: {
|
||||
standardOverrides: {
|
||||
label: 'New Label',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it('should reset label override when it matches the original value', async () => {
|
||||
const instance: UpdateOneInputType<UpdateFieldInputForTest> = {
|
||||
id: mockFieldId,
|
||||
update: {
|
||||
label: 'Default Label',
|
||||
},
|
||||
};
|
||||
|
||||
const mockField: Partial<FieldMetadataEntity> = {
|
||||
id: mockFieldId,
|
||||
isCustom: false,
|
||||
isLabelSyncedWithName: false,
|
||||
label: 'Default Label',
|
||||
standardOverrides: {
|
||||
label: 'Custom Label',
|
||||
},
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(fieldMetadataService, 'findOneWithinWorkspace')
|
||||
.mockResolvedValue(mockField as FieldMetadataEntity);
|
||||
|
||||
const result = await hook.run(
|
||||
instance as UpdateOneInputType<UpdateFieldInput>,
|
||||
{
|
||||
workspaceId: mockWorkspaceId,
|
||||
locale: undefined,
|
||||
},
|
||||
);
|
||||
|
||||
const expectedResult = {
|
||||
id: mockFieldId,
|
||||
update: {
|
||||
standardOverrides: {
|
||||
label: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it('should handle description override', async () => {
|
||||
const instance: UpdateOneInputType<UpdateFieldInputForTest> = {
|
||||
id: mockFieldId,
|
||||
update: {
|
||||
description: 'New Description',
|
||||
},
|
||||
};
|
||||
|
||||
const mockField: Partial<FieldMetadataEntity> = {
|
||||
id: mockFieldId,
|
||||
isCustom: false,
|
||||
description: 'Default Description',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(fieldMetadataService, 'findOneWithinWorkspace')
|
||||
.mockResolvedValue(mockField as FieldMetadataEntity);
|
||||
|
||||
const result = await hook.run(
|
||||
instance as UpdateOneInputType<UpdateFieldInput>,
|
||||
{
|
||||
workspaceId: mockWorkspaceId,
|
||||
locale: undefined,
|
||||
},
|
||||
);
|
||||
|
||||
const expectedResult = {
|
||||
id: mockFieldId,
|
||||
update: {
|
||||
standardOverrides: {
|
||||
description: 'New Description',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it('should handle icon override', async () => {
|
||||
const instance: UpdateOneInputType<UpdateFieldInputForTest> = {
|
||||
id: mockFieldId,
|
||||
update: {
|
||||
icon: 'IconStar',
|
||||
},
|
||||
};
|
||||
|
||||
const mockField: Partial<FieldMetadataEntity> = {
|
||||
id: mockFieldId,
|
||||
isCustom: false,
|
||||
icon: 'IconCircle',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(fieldMetadataService, 'findOneWithinWorkspace')
|
||||
.mockResolvedValue(mockField as FieldMetadataEntity);
|
||||
|
||||
const result = await hook.run(
|
||||
instance as UpdateOneInputType<UpdateFieldInput>,
|
||||
{
|
||||
workspaceId: mockWorkspaceId,
|
||||
locale: undefined,
|
||||
},
|
||||
);
|
||||
|
||||
const expectedResult = {
|
||||
id: mockFieldId,
|
||||
update: {
|
||||
standardOverrides: {
|
||||
icon: 'IconStar',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it('should handle locale-specific translations', async () => {
|
||||
const instance: UpdateOneInputType<UpdateFieldInputForTest> = {
|
||||
id: mockFieldId,
|
||||
update: {
|
||||
label: 'Étiquette',
|
||||
description: 'Description en français',
|
||||
},
|
||||
};
|
||||
|
||||
const mockField: Partial<FieldMetadataEntity> = {
|
||||
id: mockFieldId,
|
||||
isCustom: false,
|
||||
isLabelSyncedWithName: false,
|
||||
label: 'Label',
|
||||
description: 'Description',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(fieldMetadataService, 'findOneWithinWorkspace')
|
||||
.mockResolvedValue(mockField as FieldMetadataEntity);
|
||||
|
||||
const result = await hook.run(
|
||||
instance as UpdateOneInputType<UpdateFieldInput>,
|
||||
{
|
||||
workspaceId: mockWorkspaceId,
|
||||
locale: 'fr-FR',
|
||||
},
|
||||
);
|
||||
|
||||
// The expected result returned by the hook
|
||||
const expectedResult = {
|
||||
id: mockFieldId,
|
||||
update: {
|
||||
standardOverrides: {
|
||||
translations: {
|
||||
'fr-FR': {
|
||||
label: 'Étiquette',
|
||||
description: 'Description en français',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it('should reset locale-specific translations when they match translated defaults', async () => {
|
||||
const translatedLabel = 'mocked-translation';
|
||||
|
||||
const instance: UpdateOneInputType<UpdateFieldInputForTest> = {
|
||||
id: mockFieldId,
|
||||
update: {
|
||||
label: translatedLabel,
|
||||
},
|
||||
};
|
||||
|
||||
const mockField: Partial<FieldMetadataEntity> = {
|
||||
id: mockFieldId,
|
||||
isCustom: false,
|
||||
isLabelSyncedWithName: false,
|
||||
label: 'Label',
|
||||
standardOverrides: {
|
||||
translations: {
|
||||
'fr-FR': {
|
||||
label: 'Ancienne Étiquette',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(fieldMetadataService, 'findOneWithinWorkspace')
|
||||
.mockResolvedValue(mockField as FieldMetadataEntity);
|
||||
|
||||
const result = await hook.run(
|
||||
instance as UpdateOneInputType<UpdateFieldInput>,
|
||||
{
|
||||
workspaceId: mockWorkspaceId,
|
||||
locale: 'fr-FR',
|
||||
},
|
||||
);
|
||||
|
||||
const expectedResult = {
|
||||
id: mockFieldId,
|
||||
update: {
|
||||
standardOverrides: {
|
||||
translations: {
|
||||
'fr-FR': {
|
||||
label: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it('should handle multiple updates together', async () => {
|
||||
const instance: UpdateOneInputType<UpdateFieldInputForTest> = {
|
||||
id: mockFieldId,
|
||||
update: {
|
||||
isActive: false,
|
||||
isLabelSyncedWithName: false,
|
||||
label: 'New Label',
|
||||
icon: 'IconStar',
|
||||
description: 'New Description',
|
||||
},
|
||||
};
|
||||
|
||||
const mockField: Partial<FieldMetadataEntity> = {
|
||||
id: mockFieldId,
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isLabelSyncedWithName: false,
|
||||
label: 'Default Label',
|
||||
icon: 'IconCircle',
|
||||
description: 'Default Description',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(fieldMetadataService, 'findOneWithinWorkspace')
|
||||
.mockResolvedValue(mockField as FieldMetadataEntity);
|
||||
|
||||
const result = await hook.run(
|
||||
instance as UpdateOneInputType<UpdateFieldInput>,
|
||||
{
|
||||
workspaceId: mockWorkspaceId,
|
||||
locale: undefined,
|
||||
},
|
||||
);
|
||||
|
||||
const expectedResult = {
|
||||
id: mockFieldId,
|
||||
update: {
|
||||
isActive: false,
|
||||
isLabelSyncedWithName: false,
|
||||
standardOverrides: {
|
||||
label: 'New Label',
|
||||
icon: 'IconStar',
|
||||
description: 'New Description',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it('should throw ValidationError if isUnique is updated for a standard field with a standard unique index', async () => {
|
||||
const mockField: Partial<FieldMetadataEntity> = {
|
||||
id: mockFieldId,
|
||||
isCustom: false,
|
||||
isUnique: true,
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(fieldMetadataService, 'findOneWithinWorkspace')
|
||||
.mockResolvedValue(mockField as FieldMetadataEntity);
|
||||
|
||||
const mockObjectMetadata: Partial<ObjectMetadataEntity> = {
|
||||
id: mockWorkspaceId,
|
||||
indexMetadatas: [
|
||||
{
|
||||
isUnique: true,
|
||||
isCustom: false,
|
||||
indexFieldMetadatas: [{ fieldMetadataId: mockFieldId }],
|
||||
} as IndexMetadataEntity,
|
||||
],
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(objectMetadataService, 'findOneWithinWorkspace')
|
||||
.mockResolvedValue(mockObjectMetadata as ObjectMetadataEntity);
|
||||
|
||||
const instance: UpdateOneInputType<UpdateFieldInputForTest> = {
|
||||
id: mockFieldId,
|
||||
update: {
|
||||
isUnique: false,
|
||||
},
|
||||
};
|
||||
|
||||
await expect(
|
||||
hook.run(instance as UpdateOneInputType<UpdateFieldInput>, {
|
||||
workspaceId: mockWorkspaceId,
|
||||
locale: undefined,
|
||||
}),
|
||||
).rejects.toThrow(ValidationError);
|
||||
});
|
||||
|
||||
it('should not throw ValidationError if isUnique is updated for a standard field without a standard unique index', async () => {
|
||||
const mockField: Partial<FieldMetadataEntity> = {
|
||||
id: mockFieldId,
|
||||
isCustom: false,
|
||||
isUnique: true,
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(fieldMetadataService, 'findOneWithinWorkspace')
|
||||
.mockResolvedValue(mockField as FieldMetadataEntity);
|
||||
|
||||
const mockObjectMetadata: Partial<ObjectMetadataEntity> = {
|
||||
id: mockWorkspaceId,
|
||||
indexMetadatas: [
|
||||
{
|
||||
isUnique: false,
|
||||
isCustom: false,
|
||||
indexFieldMetadatas: [{ fieldMetadataId: mockFieldId }],
|
||||
} as IndexMetadataEntity,
|
||||
],
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(objectMetadataService, 'findOneWithinWorkspace')
|
||||
.mockResolvedValue(mockObjectMetadata as ObjectMetadataEntity);
|
||||
|
||||
const instance: UpdateOneInputType<UpdateFieldInputForTest> = {
|
||||
id: mockFieldId,
|
||||
update: {
|
||||
isUnique: false,
|
||||
},
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(fieldMetadataService, 'findOneWithinWorkspace')
|
||||
.mockResolvedValue(mockField as FieldMetadataEntity);
|
||||
|
||||
const result = await hook.run(
|
||||
instance as UpdateOneInputType<UpdateFieldInput>,
|
||||
{
|
||||
workspaceId: mockWorkspaceId,
|
||||
locale: undefined,
|
||||
},
|
||||
);
|
||||
|
||||
const expectedResult = {
|
||||
id: mockFieldId,
|
||||
update: {
|
||||
isUnique: false,
|
||||
standardOverrides: {},
|
||||
},
|
||||
};
|
||||
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
-448
@@ -1,448 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
type BeforeUpdateOneHook,
|
||||
type UpdateOneInputType,
|
||||
} from '@ptc-org/nestjs-query-graphql';
|
||||
import { SOURCE_LOCALE, type APP_LOCALES } from 'twenty-shared/translations';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
ForbiddenError,
|
||||
ValidationError,
|
||||
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId';
|
||||
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
|
||||
import { type FieldStandardOverridesDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-standard-overrides.dto';
|
||||
import { type UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input';
|
||||
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service';
|
||||
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
||||
|
||||
interface StandardFieldUpdate extends Partial<UpdateFieldInput> {
|
||||
standardOverrides?: FieldStandardOverridesDTO;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class BeforeUpdateOneField<T extends UpdateFieldInput>
|
||||
implements BeforeUpdateOneHook<T>
|
||||
{
|
||||
constructor(
|
||||
readonly fieldMetadataService: FieldMetadataService,
|
||||
readonly objectMetadataService: ObjectMetadataService,
|
||||
private readonly i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
async run(
|
||||
instance: UpdateOneInputType<T>,
|
||||
{
|
||||
workspaceId,
|
||||
locale,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
locale: keyof typeof APP_LOCALES | undefined;
|
||||
},
|
||||
): Promise<UpdateOneInputType<T>> {
|
||||
if (!workspaceId) {
|
||||
throw new ForbiddenError('Could not retrieve workspace ID');
|
||||
}
|
||||
|
||||
const fieldMetadata = await this.getFieldMetadata(instance, workspaceId);
|
||||
|
||||
if (!fieldMetadata.isCustom) {
|
||||
return await this.handleStandardFieldUpdate(
|
||||
instance,
|
||||
fieldMetadata,
|
||||
locale,
|
||||
);
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
private async getFieldMetadata(
|
||||
instance: UpdateOneInputType<T>,
|
||||
workspaceId: string,
|
||||
) {
|
||||
const fieldMetadata =
|
||||
await this.fieldMetadataService.findOneWithinWorkspace(workspaceId, {
|
||||
where: {
|
||||
id: instance.id.toString(),
|
||||
},
|
||||
});
|
||||
|
||||
if (!fieldMetadata) {
|
||||
throw new ValidationError('Field does not exist');
|
||||
}
|
||||
|
||||
return fieldMetadata;
|
||||
}
|
||||
|
||||
private async handleStandardFieldUpdate(
|
||||
instance: UpdateOneInputType<T>,
|
||||
fieldMetadata: FieldMetadataEntity,
|
||||
locale?: keyof typeof APP_LOCALES,
|
||||
): Promise<UpdateOneInputType<T>> {
|
||||
const update: StandardFieldUpdate = {};
|
||||
const updatableFields = [
|
||||
'isActive',
|
||||
'isLabelSyncedWithName',
|
||||
'options',
|
||||
'settings',
|
||||
'defaultValue',
|
||||
'isUnique',
|
||||
];
|
||||
const overridableFields = ['label', 'icon', 'description'];
|
||||
|
||||
const nonUpdatableFields = Object.keys(instance.update).filter(
|
||||
(key) =>
|
||||
!updatableFields.includes(key) && !overridableFields.includes(key),
|
||||
);
|
||||
|
||||
if (nonUpdatableFields.length > 0) {
|
||||
throw new ValidationError(
|
||||
`Only isActive, isLabelSyncedWithName, label, icon, description, isUnique and defaultValue fields can be updated for standard fields. Invalid fields: ${nonUpdatableFields.join(', ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Preserve existing overrides
|
||||
update.standardOverrides = fieldMetadata.standardOverrides
|
||||
? { ...fieldMetadata.standardOverrides }
|
||||
: {};
|
||||
|
||||
this.handleActiveField(instance, update);
|
||||
this.handleLabelSyncedWithNameField(instance, update);
|
||||
this.handleStandardOverrides(instance, fieldMetadata, update, locale);
|
||||
this.handleOptionsField(instance, update);
|
||||
this.handleSettingsField(instance, update);
|
||||
this.handleDefaultValueField(instance, update);
|
||||
await this.handleIsUniqueField(instance, fieldMetadata, update);
|
||||
|
||||
return {
|
||||
id: instance.id,
|
||||
update: update as T,
|
||||
};
|
||||
}
|
||||
|
||||
private async handleIsUniqueField(
|
||||
instance: UpdateOneInputType<T>,
|
||||
fieldMetadata: FieldMetadataEntity,
|
||||
update: StandardFieldUpdate,
|
||||
) {
|
||||
if (!isDefined(instance.update.isUnique)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const objectMetadata =
|
||||
await this.objectMetadataService.findOneWithinWorkspace(
|
||||
fieldMetadata.workspaceId,
|
||||
{
|
||||
where: {
|
||||
id: fieldMetadata.objectMetadataId,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const hasStandardUniqueIndex = objectMetadata?.indexMetadatas.some(
|
||||
(index) =>
|
||||
index.isUnique &&
|
||||
!index.isCustom &&
|
||||
index.indexFieldMetadatas?.some(
|
||||
(field) => field.fieldMetadataId === fieldMetadata.id,
|
||||
),
|
||||
);
|
||||
|
||||
if (hasStandardUniqueIndex && instance.update.isUnique === false) {
|
||||
throw new ValidationError(
|
||||
'Unique standard field cannot be updated to non-unique.',
|
||||
);
|
||||
}
|
||||
|
||||
update.isUnique = instance.update.isUnique;
|
||||
}
|
||||
|
||||
private handleDefaultValueField(
|
||||
instance: UpdateOneInputType<T>,
|
||||
update: StandardFieldUpdate,
|
||||
): void {
|
||||
if (!isDefined(instance.update.defaultValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
update.defaultValue = instance.update.defaultValue;
|
||||
}
|
||||
|
||||
private handleOptionsField(
|
||||
instance: UpdateOneInputType<T>,
|
||||
update: StandardFieldUpdate,
|
||||
): void {
|
||||
if (!isDefined(instance.update.options)) {
|
||||
return;
|
||||
}
|
||||
|
||||
update.options = instance.update.options;
|
||||
}
|
||||
|
||||
private handleSettingsField(
|
||||
instance: UpdateOneInputType<T>,
|
||||
update: StandardFieldUpdate,
|
||||
): void {
|
||||
if (!isDefined(instance.update.settings)) {
|
||||
return;
|
||||
}
|
||||
|
||||
update.settings = instance.update.settings;
|
||||
}
|
||||
|
||||
private handleActiveField(
|
||||
instance: UpdateOneInputType<T>,
|
||||
update: StandardFieldUpdate,
|
||||
): void {
|
||||
if (!isDefined(instance.update.isActive)) {
|
||||
return;
|
||||
}
|
||||
|
||||
update.isActive = instance.update.isActive;
|
||||
}
|
||||
|
||||
private handleLabelSyncedWithNameField(
|
||||
instance: UpdateOneInputType<T>,
|
||||
update: StandardFieldUpdate,
|
||||
): void {
|
||||
if (!isDefined(instance.update.isLabelSyncedWithName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
update.isLabelSyncedWithName = instance.update.isLabelSyncedWithName;
|
||||
|
||||
if (instance.update.isLabelSyncedWithName === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
update.standardOverrides = update.standardOverrides || {};
|
||||
update.standardOverrides.label = null;
|
||||
}
|
||||
|
||||
private handleStandardOverrides(
|
||||
instance: UpdateOneInputType<T>,
|
||||
fieldMetadata: FieldMetadataEntity,
|
||||
update: StandardFieldUpdate,
|
||||
locale?: keyof typeof APP_LOCALES,
|
||||
): void {
|
||||
const hasStandardOverrides =
|
||||
isDefined(instance.update.description) ||
|
||||
isDefined(instance.update.icon) ||
|
||||
isDefined(instance.update.label);
|
||||
|
||||
if (!hasStandardOverrides) {
|
||||
return;
|
||||
}
|
||||
|
||||
update.standardOverrides = update.standardOverrides || {};
|
||||
|
||||
this.handleDescriptionOverride(instance, fieldMetadata, update, locale);
|
||||
this.handleIconOverride(instance, fieldMetadata, update);
|
||||
this.handleLabelOverride(instance, fieldMetadata, update, locale);
|
||||
}
|
||||
|
||||
private resetOverrideIfMatchesOriginal({
|
||||
update,
|
||||
overrideKey,
|
||||
newValue,
|
||||
originalValue,
|
||||
locale,
|
||||
}: {
|
||||
update: StandardFieldUpdate;
|
||||
overrideKey: 'label' | 'description' | 'icon';
|
||||
newValue: string;
|
||||
originalValue: string | null;
|
||||
locale?: keyof typeof APP_LOCALES | undefined;
|
||||
}): boolean {
|
||||
// Handle localized overrides
|
||||
if (locale && locale !== SOURCE_LOCALE) {
|
||||
const wasOverrideReset = this.resetLocalizedOverride(
|
||||
update,
|
||||
overrideKey,
|
||||
newValue,
|
||||
originalValue,
|
||||
locale,
|
||||
);
|
||||
|
||||
return wasOverrideReset;
|
||||
}
|
||||
|
||||
// Handle default language overrides
|
||||
const wasOverrideReset = this.resetDefaultOverride(
|
||||
update,
|
||||
overrideKey,
|
||||
newValue,
|
||||
originalValue,
|
||||
);
|
||||
|
||||
return wasOverrideReset;
|
||||
}
|
||||
|
||||
private resetLocalizedOverride(
|
||||
update: StandardFieldUpdate,
|
||||
overrideKey: 'label' | 'description' | 'icon',
|
||||
newValue: string,
|
||||
originalValue: string | null,
|
||||
locale: keyof typeof APP_LOCALES,
|
||||
): boolean {
|
||||
const messageId = generateMessageId(originalValue ?? '');
|
||||
const i18n = this.i18nService.getI18nInstance(locale);
|
||||
const translatedMessage = i18n._(messageId);
|
||||
|
||||
if (newValue !== translatedMessage) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Initialize the translations structure if needed
|
||||
update.standardOverrides = update.standardOverrides || {};
|
||||
update.standardOverrides.translations =
|
||||
update.standardOverrides.translations || {};
|
||||
update.standardOverrides.translations[locale] =
|
||||
update.standardOverrides.translations[locale] || {};
|
||||
|
||||
// Reset the override by setting it to null
|
||||
const localeTranslations = update.standardOverrides.translations[locale];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(localeTranslations as Record<string, any>)[overrideKey] = null;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private resetDefaultOverride(
|
||||
update: StandardFieldUpdate,
|
||||
overrideKey: 'label' | 'description' | 'icon',
|
||||
newValue: string,
|
||||
originalValue: string | null,
|
||||
): boolean {
|
||||
if (newValue !== originalValue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
update.standardOverrides = update.standardOverrides || {};
|
||||
update.standardOverrides[overrideKey] = null;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private setOverrideValue(
|
||||
update: StandardFieldUpdate,
|
||||
overrideKey: 'label' | 'description' | 'icon',
|
||||
value: string,
|
||||
locale?: keyof typeof APP_LOCALES,
|
||||
): void {
|
||||
update.standardOverrides = update.standardOverrides || {};
|
||||
|
||||
const shouldSetLocalizedOverride =
|
||||
locale && locale !== SOURCE_LOCALE && overrideKey !== 'icon';
|
||||
|
||||
if (!shouldSetLocalizedOverride) {
|
||||
update.standardOverrides[overrideKey] = value;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.setLocalizedOverrideValue(update, overrideKey, value, locale);
|
||||
}
|
||||
|
||||
private setLocalizedOverrideValue(
|
||||
update: StandardFieldUpdate,
|
||||
overrideKey: 'label' | 'description' | 'icon',
|
||||
value: string,
|
||||
locale: keyof typeof APP_LOCALES,
|
||||
): void {
|
||||
update.standardOverrides = update.standardOverrides || {};
|
||||
update.standardOverrides.translations =
|
||||
update.standardOverrides.translations || {};
|
||||
update.standardOverrides.translations[locale] =
|
||||
update.standardOverrides.translations[locale] || {};
|
||||
|
||||
const localeTranslations = update.standardOverrides.translations[locale];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(localeTranslations as Record<string, any>)[overrideKey] = value;
|
||||
}
|
||||
|
||||
private handleDescriptionOverride(
|
||||
instance: UpdateOneInputType<T>,
|
||||
fieldMetadata: FieldMetadataEntity,
|
||||
update: StandardFieldUpdate,
|
||||
locale?: keyof typeof APP_LOCALES,
|
||||
): void {
|
||||
if (!isDefined(instance.update.description)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
this.resetOverrideIfMatchesOriginal({
|
||||
update,
|
||||
overrideKey: 'description',
|
||||
newValue: instance.update.description,
|
||||
originalValue: fieldMetadata.description,
|
||||
locale,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setOverrideValue(
|
||||
update,
|
||||
'description',
|
||||
instance.update.description,
|
||||
locale,
|
||||
);
|
||||
}
|
||||
|
||||
private handleIconOverride(
|
||||
instance: UpdateOneInputType<T>,
|
||||
fieldMetadata: FieldMetadataEntity,
|
||||
update: StandardFieldUpdate,
|
||||
): void {
|
||||
if (!isDefined(instance.update.icon)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
this.resetOverrideIfMatchesOriginal({
|
||||
update,
|
||||
overrideKey: 'icon',
|
||||
newValue: instance.update.icon,
|
||||
originalValue: fieldMetadata.icon,
|
||||
locale: undefined,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setOverrideValue(update, 'icon', instance.update.icon);
|
||||
}
|
||||
|
||||
private handleLabelOverride(
|
||||
instance: UpdateOneInputType<T>,
|
||||
fieldMetadata: FieldMetadataEntity,
|
||||
update: StandardFieldUpdate,
|
||||
locale?: keyof typeof APP_LOCALES,
|
||||
): void {
|
||||
if (!isDefined(instance.update.label)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
this.resetOverrideIfMatchesOriginal({
|
||||
update,
|
||||
overrideKey: 'label',
|
||||
newValue: instance.update.label,
|
||||
originalValue: fieldMetadata.label,
|
||||
locale,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setOverrideValue(update, 'label', instance.update.label, locale);
|
||||
}
|
||||
}
|
||||
-357
@@ -1,357 +0,0 @@
|
||||
import { type EachTestingContext } from 'twenty-shared/testing';
|
||||
|
||||
import { type FieldMetadataDefaultOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
|
||||
import { FieldMetadataRelatedRecordsService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-related-records.service';
|
||||
import { type ViewFieldService } from 'src/engine/metadata-modules/view-field/services/view-field.service';
|
||||
import { type ViewFilterService } from 'src/engine/metadata-modules/view-filter/services/view-filter.service';
|
||||
import { type ViewGroupService } from 'src/engine/metadata-modules/view-group/services/view-group.service';
|
||||
import { type ViewService } from 'src/engine/metadata-modules/view/services/view.service';
|
||||
|
||||
type GetOptionsDifferencesTestContext = EachTestingContext<{
|
||||
oldOptions: FieldMetadataDefaultOption[];
|
||||
newOptions: FieldMetadataDefaultOption[];
|
||||
compareLabel?: boolean;
|
||||
expected: {
|
||||
created: FieldMetadataDefaultOption[];
|
||||
updated: {
|
||||
old: FieldMetadataDefaultOption;
|
||||
new: FieldMetadataDefaultOption;
|
||||
}[];
|
||||
deleted: FieldMetadataDefaultOption[];
|
||||
};
|
||||
}>;
|
||||
|
||||
describe('FieldMetadataRelatedRecordsService', () => {
|
||||
describe('getOptionsDifferences', () => {
|
||||
let service: FieldMetadataRelatedRecordsService;
|
||||
let viewService: ViewService = {} as ViewService;
|
||||
let viewFieldService: ViewFieldService = {} as ViewFieldService;
|
||||
let viewFilterService: ViewFilterService = {} as ViewFilterService;
|
||||
let viewGroupService: ViewGroupService = {} as ViewGroupService;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new FieldMetadataRelatedRecordsService(
|
||||
viewService,
|
||||
viewFieldService,
|
||||
viewFilterService,
|
||||
viewGroupService,
|
||||
);
|
||||
});
|
||||
|
||||
const testCases: GetOptionsDifferencesTestContext[] = [
|
||||
{
|
||||
title: 'should identify created options',
|
||||
context: {
|
||||
oldOptions: [
|
||||
{ id: '1', label: 'Option 1', value: 'value1', position: 0 },
|
||||
],
|
||||
newOptions: [
|
||||
{ id: '1', label: 'Option 1', value: 'value1', position: 0 },
|
||||
{ id: '2', label: 'Option 2', value: 'value2', position: 1 },
|
||||
],
|
||||
expected: {
|
||||
created: [
|
||||
{ id: '2', label: 'Option 2', value: 'value2', position: 1 },
|
||||
],
|
||||
updated: [],
|
||||
deleted: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should identify updated options',
|
||||
context: {
|
||||
oldOptions: [
|
||||
{ id: '1', label: 'Option 1', value: 'value1', position: 0 },
|
||||
],
|
||||
newOptions: [
|
||||
{
|
||||
id: '1',
|
||||
label: 'Option 1',
|
||||
value: 'updated-value1',
|
||||
position: 0,
|
||||
},
|
||||
],
|
||||
expected: {
|
||||
created: [],
|
||||
updated: [
|
||||
{
|
||||
old: {
|
||||
id: '1',
|
||||
label: 'Option 1',
|
||||
value: 'value1',
|
||||
position: 0,
|
||||
},
|
||||
new: {
|
||||
id: '1',
|
||||
label: 'Option 1',
|
||||
value: 'updated-value1',
|
||||
position: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
deleted: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should identify deleted options',
|
||||
context: {
|
||||
oldOptions: [
|
||||
{ id: '1', label: 'Option 1', value: 'value1', position: 0 },
|
||||
{ id: '2', label: 'Option 2', value: 'value2', position: 1 },
|
||||
],
|
||||
newOptions: [
|
||||
{ id: '1', label: 'Option 1', value: 'value1', position: 0 },
|
||||
],
|
||||
expected: {
|
||||
created: [],
|
||||
updated: [],
|
||||
deleted: [
|
||||
{ id: '2', label: 'Option 2', value: 'value2', position: 1 },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should identify all types of changes',
|
||||
context: {
|
||||
oldOptions: [
|
||||
{ id: '1', label: 'Option 1', value: 'value1', position: 0 },
|
||||
{ id: '2', label: 'Option 2', value: 'value2', position: 1 },
|
||||
{ id: '3', label: 'Option 3', value: 'value3', position: 2 },
|
||||
],
|
||||
newOptions: [
|
||||
{
|
||||
id: '1',
|
||||
label: 'Option 1',
|
||||
value: 'updated-value1',
|
||||
position: 0,
|
||||
},
|
||||
{ id: '3', label: 'Option 3', value: 'value3', position: 1 },
|
||||
{ id: '4', label: 'Option 4', value: 'value4', position: 2 },
|
||||
],
|
||||
expected: {
|
||||
created: [
|
||||
{ id: '4', label: 'Option 4', value: 'value4', position: 2 },
|
||||
],
|
||||
updated: [
|
||||
{
|
||||
old: {
|
||||
id: '1',
|
||||
label: 'Option 1',
|
||||
value: 'value1',
|
||||
position: 0,
|
||||
},
|
||||
new: {
|
||||
id: '1',
|
||||
label: 'Option 1',
|
||||
value: 'updated-value1',
|
||||
position: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
deleted: [
|
||||
{ id: '2', label: 'Option 2', value: 'value2', position: 1 },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should handle empty arrays',
|
||||
context: {
|
||||
oldOptions: [],
|
||||
newOptions: [],
|
||||
expected: {
|
||||
created: [],
|
||||
updated: [],
|
||||
deleted: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should handle all new options',
|
||||
context: {
|
||||
oldOptions: [],
|
||||
newOptions: [
|
||||
{ id: '1', label: 'Option 1', value: 'value1', position: 0 },
|
||||
{ id: '2', label: 'Option 2', value: 'value2', position: 1 },
|
||||
],
|
||||
expected: {
|
||||
created: [
|
||||
{ id: '1', label: 'Option 1', value: 'value1', position: 0 },
|
||||
{ id: '2', label: 'Option 2', value: 'value2', position: 1 },
|
||||
],
|
||||
updated: [],
|
||||
deleted: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should handle all deleted options',
|
||||
context: {
|
||||
oldOptions: [
|
||||
{ id: '1', label: 'Option 1', value: 'value1', position: 0 },
|
||||
{ id: '2', label: 'Option 2', value: 'value2', position: 1 },
|
||||
],
|
||||
newOptions: [],
|
||||
expected: {
|
||||
created: [],
|
||||
updated: [],
|
||||
deleted: [
|
||||
{ id: '1', label: 'Option 1', value: 'value1', position: 0 },
|
||||
{ id: '2', label: 'Option 2', value: 'value2', position: 1 },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'should not consider changes to label as updates when value remains the same',
|
||||
context: {
|
||||
oldOptions: [
|
||||
{
|
||||
id: 'f86eaffd-b773-4c9a-957b-86dca4a62731',
|
||||
label: 'Option 0',
|
||||
value: 'option0',
|
||||
position: 1,
|
||||
},
|
||||
{
|
||||
id: '28d80b3c-79bd-4a1b-a868-9616534de0fa',
|
||||
label: 'Option 1',
|
||||
value: 'option1',
|
||||
position: 2,
|
||||
},
|
||||
{
|
||||
id: '25a05cd8-256f-4652-9e4a-6d9ca0b96f4d',
|
||||
label: 'Option 2',
|
||||
value: 'option2',
|
||||
position: 3,
|
||||
},
|
||||
],
|
||||
newOptions: [
|
||||
{
|
||||
id: 'f86eaffd-b773-4c9a-957b-86dca4a62731',
|
||||
label: 'Option 0_UPDATED', // Label changed but value remains the same
|
||||
value: 'option0',
|
||||
position: 1,
|
||||
},
|
||||
{
|
||||
id: '28d80b3c-79bd-4a1b-a868-9616534de0fa',
|
||||
label: 'Option 1', // No change
|
||||
value: 'option1',
|
||||
position: 2,
|
||||
},
|
||||
{
|
||||
id: '25a05cd8-256f-4652-9e4a-6d9ca0b96f4d',
|
||||
label: 'Option 2_UPDATED', // Label changed but value remains the same
|
||||
value: 'option2',
|
||||
position: 3,
|
||||
},
|
||||
],
|
||||
expected: {
|
||||
created: [],
|
||||
updated: [], // No updates because only labels changed, not values
|
||||
deleted: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'should consider changes to label as updates when value remains the same if compareLabel is true',
|
||||
context: {
|
||||
oldOptions: [
|
||||
{
|
||||
id: 'f86eaffd-b773-4c9a-957b-86dca4a62731',
|
||||
label: 'Option 0',
|
||||
value: 'option0',
|
||||
position: 1,
|
||||
},
|
||||
{
|
||||
id: '28d80b3c-79bd-4a1b-a868-9616534de0fa',
|
||||
label: 'Option 1',
|
||||
value: 'option1',
|
||||
position: 2,
|
||||
},
|
||||
{
|
||||
id: '25a05cd8-256f-4652-9e4a-6d9ca0b96f4d',
|
||||
label: 'Option 2',
|
||||
value: 'option2',
|
||||
position: 3,
|
||||
},
|
||||
],
|
||||
newOptions: [
|
||||
{
|
||||
id: 'f86eaffd-b773-4c9a-957b-86dca4a62731',
|
||||
label: 'Option 0_UPDATED', // Label changed but value remains the same
|
||||
value: 'option0',
|
||||
position: 1,
|
||||
},
|
||||
{
|
||||
id: '28d80b3c-79bd-4a1b-a868-9616534de0fa',
|
||||
label: 'Option 1', // No change
|
||||
value: 'option1',
|
||||
position: 2,
|
||||
},
|
||||
{
|
||||
id: '25a05cd8-256f-4652-9e4a-6d9ca0b96f4d',
|
||||
label: 'Option 2_UPDATED', // Label changed but value remains the same
|
||||
value: 'option2',
|
||||
position: 3,
|
||||
},
|
||||
],
|
||||
expected: {
|
||||
created: [],
|
||||
updated: [
|
||||
{
|
||||
new: {
|
||||
id: 'f86eaffd-b773-4c9a-957b-86dca4a62731',
|
||||
label: 'Option 0_UPDATED',
|
||||
position: 1,
|
||||
value: 'option0',
|
||||
},
|
||||
old: {
|
||||
id: 'f86eaffd-b773-4c9a-957b-86dca4a62731',
|
||||
label: 'Option 0',
|
||||
position: 1,
|
||||
value: 'option0',
|
||||
},
|
||||
},
|
||||
{
|
||||
new: {
|
||||
id: '25a05cd8-256f-4652-9e4a-6d9ca0b96f4d',
|
||||
label: 'Option 2_UPDATED',
|
||||
position: 3,
|
||||
value: 'option2',
|
||||
},
|
||||
old: {
|
||||
id: '25a05cd8-256f-4652-9e4a-6d9ca0b96f4d',
|
||||
label: 'Option 2',
|
||||
position: 3,
|
||||
value: 'option2',
|
||||
},
|
||||
},
|
||||
],
|
||||
deleted: [],
|
||||
},
|
||||
compareLabel: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
test.each(testCases)(
|
||||
'$title',
|
||||
({ context: { oldOptions, newOptions, expected, compareLabel } }) => {
|
||||
const result = service.getOptionsDifferences(
|
||||
oldOptions,
|
||||
newOptions,
|
||||
compareLabel,
|
||||
);
|
||||
|
||||
expect(result.created).toEqual(expected.created);
|
||||
expect(result.updated).toEqual(expected.updated);
|
||||
expect(result.deleted).toEqual(expected.deleted);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
-390
@@ -1,390 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { isDefined, parseJson } from 'twenty-shared/utils';
|
||||
|
||||
import { settings } from 'src/engine/constants/settings';
|
||||
import {
|
||||
type FieldMetadataComplexOption,
|
||||
type FieldMetadataDefaultOption,
|
||||
} from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
|
||||
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import {
|
||||
FieldMetadataException,
|
||||
FieldMetadataExceptionCode,
|
||||
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
|
||||
import { isSelectFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-select-field-metadata-type.util';
|
||||
import { type SelectOrMultiSelectFieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/utils/is-select-or-multi-select-field-metadata.util';
|
||||
import { ViewFieldService } from 'src/engine/metadata-modules/view-field/services/view-field.service';
|
||||
import { ViewFilterService } from 'src/engine/metadata-modules/view-filter/services/view-filter.service';
|
||||
import { ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity';
|
||||
import { ViewGroupService } from 'src/engine/metadata-modules/view-group/services/view-group.service';
|
||||
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
|
||||
import { ViewKey } from 'src/engine/metadata-modules/view/enums/view-key.enum';
|
||||
import { ViewService } from 'src/engine/metadata-modules/view/services/view.service';
|
||||
import { DEFAULT_VIEW_FIELD_SIZE } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/constants/DEFAULT_VIEW_FIELD_SIZE';
|
||||
|
||||
type Differences<T> = {
|
||||
created: T[];
|
||||
updated: { old: T; new: T }[];
|
||||
deleted: T[];
|
||||
};
|
||||
|
||||
type GetOptionsDifferences = Differences<
|
||||
FieldMetadataDefaultOption | FieldMetadataComplexOption
|
||||
>;
|
||||
|
||||
@Injectable()
|
||||
export class FieldMetadataRelatedRecordsService {
|
||||
constructor(
|
||||
private readonly viewService: ViewService,
|
||||
private readonly viewFieldService: ViewFieldService,
|
||||
private readonly viewFilterService: ViewFilterService,
|
||||
private readonly viewGroupService: ViewGroupService,
|
||||
) {}
|
||||
|
||||
public async updateRelatedViewGroups(
|
||||
oldFieldMetadata: SelectOrMultiSelectFieldMetadataEntity,
|
||||
newFieldMetadata: SelectOrMultiSelectFieldMetadataEntity,
|
||||
): Promise<void> {
|
||||
// TODO legacy should support multi-select and rating ?
|
||||
if (
|
||||
!isSelectFieldMetadataType(newFieldMetadata.type) ||
|
||||
!isSelectFieldMetadataType(oldFieldMetadata.type)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const objectMetadataViews = await this.viewService.findByObjectMetadataId(
|
||||
newFieldMetadata.workspaceId,
|
||||
newFieldMetadata.objectMetadataId,
|
||||
);
|
||||
|
||||
const { created, updated, deleted } = this.getOptionsDifferences(
|
||||
oldFieldMetadata.options ?? [],
|
||||
newFieldMetadata.options ?? [],
|
||||
);
|
||||
|
||||
for (const view of objectMetadataViews) {
|
||||
if (view.viewGroups.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const valuesToDelete = deleted.map((option) => option.value);
|
||||
|
||||
if (valuesToDelete.length > 0) {
|
||||
for (const valueToDelete of valuesToDelete) {
|
||||
const viewGroupsToDelete = view.viewGroups.filter(
|
||||
(group) => group.fieldValue === valueToDelete,
|
||||
);
|
||||
|
||||
for (const viewGroup of viewGroupsToDelete) {
|
||||
await this.viewGroupService.destroy(
|
||||
viewGroup.id,
|
||||
newFieldMetadata.workspaceId,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
const maxPosition = this.getMaxPosition(view.viewGroups);
|
||||
|
||||
for (const [index, option] of created.entries()) {
|
||||
this.viewGroupService.create({
|
||||
fieldMetadataId: newFieldMetadata.id,
|
||||
fieldValue: option.value,
|
||||
position: maxPosition + index,
|
||||
isVisible: true,
|
||||
viewId: view.id,
|
||||
workspaceId: newFieldMetadata.workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
for (const { old: oldOption, new: newOption } of updated) {
|
||||
const existingViewGroup = view.viewGroups.find(
|
||||
(group) => group.fieldValue === oldOption.value,
|
||||
);
|
||||
|
||||
if (!existingViewGroup) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.viewGroupService.update(
|
||||
existingViewGroup.id,
|
||||
newFieldMetadata.workspaceId,
|
||||
{ fieldValue: newOption.value },
|
||||
);
|
||||
}
|
||||
|
||||
await this.syncNoValueViewGroup(newFieldMetadata, view);
|
||||
}
|
||||
await this.viewService.flushGraphQLCache(newFieldMetadata.workspaceId);
|
||||
}
|
||||
|
||||
public async resetViewKanbanAggregateOperation(
|
||||
fieldMetadata: Pick<
|
||||
FieldMetadataEntity,
|
||||
'id' | 'workspaceId' | 'objectMetadataId'
|
||||
>,
|
||||
): Promise<void> {
|
||||
const views = await this.viewService.findByObjectMetadataId(
|
||||
fieldMetadata.workspaceId,
|
||||
fieldMetadata.objectMetadataId,
|
||||
);
|
||||
|
||||
const viewsHavingFieldAsAggregateOperation = views.filter(
|
||||
(view) =>
|
||||
view.kanbanAggregateOperationFieldMetadataId === fieldMetadata.id,
|
||||
);
|
||||
|
||||
for (const view of viewsHavingFieldAsAggregateOperation) {
|
||||
await this.viewService.update(view.id, fieldMetadata.workspaceId, {
|
||||
kanbanAggregateOperationFieldMetadataId: null,
|
||||
kanbanAggregateOperation: null,
|
||||
});
|
||||
}
|
||||
await this.viewService.flushGraphQLCache(fieldMetadata.workspaceId);
|
||||
}
|
||||
|
||||
public async updateRelatedViewFilters(
|
||||
oldFieldMetadata: SelectOrMultiSelectFieldMetadataEntity,
|
||||
newFieldMetadata: SelectOrMultiSelectFieldMetadataEntity,
|
||||
): Promise<void> {
|
||||
const views = await this.viewService.findByObjectMetadataId(
|
||||
newFieldMetadata.workspaceId,
|
||||
newFieldMetadata.objectMetadataId,
|
||||
);
|
||||
|
||||
const fieldMetadataViews = views.filter((view) =>
|
||||
view.viewFilters.some(
|
||||
(filter) => filter.fieldMetadataId === newFieldMetadata.id,
|
||||
),
|
||||
);
|
||||
|
||||
const alsoCompareLabel = true;
|
||||
const {
|
||||
updated: updatedFieldMetadataOptions,
|
||||
deleted: deletedFieldMetadataOptions,
|
||||
} = this.getOptionsDifferences(
|
||||
oldFieldMetadata.options,
|
||||
newFieldMetadata.options,
|
||||
alsoCompareLabel,
|
||||
);
|
||||
|
||||
if (
|
||||
updatedFieldMetadataOptions.length === 0 &&
|
||||
deletedFieldMetadataOptions.length === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const view of fieldMetadataViews) {
|
||||
if (view.viewFilters.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const viewFilter of view.viewFilters) {
|
||||
if (!isDefined(viewFilter.value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: all view filter value should be stored as JSON, this is ongoing work (we are missing a command to migrate the data)
|
||||
const parsedValue = isNonEmptyString(viewFilter.value)
|
||||
? parseJson(viewFilter.value)
|
||||
: viewFilter.value;
|
||||
|
||||
if (!isDefined(parsedValue) || !Array.isArray(parsedValue)) {
|
||||
throw new FieldMetadataException(
|
||||
`Unexpected invalid view filter value for filter ${viewFilter.id}`,
|
||||
FieldMetadataExceptionCode.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
const viewFilterOptions = parsedValue
|
||||
.map((value) => {
|
||||
if (!isDefined(oldFieldMetadata.options)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return oldFieldMetadata.options.find(
|
||||
(option) => option.value === value,
|
||||
);
|
||||
})
|
||||
.filter(isDefined);
|
||||
|
||||
const afterDeleteViewFilterOptions = viewFilterOptions.filter(
|
||||
(viewFilterOption) =>
|
||||
!deletedFieldMetadataOptions.some(
|
||||
(option) => option.value === viewFilterOption.value,
|
||||
),
|
||||
);
|
||||
|
||||
if (afterDeleteViewFilterOptions.length === 0) {
|
||||
await this.viewFilterService.destroy(
|
||||
viewFilter.id,
|
||||
newFieldMetadata.workspaceId,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const afterUpdateAndDeleteViewFilterOptions =
|
||||
afterDeleteViewFilterOptions.map((viewFilterOption) => {
|
||||
const updatedOption = updatedFieldMetadataOptions.find(
|
||||
({ old }) => viewFilterOption.value === old.value,
|
||||
);
|
||||
|
||||
return isDefined(updatedOption)
|
||||
? updatedOption.new
|
||||
: viewFilterOption;
|
||||
});
|
||||
|
||||
const value = afterUpdateAndDeleteViewFilterOptions.map(
|
||||
(option) => option.value,
|
||||
);
|
||||
|
||||
await this.viewFilterService.update(
|
||||
viewFilter.id,
|
||||
newFieldMetadata.workspaceId,
|
||||
{
|
||||
value,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
await this.viewService.flushGraphQLCache(newFieldMetadata.workspaceId);
|
||||
}
|
||||
|
||||
async syncNoValueViewGroup(
|
||||
fieldMetadata: SelectOrMultiSelectFieldMetadataEntity,
|
||||
view: ViewEntity,
|
||||
): Promise<void> {
|
||||
const noValueGroup = view.viewGroups.find(
|
||||
(group) => group.fieldValue === '',
|
||||
);
|
||||
|
||||
if (fieldMetadata.isNullable && !noValueGroup) {
|
||||
const maxPosition = this.getMaxPosition(view.viewGroups);
|
||||
|
||||
this.viewGroupService.create({
|
||||
fieldMetadataId: fieldMetadata.id,
|
||||
fieldValue: '',
|
||||
position: maxPosition + 1,
|
||||
isVisible: true,
|
||||
viewId: view.id,
|
||||
workspaceId: fieldMetadata.workspaceId,
|
||||
});
|
||||
} else if (!fieldMetadata.isNullable && noValueGroup) {
|
||||
await this.viewGroupService.destroy(
|
||||
noValueGroup.id,
|
||||
fieldMetadata.workspaceId,
|
||||
);
|
||||
}
|
||||
await this.viewService.flushGraphQLCache(fieldMetadata.workspaceId);
|
||||
}
|
||||
|
||||
public getOptionsDifferences(
|
||||
rawOldOptions:
|
||||
| (FieldMetadataDefaultOption | FieldMetadataComplexOption)[]
|
||||
| null,
|
||||
rawNewOptions:
|
||||
| (FieldMetadataDefaultOption | FieldMetadataComplexOption)[]
|
||||
| null,
|
||||
compareLabel = false,
|
||||
): GetOptionsDifferences {
|
||||
const differences: Differences<
|
||||
FieldMetadataDefaultOption | FieldMetadataComplexOption
|
||||
> = {
|
||||
created: [],
|
||||
updated: [],
|
||||
deleted: [],
|
||||
};
|
||||
|
||||
const oldOptions = rawOldOptions ?? [];
|
||||
const newOptions = rawNewOptions ?? [];
|
||||
|
||||
const oldOptionsMap = new Map(oldOptions.map((opt) => [opt.id, opt]));
|
||||
|
||||
for (const newOption of newOptions) {
|
||||
const oldOption = oldOptionsMap.get(newOption.id);
|
||||
|
||||
if (!isDefined(oldOption)) {
|
||||
differences.created.push(newOption);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
oldOption.value !== newOption.value ||
|
||||
(compareLabel && oldOption.label !== newOption.label)
|
||||
) {
|
||||
differences.updated.push({ old: oldOption, new: newOption });
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const newOptionsMap = new Map(newOptions.map((opt) => [opt.id, opt]));
|
||||
|
||||
for (const oldOption of oldOptions) {
|
||||
if (!newOptionsMap.has(oldOption.id)) {
|
||||
differences.deleted.push(oldOption);
|
||||
}
|
||||
}
|
||||
|
||||
return differences;
|
||||
}
|
||||
|
||||
private getMaxPosition(viewGroups: ViewGroupEntity[]): number {
|
||||
return viewGroups.reduce((max, group) => Math.max(max, group.position), 0);
|
||||
}
|
||||
|
||||
async createViewAndViewFields(
|
||||
createdFieldMetadatas: FieldMetadataEntity[],
|
||||
workspaceId: string,
|
||||
) {
|
||||
const views = await this.viewService.findByWorkspaceId(workspaceId);
|
||||
|
||||
for (const createdFieldMetadata of createdFieldMetadatas) {
|
||||
const objectViews = views.filter(
|
||||
(view) =>
|
||||
view.objectMetadataId === createdFieldMetadata.objectMetadataId,
|
||||
);
|
||||
|
||||
if (objectViews.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const indexView = objectViews.find((view) => view.key === ViewKey.INDEX);
|
||||
|
||||
if (!indexView) {
|
||||
return;
|
||||
}
|
||||
const isVisible =
|
||||
indexView.viewFields.length < settings.maxVisibleViewFields;
|
||||
|
||||
const createdFieldIsAlreadyInView = indexView.viewFields.some(
|
||||
(existingViewField) =>
|
||||
existingViewField.fieldMetadataId === createdFieldMetadata.id,
|
||||
);
|
||||
|
||||
if (createdFieldIsAlreadyInView) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const lastPosition = indexView.viewFields
|
||||
.map((viewField) => viewField.position)
|
||||
.reduce((acc, position) => {
|
||||
if (position > acc) {
|
||||
return position;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, -1);
|
||||
|
||||
await this.viewFieldService.create({
|
||||
fieldMetadataId: createdFieldMetadata.id,
|
||||
position: lastPosition + 1,
|
||||
isVisible,
|
||||
size: DEFAULT_VIEW_FIELD_SIZE,
|
||||
viewId: indexView.id,
|
||||
workspaceId: createdFieldMetadata.workspaceId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
+55
-28
@@ -1,12 +1,12 @@
|
||||
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 { In, Repository } from 'typeorm';
|
||||
import { FindOneOptions, Repository } from 'typeorm';
|
||||
|
||||
import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
|
||||
import { type DeleteOneFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/delete-field.input';
|
||||
import { 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';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import {
|
||||
@@ -15,31 +15,35 @@ import {
|
||||
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { computeFlatEntityMapsFromTo } from 'src/engine/metadata-modules/flat-entity/utils/compute-flat-entity-maps-from-to.util';
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { fromCreateFieldInputToFlatFieldMetadatasToCreate } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-create-field-input-to-flat-field-metadatas-to-create.util';
|
||||
import { fromDeleteFieldInputToFlatFieldMetadatasToDelete } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-delete-field-input-to-flat-field-metadatas-to-delete.util';
|
||||
import { fromFlatFieldMetadataToFieldMetadataDto } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-flat-field-metadata-to-field-metadata-dto.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 { 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';
|
||||
|
||||
@Injectable()
|
||||
export class FieldMetadataServiceV2 {
|
||||
export class FieldMetadataServiceV2 extends TypeOrmQueryService<FieldMetadataEntity> {
|
||||
constructor(
|
||||
@InjectRepository(FieldMetadataEntity)
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
||||
) {}
|
||||
) {
|
||||
super(fieldMetadataRepository);
|
||||
}
|
||||
|
||||
async createOne({
|
||||
async createOneField({
|
||||
createFieldInput,
|
||||
workspaceId,
|
||||
}: {
|
||||
createFieldInput: Omit<CreateFieldInput, 'workspaceId'>;
|
||||
workspaceId: string;
|
||||
}): Promise<FieldMetadataEntity> {
|
||||
const [createdFieldMetadata] = await this.createMany({
|
||||
}): Promise<FlatFieldMetadata> {
|
||||
const [createdFieldMetadata] = await this.createManyFields({
|
||||
workspaceId,
|
||||
createFieldInputs: [createFieldInput],
|
||||
});
|
||||
@@ -54,7 +58,7 @@ export class FieldMetadataServiceV2 {
|
||||
return createdFieldMetadata;
|
||||
}
|
||||
|
||||
public async deleteOneField({
|
||||
async deleteOneField({
|
||||
deleteOneFieldInput,
|
||||
workspaceId,
|
||||
isSystemBuild = false,
|
||||
@@ -62,7 +66,7 @@ export class FieldMetadataServiceV2 {
|
||||
deleteOneFieldInput: DeleteOneFieldInput;
|
||||
workspaceId: string;
|
||||
isSystemBuild?: boolean;
|
||||
}): Promise<FieldMetadataDTO> {
|
||||
}): Promise<FlatFieldMetadata> {
|
||||
const {
|
||||
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
|
||||
flatIndexMaps: existingFlatIndexMaps,
|
||||
@@ -127,18 +131,16 @@ export class FieldMetadataServiceV2 {
|
||||
);
|
||||
}
|
||||
|
||||
return fromFlatFieldMetadataToFieldMetadataDto(
|
||||
flatFieldMetadatasToDelete[0],
|
||||
);
|
||||
return flatFieldMetadatasToDelete[0];
|
||||
}
|
||||
|
||||
async updateOne({
|
||||
async updateOneField({
|
||||
updateFieldInput,
|
||||
workspaceId,
|
||||
}: {
|
||||
updateFieldInput: Omit<UpdateFieldInput, 'workspaceId'>;
|
||||
workspaceId: string;
|
||||
}): Promise<FieldMetadataEntity> {
|
||||
}): Promise<FlatFieldMetadata> {
|
||||
const {
|
||||
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
|
||||
flatIndexMaps: existingFlatIndexMaps,
|
||||
@@ -264,21 +266,27 @@ export class FieldMetadataServiceV2 {
|
||||
);
|
||||
}
|
||||
|
||||
return this.fieldMetadataRepository.findOneOrFail({
|
||||
where: {
|
||||
id: flatFieldMetadatasToUpdate[0].id,
|
||||
workspaceId,
|
||||
},
|
||||
const { flatFieldMetadataMaps: recomputedFlatFieldMetadataMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatFieldMetadataMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
return findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: flatFieldMetadatasToUpdate[0].id,
|
||||
flatEntityMaps: recomputedFlatFieldMetadataMaps,
|
||||
});
|
||||
}
|
||||
|
||||
async createMany({
|
||||
async createManyFields({
|
||||
createFieldInputs,
|
||||
workspaceId,
|
||||
}: {
|
||||
createFieldInputs: Omit<CreateFieldInput, 'workspaceId'>[];
|
||||
workspaceId: string;
|
||||
}): Promise<FieldMetadataEntity[]> {
|
||||
}): Promise<FlatFieldMetadata[]> {
|
||||
if (createFieldInputs.length === 0) {
|
||||
return [];
|
||||
}
|
||||
@@ -365,15 +373,34 @@ export class FieldMetadataServiceV2 {
|
||||
);
|
||||
}
|
||||
|
||||
return this.fieldMetadataRepository.find({
|
||||
const { flatFieldMetadataMaps: recomputedFlatFieldMetadataMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatFieldMetadataMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
return findManyFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityIds: allTranspiledTranspilationInputs.map(
|
||||
({ result: { flatFieldMetadatas } }) => flatFieldMetadatas[0].id,
|
||||
),
|
||||
flatEntityMaps: recomputedFlatFieldMetadataMaps,
|
||||
});
|
||||
}
|
||||
|
||||
public async findOneWithinWorkspace(
|
||||
workspaceId: string,
|
||||
options: FindOneOptions<FieldMetadataEntity>,
|
||||
) {
|
||||
const [fieldMetadata] = await this.fieldMetadataRepository.find({
|
||||
...options,
|
||||
where: {
|
||||
id: In(
|
||||
allTranspiledTranspilationInputs.map(
|
||||
({ result: { flatFieldMetadatas } }) => flatFieldMetadatas[0].id,
|
||||
),
|
||||
),
|
||||
...options.where,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
return fieldMetadata;
|
||||
}
|
||||
}
|
||||
|
||||
-1148
File diff suppressed because it is too large
Load Diff
+1
-2
@@ -3,7 +3,6 @@ import { Injectable } from '@nestjs/common';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { isDefined } from 'class-validator';
|
||||
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
|
||||
import { type FlatFieldMetadataTypeValidator } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-type-validator.type';
|
||||
import { FlatFieldMetadataValidationError } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-validation-error.type';
|
||||
@@ -17,7 +16,7 @@ const DEFAULT_NO_VALIDATION = async (): Promise<
|
||||
|
||||
@Injectable()
|
||||
export class FlatFieldMetadataTypeValidatorService {
|
||||
constructor(private readonly featureFlagService: FeatureFlagService) {}
|
||||
constructor() {}
|
||||
|
||||
private readonly FIELD_METADATA_TYPE_VALIDATOR_HASHMAP: FlatFieldMetadataTypeValidator =
|
||||
{
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ import { buildDefaultRelationFlatFieldMetadatasForCustomObject } from 'src/engin
|
||||
|
||||
type FromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCreateArgs =
|
||||
{
|
||||
createObjectInput: Omit<CreateObjectInput, 'workspaceId'>;
|
||||
createObjectInput: CreateObjectInput;
|
||||
workspaceId: string;
|
||||
} & Pick<AllFlatEntityMaps, 'flatObjectMetadataMaps'>;
|
||||
export const fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCreate =
|
||||
|
||||
+19
-8
@@ -1,18 +1,22 @@
|
||||
import { Field, HideField, InputType } from '@nestjs/graphql';
|
||||
|
||||
import { BeforeCreateOne } from '@ptc-org/nestjs-query-graphql';
|
||||
import { IsBoolean, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
import {
|
||||
IsBoolean,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import GraphQLJSON from 'graphql-type-json';
|
||||
import {
|
||||
type FieldMetadataType,
|
||||
type FieldMetadataSettings,
|
||||
type FieldMetadataType,
|
||||
} from 'twenty-shared/types';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
import { IsValidMetadataName } from 'src/engine/decorators/metadata/is-valid-metadata-name.decorator';
|
||||
import { BeforeCreateOneObject } from 'src/engine/metadata-modules/object-metadata/hooks/before-create-one-object.hook';
|
||||
|
||||
@InputType()
|
||||
@BeforeCreateOne(BeforeCreateOneObject)
|
||||
export class CreateObjectInput {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@@ -54,9 +58,6 @@ export class CreateObjectInput {
|
||||
@HideField()
|
||||
dataSourceId: string;
|
||||
|
||||
@HideField()
|
||||
workspaceId: string;
|
||||
|
||||
@HideField()
|
||||
applicationId?: string;
|
||||
|
||||
@@ -81,3 +82,13 @@ export class CreateObjectInput {
|
||||
@Field({ nullable: true }) // Not nullable to me
|
||||
isLabelSyncedWithName?: boolean;
|
||||
}
|
||||
|
||||
@InputType()
|
||||
export class CreateOneObjectInput {
|
||||
@Type(() => CreateObjectInput)
|
||||
@ValidateNested()
|
||||
@Field(() => CreateObjectInput, {
|
||||
description: 'The object to create',
|
||||
})
|
||||
object!: CreateObjectInput;
|
||||
}
|
||||
|
||||
+1
-3
@@ -1,13 +1,11 @@
|
||||
import { InputType } from '@nestjs/graphql';
|
||||
|
||||
import { BeforeDeleteOne, IDField } from '@ptc-org/nestjs-query-graphql';
|
||||
import { IDField } from '@ptc-org/nestjs-query-graphql';
|
||||
import { IsUUID } from 'class-validator';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { BeforeDeleteOneObject } from 'src/engine/metadata-modules/object-metadata/hooks/before-delete-one-object.hook';
|
||||
|
||||
@InputType()
|
||||
@BeforeDeleteOne(BeforeDeleteOneObject)
|
||||
export class DeleteOneObjectInput {
|
||||
@IDField(() => UUIDScalarType, {
|
||||
description: 'The id of the record to delete.',
|
||||
|
||||
-3
@@ -2,7 +2,6 @@ import { Field, HideField, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
Authorize,
|
||||
BeforeDeleteOne,
|
||||
CursorConnection,
|
||||
FilterableField,
|
||||
IDField,
|
||||
@@ -14,7 +13,6 @@ import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/
|
||||
import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto';
|
||||
import { IndexMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-metadata.dto';
|
||||
import { ObjectStandardOverridesDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-standard-overrides.dto';
|
||||
import { BeforeDeleteOneObject } from 'src/engine/metadata-modules/object-metadata/hooks/before-delete-one-object.hook';
|
||||
|
||||
@ObjectType('Object')
|
||||
@Authorize({
|
||||
@@ -28,7 +26,6 @@ import { BeforeDeleteOneObject } from 'src/engine/metadata-modules/object-metada
|
||||
disableSort: true,
|
||||
maxResultsSize: 1000,
|
||||
})
|
||||
@BeforeDeleteOne(BeforeDeleteOneObject)
|
||||
@CursorConnection('fields', () => FieldMetadataDTO)
|
||||
@CursorConnection('indexMetadatas', () => IndexMetadataDTO)
|
||||
export class ObjectMetadataDTO {
|
||||
|
||||
-3
@@ -1,6 +1,5 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import { BeforeUpdateOne } from '@ptc-org/nestjs-query-graphql';
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
IsBoolean,
|
||||
@@ -13,7 +12,6 @@ import {
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { IsValidMetadataName } from 'src/engine/decorators/metadata/is-valid-metadata-name.decorator';
|
||||
import { BeforeUpdateOneObject } from 'src/engine/metadata-modules/object-metadata/hooks/before-update-one-object.hook';
|
||||
|
||||
@InputType()
|
||||
export class UpdateObjectPayload {
|
||||
@@ -76,7 +74,6 @@ export class UpdateObjectPayload {
|
||||
}
|
||||
|
||||
@InputType()
|
||||
@BeforeUpdateOne(BeforeUpdateOneObject)
|
||||
export class UpdateOneObjectInput {
|
||||
@Type(() => UpdateObjectPayload)
|
||||
@ValidateNested()
|
||||
|
||||
-697
@@ -1,697 +0,0 @@
|
||||
import { BadRequestException, UnauthorizedException } from '@nestjs/common';
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
import { type UpdateOneInputType } from '@ptc-org/nestjs-query-graphql';
|
||||
import { type Repository } from 'typeorm';
|
||||
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { type UpdateObjectPayload } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input';
|
||||
import { BeforeUpdateOneObject } from 'src/engine/metadata-modules/object-metadata/hooks/before-update-one-object.hook';
|
||||
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
||||
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
|
||||
|
||||
type UpdateObjectPayloadForTest = Omit<
|
||||
UpdateObjectPayload,
|
||||
'id' | 'workspaceId'
|
||||
>;
|
||||
|
||||
describe('BeforeUpdateOneObject', () => {
|
||||
let hook: BeforeUpdateOneObject<UpdateObjectPayload>;
|
||||
let objectMetadataService: ObjectMetadataService;
|
||||
let fieldMetadataRepository: Repository<FieldMetadataEntity>;
|
||||
|
||||
const mockWorkspaceId = 'workspace-id';
|
||||
const mockObjectId = 'object-id';
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
BeforeUpdateOneObject,
|
||||
{
|
||||
provide: ObjectMetadataService,
|
||||
useValue: {
|
||||
findOneWithinWorkspace: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(FieldMetadataEntity),
|
||||
useValue: {
|
||||
findBy: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: I18nService,
|
||||
useValue: {
|
||||
getI18nInstance: jest.fn().mockReturnValue({
|
||||
_: jest.fn().mockReturnValue('mocked-translation'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
hook = module.get<BeforeUpdateOneObject<UpdateObjectPayload>>(
|
||||
BeforeUpdateOneObject,
|
||||
);
|
||||
objectMetadataService = module.get<ObjectMetadataService>(
|
||||
ObjectMetadataService,
|
||||
);
|
||||
fieldMetadataRepository = module.get<Repository<FieldMetadataEntity>>(
|
||||
getRepositoryToken(FieldMetadataEntity),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should throw UnauthorizedException if workspaceId is not provided', async () => {
|
||||
const instance: UpdateOneInputType<UpdateObjectPayloadForTest> = {
|
||||
id: mockObjectId,
|
||||
update: {
|
||||
isActive: true,
|
||||
},
|
||||
};
|
||||
|
||||
await expect(
|
||||
hook.run(instance as UpdateOneInputType<UpdateObjectPayload>, {
|
||||
workspaceId: '',
|
||||
locale: undefined,
|
||||
}),
|
||||
).rejects.toThrow(UnauthorizedException);
|
||||
});
|
||||
|
||||
it('should throw BadRequestException if object does not exist', async () => {
|
||||
const instance: UpdateOneInputType<UpdateObjectPayloadForTest> = {
|
||||
id: mockObjectId,
|
||||
update: {
|
||||
isActive: true,
|
||||
},
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(objectMetadataService, 'findOneWithinWorkspace')
|
||||
.mockResolvedValue(null as unknown as ObjectMetadataEntity);
|
||||
|
||||
await expect(
|
||||
hook.run(instance as UpdateOneInputType<UpdateObjectPayload>, {
|
||||
workspaceId: mockWorkspaceId,
|
||||
locale: undefined,
|
||||
}),
|
||||
).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
|
||||
it('should not affect custom objects', async () => {
|
||||
const instance: UpdateOneInputType<UpdateObjectPayloadForTest> = {
|
||||
id: mockObjectId,
|
||||
update: {
|
||||
nameSingular: 'newName',
|
||||
isActive: true,
|
||||
},
|
||||
};
|
||||
|
||||
const mockObject: Partial<ObjectMetadataEntity> = {
|
||||
id: mockObjectId,
|
||||
isCustom: true,
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(objectMetadataService, 'findOneWithinWorkspace')
|
||||
.mockResolvedValue(mockObject as ObjectMetadataEntity);
|
||||
|
||||
jest.spyOn(fieldMetadataRepository, 'findBy').mockResolvedValue([]);
|
||||
|
||||
const result = await hook.run(
|
||||
instance as UpdateOneInputType<UpdateObjectPayload>,
|
||||
{
|
||||
workspaceId: mockWorkspaceId,
|
||||
locale: undefined,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toEqual(instance);
|
||||
});
|
||||
|
||||
it('should throw BadRequestException when trying to update non-updatable fields on standard objects', async () => {
|
||||
const instance: UpdateOneInputType<UpdateObjectPayloadForTest> = {
|
||||
id: mockObjectId,
|
||||
update: {
|
||||
nameSingular: 'newName',
|
||||
},
|
||||
};
|
||||
|
||||
const mockObject: Partial<ObjectMetadataEntity> = {
|
||||
id: mockObjectId,
|
||||
isCustom: false,
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(objectMetadataService, 'findOneWithinWorkspace')
|
||||
.mockResolvedValue(mockObject as ObjectMetadataEntity);
|
||||
|
||||
await expect(
|
||||
hook.run(instance as UpdateOneInputType<UpdateObjectPayload>, {
|
||||
workspaceId: mockWorkspaceId,
|
||||
locale: undefined,
|
||||
}),
|
||||
).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
|
||||
it('should throw BadRequestException when trying to update labels when they are synced with name', async () => {
|
||||
const instance: UpdateOneInputType<UpdateObjectPayloadForTest> = {
|
||||
id: mockObjectId,
|
||||
update: {
|
||||
labelSingular: 'New Label',
|
||||
},
|
||||
};
|
||||
|
||||
const mockObject: Partial<ObjectMetadataEntity> = {
|
||||
id: mockObjectId,
|
||||
isCustom: false,
|
||||
isLabelSyncedWithName: true,
|
||||
labelSingular: 'Old Label',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(objectMetadataService, 'findOneWithinWorkspace')
|
||||
.mockResolvedValue(mockObject as ObjectMetadataEntity);
|
||||
|
||||
await expect(
|
||||
hook.run(instance as UpdateOneInputType<UpdateObjectPayload>, {
|
||||
workspaceId: mockWorkspaceId,
|
||||
locale: undefined,
|
||||
}),
|
||||
).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
|
||||
it('should handle isActive updates for standard objects', async () => {
|
||||
const instance: UpdateOneInputType<UpdateObjectPayloadForTest> = {
|
||||
id: mockObjectId,
|
||||
update: {
|
||||
isActive: false,
|
||||
},
|
||||
};
|
||||
|
||||
const mockObject: Partial<ObjectMetadataEntity> = {
|
||||
id: mockObjectId,
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(objectMetadataService, 'findOneWithinWorkspace')
|
||||
.mockResolvedValue(mockObject as ObjectMetadataEntity);
|
||||
|
||||
const result = await hook.run(
|
||||
instance as UpdateOneInputType<UpdateObjectPayload>,
|
||||
{
|
||||
workspaceId: mockWorkspaceId,
|
||||
locale: undefined,
|
||||
},
|
||||
);
|
||||
|
||||
const expectedResult = {
|
||||
id: mockObjectId,
|
||||
update: {
|
||||
isActive: false,
|
||||
standardOverrides: {},
|
||||
},
|
||||
};
|
||||
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it('should handle isLabelSyncedWithName updates for standard objects', async () => {
|
||||
const instance: UpdateOneInputType<UpdateObjectPayloadForTest> = {
|
||||
id: mockObjectId,
|
||||
update: {
|
||||
isLabelSyncedWithName: true,
|
||||
},
|
||||
};
|
||||
|
||||
const mockObject: Partial<ObjectMetadataEntity> = {
|
||||
id: mockObjectId,
|
||||
isCustom: false,
|
||||
isLabelSyncedWithName: false,
|
||||
standardOverrides: {
|
||||
labelSingular: 'Custom Label',
|
||||
labelPlural: 'Custom Labels',
|
||||
},
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(objectMetadataService, 'findOneWithinWorkspace')
|
||||
.mockResolvedValue(mockObject as ObjectMetadataEntity);
|
||||
|
||||
const result = await hook.run(
|
||||
instance as UpdateOneInputType<UpdateObjectPayload>,
|
||||
{
|
||||
workspaceId: mockWorkspaceId,
|
||||
locale: undefined,
|
||||
},
|
||||
);
|
||||
|
||||
const expectedResult = {
|
||||
id: mockObjectId,
|
||||
update: {
|
||||
isLabelSyncedWithName: true,
|
||||
standardOverrides: {
|
||||
labelSingular: null,
|
||||
labelPlural: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it('should handle labelSingular override when not synced with name', async () => {
|
||||
const instance: UpdateOneInputType<UpdateObjectPayloadForTest> = {
|
||||
id: mockObjectId,
|
||||
update: {
|
||||
labelSingular: 'New Label',
|
||||
},
|
||||
};
|
||||
|
||||
const mockObject: Partial<ObjectMetadataEntity> = {
|
||||
id: mockObjectId,
|
||||
isCustom: false,
|
||||
isLabelSyncedWithName: false,
|
||||
labelSingular: 'Default Label',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(objectMetadataService, 'findOneWithinWorkspace')
|
||||
.mockResolvedValue(mockObject as ObjectMetadataEntity);
|
||||
|
||||
const result = await hook.run(
|
||||
instance as UpdateOneInputType<UpdateObjectPayload>,
|
||||
{
|
||||
workspaceId: mockWorkspaceId,
|
||||
locale: undefined,
|
||||
},
|
||||
);
|
||||
|
||||
const expectedResult = {
|
||||
id: mockObjectId,
|
||||
update: {
|
||||
standardOverrides: {
|
||||
labelSingular: 'New Label',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it('should handle labelPlural override when not synced with name', async () => {
|
||||
const instance: UpdateOneInputType<UpdateObjectPayloadForTest> = {
|
||||
id: mockObjectId,
|
||||
update: {
|
||||
labelPlural: 'New Labels',
|
||||
},
|
||||
};
|
||||
|
||||
const mockObject: Partial<ObjectMetadataEntity> = {
|
||||
id: mockObjectId,
|
||||
isCustom: false,
|
||||
isLabelSyncedWithName: false,
|
||||
labelPlural: 'Default Labels',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(objectMetadataService, 'findOneWithinWorkspace')
|
||||
.mockResolvedValue(mockObject as ObjectMetadataEntity);
|
||||
|
||||
const result = await hook.run(
|
||||
instance as UpdateOneInputType<UpdateObjectPayload>,
|
||||
{
|
||||
workspaceId: mockWorkspaceId,
|
||||
locale: undefined,
|
||||
},
|
||||
);
|
||||
|
||||
const expectedResult = {
|
||||
id: mockObjectId,
|
||||
update: {
|
||||
standardOverrides: {
|
||||
labelPlural: 'New Labels',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it('should reset labelSingular override when it matches the original value', async () => {
|
||||
const instance: UpdateOneInputType<UpdateObjectPayloadForTest> = {
|
||||
id: mockObjectId,
|
||||
update: {
|
||||
labelSingular: 'Default Label',
|
||||
},
|
||||
};
|
||||
|
||||
const mockObject: Partial<ObjectMetadataEntity> = {
|
||||
id: mockObjectId,
|
||||
isCustom: false,
|
||||
isLabelSyncedWithName: false,
|
||||
labelSingular: 'Default Label',
|
||||
standardOverrides: {
|
||||
labelSingular: 'Custom Label',
|
||||
},
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(objectMetadataService, 'findOneWithinWorkspace')
|
||||
.mockResolvedValue(mockObject as ObjectMetadataEntity);
|
||||
|
||||
const result = await hook.run(
|
||||
instance as UpdateOneInputType<UpdateObjectPayload>,
|
||||
{
|
||||
workspaceId: mockWorkspaceId,
|
||||
locale: undefined,
|
||||
},
|
||||
);
|
||||
|
||||
const expectedResult = {
|
||||
id: mockObjectId,
|
||||
update: {
|
||||
standardOverrides: {
|
||||
labelSingular: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it('should handle description override', async () => {
|
||||
const instance: UpdateOneInputType<UpdateObjectPayloadForTest> = {
|
||||
id: mockObjectId,
|
||||
update: {
|
||||
description: 'New Description',
|
||||
},
|
||||
};
|
||||
|
||||
const mockObject: Partial<ObjectMetadataEntity> = {
|
||||
id: mockObjectId,
|
||||
isCustom: false,
|
||||
description: 'Default Description',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(objectMetadataService, 'findOneWithinWorkspace')
|
||||
.mockResolvedValue(mockObject as ObjectMetadataEntity);
|
||||
|
||||
const result = await hook.run(
|
||||
instance as UpdateOneInputType<UpdateObjectPayload>,
|
||||
{
|
||||
workspaceId: mockWorkspaceId,
|
||||
locale: undefined,
|
||||
},
|
||||
);
|
||||
|
||||
const expectedResult = {
|
||||
id: mockObjectId,
|
||||
update: {
|
||||
standardOverrides: {
|
||||
description: 'New Description',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it('should handle icon override', async () => {
|
||||
const instance: UpdateOneInputType<UpdateObjectPayloadForTest> = {
|
||||
id: mockObjectId,
|
||||
update: {
|
||||
icon: 'IconStar',
|
||||
},
|
||||
};
|
||||
|
||||
const mockObject: Partial<ObjectMetadataEntity> = {
|
||||
id: mockObjectId,
|
||||
isCustom: false,
|
||||
icon: 'IconCircle',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(objectMetadataService, 'findOneWithinWorkspace')
|
||||
.mockResolvedValue(mockObject as ObjectMetadataEntity);
|
||||
|
||||
const result = await hook.run(
|
||||
instance as UpdateOneInputType<UpdateObjectPayload>,
|
||||
{
|
||||
workspaceId: mockWorkspaceId,
|
||||
locale: undefined,
|
||||
},
|
||||
);
|
||||
|
||||
const expectedResult = {
|
||||
id: mockObjectId,
|
||||
update: {
|
||||
standardOverrides: {
|
||||
icon: 'IconStar',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it('should handle locale-specific translations', async () => {
|
||||
const instance: UpdateOneInputType<UpdateObjectPayloadForTest> = {
|
||||
id: mockObjectId,
|
||||
update: {
|
||||
labelSingular: 'Étiquette',
|
||||
labelPlural: 'Étiquettes',
|
||||
description: 'Description en français',
|
||||
},
|
||||
};
|
||||
|
||||
const mockObject: Partial<ObjectMetadataEntity> = {
|
||||
id: mockObjectId,
|
||||
isCustom: false,
|
||||
isLabelSyncedWithName: false,
|
||||
labelSingular: 'Label',
|
||||
labelPlural: 'Labels',
|
||||
description: 'Description',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(objectMetadataService, 'findOneWithinWorkspace')
|
||||
.mockResolvedValue(mockObject as ObjectMetadataEntity);
|
||||
|
||||
const result = await hook.run(
|
||||
instance as UpdateOneInputType<UpdateObjectPayload>,
|
||||
{
|
||||
workspaceId: mockWorkspaceId,
|
||||
locale: 'fr-FR',
|
||||
},
|
||||
);
|
||||
|
||||
const expectedResult = {
|
||||
id: mockObjectId,
|
||||
update: {
|
||||
standardOverrides: {
|
||||
translations: {
|
||||
'fr-FR': {
|
||||
labelSingular: 'Étiquette',
|
||||
labelPlural: 'Étiquettes',
|
||||
description: 'Description en français',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it('should reset locale-specific translations when they match translated defaults', async () => {
|
||||
const translatedLabel = 'mocked-translation';
|
||||
|
||||
const instance: UpdateOneInputType<UpdateObjectPayloadForTest> = {
|
||||
id: mockObjectId,
|
||||
update: {
|
||||
labelSingular: translatedLabel,
|
||||
},
|
||||
};
|
||||
|
||||
const mockObject: Partial<ObjectMetadataEntity> = {
|
||||
id: mockObjectId,
|
||||
isCustom: false,
|
||||
isLabelSyncedWithName: false,
|
||||
labelSingular: 'Label',
|
||||
standardOverrides: {
|
||||
translations: {
|
||||
'fr-FR': {
|
||||
labelSingular: 'Ancienne Étiquette',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(objectMetadataService, 'findOneWithinWorkspace')
|
||||
.mockResolvedValue(mockObject as ObjectMetadataEntity);
|
||||
|
||||
const result = await hook.run(
|
||||
instance as UpdateOneInputType<UpdateObjectPayload>,
|
||||
{
|
||||
workspaceId: mockWorkspaceId,
|
||||
locale: 'fr-FR',
|
||||
},
|
||||
);
|
||||
|
||||
const expectedResult = {
|
||||
id: mockObjectId,
|
||||
update: {
|
||||
standardOverrides: {
|
||||
translations: {
|
||||
'fr-FR': {
|
||||
labelSingular: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it('should handle multiple updates together', async () => {
|
||||
const instance: UpdateOneInputType<UpdateObjectPayloadForTest> = {
|
||||
id: mockObjectId,
|
||||
update: {
|
||||
isActive: false,
|
||||
isLabelSyncedWithName: false,
|
||||
labelSingular: 'New Label',
|
||||
labelPlural: 'New Labels',
|
||||
icon: 'IconStar',
|
||||
description: 'New Description',
|
||||
},
|
||||
};
|
||||
|
||||
const mockObject: Partial<ObjectMetadataEntity> = {
|
||||
id: mockObjectId,
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isLabelSyncedWithName: false,
|
||||
labelSingular: 'Default Label',
|
||||
labelPlural: 'Default Labels',
|
||||
icon: 'IconCircle',
|
||||
description: 'Default Description',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(objectMetadataService, 'findOneWithinWorkspace')
|
||||
.mockResolvedValue(mockObject as ObjectMetadataEntity);
|
||||
|
||||
const result = await hook.run(
|
||||
instance as UpdateOneInputType<UpdateObjectPayload>,
|
||||
{
|
||||
workspaceId: mockWorkspaceId,
|
||||
locale: undefined,
|
||||
},
|
||||
);
|
||||
|
||||
const expectedResult = {
|
||||
id: mockObjectId,
|
||||
update: {
|
||||
isActive: false,
|
||||
isLabelSyncedWithName: false,
|
||||
standardOverrides: {
|
||||
labelSingular: 'New Label',
|
||||
labelPlural: 'New Labels',
|
||||
icon: 'IconStar',
|
||||
description: 'New Description',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it('should validate label identifier field correctly for custom objects', async () => {
|
||||
const labelIdentifierFieldId = 'label-field-id';
|
||||
const instance: UpdateOneInputType<UpdateObjectPayloadForTest> = {
|
||||
id: mockObjectId,
|
||||
update: {
|
||||
labelIdentifierFieldMetadataId: labelIdentifierFieldId,
|
||||
},
|
||||
};
|
||||
|
||||
const mockObject: Partial<ObjectMetadataEntity> = {
|
||||
id: mockObjectId,
|
||||
isCustom: true,
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(objectMetadataService, 'findOneWithinWorkspace')
|
||||
.mockResolvedValue(mockObject as ObjectMetadataEntity);
|
||||
|
||||
jest
|
||||
.spyOn(fieldMetadataRepository, 'findBy')
|
||||
.mockImplementation(async (criteria) => {
|
||||
expect(criteria).toHaveProperty('workspaceId');
|
||||
expect(criteria).toHaveProperty('objectMetadataId');
|
||||
expect(criteria).toHaveProperty('id');
|
||||
|
||||
return [{ id: labelIdentifierFieldId } as FieldMetadataEntity];
|
||||
});
|
||||
|
||||
const result = await hook.run(
|
||||
instance as UpdateOneInputType<UpdateObjectPayload>,
|
||||
{
|
||||
workspaceId: mockWorkspaceId,
|
||||
locale: undefined,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toEqual(instance);
|
||||
});
|
||||
|
||||
it('should validate image identifier field correctly for custom objects', async () => {
|
||||
const imageIdentifierFieldId = 'image-field-id';
|
||||
const instance: UpdateOneInputType<UpdateObjectPayloadForTest> = {
|
||||
id: mockObjectId,
|
||||
update: {
|
||||
imageIdentifierFieldMetadataId: imageIdentifierFieldId,
|
||||
},
|
||||
};
|
||||
|
||||
const mockObject: Partial<ObjectMetadataEntity> = {
|
||||
id: mockObjectId,
|
||||
isCustom: true,
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(objectMetadataService, 'findOneWithinWorkspace')
|
||||
.mockResolvedValue(mockObject as ObjectMetadataEntity);
|
||||
|
||||
jest
|
||||
.spyOn(fieldMetadataRepository, 'findBy')
|
||||
.mockImplementation(async (criteria) => {
|
||||
expect(criteria).toHaveProperty('workspaceId');
|
||||
expect(criteria).toHaveProperty('objectMetadataId');
|
||||
expect(criteria).toHaveProperty('id');
|
||||
|
||||
return [{ id: imageIdentifierFieldId } as FieldMetadataEntity];
|
||||
});
|
||||
|
||||
const result = await hook.run(
|
||||
instance as UpdateOneInputType<UpdateObjectPayload>,
|
||||
{
|
||||
workspaceId: mockWorkspaceId,
|
||||
locale: undefined,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toEqual(instance);
|
||||
});
|
||||
});
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
type BeforeCreateOneHook,
|
||||
type CreateOneInputType,
|
||||
} from '@ptc-org/nestjs-query-graphql';
|
||||
|
||||
import { type CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input';
|
||||
|
||||
@Injectable()
|
||||
export class BeforeCreateOneObject<T extends CreateObjectInput>
|
||||
implements BeforeCreateOneHook<T>
|
||||
{
|
||||
async run(
|
||||
instance: CreateOneInputType<T>,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
context: any,
|
||||
): Promise<CreateOneInputType<T>> {
|
||||
const workspaceId = context?.req?.workspace?.id;
|
||||
|
||||
if (!workspaceId) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
instance.input.workspaceId = workspaceId;
|
||||
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
-50
@@ -1,50 +0,0 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import {
|
||||
type BeforeDeleteOneHook,
|
||||
type DeleteOneInputType,
|
||||
} from '@ptc-org/nestjs-query-graphql';
|
||||
|
||||
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
||||
|
||||
@Injectable()
|
||||
export class BeforeDeleteOneObject implements BeforeDeleteOneHook {
|
||||
constructor(readonly objectMetadataService: ObjectMetadataService) {}
|
||||
|
||||
async run(
|
||||
instance: DeleteOneInputType,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
context: any,
|
||||
): Promise<DeleteOneInputType> {
|
||||
const workspaceId = context?.req?.workspace?.id;
|
||||
|
||||
if (!workspaceId) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
const objectMetadata =
|
||||
await this.objectMetadataService.findOneWithinWorkspace(workspaceId, {
|
||||
where: {
|
||||
id: instance.id.toString(),
|
||||
},
|
||||
});
|
||||
|
||||
if (!objectMetadata) {
|
||||
throw new BadRequestException('Object does not exist');
|
||||
}
|
||||
|
||||
if (!objectMetadata.isCustom) {
|
||||
throw new BadRequestException("Standard Objects can't be deleted");
|
||||
}
|
||||
|
||||
if (objectMetadata.isActive) {
|
||||
throw new BadRequestException("Active objects can't be deleted");
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
-434
@@ -1,434 +0,0 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import {
|
||||
type BeforeUpdateOneHook,
|
||||
type UpdateOneInputType,
|
||||
} from '@ptc-org/nestjs-query-graphql';
|
||||
import { type APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId';
|
||||
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
|
||||
import { type ObjectStandardOverridesDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-standard-overrides.dto';
|
||||
import { type UpdateObjectPayload } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input';
|
||||
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
||||
|
||||
interface StandardObjectUpdate extends Partial<UpdateObjectPayload> {
|
||||
standardOverrides?: ObjectStandardOverridesDTO;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class BeforeUpdateOneObject<T extends UpdateObjectPayload>
|
||||
implements BeforeUpdateOneHook<T>
|
||||
{
|
||||
constructor(
|
||||
readonly objectMetadataService: ObjectMetadataService,
|
||||
private readonly i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
// TODO: this logic could be moved to a policy guard
|
||||
async run(
|
||||
instance: UpdateOneInputType<T>,
|
||||
{
|
||||
workspaceId,
|
||||
locale,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
locale: keyof typeof APP_LOCALES | undefined;
|
||||
},
|
||||
): Promise<UpdateOneInputType<T>> {
|
||||
if (!workspaceId) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
const objectMetadata = await this.getObjectMetadata(instance, workspaceId);
|
||||
|
||||
if (!objectMetadata.isCustom) {
|
||||
return this.handleStandardObjectUpdate(instance, objectMetadata, locale);
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
private async getObjectMetadata(
|
||||
instance: UpdateOneInputType<T>,
|
||||
workspaceId: string,
|
||||
) {
|
||||
const objectMetadata =
|
||||
await this.objectMetadataService.findOneWithinWorkspace(workspaceId, {
|
||||
where: {
|
||||
id: instance.id.toString(),
|
||||
},
|
||||
});
|
||||
|
||||
if (!objectMetadata) {
|
||||
throw new BadRequestException('Object does not exist');
|
||||
}
|
||||
|
||||
return objectMetadata;
|
||||
}
|
||||
|
||||
private handleStandardObjectUpdate(
|
||||
instance: UpdateOneInputType<T>,
|
||||
objectMetadata: ObjectMetadataEntity,
|
||||
locale: keyof typeof APP_LOCALES | undefined,
|
||||
): UpdateOneInputType<T> {
|
||||
const update: StandardObjectUpdate = {};
|
||||
const updatableFields = ['isActive', 'isLabelSyncedWithName'];
|
||||
const overridableFields = [
|
||||
'labelSingular',
|
||||
'labelPlural',
|
||||
'icon',
|
||||
'description',
|
||||
];
|
||||
|
||||
const nonUpdatableFields = Object.keys(instance.update).filter(
|
||||
(key) =>
|
||||
!updatableFields.includes(key) && !overridableFields.includes(key),
|
||||
);
|
||||
|
||||
const isUpdatingLabelsWhenSynced =
|
||||
(instance.update.labelSingular || instance.update.labelPlural) &&
|
||||
objectMetadata.isLabelSyncedWithName &&
|
||||
instance.update.isLabelSyncedWithName !== false &&
|
||||
(instance.update.labelSingular !== objectMetadata.labelSingular ||
|
||||
instance.update.labelPlural !== objectMetadata.labelPlural);
|
||||
|
||||
if (isUpdatingLabelsWhenSynced) {
|
||||
throw new BadRequestException(
|
||||
'Cannot update labels when they are synced with name',
|
||||
);
|
||||
}
|
||||
|
||||
if (nonUpdatableFields.length > 0) {
|
||||
throw new BadRequestException(
|
||||
`Only isActive, isLabelSyncedWithName, labelSingular, labelPlural, icon and description fields can be updated for standard objects. Disallowed fields: ${nonUpdatableFields.join(', ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
// preserve existing overrides
|
||||
update.standardOverrides = objectMetadata.standardOverrides
|
||||
? { ...objectMetadata.standardOverrides }
|
||||
: {};
|
||||
|
||||
this.handleActiveField(instance, update);
|
||||
this.handleLabelSyncedWithNameField(instance, update);
|
||||
this.handleStandardOverrides(instance, objectMetadata, update, locale);
|
||||
|
||||
return {
|
||||
id: instance.id,
|
||||
update: update as T,
|
||||
};
|
||||
}
|
||||
|
||||
private handleActiveField(
|
||||
instance: UpdateOneInputType<T>,
|
||||
update: StandardObjectUpdate,
|
||||
): void {
|
||||
if (!isDefined(instance.update.isActive)) {
|
||||
return;
|
||||
}
|
||||
|
||||
update.isActive = instance.update.isActive;
|
||||
}
|
||||
|
||||
private handleLabelSyncedWithNameField(
|
||||
instance: UpdateOneInputType<T>,
|
||||
update: StandardObjectUpdate,
|
||||
): void {
|
||||
if (!isDefined(instance.update.isLabelSyncedWithName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
update.isLabelSyncedWithName = instance.update.isLabelSyncedWithName;
|
||||
|
||||
if (instance.update.isLabelSyncedWithName === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If setting isLabelSyncedWithName to true, clear label overrides
|
||||
update.standardOverrides = update.standardOverrides || {};
|
||||
update.standardOverrides.labelSingular = null;
|
||||
update.standardOverrides.labelPlural = null;
|
||||
}
|
||||
|
||||
private handleStandardOverrides(
|
||||
instance: UpdateOneInputType<T>,
|
||||
objectMetadata: ObjectMetadataEntity,
|
||||
update: StandardObjectUpdate,
|
||||
locale: keyof typeof APP_LOCALES | undefined,
|
||||
): void {
|
||||
const hasStandardOverrides =
|
||||
isDefined(instance.update.description) ||
|
||||
isDefined(instance.update.icon) ||
|
||||
isDefined(instance.update.labelSingular) ||
|
||||
isDefined(instance.update.labelPlural);
|
||||
|
||||
if (!hasStandardOverrides) {
|
||||
return;
|
||||
}
|
||||
|
||||
update.standardOverrides = update.standardOverrides || {};
|
||||
|
||||
this.handleIconOverride(instance, objectMetadata, update);
|
||||
this.handleDescriptionOverride(instance, objectMetadata, update, locale);
|
||||
this.handleLabelOverrides(instance, objectMetadata, update, locale);
|
||||
}
|
||||
|
||||
private resetOverrideIfMatchesOriginal({
|
||||
update,
|
||||
overrideKey,
|
||||
newValue,
|
||||
originalValue,
|
||||
locale,
|
||||
}: {
|
||||
update: StandardObjectUpdate;
|
||||
overrideKey: 'labelSingular' | 'labelPlural' | 'description' | 'icon';
|
||||
newValue: string;
|
||||
originalValue: string | null;
|
||||
locale?: keyof typeof APP_LOCALES | undefined;
|
||||
}): boolean {
|
||||
if (locale && locale !== SOURCE_LOCALE) {
|
||||
const wasOverrideReset = this.resetLocalizedOverride(
|
||||
update,
|
||||
overrideKey,
|
||||
newValue,
|
||||
originalValue,
|
||||
locale,
|
||||
);
|
||||
|
||||
return wasOverrideReset;
|
||||
}
|
||||
|
||||
const wasOverrideReset = this.resetDefaultOverride(
|
||||
update,
|
||||
overrideKey,
|
||||
newValue,
|
||||
originalValue,
|
||||
);
|
||||
|
||||
return wasOverrideReset;
|
||||
}
|
||||
|
||||
private resetLocalizedOverride(
|
||||
update: StandardObjectUpdate,
|
||||
overrideKey: 'labelSingular' | 'labelPlural' | 'description' | 'icon',
|
||||
newValue: string,
|
||||
originalValue: string | null,
|
||||
locale: keyof typeof APP_LOCALES,
|
||||
): boolean {
|
||||
const messageId = generateMessageId(originalValue ?? '');
|
||||
const i18n = this.i18nService.getI18nInstance(locale);
|
||||
const translatedMessage = i18n._(messageId);
|
||||
|
||||
if (newValue !== translatedMessage) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Initialize the translations structure if needed
|
||||
update.standardOverrides = update.standardOverrides || {};
|
||||
update.standardOverrides.translations =
|
||||
update.standardOverrides.translations || {};
|
||||
update.standardOverrides.translations[locale] =
|
||||
update.standardOverrides.translations[locale] || {};
|
||||
|
||||
// Reset the override by setting it to null
|
||||
const localeTranslations = update.standardOverrides.translations[locale];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(localeTranslations as Record<string, any>)[overrideKey] = null;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private resetDefaultOverride(
|
||||
update: StandardObjectUpdate,
|
||||
overrideKey: 'labelSingular' | 'labelPlural' | 'description' | 'icon',
|
||||
newValue: string,
|
||||
originalValue: string | null,
|
||||
): boolean {
|
||||
if (newValue !== originalValue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
update.standardOverrides = update.standardOverrides || {};
|
||||
update.standardOverrides[overrideKey] = null;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private setOverrideValue(
|
||||
update: StandardObjectUpdate,
|
||||
overrideKey: 'labelSingular' | 'labelPlural' | 'description' | 'icon',
|
||||
value: string,
|
||||
locale?: keyof typeof APP_LOCALES,
|
||||
): void {
|
||||
update.standardOverrides = update.standardOverrides || {};
|
||||
|
||||
const shouldSetLocalizedOverride =
|
||||
locale && locale !== SOURCE_LOCALE && overrideKey !== 'icon';
|
||||
|
||||
if (!shouldSetLocalizedOverride) {
|
||||
update.standardOverrides[overrideKey] = value;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.setLocalizedOverrideValue(update, overrideKey, value, locale);
|
||||
}
|
||||
|
||||
private setLocalizedOverrideValue(
|
||||
update: StandardObjectUpdate,
|
||||
overrideKey: 'labelSingular' | 'labelPlural' | 'description' | 'icon',
|
||||
value: string,
|
||||
locale: keyof typeof APP_LOCALES,
|
||||
): void {
|
||||
update.standardOverrides = update.standardOverrides || {};
|
||||
update.standardOverrides.translations =
|
||||
update.standardOverrides.translations || {};
|
||||
update.standardOverrides.translations[locale] =
|
||||
update.standardOverrides.translations[locale] || {};
|
||||
|
||||
const localeTranslations = update.standardOverrides.translations[locale];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(localeTranslations as Record<string, any>)[overrideKey] = value;
|
||||
}
|
||||
|
||||
private handleDescriptionOverride(
|
||||
instance: UpdateOneInputType<T>,
|
||||
objectMetadata: ObjectMetadataEntity,
|
||||
update: StandardObjectUpdate,
|
||||
locale?: keyof typeof APP_LOCALES,
|
||||
): void {
|
||||
if (!isDefined(instance.update.description)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
this.resetOverrideIfMatchesOriginal({
|
||||
update,
|
||||
overrideKey: 'description',
|
||||
newValue: instance.update.description,
|
||||
originalValue: objectMetadata.description,
|
||||
locale,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setOverrideValue(
|
||||
update,
|
||||
'description',
|
||||
instance.update.description,
|
||||
locale,
|
||||
);
|
||||
}
|
||||
|
||||
private handleIconOverride(
|
||||
instance: UpdateOneInputType<T>,
|
||||
objectMetadata: ObjectMetadataEntity,
|
||||
update: StandardObjectUpdate,
|
||||
): void {
|
||||
if (!isDefined(instance.update.icon)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
this.resetOverrideIfMatchesOriginal({
|
||||
update,
|
||||
overrideKey: 'icon',
|
||||
newValue: instance.update.icon,
|
||||
originalValue: objectMetadata.icon,
|
||||
locale: undefined,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setOverrideValue(update, 'icon', instance.update.icon);
|
||||
}
|
||||
|
||||
private handleLabelOverrides(
|
||||
instance: UpdateOneInputType<T>,
|
||||
objectMetadata: ObjectMetadataEntity,
|
||||
update: StandardObjectUpdate,
|
||||
locale?: keyof typeof APP_LOCALES,
|
||||
): void {
|
||||
// Skip label updates if labels are synced with name or will be synced
|
||||
if (
|
||||
objectMetadata.isLabelSyncedWithName ||
|
||||
update.isLabelSyncedWithName === true
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.handleLabelSingularOverride(instance, objectMetadata, update, locale);
|
||||
this.handleLabelPluralOverride(instance, objectMetadata, update, locale);
|
||||
}
|
||||
|
||||
private handleLabelSingularOverride(
|
||||
instance: UpdateOneInputType<T>,
|
||||
objectMetadata: ObjectMetadataEntity,
|
||||
update: StandardObjectUpdate,
|
||||
locale?: keyof typeof APP_LOCALES,
|
||||
): void {
|
||||
if (!isDefined(instance.update.labelSingular)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
this.resetOverrideIfMatchesOriginal({
|
||||
update,
|
||||
overrideKey: 'labelSingular',
|
||||
newValue: instance.update.labelSingular,
|
||||
originalValue: objectMetadata.labelSingular,
|
||||
locale,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setOverrideValue(
|
||||
update,
|
||||
'labelSingular',
|
||||
instance.update.labelSingular,
|
||||
locale,
|
||||
);
|
||||
}
|
||||
|
||||
private handleLabelPluralOverride(
|
||||
instance: UpdateOneInputType<T>,
|
||||
objectMetadata: ObjectMetadataEntity,
|
||||
update: StandardObjectUpdate,
|
||||
locale?: keyof typeof APP_LOCALES,
|
||||
): void {
|
||||
if (!isDefined(instance.update.labelPlural)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
this.resetOverrideIfMatchesOriginal({
|
||||
update,
|
||||
overrideKey: 'labelPlural',
|
||||
newValue: instance.update.labelPlural,
|
||||
originalValue: objectMetadata.labelPlural,
|
||||
locale,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setOverrideValue(
|
||||
update,
|
||||
'labelPlural',
|
||||
instance.update.labelPlural,
|
||||
locale,
|
||||
);
|
||||
}
|
||||
}
|
||||
+58
-15
@@ -1,6 +1,9 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
|
||||
import { fromArrayToUniqueKeyRecord, isDefined } from 'twenty-shared/utils';
|
||||
import { FindManyOptions, FindOneOptions, Repository } from 'typeorm';
|
||||
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { computeFlatEntityMapsFromTo } from 'src/engine/metadata-modules/flat-entity/utils/compute-flat-entity-maps-from-to.util';
|
||||
@@ -10,15 +13,14 @@ import { FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metada
|
||||
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCreate } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-create-object-input-to-flat-object-metadata-and-flat-field-metadatas-to-create.util';
|
||||
import { fromDeleteObjectInputToFlatFieldMetadatasToDelete } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-delete-object-input-to-flat-field-metadatas-to-delete.util';
|
||||
import { fromFlatObjectMetadataToObjectMetadataDto } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-flat-object-metadata-to-object-metadata-dto.util';
|
||||
import { fromUpdateObjectInputToFlatObjectMetadataAndRelatedFlatEntities } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-update-object-input-to-flat-object-metadata-and-related-flat-entities.util';
|
||||
import { fromCreateViewFieldInputToFlatViewFieldToCreate } from 'src/engine/metadata-modules/flat-view-field/utils/from-create-view-field-input-to-flat-view-field-to-create.util';
|
||||
import { FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type';
|
||||
import { fromCreateViewInputToFlatViewToCreate } from 'src/engine/metadata-modules/flat-view/utils/from-create-view-input-to-flat-view-to-create.util';
|
||||
import { CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input';
|
||||
import { DeleteOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/delete-object.input';
|
||||
import { ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto';
|
||||
import { UpdateOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import {
|
||||
ObjectMetadataException,
|
||||
ObjectMetadataExceptionCode,
|
||||
@@ -33,21 +35,25 @@ import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspa
|
||||
import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity';
|
||||
|
||||
@Injectable()
|
||||
export class ObjectMetadataServiceV2 {
|
||||
export class ObjectMetadataServiceV2 extends TypeOrmQueryService<ObjectMetadataEntity> {
|
||||
constructor(
|
||||
@InjectRepository(ObjectMetadataEntity)
|
||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
||||
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {}
|
||||
) {
|
||||
super(objectMetadataRepository);
|
||||
}
|
||||
|
||||
async updateOne({
|
||||
async updateOneObject({
|
||||
updateObjectInput,
|
||||
workspaceId,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
updateObjectInput: UpdateOneObjectInput;
|
||||
}): Promise<ObjectMetadataDTO> {
|
||||
}): Promise<FlatObjectMetadata> {
|
||||
const {
|
||||
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
|
||||
flatIndexMaps: existingFlatIndexMaps,
|
||||
@@ -154,10 +160,10 @@ export class ObjectMetadataServiceV2 {
|
||||
);
|
||||
}
|
||||
|
||||
return fromFlatObjectMetadataToObjectMetadataDto(updatedFlatObjectMetadata);
|
||||
return updatedFlatObjectMetadata;
|
||||
}
|
||||
|
||||
async deleteOne({
|
||||
async deleteOneObject({
|
||||
deleteObjectInput,
|
||||
workspaceId,
|
||||
isSystemBuild = false,
|
||||
@@ -165,7 +171,7 @@ export class ObjectMetadataServiceV2 {
|
||||
deleteObjectInput: DeleteOneObjectInput;
|
||||
workspaceId: string;
|
||||
isSystemBuild?: boolean;
|
||||
}): Promise<ObjectMetadataDTO> {
|
||||
}): Promise<FlatObjectMetadata> {
|
||||
const deletedObjectMetadataDtos = await this.deleteManyObjectMetadatas({
|
||||
deleteObjectInputs: [deleteObjectInput],
|
||||
workspaceId,
|
||||
@@ -192,7 +198,7 @@ export class ObjectMetadataServiceV2 {
|
||||
deleteObjectInputs: DeleteOneObjectInput[];
|
||||
workspaceId: string;
|
||||
isSystemBuild?: boolean;
|
||||
}): Promise<ObjectMetadataDTO[]> {
|
||||
}): Promise<FlatObjectMetadata[]> {
|
||||
if (deleteObjectInputs.length === 0) {
|
||||
return [];
|
||||
}
|
||||
@@ -311,16 +317,14 @@ export class ObjectMetadataServiceV2 {
|
||||
);
|
||||
}
|
||||
|
||||
return flatObjectMetadatasToDelete.map(
|
||||
fromFlatObjectMetadataToObjectMetadataDto,
|
||||
);
|
||||
return flatObjectMetadatasToDelete;
|
||||
}
|
||||
|
||||
async createOne({
|
||||
async createOneObject({
|
||||
createObjectInput,
|
||||
workspaceId,
|
||||
}: {
|
||||
createObjectInput: Omit<CreateObjectInput, 'workspaceId'>;
|
||||
createObjectInput: CreateObjectInput;
|
||||
workspaceId: string;
|
||||
}): Promise<FlatObjectMetadata> {
|
||||
const {
|
||||
@@ -549,4 +553,43 @@ export class ObjectMetadataServiceV2 {
|
||||
isSystemBuild: true,
|
||||
});
|
||||
}
|
||||
|
||||
public async findOneWithinWorkspace(
|
||||
workspaceId: string,
|
||||
options: FindOneOptions<ObjectMetadataEntity>,
|
||||
): Promise<ObjectMetadataEntity | null> {
|
||||
return this.objectMetadataRepository.findOne({
|
||||
relations: [
|
||||
'fields',
|
||||
'indexMetadatas',
|
||||
'indexMetadatas.indexFieldMetadatas',
|
||||
],
|
||||
...options,
|
||||
where: {
|
||||
...options.where,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public async findManyWithinWorkspace(
|
||||
workspaceId: string,
|
||||
options?: FindManyOptions<ObjectMetadataEntity>,
|
||||
) {
|
||||
return this.objectMetadataRepository.find({
|
||||
relations: [
|
||||
'fields.object',
|
||||
'fields',
|
||||
'fields.relationTargetObjectMetadata',
|
||||
],
|
||||
...options,
|
||||
where: {
|
||||
...options?.where,
|
||||
workspaceId,
|
||||
},
|
||||
order: {
|
||||
...options?.order,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+4
-23
@@ -11,7 +11,6 @@ import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
|
||||
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
@@ -21,20 +20,13 @@ import { IndexMetadataModule } from 'src/engine/metadata-modules/index-metadata/
|
||||
import { CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input';
|
||||
import { ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto';
|
||||
import { UpdateObjectPayload } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input';
|
||||
import { BeforeUpdateOneObject } from 'src/engine/metadata-modules/object-metadata/hooks/before-update-one-object.hook';
|
||||
import { ObjectMetadataGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/object-metadata/interceptors/object-metadata-graphql-api-exception.interceptor';
|
||||
import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ObjectMetadataResolver } from 'src/engine/metadata-modules/object-metadata/object-metadata.resolver';
|
||||
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
||||
import { ObjectMetadataFieldRelationService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-field-relation.service';
|
||||
import { ObjectMetadataMigrationService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-migration.service';
|
||||
import { ObjectMetadataRelatedRecordsService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-related-records.service';
|
||||
import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
|
||||
import { RemoteTableRelationsModule } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/remote-table-relations.module';
|
||||
import { SearchVectorModule } from 'src/engine/metadata-modules/search-vector/search-vector.module';
|
||||
import { ViewFieldModule } from 'src/engine/metadata-modules/view-field/view-field.module';
|
||||
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
|
||||
import { ViewModule } from 'src/engine/metadata-modules/view/view.module';
|
||||
@@ -63,7 +55,6 @@ import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspa
|
||||
WorkspaceMigrationRunnerModule,
|
||||
WorkspaceMetadataVersionModule,
|
||||
RemoteTableRelationsModule,
|
||||
SearchVectorModule,
|
||||
IndexMetadataModule,
|
||||
PermissionsModule,
|
||||
WorkspacePermissionsCacheModule,
|
||||
@@ -77,13 +68,9 @@ import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspa
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||
],
|
||||
services: [
|
||||
ObjectMetadataService,
|
||||
ObjectMetadataServiceV2,
|
||||
FlatFieldMetadataValidatorService,
|
||||
FlatFieldMetadataTypeValidatorService,
|
||||
ObjectMetadataMigrationService,
|
||||
ObjectMetadataFieldRelationService,
|
||||
ObjectMetadataRelatedRecordsService,
|
||||
],
|
||||
resolvers: [
|
||||
{
|
||||
@@ -91,14 +78,13 @@ import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspa
|
||||
DTOClass: ObjectMetadataDTO,
|
||||
CreateDTOClass: CreateObjectInput,
|
||||
UpdateDTOClass: UpdateObjectPayload,
|
||||
ServiceClass: ObjectMetadataService,
|
||||
ServiceClass: ObjectMetadataServiceV2,
|
||||
pagingStrategy: PagingStrategies.CURSOR,
|
||||
read: {
|
||||
defaultSort: [{ field: 'id', direction: SortDirection.DESC }],
|
||||
},
|
||||
create: {
|
||||
many: { disabled: true },
|
||||
guards: [SettingsPermissionGuard(PermissionFlagType.DATA_MODEL)],
|
||||
disabled: true,
|
||||
},
|
||||
update: { disabled: true },
|
||||
delete: { disabled: true },
|
||||
@@ -109,12 +95,7 @@ import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspa
|
||||
],
|
||||
}),
|
||||
],
|
||||
providers: [
|
||||
ObjectMetadataService,
|
||||
ObjectMetadataServiceV2,
|
||||
ObjectMetadataResolver,
|
||||
BeforeUpdateOneObject,
|
||||
],
|
||||
exports: [ObjectMetadataService, ObjectMetadataServiceV2],
|
||||
providers: [ObjectMetadataServiceV2, ObjectMetadataResolver],
|
||||
exports: [ObjectMetadataServiceV2],
|
||||
})
|
||||
export class ObjectMetadataModule {}
|
||||
|
||||
+38
-45
@@ -8,8 +8,6 @@ import {
|
||||
Resolver,
|
||||
} from '@nestjs/graphql';
|
||||
|
||||
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 { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
|
||||
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
|
||||
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
|
||||
@@ -20,16 +18,13 @@ import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorat
|
||||
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto';
|
||||
import { fromFlatObjectMetadataToObjectMetadataDto } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-flat-object-metadata-to-object-metadata-dto.util';
|
||||
import { IndexMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-metadata.dto';
|
||||
import { CreateOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input';
|
||||
import { DeleteOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/delete-object.input';
|
||||
import { ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto';
|
||||
import {
|
||||
UpdateOneObjectInput,
|
||||
type UpdateObjectPayload,
|
||||
} from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input';
|
||||
import { BeforeUpdateOneObject } from 'src/engine/metadata-modules/object-metadata/hooks/before-update-one-object.hook';
|
||||
import { UpdateOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input';
|
||||
import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service';
|
||||
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
||||
import { objectMetadataGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/object-metadata/utils/object-metadata-graphql-api-exception-handler.util';
|
||||
import { resolveObjectMetadataStandardOverride } from 'src/engine/metadata-modules/object-metadata/utils/resolve-object-metadata-standard-override.util';
|
||||
import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants';
|
||||
@@ -44,9 +39,6 @@ import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-module
|
||||
)
|
||||
export class ObjectMetadataResolver {
|
||||
constructor(
|
||||
private readonly objectMetadataService: ObjectMetadataService,
|
||||
private readonly beforeUpdateOneObject: BeforeUpdateOneObject<UpdateObjectPayload>,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
private readonly objectMetadataServiceV2: ObjectMetadataServiceV2,
|
||||
private readonly i18nService: I18nService,
|
||||
) {}
|
||||
@@ -114,16 +106,42 @@ export class ObjectMetadataResolver {
|
||||
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.DATA_MODEL))
|
||||
@Mutation(() => ObjectMetadataDTO)
|
||||
async deleteOneObject(
|
||||
@Args('input') input: DeleteOneObjectInput,
|
||||
async createOneObject(
|
||||
@Args('input') input: CreateOneObjectInput,
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
@Context() context: I18nContext,
|
||||
) {
|
||||
try {
|
||||
return await this.objectMetadataService.deleteOneObject(
|
||||
input,
|
||||
workspaceId,
|
||||
const flatobjectMetadata =
|
||||
await this.objectMetadataServiceV2.createOneObject({
|
||||
createObjectInput: input.object,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
return fromFlatObjectMetadataToObjectMetadataDto(flatobjectMetadata);
|
||||
} catch (error) {
|
||||
objectMetadataGraphqlApiExceptionHandler(
|
||||
error,
|
||||
this.i18nService.getI18nInstance(context.req.locale),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.DATA_MODEL))
|
||||
@Mutation(() => ObjectMetadataDTO)
|
||||
async deleteOneObject(
|
||||
@Args('input') deleteObjectInput: DeleteOneObjectInput,
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
@Context() context: I18nContext,
|
||||
) {
|
||||
try {
|
||||
const flatobjectMetadata =
|
||||
await this.objectMetadataServiceV2.deleteOneObject({
|
||||
deleteObjectInput,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
return fromFlatObjectMetadataToObjectMetadataDto(flatobjectMetadata);
|
||||
} catch (error) {
|
||||
objectMetadataGraphqlApiExceptionHandler(
|
||||
error,
|
||||
@@ -139,39 +157,14 @@ export class ObjectMetadataResolver {
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
@Context() context: I18nContext,
|
||||
) {
|
||||
const isWorkspaceMigrationV2Enabled =
|
||||
await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (isWorkspaceMigrationV2Enabled) {
|
||||
try {
|
||||
return await this.objectMetadataServiceV2.updateOne({
|
||||
try {
|
||||
const flatobjectMetadata =
|
||||
await this.objectMetadataServiceV2.updateOneObject({
|
||||
updateObjectInput,
|
||||
workspaceId,
|
||||
});
|
||||
} catch (error) {
|
||||
objectMetadataGraphqlApiExceptionHandler(
|
||||
error,
|
||||
this.i18nService.getI18nInstance(context.req.locale),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const updatedInput = (await this.beforeUpdateOneObject.run(
|
||||
updateObjectInput,
|
||||
{
|
||||
workspaceId,
|
||||
locale: context.req.locale,
|
||||
},
|
||||
)) as UpdateOneObjectInput;
|
||||
|
||||
return await this.objectMetadataService.updateOneObject(
|
||||
updatedInput,
|
||||
workspaceId,
|
||||
);
|
||||
return fromFlatObjectMetadataToObjectMetadataDto(flatobjectMetadata);
|
||||
} catch (error) {
|
||||
objectMetadataGraphqlApiExceptionHandler(
|
||||
error,
|
||||
|
||||
-792
@@ -1,792 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { type Query, type QueryOptions } from '@ptc-org/nestjs-query-core';
|
||||
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
DataSource,
|
||||
In,
|
||||
Repository,
|
||||
type FindManyOptions,
|
||||
type FindOneOptions,
|
||||
type QueryRunner,
|
||||
} from 'typeorm';
|
||||
|
||||
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 { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { IndexMetadataService } from 'src/engine/metadata-modules/index-metadata/index-metadata.service';
|
||||
import { type DeleteOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/delete-object.input';
|
||||
import {
|
||||
type UpdateObjectPayload,
|
||||
type UpdateOneObjectInput,
|
||||
} from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input';
|
||||
import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service';
|
||||
import {
|
||||
ObjectMetadataException,
|
||||
ObjectMetadataExceptionCode,
|
||||
} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
|
||||
import { ObjectMetadataFieldRelationService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-field-relation.service';
|
||||
import { ObjectMetadataMigrationService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-migration.service';
|
||||
import { ObjectMetadataRelatedRecordsService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-related-records.service';
|
||||
import { buildDefaultFieldsForCustomObject } from 'src/engine/metadata-modules/object-metadata/utils/build-default-fields-for-custom-object.util';
|
||||
import {
|
||||
validateLowerCasedAndTrimmedStringsAreDifferentOrThrow,
|
||||
validateObjectMetadataInputLabelsOrThrow,
|
||||
validateObjectMetadataInputNamesOrThrow,
|
||||
} from 'src/engine/metadata-modules/object-metadata/utils/validate-object-metadata-input.util';
|
||||
import { SearchVectorService } from 'src/engine/metadata-modules/search-vector/search-vector.service';
|
||||
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
import { validateMetadataIdentifierFieldMetadataIds } from 'src/engine/metadata-modules/utils/validate-metadata-identifier-field-metadata-id.utils';
|
||||
import { validateNameAndLabelAreSyncOrThrow } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util';
|
||||
import { validatesNoOtherObjectWithSameNameExistsOrThrows } from 'src/engine/metadata-modules/utils/validate-no-other-object-with-same-name-exists-or-throw.util';
|
||||
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
|
||||
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
|
||||
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
|
||||
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
||||
import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
|
||||
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service';
|
||||
import { CUSTOM_OBJECT_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||
import { isSearchableFieldType } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/is-searchable-field.util';
|
||||
|
||||
import { ObjectMetadataEntity } from './object-metadata.entity';
|
||||
|
||||
import { type CreateObjectInput } from './dtos/create-object.input';
|
||||
|
||||
@Injectable()
|
||||
export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEntity> {
|
||||
constructor(
|
||||
@InjectRepository(ObjectMetadataEntity)
|
||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||
@InjectRepository(FieldMetadataEntity)
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
|
||||
private readonly dataSourceService: DataSourceService,
|
||||
private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService,
|
||||
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
|
||||
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
|
||||
private readonly searchVectorService: SearchVectorService,
|
||||
private readonly objectMetadataFieldRelationService: ObjectMetadataFieldRelationService,
|
||||
private readonly objectMetadataMigrationService: ObjectMetadataMigrationService,
|
||||
private readonly objectMetadataRelatedRecordsService: ObjectMetadataRelatedRecordsService,
|
||||
private readonly indexMetadataService: IndexMetadataService,
|
||||
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
|
||||
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
@InjectDataSource()
|
||||
private readonly coreDataSource: DataSource,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
private readonly objectMetadataServiceV2: ObjectMetadataServiceV2,
|
||||
) {
|
||||
super(objectMetadataRepository);
|
||||
}
|
||||
|
||||
override async query(
|
||||
query: Query<ObjectMetadataEntity>,
|
||||
opts?: QueryOptions<ObjectMetadataEntity> | undefined,
|
||||
): Promise<ObjectMetadataEntity[]> {
|
||||
const start = performance.now();
|
||||
|
||||
const result = super.query(query, opts);
|
||||
|
||||
const end = performance.now();
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`metadata query time: ${end - start} ms`);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
override async createOne(
|
||||
createObjectInput: CreateObjectInput,
|
||||
): Promise<ObjectMetadataEntity> {
|
||||
const { workspaceId } = createObjectInput;
|
||||
const isWorkspaceMigrationV2Enabled =
|
||||
await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (isWorkspaceMigrationV2Enabled) {
|
||||
const flatObjectMetadata = await this.objectMetadataServiceV2.createOne({
|
||||
createObjectInput,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
const createdObjectMetadata = await this.objectMetadataRepository.findOne(
|
||||
{
|
||||
where: {
|
||||
id: flatObjectMetadata.id,
|
||||
workspaceId,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!isDefined(createdObjectMetadata)) {
|
||||
throw new ObjectMetadataException(
|
||||
'Created object metadata not found',
|
||||
ObjectMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
return createdObjectMetadata;
|
||||
}
|
||||
|
||||
const queryRunner = this.coreDataSource.createQueryRunner();
|
||||
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
const objectMetadataRepository =
|
||||
queryRunner.manager.getRepository(ObjectMetadataEntity);
|
||||
|
||||
const { objectMetadataMaps } =
|
||||
await this.workspaceMetadataCacheService.getExistingOrRecomputeMetadataMaps(
|
||||
{
|
||||
workspaceId,
|
||||
},
|
||||
);
|
||||
|
||||
const lastDataSourceMetadata =
|
||||
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
createObjectInput.labelSingular = capitalize(
|
||||
createObjectInput.labelSingular,
|
||||
);
|
||||
createObjectInput.labelPlural = capitalize(createObjectInput.labelPlural);
|
||||
|
||||
validateObjectMetadataInputNamesOrThrow(createObjectInput);
|
||||
validateObjectMetadataInputLabelsOrThrow(createObjectInput);
|
||||
|
||||
validateLowerCasedAndTrimmedStringsAreDifferentOrThrow({
|
||||
inputs: [createObjectInput.nameSingular, createObjectInput.namePlural],
|
||||
message:
|
||||
'The singular and plural names cannot be the same for an object',
|
||||
});
|
||||
validateLowerCasedAndTrimmedStringsAreDifferentOrThrow({
|
||||
inputs: [
|
||||
createObjectInput.labelPlural,
|
||||
createObjectInput.labelSingular,
|
||||
],
|
||||
message:
|
||||
'The singular and plural labels cannot be the same for an object',
|
||||
});
|
||||
|
||||
if (createObjectInput.isLabelSyncedWithName === true) {
|
||||
validateNameAndLabelAreSyncOrThrow({
|
||||
label: createObjectInput.labelSingular,
|
||||
name: createObjectInput.nameSingular,
|
||||
});
|
||||
validateNameAndLabelAreSyncOrThrow({
|
||||
label: createObjectInput.labelPlural,
|
||||
name: createObjectInput.namePlural,
|
||||
});
|
||||
}
|
||||
|
||||
validatesNoOtherObjectWithSameNameExistsOrThrows({
|
||||
objectMetadataNamePlural: createObjectInput.namePlural,
|
||||
objectMetadataNameSingular: createObjectInput.nameSingular,
|
||||
objectMetadataMaps,
|
||||
});
|
||||
|
||||
const baseCustomFields = buildDefaultFieldsForCustomObject(workspaceId);
|
||||
|
||||
const labelIdentifierFieldMetadataId = baseCustomFields.find(
|
||||
(field) => field.standardId === CUSTOM_OBJECT_STANDARD_FIELD_IDS.name,
|
||||
)?.id;
|
||||
|
||||
if (!isDefined(labelIdentifierFieldMetadataId)) {
|
||||
throw new ObjectMetadataException(
|
||||
'Label identifier field metadata not created properly',
|
||||
ObjectMetadataExceptionCode.MISSING_CUSTOM_OBJECT_DEFAULT_LABEL_IDENTIFIER_FIELD,
|
||||
);
|
||||
}
|
||||
|
||||
const createdObjectMetadata = await objectMetadataRepository.save({
|
||||
...createObjectInput,
|
||||
dataSourceId: lastDataSourceMetadata.id,
|
||||
targetTableName: 'DEPRECATED',
|
||||
isActive: true,
|
||||
isCustom: !createObjectInput.isRemote,
|
||||
isSystem: false,
|
||||
isRemote: createObjectInput.isRemote,
|
||||
isSearchable: !createObjectInput.isRemote,
|
||||
fields: createObjectInput.isRemote ? [] : baseCustomFields,
|
||||
labelIdentifierFieldMetadataId,
|
||||
});
|
||||
|
||||
if (createObjectInput.isRemote) {
|
||||
throw new Error('Remote objects are not supported yet');
|
||||
} else {
|
||||
const fieldsById = createdObjectMetadata.fields.reduce(
|
||||
(acc, field) => ({
|
||||
...acc,
|
||||
[field.id]: field,
|
||||
}),
|
||||
{},
|
||||
);
|
||||
|
||||
const createdRelatedObjectMetadataCollection =
|
||||
await this.objectMetadataFieldRelationService.createRelationsAndForeignKeysMetadata(
|
||||
workspaceId,
|
||||
{ ...createdObjectMetadata, fieldsById },
|
||||
objectMetadataMaps,
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
await this.objectMetadataMigrationService.createTableMigration(
|
||||
createdObjectMetadata,
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
await this.objectMetadataMigrationService.createColumnsMigrations(
|
||||
createdObjectMetadata,
|
||||
createdObjectMetadata.fields,
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
await this.objectMetadataMigrationService.createRelationMigrations(
|
||||
createdObjectMetadata,
|
||||
createdRelatedObjectMetadataCollection,
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
await this.searchVectorService.createSearchVectorFieldForObject(
|
||||
createObjectInput,
|
||||
createdObjectMetadata,
|
||||
queryRunner,
|
||||
);
|
||||
}
|
||||
|
||||
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrationsWithinTransaction(
|
||||
createdObjectMetadata.workspaceId,
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
|
||||
// After commit, do non-transactional work
|
||||
await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache(
|
||||
{
|
||||
workspaceId,
|
||||
},
|
||||
);
|
||||
await this.objectMetadataRelatedRecordsService.createObjectRelatedRecords(
|
||||
createdObjectMetadata,
|
||||
);
|
||||
|
||||
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
await this.flatEntityMapsCacheService.invalidateFlatEntityMaps({
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatFieldMetadataMaps', 'flatObjectMetadataMaps'],
|
||||
});
|
||||
|
||||
return createdObjectMetadata;
|
||||
} catch (error) {
|
||||
if (queryRunner.isTransactionActive) {
|
||||
try {
|
||||
await queryRunner.rollbackTransaction();
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.trace(`Failed to rollback transaction: ${error.message}`);
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
public async updateOneObject(
|
||||
updateObjectInput: UpdateOneObjectInput,
|
||||
workspaceId: string,
|
||||
): Promise<ObjectMetadataEntity> {
|
||||
const queryRunner = this.coreDataSource.createQueryRunner();
|
||||
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
const objectMetadataRepository =
|
||||
queryRunner.manager.getRepository(ObjectMetadataEntity);
|
||||
|
||||
const { objectMetadataMaps } =
|
||||
await this.workspaceMetadataCacheService.getExistingOrRecomputeMetadataMaps(
|
||||
{ workspaceId },
|
||||
);
|
||||
const inputId = updateObjectInput.id;
|
||||
const inputPayload = {
|
||||
...updateObjectInput.update,
|
||||
...(isDefined(updateObjectInput.update.labelSingular)
|
||||
? {
|
||||
labelSingular: capitalize(updateObjectInput.update.labelSingular),
|
||||
}
|
||||
: {}),
|
||||
...(isDefined(updateObjectInput.update.labelPlural)
|
||||
? { labelPlural: capitalize(updateObjectInput.update.labelPlural) }
|
||||
: {}),
|
||||
};
|
||||
|
||||
validateObjectMetadataInputNamesOrThrow(inputPayload);
|
||||
const existingObjectMetadata = objectMetadataMaps.byId[inputId];
|
||||
|
||||
if (!existingObjectMetadata) {
|
||||
throw new ObjectMetadataException(
|
||||
'Object does not exist',
|
||||
ObjectMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
const existingObjectMetadataCombinedWithUpdateInput = {
|
||||
...existingObjectMetadata,
|
||||
...inputPayload,
|
||||
};
|
||||
|
||||
validatesNoOtherObjectWithSameNameExistsOrThrows({
|
||||
objectMetadataNameSingular:
|
||||
existingObjectMetadataCombinedWithUpdateInput.nameSingular,
|
||||
objectMetadataNamePlural:
|
||||
existingObjectMetadataCombinedWithUpdateInput.namePlural,
|
||||
existingObjectMetadataId:
|
||||
existingObjectMetadataCombinedWithUpdateInput.id,
|
||||
objectMetadataMaps,
|
||||
});
|
||||
if (existingObjectMetadataCombinedWithUpdateInput.isLabelSyncedWithName) {
|
||||
validateNameAndLabelAreSyncOrThrow({
|
||||
label: existingObjectMetadataCombinedWithUpdateInput.labelSingular,
|
||||
name: existingObjectMetadataCombinedWithUpdateInput.nameSingular,
|
||||
});
|
||||
validateNameAndLabelAreSyncOrThrow({
|
||||
label: existingObjectMetadataCombinedWithUpdateInput.labelPlural,
|
||||
name: existingObjectMetadataCombinedWithUpdateInput.namePlural,
|
||||
});
|
||||
}
|
||||
if (
|
||||
isDefined(inputPayload.nameSingular) ||
|
||||
isDefined(inputPayload.namePlural)
|
||||
) {
|
||||
validateLowerCasedAndTrimmedStringsAreDifferentOrThrow({
|
||||
inputs: [
|
||||
existingObjectMetadataCombinedWithUpdateInput.nameSingular,
|
||||
existingObjectMetadataCombinedWithUpdateInput.namePlural,
|
||||
],
|
||||
message:
|
||||
'The singular and plural names cannot be the same for an object',
|
||||
});
|
||||
}
|
||||
validateMetadataIdentifierFieldMetadataIds({
|
||||
fieldMetadataItems: Object.values(existingObjectMetadata.fieldsById),
|
||||
labelIdentifierFieldMetadataId:
|
||||
inputPayload.labelIdentifierFieldMetadataId,
|
||||
imageIdentifierFieldMetadataId:
|
||||
inputPayload.imageIdentifierFieldMetadataId,
|
||||
});
|
||||
const updatedObject = await objectMetadataRepository.save({
|
||||
...existingObjectMetadata,
|
||||
...inputPayload,
|
||||
});
|
||||
|
||||
const { didUpdateLabelOrIcon } =
|
||||
await this.handleObjectNameAndLabelUpdates({
|
||||
existingObjectMetadata,
|
||||
objectMetadataForUpdate:
|
||||
existingObjectMetadataCombinedWithUpdateInput,
|
||||
inputPayload,
|
||||
queryRunner,
|
||||
objectMetadataMaps,
|
||||
});
|
||||
|
||||
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrationsWithinTransaction(
|
||||
workspaceId,
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
if (inputPayload.labelIdentifierFieldMetadataId) {
|
||||
const labelIdentifierFieldMetadata =
|
||||
existingObjectMetadata.fieldsById[
|
||||
inputPayload.labelIdentifierFieldMetadataId
|
||||
];
|
||||
|
||||
if (isSearchableFieldType(labelIdentifierFieldMetadata.type)) {
|
||||
await this.searchVectorService.updateSearchVector(
|
||||
inputId,
|
||||
[
|
||||
{
|
||||
name: labelIdentifierFieldMetadata.name,
|
||||
type: labelIdentifierFieldMetadata.type,
|
||||
},
|
||||
],
|
||||
workspaceId,
|
||||
queryRunner,
|
||||
);
|
||||
}
|
||||
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrationsWithinTransaction(
|
||||
workspaceId,
|
||||
queryRunner,
|
||||
);
|
||||
}
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
|
||||
// After commit, do non-transactional work
|
||||
await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache(
|
||||
{
|
||||
workspaceId,
|
||||
},
|
||||
);
|
||||
|
||||
if (didUpdateLabelOrIcon) {
|
||||
await this.objectMetadataRelatedRecordsService.updateObjectViews(
|
||||
updatedObject,
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
isDefined(inputPayload.labelIdentifierFieldMetadataId) &&
|
||||
inputPayload.labelIdentifierFieldMetadataId !==
|
||||
existingObjectMetadata.labelIdentifierFieldMetadataId
|
||||
) {
|
||||
const labelIdentifierFieldMetadata =
|
||||
existingObjectMetadata.fieldsById[
|
||||
inputPayload.labelIdentifierFieldMetadataId
|
||||
];
|
||||
|
||||
await this.objectMetadataRelatedRecordsService.updateLabelMetadataIdentifierInObjectViews(
|
||||
{
|
||||
newLabelMetadataIdentifierFieldMetadata:
|
||||
labelIdentifierFieldMetadata,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
await this.flatEntityMapsCacheService.invalidateFlatEntityMaps({
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatObjectMetadataMaps', 'flatFieldMetadataMaps'],
|
||||
});
|
||||
|
||||
const formattedUpdatedObject = {
|
||||
...updatedObject,
|
||||
createdAt: new Date(updatedObject.createdAt),
|
||||
updatedAt: new Date(updatedObject.updatedAt),
|
||||
};
|
||||
|
||||
return formattedUpdatedObject;
|
||||
} catch (error) {
|
||||
if (queryRunner.isTransactionActive) {
|
||||
try {
|
||||
await queryRunner.rollbackTransaction();
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.trace(`Failed to rollback transaction: ${error.message}`);
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteOneObject(
|
||||
deleteObjectInput: DeleteOneObjectInput,
|
||||
workspaceId: string,
|
||||
): Promise<Partial<ObjectMetadataEntity>> {
|
||||
const isWorkspaceMigrationV2Enabled =
|
||||
await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (isWorkspaceMigrationV2Enabled) {
|
||||
return await this.objectMetadataServiceV2.deleteOne({
|
||||
deleteObjectInput,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
const queryRunner = this.coreDataSource.createQueryRunner();
|
||||
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
const objectMetadataRepository =
|
||||
queryRunner.manager.getRepository(ObjectMetadataEntity);
|
||||
const fieldMetadataRepository =
|
||||
queryRunner.manager.getRepository(FieldMetadataEntity);
|
||||
|
||||
const objectMetadata = await objectMetadataRepository.findOne({
|
||||
relations: [
|
||||
'fields',
|
||||
'fields.object',
|
||||
'fields.relationTargetFieldMetadata',
|
||||
'fields.relationTargetFieldMetadata.object',
|
||||
],
|
||||
where: {
|
||||
id: deleteObjectInput.id,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!objectMetadata) {
|
||||
throw new ObjectMetadataException(
|
||||
'Object does not exist',
|
||||
ObjectMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
if (objectMetadata.isRemote) {
|
||||
throw new ObjectMetadataException(
|
||||
'Remote objects are not supported yet',
|
||||
ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
|
||||
);
|
||||
} else {
|
||||
await this.objectMetadataMigrationService.deleteAllRelationsAndDropTable(
|
||||
objectMetadata,
|
||||
workspaceId,
|
||||
queryRunner,
|
||||
);
|
||||
}
|
||||
|
||||
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrationsWithinTransaction(
|
||||
workspaceId,
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
const fieldMetadataIds = objectMetadata.fields.map((field) => field.id);
|
||||
const relationMetadataIds = objectMetadata.fields.flatMap((field) => {
|
||||
if (
|
||||
isFieldMetadataEntityOfType(field, FieldMetadataType.MORPH_RELATION)
|
||||
) {
|
||||
return field.relationTargetFieldMetadata.id;
|
||||
}
|
||||
|
||||
return [];
|
||||
});
|
||||
|
||||
await fieldMetadataRepository.delete({
|
||||
id: In(fieldMetadataIds.concat(relationMetadataIds)),
|
||||
});
|
||||
|
||||
await objectMetadataRepository.delete(objectMetadata.id);
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
|
||||
// After commit, do non-transactional work
|
||||
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
await this.flatEntityMapsCacheService.invalidateFlatEntityMaps({
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatObjectMetadataMaps', 'flatFieldMetadataMaps'],
|
||||
});
|
||||
|
||||
await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache(
|
||||
{
|
||||
workspaceId,
|
||||
},
|
||||
);
|
||||
|
||||
await this.objectMetadataRelatedRecordsService.deleteObjectViews(
|
||||
objectMetadata,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
return objectMetadata;
|
||||
} catch (error) {
|
||||
if (queryRunner.isTransactionActive) {
|
||||
try {
|
||||
await queryRunner.rollbackTransaction();
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.trace(`Failed to rollback transaction: ${error.message}`);
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
public async findOneWithinWorkspace(
|
||||
workspaceId: string,
|
||||
options: FindOneOptions<ObjectMetadataEntity>,
|
||||
): Promise<ObjectMetadataEntity | null> {
|
||||
return this.objectMetadataRepository.findOne({
|
||||
relations: [
|
||||
'fields',
|
||||
'indexMetadatas',
|
||||
'indexMetadatas.indexFieldMetadatas',
|
||||
],
|
||||
...options,
|
||||
where: {
|
||||
...options.where,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public async findManyWithinWorkspace(
|
||||
workspaceId: string,
|
||||
options?: FindManyOptions<ObjectMetadataEntity>,
|
||||
) {
|
||||
return this.objectMetadataRepository.find({
|
||||
relations: [
|
||||
'fields.object',
|
||||
'fields',
|
||||
'fields.relationTargetObjectMetadata',
|
||||
],
|
||||
...options,
|
||||
where: {
|
||||
...options?.where,
|
||||
workspaceId,
|
||||
},
|
||||
order: {
|
||||
...options?.order,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public async deleteObjectsMetadata(workspaceId: string) {
|
||||
const objectsMetadata = await this.objectMetadataRepository.find({
|
||||
where: {
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
await this.fieldMetadataRepository.delete({
|
||||
workspaceId,
|
||||
type: In([FieldMetadataType.MORPH_RELATION, FieldMetadataType.RELATION]),
|
||||
});
|
||||
|
||||
for (const objectMetadata of objectsMetadata) {
|
||||
await this.objectMetadataRepository.delete({
|
||||
id: objectMetadata.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async handleObjectNameAndLabelUpdates({
|
||||
existingObjectMetadata,
|
||||
objectMetadataForUpdate,
|
||||
inputPayload,
|
||||
queryRunner,
|
||||
objectMetadataMaps,
|
||||
}: {
|
||||
existingObjectMetadata: Pick<
|
||||
ObjectMetadataItemWithFieldMaps,
|
||||
'nameSingular' | 'isCustom' | 'id' | 'labelPlural' | 'icon' | 'fieldsById'
|
||||
>;
|
||||
objectMetadataForUpdate: Pick<
|
||||
ObjectMetadataItemWithFieldMaps,
|
||||
| 'nameSingular'
|
||||
| 'isCustom'
|
||||
| 'workspaceId'
|
||||
| 'id'
|
||||
| 'labelSingular'
|
||||
| 'labelPlural'
|
||||
| 'icon'
|
||||
| 'fieldsById'
|
||||
>;
|
||||
inputPayload: UpdateObjectPayload;
|
||||
queryRunner: QueryRunner;
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
}): Promise<{ didUpdateLabelOrIcon: boolean }> {
|
||||
const newTargetTableName = computeObjectTargetTable(
|
||||
objectMetadataForUpdate,
|
||||
);
|
||||
const existingTargetTableName = computeObjectTargetTable(
|
||||
existingObjectMetadata,
|
||||
);
|
||||
|
||||
if (newTargetTableName !== existingTargetTableName) {
|
||||
await this.objectMetadataMigrationService.createRenameTableMigration(
|
||||
existingObjectMetadata,
|
||||
objectMetadataForUpdate,
|
||||
objectMetadataForUpdate.workspaceId,
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
const relationMetadataCollection =
|
||||
await this.objectMetadataFieldRelationService.updateRelationsAndForeignKeysMetadata(
|
||||
{
|
||||
workspaceId: objectMetadataForUpdate.workspaceId,
|
||||
updatedObjectMetadata: objectMetadataForUpdate,
|
||||
queryRunner,
|
||||
objectMetadataMaps,
|
||||
},
|
||||
);
|
||||
|
||||
await this.objectMetadataMigrationService.updateRelationMigrations(
|
||||
existingObjectMetadata,
|
||||
objectMetadataForUpdate,
|
||||
relationMetadataCollection,
|
||||
objectMetadataForUpdate.workspaceId,
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
const morphRelationFieldMetadataToUpdate =
|
||||
await this.objectMetadataFieldRelationService.updateMorphRelationsJoinColumnName(
|
||||
{
|
||||
existingObjectMetadata,
|
||||
objectMetadataForUpdate,
|
||||
queryRunner,
|
||||
},
|
||||
);
|
||||
|
||||
await this.objectMetadataMigrationService.updateMorphRelationMigrations({
|
||||
workspaceId: objectMetadataForUpdate.workspaceId,
|
||||
morphRelationFieldMetadataToUpdate: morphRelationFieldMetadataToUpdate,
|
||||
queryRunner,
|
||||
});
|
||||
|
||||
await this.objectMetadataMigrationService.recomputeEnumNames(
|
||||
objectMetadataForUpdate,
|
||||
objectMetadataForUpdate.workspaceId,
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
const recomputedIndexes =
|
||||
await this.indexMetadataService.recomputeIndexMetadataForObject(
|
||||
objectMetadataForUpdate.workspaceId,
|
||||
objectMetadataForUpdate,
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
await this.indexMetadataService.createIndexRecomputeMigrations(
|
||||
objectMetadataForUpdate.workspaceId,
|
||||
objectMetadataForUpdate,
|
||||
recomputedIndexes,
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
if (
|
||||
(inputPayload.labelPlural || inputPayload.icon) &&
|
||||
(inputPayload.labelPlural !== existingObjectMetadata.labelPlural ||
|
||||
inputPayload.icon !== existingObjectMetadata.icon)
|
||||
) {
|
||||
return {
|
||||
didUpdateLabelOrIcon: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
didUpdateLabelOrIcon: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
-617
@@ -1,617 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { FieldMetadataType, RelationOnDeleteAction } from 'twenty-shared/types';
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
import { type QueryRunner, Repository } from 'typeorm';
|
||||
import { v4 as uuidV4 } from 'uuid';
|
||||
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import {
|
||||
ObjectMetadataException,
|
||||
ObjectMetadataExceptionCode,
|
||||
} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
|
||||
import { buildDescriptionForRelationFieldMetadataOnFromField } from 'src/engine/metadata-modules/object-metadata/utils/build-description-for-relation-field-on-from-field.util';
|
||||
import { buildDescriptionForRelationFieldMetadataOnToField } from 'src/engine/metadata-modules/object-metadata/utils/build-description-for-relation-field-on-to-field.util';
|
||||
import { type FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
|
||||
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
import { InvalidMetadataException } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception';
|
||||
import { validateFieldNameAvailabilityOrThrow } from 'src/engine/metadata-modules/utils/validate-field-name-availability.utils';
|
||||
import {
|
||||
CUSTOM_OBJECT_STANDARD_FIELD_IDS,
|
||||
STANDARD_OBJECT_FIELD_IDS,
|
||||
} from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||
import { STANDARD_OBJECT_ICONS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-icons';
|
||||
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||
import { createRelationDeterministicUuid } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/create-deterministic-uuid.util';
|
||||
|
||||
export const DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS = [
|
||||
STANDARD_OBJECT_IDS.timelineActivity,
|
||||
STANDARD_OBJECT_IDS.favorite,
|
||||
STANDARD_OBJECT_IDS.attachment,
|
||||
STANDARD_OBJECT_IDS.noteTarget,
|
||||
STANDARD_OBJECT_IDS.taskTarget,
|
||||
];
|
||||
|
||||
type PartialRelationFieldMetadata = Partial<
|
||||
FieldMetadataEntity<FieldMetadataType.RELATION>
|
||||
> &
|
||||
Required<Pick<FieldMetadataEntity<FieldMetadataType.RELATION>, 'name'>>;
|
||||
|
||||
@Injectable()
|
||||
export class ObjectMetadataFieldRelationService {
|
||||
constructor(
|
||||
@InjectRepository(ObjectMetadataEntity)
|
||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||
@InjectRepository(FieldMetadataEntity)
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
) {}
|
||||
|
||||
public async createRelationsAndForeignKeysMetadata(
|
||||
workspaceId: string,
|
||||
sourceObjectMetadata: Pick<
|
||||
ObjectMetadataItemWithFieldMaps,
|
||||
'id' | 'nameSingular' | 'labelSingular' | 'fieldsById'
|
||||
>,
|
||||
objectMetadataMaps: ObjectMetadataMaps,
|
||||
queryRunner?: QueryRunner,
|
||||
) {
|
||||
const relatedObjectMetadataCollection = await Promise.all(
|
||||
DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS.map(
|
||||
async (relationObjectMetadataStandardId) =>
|
||||
this.createRelationAndForeignKeyMetadata({
|
||||
workspaceId,
|
||||
sourceObjectMetadata,
|
||||
relationObjectMetadataStandardId,
|
||||
objectMetadataMaps,
|
||||
queryRunner,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
return relatedObjectMetadataCollection;
|
||||
}
|
||||
|
||||
private async createRelationAndForeignKeyMetadata({
|
||||
workspaceId,
|
||||
sourceObjectMetadata,
|
||||
relationObjectMetadataStandardId,
|
||||
objectMetadataMaps,
|
||||
queryRunner,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
sourceObjectMetadata: Pick<
|
||||
ObjectMetadataItemWithFieldMaps,
|
||||
'id' | 'nameSingular' | 'labelSingular' | 'fieldsById'
|
||||
>;
|
||||
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
relationObjectMetadataStandardId: string;
|
||||
queryRunner?: QueryRunner;
|
||||
}) {
|
||||
const targetObjectMetadata = Object.values(objectMetadataMaps.byId)
|
||||
.filter(isDefined)
|
||||
.find(
|
||||
(objectMetadata) =>
|
||||
objectMetadata.standardId === relationObjectMetadataStandardId,
|
||||
);
|
||||
|
||||
if (!targetObjectMetadata) {
|
||||
throw new Error(
|
||||
`Target object metadata not found for standard ID: ${relationObjectMetadataStandardId}`,
|
||||
);
|
||||
}
|
||||
|
||||
await this.createFieldMetadataRelation(
|
||||
workspaceId,
|
||||
sourceObjectMetadata,
|
||||
targetObjectMetadata,
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
return targetObjectMetadata;
|
||||
}
|
||||
|
||||
private async createFieldMetadataRelation(
|
||||
workspaceId: string,
|
||||
sourceObjectMetadata: Pick<
|
||||
ObjectMetadataItemWithFieldMaps,
|
||||
'id' | 'nameSingular' | 'labelSingular' | 'fieldsById'
|
||||
>,
|
||||
targetObjectMetadata: ObjectMetadataItemWithFieldMaps,
|
||||
queryRunner?: QueryRunner,
|
||||
): Promise<FieldMetadataEntity<FieldMetadataType.RELATION>[]> {
|
||||
const sourceFieldMetadata = this.createSourceFieldMetadata(
|
||||
workspaceId,
|
||||
sourceObjectMetadata,
|
||||
targetObjectMetadata,
|
||||
);
|
||||
|
||||
this.validateFieldNameAvailabilityOrThrow({
|
||||
name: sourceFieldMetadata.name,
|
||||
fieldMetadataMapById: sourceObjectMetadata.fieldsById,
|
||||
});
|
||||
|
||||
const targetFieldMetadata = this.createTargetFieldMetadata(
|
||||
workspaceId,
|
||||
sourceObjectMetadata,
|
||||
targetObjectMetadata,
|
||||
);
|
||||
|
||||
this.validateFieldNameAvailabilityOrThrow({
|
||||
name: targetFieldMetadata.name,
|
||||
fieldMetadataMapById: sourceObjectMetadata.fieldsById,
|
||||
});
|
||||
|
||||
const fieldMetadataRepository = queryRunner
|
||||
? queryRunner.manager.getRepository(FieldMetadataEntity)
|
||||
: this.fieldMetadataRepository;
|
||||
|
||||
return fieldMetadataRepository.save([
|
||||
{
|
||||
...sourceFieldMetadata,
|
||||
settings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
relationTargetObjectMetadataId: targetObjectMetadata.id,
|
||||
relationTargetFieldMetadataId: targetFieldMetadata.id,
|
||||
} as Partial<FieldMetadataEntity<FieldMetadataType.RELATION>>,
|
||||
{
|
||||
...targetFieldMetadata,
|
||||
settings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
joinColumnName: `${sourceObjectMetadata.nameSingular}Id`,
|
||||
},
|
||||
relationTargetObjectMetadataId: sourceObjectMetadata.id,
|
||||
relationTargetFieldMetadataId: sourceFieldMetadata.id,
|
||||
} as Partial<FieldMetadataEntity<FieldMetadataType.RELATION>>,
|
||||
]);
|
||||
}
|
||||
|
||||
public async updateRelationsAndForeignKeysMetadata({
|
||||
workspaceId,
|
||||
updatedObjectMetadata,
|
||||
objectMetadataMaps,
|
||||
queryRunner,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
updatedObjectMetadata: Pick<
|
||||
ObjectMetadataEntity,
|
||||
'nameSingular' | 'isCustom' | 'id' | 'labelSingular'
|
||||
>;
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
queryRunner?: QueryRunner;
|
||||
}): Promise<
|
||||
{
|
||||
targetObjectMetadata: ObjectMetadataItemWithFieldMaps;
|
||||
targetFieldMetadata: FieldMetadataEntity;
|
||||
sourceFieldMetadata: FieldMetadataEntity;
|
||||
}[]
|
||||
> {
|
||||
return await Promise.all(
|
||||
DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS.map(
|
||||
async (relationObjectMetadataStandardId) =>
|
||||
this.updateRelationAndForeignKeyMetadata({
|
||||
workspaceId,
|
||||
sourceObjectMetadata: updatedObjectMetadata,
|
||||
targetObjectMetadataStandardId: relationObjectMetadataStandardId,
|
||||
objectMetadataMaps,
|
||||
queryRunner,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private async updateRelationAndForeignKeyMetadata({
|
||||
workspaceId,
|
||||
sourceObjectMetadata,
|
||||
targetObjectMetadataStandardId,
|
||||
objectMetadataMaps,
|
||||
queryRunner,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
sourceObjectMetadata: Pick<
|
||||
ObjectMetadataEntity,
|
||||
'nameSingular' | 'id' | 'isCustom' | 'labelSingular'
|
||||
>;
|
||||
targetObjectMetadataStandardId: string;
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
queryRunner?: QueryRunner;
|
||||
}): Promise<{
|
||||
targetObjectMetadata: ObjectMetadataItemWithFieldMaps;
|
||||
targetFieldMetadata: FieldMetadataEntity;
|
||||
sourceFieldMetadata: FieldMetadataEntity;
|
||||
}> {
|
||||
const fieldMetadataRepository = queryRunner
|
||||
? queryRunner.manager.getRepository(FieldMetadataEntity)
|
||||
: this.fieldMetadataRepository;
|
||||
|
||||
const targetObjectMetadataId = Object.values(objectMetadataMaps.byId).find(
|
||||
(objectMetadata) =>
|
||||
objectMetadata?.standardId === targetObjectMetadataStandardId &&
|
||||
objectMetadata.isCustom === false,
|
||||
)?.id;
|
||||
|
||||
if (!targetObjectMetadataId) {
|
||||
throw new Error(
|
||||
`Target object metadata id not found for standard ID: ${targetObjectMetadataStandardId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const targetObjectMetadata =
|
||||
objectMetadataMaps.byId[targetObjectMetadataId];
|
||||
|
||||
if (!targetObjectMetadata) {
|
||||
throw new Error(
|
||||
`Target object metadata not found for id: ${targetObjectMetadataId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const targetFieldMetadataUpdateData = this.updateTargetFieldMetadata(
|
||||
sourceObjectMetadata,
|
||||
targetObjectMetadata,
|
||||
);
|
||||
const targetFieldMetadataToUpdate =
|
||||
await fieldMetadataRepository.findOneByOrFail({
|
||||
standardId: createRelationDeterministicUuid({
|
||||
objectId: sourceObjectMetadata.id,
|
||||
standardId:
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
STANDARD_OBJECT_FIELD_IDS[targetObjectMetadata.nameSingular].custom,
|
||||
}),
|
||||
objectMetadataId: targetObjectMetadata.id,
|
||||
workspaceId: workspaceId,
|
||||
});
|
||||
|
||||
const nameIsUpdated =
|
||||
targetFieldMetadataUpdateData.name !== targetFieldMetadataToUpdate.name;
|
||||
|
||||
if (nameIsUpdated) {
|
||||
const targetObjectMetadataFieldsById =
|
||||
objectMetadataMaps.byId[targetObjectMetadata.id]?.fieldsById;
|
||||
|
||||
if (!targetObjectMetadataFieldsById) {
|
||||
throw new Error(
|
||||
`Target object metadata fields not found for ${targetObjectMetadata.id}`,
|
||||
);
|
||||
}
|
||||
|
||||
this.validateFieldNameAvailabilityOrThrow({
|
||||
name: targetFieldMetadataUpdateData.name,
|
||||
fieldMetadataMapById: targetObjectMetadataFieldsById,
|
||||
});
|
||||
}
|
||||
|
||||
const isTargetFieldMetadataManyToOneRelation =
|
||||
(
|
||||
targetFieldMetadataToUpdate as FieldMetadataEntity<FieldMetadataType.RELATION>
|
||||
).settings?.relationType === RelationType.MANY_TO_ONE;
|
||||
|
||||
const targetFieldMetadata = await fieldMetadataRepository.save({
|
||||
id: targetFieldMetadataToUpdate.id,
|
||||
...targetFieldMetadataUpdateData,
|
||||
settings: {
|
||||
...targetFieldMetadataToUpdate.settings,
|
||||
...(isTargetFieldMetadataManyToOneRelation
|
||||
? {
|
||||
joinColumnName: `${sourceObjectMetadata.nameSingular}Id`,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
|
||||
const sourceFieldMetadataUpdateData = this.updateSourceFieldMetadata(
|
||||
sourceObjectMetadata,
|
||||
targetObjectMetadata,
|
||||
);
|
||||
|
||||
const sourceFieldMetadataToUpdate =
|
||||
await fieldMetadataRepository.findOneByOrFail({
|
||||
standardId:
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
CUSTOM_OBJECT_STANDARD_FIELD_IDS[targetObjectMetadata.namePlural],
|
||||
objectMetadataId: sourceObjectMetadata.id,
|
||||
workspaceId: workspaceId,
|
||||
});
|
||||
|
||||
const isSourceFieldMetadataManyToOneRelation =
|
||||
(
|
||||
sourceFieldMetadataToUpdate as FieldMetadataEntity<FieldMetadataType.RELATION>
|
||||
).settings?.relationType === RelationType.MANY_TO_ONE;
|
||||
|
||||
const sourceFieldMetadata = await fieldMetadataRepository.save({
|
||||
id: sourceFieldMetadataToUpdate.id,
|
||||
...sourceFieldMetadataUpdateData,
|
||||
settings: {
|
||||
...sourceFieldMetadataToUpdate.settings,
|
||||
...(isSourceFieldMetadataManyToOneRelation
|
||||
? {
|
||||
joinColumnName: `${targetObjectMetadata.nameSingular}Id`,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
targetObjectMetadata,
|
||||
targetFieldMetadata,
|
||||
sourceFieldMetadata,
|
||||
};
|
||||
}
|
||||
|
||||
private createSourceFieldMetadata(
|
||||
workspaceId: string,
|
||||
sourceObjectMetadata: Pick<
|
||||
ObjectMetadataItemWithFieldMaps,
|
||||
'labelSingular' | 'id'
|
||||
>,
|
||||
targetObjectMetadata: Pick<
|
||||
ObjectMetadataItemWithFieldMaps,
|
||||
'namePlural' | 'labelSingular'
|
||||
>,
|
||||
): PartialRelationFieldMetadata {
|
||||
const relationObjectMetadataNamePlural = targetObjectMetadata.namePlural;
|
||||
|
||||
const { description } = buildDescriptionForRelationFieldMetadataOnFromField(
|
||||
{
|
||||
relationObjectMetadataNamePlural,
|
||||
targetObjectLabelSingular: sourceObjectMetadata.labelSingular,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
id: uuidV4(),
|
||||
standardId:
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
CUSTOM_OBJECT_STANDARD_FIELD_IDS[relationObjectMetadataNamePlural],
|
||||
objectMetadataId: sourceObjectMetadata.id,
|
||||
workspaceId: workspaceId,
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: true,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: targetObjectMetadata.namePlural,
|
||||
label: capitalize(relationObjectMetadataNamePlural),
|
||||
description,
|
||||
icon:
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
STANDARD_OBJECT_ICONS[targetObjectMetadata.nameSingular] ||
|
||||
'IconBuildingSkyscraper',
|
||||
isNullable: true,
|
||||
};
|
||||
}
|
||||
|
||||
private updateSourceFieldMetadata(
|
||||
sourceObjectMetadata: Pick<ObjectMetadataEntity, 'labelSingular'>,
|
||||
targetObjectMetadata: Pick<ObjectMetadataEntity, 'namePlural'>,
|
||||
) {
|
||||
const relationObjectMetadataNamePlural = targetObjectMetadata.namePlural;
|
||||
|
||||
const { description } = buildDescriptionForRelationFieldMetadataOnFromField(
|
||||
{
|
||||
relationObjectMetadataNamePlural,
|
||||
targetObjectLabelSingular: sourceObjectMetadata.labelSingular,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
description,
|
||||
};
|
||||
}
|
||||
|
||||
private createTargetFieldMetadata(
|
||||
workspaceId: string,
|
||||
sourceObjectMetadata: Pick<
|
||||
ObjectMetadataItemWithFieldMaps,
|
||||
'labelSingular' | 'id' | 'nameSingular'
|
||||
>,
|
||||
targetObjectMetadata: Pick<
|
||||
ObjectMetadataItemWithFieldMaps,
|
||||
'namePlural' | 'labelSingular' | 'id' | 'nameSingular'
|
||||
>,
|
||||
): PartialRelationFieldMetadata {
|
||||
const customStandardFieldId =
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
STANDARD_OBJECT_FIELD_IDS[targetObjectMetadata.nameSingular].custom;
|
||||
|
||||
if (!customStandardFieldId) {
|
||||
throw new Error(
|
||||
`Custom standard field ID not found for ${targetObjectMetadata.nameSingular}`,
|
||||
);
|
||||
}
|
||||
|
||||
const { description } = buildDescriptionForRelationFieldMetadataOnToField({
|
||||
relationObjectMetadataNamePlural: targetObjectMetadata.namePlural,
|
||||
targetObjectLabelSingular: sourceObjectMetadata.labelSingular,
|
||||
});
|
||||
|
||||
return {
|
||||
id: uuidV4(),
|
||||
name: sourceObjectMetadata.nameSingular,
|
||||
label: sourceObjectMetadata.labelSingular,
|
||||
description,
|
||||
standardId: createRelationDeterministicUuid({
|
||||
objectId: sourceObjectMetadata.id,
|
||||
standardId: customStandardFieldId,
|
||||
}),
|
||||
objectMetadataId: targetObjectMetadata.id,
|
||||
workspaceId: workspaceId,
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: true,
|
||||
type: FieldMetadataType.RELATION,
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
isNullable: true,
|
||||
};
|
||||
}
|
||||
|
||||
private updateTargetFieldMetadata(
|
||||
sourceObjectMetadata: Pick<
|
||||
ObjectMetadataEntity,
|
||||
'nameSingular' | 'labelSingular'
|
||||
>,
|
||||
targetObjectMetadata: Pick<
|
||||
ObjectMetadataEntity,
|
||||
'nameSingular' | 'namePlural'
|
||||
>,
|
||||
) {
|
||||
const customStandardFieldId =
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
STANDARD_OBJECT_FIELD_IDS[targetObjectMetadata.nameSingular].custom;
|
||||
|
||||
if (!customStandardFieldId) {
|
||||
throw new Error(
|
||||
`Custom standard field ID not found for ${targetObjectMetadata.nameSingular}`,
|
||||
);
|
||||
}
|
||||
|
||||
const { description } = buildDescriptionForRelationFieldMetadataOnToField({
|
||||
relationObjectMetadataNamePlural: targetObjectMetadata.namePlural,
|
||||
targetObjectLabelSingular: sourceObjectMetadata.labelSingular,
|
||||
});
|
||||
|
||||
return {
|
||||
name: sourceObjectMetadata.nameSingular,
|
||||
label: sourceObjectMetadata.labelSingular,
|
||||
description,
|
||||
};
|
||||
}
|
||||
|
||||
private validateFieldMetadataTypeIsMorphRelation = (
|
||||
fieldMetadatas: FieldMetadataEntity[],
|
||||
): fieldMetadatas is Array<
|
||||
FieldMetadataEntity & FieldMetadataEntity<FieldMetadataType.MORPH_RELATION>
|
||||
> => {
|
||||
return fieldMetadatas.every(
|
||||
(fieldMetadata) =>
|
||||
fieldMetadata.type === FieldMetadataType.MORPH_RELATION,
|
||||
);
|
||||
};
|
||||
|
||||
public async findTargetMorphRelationFieldMetadatas(
|
||||
objectMetadataId: string,
|
||||
): Promise<FieldMetadataEntity<FieldMetadataType.MORPH_RELATION>[]> {
|
||||
const fieldMetadatas = await this.fieldMetadataRepository.find({
|
||||
where: {
|
||||
relationTargetObjectMetadataId: objectMetadataId,
|
||||
type: FieldMetadataType.MORPH_RELATION,
|
||||
},
|
||||
relations: {
|
||||
relationTargetObjectMetadata: true,
|
||||
object: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!this.validateFieldMetadataTypeIsMorphRelation(fieldMetadatas)) {
|
||||
throw new ObjectMetadataException(
|
||||
'Invalid field metadata type. Expected MORPH_RELATION only',
|
||||
ObjectMetadataExceptionCode.INVALID_ORM_OUTPUT,
|
||||
);
|
||||
}
|
||||
|
||||
return fieldMetadatas;
|
||||
}
|
||||
|
||||
// Not maintained on v1, this side effect is broken and will duplicated field name references on
|
||||
// object update
|
||||
// It's functional on v2
|
||||
public async updateMorphRelationsJoinColumnName({
|
||||
existingObjectMetadata,
|
||||
objectMetadataForUpdate: _,
|
||||
queryRunner,
|
||||
}: {
|
||||
existingObjectMetadata: Pick<
|
||||
ObjectMetadataItemWithFieldMaps,
|
||||
'nameSingular' | 'isCustom' | 'id' | 'labelPlural' | 'icon' | 'fieldsById'
|
||||
>;
|
||||
objectMetadataForUpdate: Pick<
|
||||
ObjectMetadataItemWithFieldMaps,
|
||||
| 'nameSingular'
|
||||
| 'isCustom'
|
||||
| 'workspaceId'
|
||||
| 'id'
|
||||
| 'labelSingular'
|
||||
| 'labelPlural'
|
||||
| 'icon'
|
||||
| 'fieldsById'
|
||||
>;
|
||||
queryRunner: QueryRunner;
|
||||
}): Promise<
|
||||
{
|
||||
fieldMetadata: FieldMetadataEntity<FieldMetadataType.MORPH_RELATION>;
|
||||
newJoinColumnName: string;
|
||||
}[]
|
||||
> {
|
||||
const fieldMetadataRepository =
|
||||
queryRunner.manager.getRepository(FieldMetadataEntity);
|
||||
|
||||
const morphRelationFieldMetadataTargets =
|
||||
await this.findTargetMorphRelationFieldMetadatas(
|
||||
existingObjectMetadata.id,
|
||||
);
|
||||
const morphRelationFieldMetadataToUpdate =
|
||||
morphRelationFieldMetadataTargets.filter(
|
||||
(morphRelationFieldMetadata) =>
|
||||
morphRelationFieldMetadata.settings?.relationType ===
|
||||
RelationType.MANY_TO_ONE,
|
||||
);
|
||||
|
||||
const morphRelationFieldMetadataToUpdateWithNewJoinColumnName = [];
|
||||
|
||||
if (morphRelationFieldMetadataToUpdate.length > 0) {
|
||||
for (const morphRelationFieldMetadata of morphRelationFieldMetadataToUpdate) {
|
||||
const newJoinColumnName = computeMorphOrRelationFieldJoinColumnName({
|
||||
name: morphRelationFieldMetadata.name,
|
||||
});
|
||||
|
||||
await fieldMetadataRepository.save({
|
||||
...morphRelationFieldMetadata,
|
||||
settings: {
|
||||
...morphRelationFieldMetadata.settings,
|
||||
joinColumnName: newJoinColumnName,
|
||||
},
|
||||
});
|
||||
|
||||
morphRelationFieldMetadataToUpdateWithNewJoinColumnName.push({
|
||||
fieldMetadata: morphRelationFieldMetadata,
|
||||
newJoinColumnName,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return morphRelationFieldMetadataToUpdateWithNewJoinColumnName;
|
||||
}
|
||||
|
||||
private validateFieldNameAvailabilityOrThrow({
|
||||
name,
|
||||
fieldMetadataMapById,
|
||||
}: {
|
||||
name: string;
|
||||
fieldMetadataMapById: FieldMetadataMap;
|
||||
}) {
|
||||
try {
|
||||
validateFieldNameAvailabilityOrThrow({
|
||||
name,
|
||||
fieldMetadataMapById,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof InvalidMetadataException) {
|
||||
throw new ObjectMetadataException(
|
||||
`Name "${name}" is not available.`,
|
||||
ObjectMetadataExceptionCode.NAME_CONFLICT,
|
||||
{
|
||||
userFriendlyMessage: msg`Name "${name}" is not available.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
-424
@@ -1,424 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { type QueryRunner, Repository } from 'typeorm';
|
||||
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import {
|
||||
ObjectMetadataException,
|
||||
ObjectMetadataExceptionCode,
|
||||
} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
|
||||
import { buildMigrationsForCustomObjectRelations } from 'src/engine/metadata-modules/object-metadata/utils/build-migrations-for-custom-object-relations.util';
|
||||
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util';
|
||||
import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
|
||||
import {
|
||||
WorkspaceMigrationColumnActionType,
|
||||
type WorkspaceMigrationColumnDrop,
|
||||
type WorkspaceMigrationTableAction,
|
||||
WorkspaceMigrationTableActionType,
|
||||
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
|
||||
import { WorkspaceMigrationFactory } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.factory';
|
||||
import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service';
|
||||
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
||||
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
|
||||
import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
|
||||
import { RELATION_MIGRATION_PRIORITY_PREFIX } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service';
|
||||
|
||||
@Injectable()
|
||||
export class ObjectMetadataMigrationService {
|
||||
constructor(
|
||||
@InjectRepository(FieldMetadataEntity)
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
private readonly workspaceMigrationService: WorkspaceMigrationService,
|
||||
private readonly workspaceMigrationFactory: WorkspaceMigrationFactory,
|
||||
) {}
|
||||
|
||||
public async createTableMigration(
|
||||
createdObjectMetadata: ObjectMetadataEntity,
|
||||
queryRunner?: QueryRunner,
|
||||
) {
|
||||
await this.workspaceMigrationService.createCustomMigration(
|
||||
generateMigrationName(`create-${createdObjectMetadata.nameSingular}`),
|
||||
createdObjectMetadata.workspaceId,
|
||||
[
|
||||
{
|
||||
name: computeObjectTargetTable(createdObjectMetadata),
|
||||
action: WorkspaceMigrationTableActionType.CREATE,
|
||||
} satisfies WorkspaceMigrationTableAction,
|
||||
],
|
||||
queryRunner,
|
||||
);
|
||||
}
|
||||
|
||||
public async createColumnsMigrations(
|
||||
createdObjectMetadata: ObjectMetadataEntity,
|
||||
fieldMetadataCollection: FieldMetadataEntity[],
|
||||
queryRunner?: QueryRunner,
|
||||
) {
|
||||
await this.workspaceMigrationService.createCustomMigration(
|
||||
generateMigrationName(
|
||||
`create-${createdObjectMetadata.nameSingular}-fields`,
|
||||
),
|
||||
createdObjectMetadata.workspaceId,
|
||||
[
|
||||
{
|
||||
name: computeObjectTargetTable(createdObjectMetadata),
|
||||
action: WorkspaceMigrationTableActionType.ALTER,
|
||||
columns: fieldMetadataCollection.flatMap((fieldMetadata) =>
|
||||
this.workspaceMigrationFactory.createColumnActions(
|
||||
WorkspaceMigrationColumnActionType.CREATE,
|
||||
fieldMetadata,
|
||||
),
|
||||
),
|
||||
},
|
||||
],
|
||||
queryRunner,
|
||||
);
|
||||
}
|
||||
|
||||
public async createRelationMigrations(
|
||||
createdObjectMetadata: Pick<
|
||||
ObjectMetadataItemWithFieldMaps,
|
||||
'nameSingular' | 'workspaceId' | 'isCustom'
|
||||
>,
|
||||
relatedObjectMetadataCollection: Pick<
|
||||
ObjectMetadataItemWithFieldMaps,
|
||||
'nameSingular' | 'isCustom'
|
||||
>[],
|
||||
queryRunner?: QueryRunner,
|
||||
) {
|
||||
await this.workspaceMigrationService.createCustomMigration(
|
||||
generateMigrationName(
|
||||
`create-${createdObjectMetadata.nameSingular}-relations`,
|
||||
),
|
||||
createdObjectMetadata.workspaceId,
|
||||
buildMigrationsForCustomObjectRelations(
|
||||
createdObjectMetadata,
|
||||
relatedObjectMetadataCollection,
|
||||
),
|
||||
queryRunner,
|
||||
);
|
||||
}
|
||||
|
||||
public async createRenameTableMigration(
|
||||
existingObjectMetadata: Pick<
|
||||
ObjectMetadataEntity,
|
||||
'nameSingular' | 'isCustom'
|
||||
>,
|
||||
objectMetadataForUpdate: Pick<
|
||||
ObjectMetadataEntity,
|
||||
'nameSingular' | 'isCustom'
|
||||
>,
|
||||
workspaceId: string,
|
||||
queryRunner?: QueryRunner,
|
||||
) {
|
||||
const newTargetTableName = computeObjectTargetTable(
|
||||
objectMetadataForUpdate,
|
||||
);
|
||||
const existingTargetTableName = computeObjectTargetTable(
|
||||
existingObjectMetadata,
|
||||
);
|
||||
|
||||
await this.workspaceMigrationService.createCustomMigration(
|
||||
generateMigrationName(`rename-${existingObjectMetadata.nameSingular}`),
|
||||
workspaceId,
|
||||
[
|
||||
{
|
||||
name: existingTargetTableName,
|
||||
newName: newTargetTableName,
|
||||
action: WorkspaceMigrationTableActionType.ALTER,
|
||||
},
|
||||
],
|
||||
queryRunner,
|
||||
);
|
||||
}
|
||||
|
||||
public async updateRelationMigrations(
|
||||
currentObjectMetadata: Pick<ObjectMetadataEntity, 'nameSingular'>,
|
||||
alteredObjectMetadata: Pick<ObjectMetadataEntity, 'nameSingular'>,
|
||||
relationMetadataCollection: {
|
||||
targetObjectMetadata: ObjectMetadataItemWithFieldMaps;
|
||||
targetFieldMetadata: FieldMetadataEntity;
|
||||
sourceFieldMetadata: FieldMetadataEntity;
|
||||
}[],
|
||||
workspaceId: string,
|
||||
queryRunner?: QueryRunner,
|
||||
) {
|
||||
for (const { targetObjectMetadata } of relationMetadataCollection) {
|
||||
const targetTableName = computeObjectTargetTable(targetObjectMetadata);
|
||||
const columnName = `${currentObjectMetadata.nameSingular}Id`;
|
||||
|
||||
await this.workspaceMigrationService.createCustomMigration(
|
||||
generateMigrationName(
|
||||
`rename-${currentObjectMetadata.nameSingular}-to-${alteredObjectMetadata.nameSingular}-in-${targetObjectMetadata.nameSingular}`,
|
||||
),
|
||||
workspaceId,
|
||||
[
|
||||
{
|
||||
name: targetTableName,
|
||||
action: WorkspaceMigrationTableActionType.ALTER,
|
||||
columns: [
|
||||
{
|
||||
action: WorkspaceMigrationColumnActionType.ALTER,
|
||||
currentColumnDefinition: {
|
||||
columnName,
|
||||
columnType: 'uuid',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
alteredColumnDefinition: {
|
||||
columnName: `${alteredObjectMetadata.nameSingular}Id`,
|
||||
columnType: 'uuid',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
queryRunner,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async createUpdateForeignKeysMigrations(
|
||||
existingObjectMetadata: ObjectMetadataEntity,
|
||||
updatedObjectMetadata: ObjectMetadataEntity,
|
||||
relationsAndForeignKeysMetadata: {
|
||||
relatedObjectMetadata: ObjectMetadataEntity;
|
||||
foreignKeyFieldMetadata: FieldMetadataEntity;
|
||||
}[],
|
||||
workspaceId: string,
|
||||
queryRunner?: QueryRunner,
|
||||
) {
|
||||
for (const {
|
||||
relatedObjectMetadata,
|
||||
foreignKeyFieldMetadata,
|
||||
} of relationsAndForeignKeysMetadata) {
|
||||
const relatedObjectTableName = computeObjectTargetTable(
|
||||
relatedObjectMetadata,
|
||||
);
|
||||
const columnName = `${existingObjectMetadata.nameSingular}Id`;
|
||||
const columnType = fieldMetadataTypeToColumnType(
|
||||
foreignKeyFieldMetadata.type,
|
||||
);
|
||||
|
||||
await this.workspaceMigrationService.createCustomMigration(
|
||||
generateMigrationName(
|
||||
`rename-${existingObjectMetadata.nameSingular}-to-${updatedObjectMetadata.nameSingular}-in-${relatedObjectMetadata.nameSingular}`,
|
||||
),
|
||||
workspaceId,
|
||||
[
|
||||
{
|
||||
name: relatedObjectTableName,
|
||||
action: WorkspaceMigrationTableActionType.ALTER,
|
||||
columns: [
|
||||
{
|
||||
action: WorkspaceMigrationColumnActionType.ALTER,
|
||||
currentColumnDefinition: {
|
||||
columnName,
|
||||
columnType,
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
alteredColumnDefinition: {
|
||||
columnName: `${updatedObjectMetadata.nameSingular}Id`,
|
||||
columnType,
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
queryRunner,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteAllRelationsAndDropTable(
|
||||
objectMetadata: ObjectMetadataEntity,
|
||||
workspaceId: string,
|
||||
queryRunner?: QueryRunner,
|
||||
) {
|
||||
const relationFields = objectMetadata.fields.filter(
|
||||
(field) =>
|
||||
isFieldMetadataEntityOfType(field, FieldMetadataType.RELATION) ||
|
||||
isFieldMetadataEntityOfType(field, FieldMetadataType.MORPH_RELATION),
|
||||
) as FieldMetadataEntity<
|
||||
FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION
|
||||
>[];
|
||||
|
||||
const relationFieldsToDelete = [
|
||||
...relationFields,
|
||||
...(relationFields.map(
|
||||
(relation) => relation.relationTargetFieldMetadata,
|
||||
) as FieldMetadataEntity<
|
||||
FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION
|
||||
>[]),
|
||||
];
|
||||
|
||||
if (relationFieldsToDelete.length !== 0) {
|
||||
await this.fieldMetadataRepository.delete(
|
||||
relationFieldsToDelete.map((relation) => relation.id),
|
||||
);
|
||||
}
|
||||
|
||||
for (const relationToDelete of relationFieldsToDelete) {
|
||||
if (
|
||||
relationToDelete.settings?.relationType === RelationType.ONE_TO_MANY
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const joinColumnName = relationToDelete.settings?.joinColumnName;
|
||||
|
||||
if (!joinColumnName) {
|
||||
throw new Error(
|
||||
`Join column name is not set for relation field ${relationToDelete.name}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (relationToDelete.type !== FieldMetadataType.MORPH_RELATION) {
|
||||
await this.workspaceMigrationService.createCustomMigration(
|
||||
generateMigrationName(
|
||||
`delete-${RELATION_MIGRATION_PRIORITY_PREFIX}-${relationToDelete.name}`,
|
||||
),
|
||||
workspaceId,
|
||||
[
|
||||
{
|
||||
name: computeTableName(
|
||||
relationToDelete.object.nameSingular,
|
||||
relationToDelete.object.isCustom,
|
||||
),
|
||||
action: WorkspaceMigrationTableActionType.ALTER,
|
||||
columns: [
|
||||
{
|
||||
action: WorkspaceMigrationColumnActionType.DROP,
|
||||
columnName: joinColumnName,
|
||||
} satisfies WorkspaceMigrationColumnDrop,
|
||||
],
|
||||
},
|
||||
],
|
||||
queryRunner,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await this.workspaceMigrationService.createCustomMigration(
|
||||
generateMigrationName(`delete-${objectMetadata.nameSingular}`),
|
||||
workspaceId,
|
||||
[
|
||||
{
|
||||
name: computeObjectTargetTable(objectMetadata),
|
||||
action: WorkspaceMigrationTableActionType.DROP,
|
||||
},
|
||||
],
|
||||
queryRunner,
|
||||
);
|
||||
}
|
||||
|
||||
public async recomputeEnumNames(
|
||||
updatedObjectMetadata: Pick<
|
||||
ObjectMetadataItemWithFieldMaps,
|
||||
'nameSingular' | 'isCustom' | 'id' | 'fieldsById'
|
||||
>,
|
||||
workspaceId: string,
|
||||
queryRunner?: QueryRunner,
|
||||
) {
|
||||
const enumFieldMetadataTypes = [
|
||||
FieldMetadataType.SELECT,
|
||||
FieldMetadataType.MULTI_SELECT,
|
||||
FieldMetadataType.RATING,
|
||||
FieldMetadataType.ACTOR,
|
||||
];
|
||||
|
||||
const fieldMetadataToUpdate = Object.values(
|
||||
updatedObjectMetadata.fieldsById,
|
||||
).filter((field) => enumFieldMetadataTypes.includes(field.type));
|
||||
|
||||
for (const fieldMetadata of fieldMetadataToUpdate) {
|
||||
await this.workspaceMigrationService.createCustomMigration(
|
||||
generateMigrationName(`update-${fieldMetadata.name}-enum-name`),
|
||||
workspaceId,
|
||||
[
|
||||
{
|
||||
name: computeTableName(
|
||||
updatedObjectMetadata.nameSingular,
|
||||
updatedObjectMetadata.isCustom,
|
||||
),
|
||||
action: WorkspaceMigrationTableActionType.ALTER,
|
||||
columns: this.workspaceMigrationFactory.createColumnActions(
|
||||
WorkspaceMigrationColumnActionType.ALTER,
|
||||
fieldMetadata,
|
||||
fieldMetadata,
|
||||
),
|
||||
},
|
||||
],
|
||||
queryRunner,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async updateMorphRelationMigrations({
|
||||
workspaceId,
|
||||
morphRelationFieldMetadataToUpdate,
|
||||
queryRunner,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
morphRelationFieldMetadataToUpdate: {
|
||||
fieldMetadata: FieldMetadataEntity<FieldMetadataType.MORPH_RELATION>;
|
||||
newJoinColumnName: string;
|
||||
}[];
|
||||
queryRunner?: QueryRunner;
|
||||
}) {
|
||||
for (const morphRelationFieldMetadata of morphRelationFieldMetadataToUpdate) {
|
||||
if (!morphRelationFieldMetadata.fieldMetadata.settings?.joinColumnName) {
|
||||
throw new ObjectMetadataException(
|
||||
`Settings for morph relation field should be defined ${morphRelationFieldMetadata.fieldMetadata.name}`,
|
||||
ObjectMetadataExceptionCode.INVALID_ORM_OUTPUT,
|
||||
);
|
||||
}
|
||||
|
||||
await this.workspaceMigrationService.createCustomMigration(
|
||||
generateMigrationName(
|
||||
`rename-join-column-name-${morphRelationFieldMetadata.fieldMetadata.name}-to-${morphRelationFieldMetadata.newJoinColumnName}-in-${morphRelationFieldMetadata.fieldMetadata.object.nameSingular}`,
|
||||
),
|
||||
workspaceId,
|
||||
[
|
||||
{
|
||||
name: computeObjectTargetTable(
|
||||
morphRelationFieldMetadata.fieldMetadata.object,
|
||||
),
|
||||
action: WorkspaceMigrationTableActionType.ALTER,
|
||||
columns: [
|
||||
{
|
||||
action: WorkspaceMigrationColumnActionType.ALTER,
|
||||
currentColumnDefinition: {
|
||||
columnName:
|
||||
morphRelationFieldMetadata.fieldMetadata.settings
|
||||
?.joinColumnName,
|
||||
columnType: 'uuid',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
alteredColumnDefinition: {
|
||||
columnName: `${morphRelationFieldMetadata.newJoinColumnName}`,
|
||||
columnType: 'uuid',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
queryRunner,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
-176
@@ -1,176 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
|
||||
import { ViewFieldService } from 'src/engine/metadata-modules/view-field/services/view-field.service';
|
||||
import { ViewDTO } from 'src/engine/metadata-modules/view/dtos/view.dto';
|
||||
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
|
||||
import { ViewKey } from 'src/engine/metadata-modules/view/enums/view-key.enum';
|
||||
import { ViewType } from 'src/engine/metadata-modules/view/enums/view-type.enum';
|
||||
import { ViewService } from 'src/engine/metadata-modules/view/services/view.service';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { DEFAULT_VIEW_FIELD_SIZE } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/constants/DEFAULT_VIEW_FIELD_SIZE';
|
||||
import { type FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity';
|
||||
|
||||
@Injectable()
|
||||
export class ObjectMetadataRelatedRecordsService {
|
||||
constructor(
|
||||
private readonly viewService: ViewService,
|
||||
private readonly viewFieldService: ViewFieldService,
|
||||
@InjectRepository(ViewEntity)
|
||||
private readonly viewRepository: Repository<ViewEntity>,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {}
|
||||
|
||||
public async createObjectRelatedRecords(
|
||||
objectMetadata: ObjectMetadataEntity,
|
||||
) {
|
||||
const view = await this.createView(objectMetadata);
|
||||
|
||||
await this.createViewFields(objectMetadata, view.id);
|
||||
await this.createViewWorkspaceFavorite(objectMetadata.workspaceId, view.id);
|
||||
await this.viewService.flushGraphQLCache(objectMetadata.workspaceId);
|
||||
}
|
||||
|
||||
public async updateLabelMetadataIdentifierInObjectViews({
|
||||
newLabelMetadataIdentifierFieldMetadata,
|
||||
}: {
|
||||
newLabelMetadataIdentifierFieldMetadata: FieldMetadataEntity;
|
||||
}) {
|
||||
const objectMetadataViews = await this.viewService.findByObjectMetadataId(
|
||||
newLabelMetadataIdentifierFieldMetadata.workspaceId,
|
||||
newLabelMetadataIdentifierFieldMetadata.objectMetadataId,
|
||||
);
|
||||
|
||||
for (const view of objectMetadataViews) {
|
||||
let labelMetadataIdentifierViewField = view.viewFields.find(
|
||||
(viewField) =>
|
||||
viewField.fieldMetadataId ===
|
||||
newLabelMetadataIdentifierFieldMetadata.id,
|
||||
);
|
||||
|
||||
const currentMinPositionAmongViewFields = this.getViewFieldsMinPosition(
|
||||
view.viewFields,
|
||||
);
|
||||
|
||||
if (!labelMetadataIdentifierViewField) {
|
||||
await this.viewFieldService.create({
|
||||
fieldMetadataId: newLabelMetadataIdentifierFieldMetadata.id,
|
||||
position: currentMinPositionAmongViewFields - 1,
|
||||
isVisible: true,
|
||||
size: DEFAULT_VIEW_FIELD_SIZE,
|
||||
viewId: view.id,
|
||||
workspaceId: newLabelMetadataIdentifierFieldMetadata.workspaceId,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.viewFieldService.update(
|
||||
labelMetadataIdentifierViewField.id,
|
||||
newLabelMetadataIdentifierFieldMetadata.workspaceId,
|
||||
{
|
||||
position: currentMinPositionAmongViewFields - 1,
|
||||
isVisible: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private getViewFieldsMinPosition(viewFields: ViewFieldEntity[]): number {
|
||||
if (viewFields.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return viewFields.reduce((min, field) => Math.min(min, field.position), 0);
|
||||
}
|
||||
|
||||
private async createView(
|
||||
objectMetadata: ObjectMetadataEntity,
|
||||
): Promise<ViewDTO> {
|
||||
return await this.viewService.create({
|
||||
objectMetadataId: objectMetadata.id,
|
||||
type: ViewType.TABLE,
|
||||
name: `All ${objectMetadata.labelPlural}`,
|
||||
key: ViewKey.INDEX,
|
||||
icon: 'IconList',
|
||||
workspaceId: objectMetadata.workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
private async createViewFields(
|
||||
objectMetadata: ObjectMetadataEntity,
|
||||
viewId: string,
|
||||
): Promise<void> {
|
||||
const viewFields = objectMetadata.fields
|
||||
.filter((field) => field.name !== 'id' && field.name !== 'deletedAt')
|
||||
.map((field, index) => ({
|
||||
fieldMetadataId: field.id,
|
||||
position: index,
|
||||
isVisible: true,
|
||||
size: DEFAULT_VIEW_FIELD_SIZE,
|
||||
viewId: viewId,
|
||||
workspaceId: objectMetadata.workspaceId,
|
||||
}));
|
||||
|
||||
for (const viewField of viewFields) {
|
||||
await this.viewFieldService.create(viewField);
|
||||
}
|
||||
}
|
||||
|
||||
public async createViewWorkspaceFavorite(
|
||||
workspaceId: string,
|
||||
viewId: string,
|
||||
): Promise<void> {
|
||||
const favoriteRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<FavoriteWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'favorite',
|
||||
);
|
||||
|
||||
const favoriteCount = await favoriteRepository.count();
|
||||
|
||||
await favoriteRepository.insert(
|
||||
favoriteRepository.create({
|
||||
viewId: viewId,
|
||||
position: favoriteCount,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
public async updateObjectViews(
|
||||
updatedObjectMetadata: Pick<
|
||||
ObjectMetadataEntity,
|
||||
'id' | 'labelPlural' | 'icon'
|
||||
>,
|
||||
workspaceId: string,
|
||||
) {
|
||||
await this.viewRepository.update(
|
||||
{
|
||||
objectMetadataId: updatedObjectMetadata.id,
|
||||
key: ViewKey.INDEX,
|
||||
workspaceId,
|
||||
},
|
||||
{
|
||||
name: `All ${updatedObjectMetadata.labelPlural}`,
|
||||
...(isDefined(updatedObjectMetadata.icon)
|
||||
? { icon: updatedObjectMetadata.icon }
|
||||
: {}),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public async deleteObjectViews(
|
||||
objectMetadata: ObjectMetadataEntity,
|
||||
workspaceId: string,
|
||||
) {
|
||||
await this.viewRepository.delete({
|
||||
objectMetadataId: objectMetadata.id,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
}
|
||||
+50
-39
@@ -6,11 +6,9 @@ import { plural } from 'pluralize';
|
||||
import { DataSource, Repository } from 'typeorm';
|
||||
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
|
||||
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service';
|
||||
import { type CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input';
|
||||
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
||||
import { FieldMetadataServiceV2 } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service-v2';
|
||||
import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service';
|
||||
import {
|
||||
RemoteServerEntity,
|
||||
type RemoteServerType,
|
||||
@@ -57,8 +55,8 @@ export class RemoteTableService {
|
||||
>,
|
||||
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
|
||||
private readonly dataSourceService: DataSourceService,
|
||||
private readonly objectMetadataService: ObjectMetadataService,
|
||||
private readonly fieldMetadataService: FieldMetadataService,
|
||||
private readonly objectMetadataService: ObjectMetadataServiceV2,
|
||||
private readonly fieldMetadataService: FieldMetadataServiceV2,
|
||||
private readonly distantTableService: DistantTableService,
|
||||
private readonly foreignTableService: ForeignTableService,
|
||||
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
||||
@@ -421,10 +419,10 @@ export class RemoteTableService {
|
||||
});
|
||||
|
||||
if (objectMetadata) {
|
||||
await this.objectMetadataService.deleteOneObject(
|
||||
{ id: objectMetadata.id },
|
||||
await this.objectMetadataService.deleteOneObject({
|
||||
deleteObjectInput: { id: objectMetadata.id },
|
||||
workspaceId,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
await this.foreignTableService.deleteForeignTable(
|
||||
@@ -455,21 +453,23 @@ export class RemoteTableService {
|
||||
? `${plural(localTableBaseName)}${localTableSuffix}`
|
||||
: plural(localTableBaseName);
|
||||
|
||||
const objectMetadata = await this.objectMetadataService.createOne({
|
||||
nameSingular: camelCase(localTableNameSingular),
|
||||
namePlural: camelCase(localTableNamePlural),
|
||||
labelSingular: camelToTitleCase(camelCase(localTableBaseName)),
|
||||
labelPlural: camelToTitleCase(plural(camelCase(localTableBaseName))),
|
||||
description: 'Remote table',
|
||||
dataSourceId: dataSourceMetadataId,
|
||||
const objectMetadata = await this.objectMetadataService.createOneObject({
|
||||
createObjectInput: {
|
||||
nameSingular: camelCase(localTableNameSingular),
|
||||
namePlural: camelCase(localTableNamePlural),
|
||||
labelSingular: camelToTitleCase(camelCase(localTableBaseName)),
|
||||
labelPlural: camelToTitleCase(plural(camelCase(localTableBaseName))),
|
||||
description: 'Remote table',
|
||||
dataSourceId: dataSourceMetadataId,
|
||||
icon: 'IconPlug',
|
||||
isRemote: true,
|
||||
primaryKeyColumnType: distantTableIdColumn.udtName,
|
||||
primaryKeyFieldMetadataSettings: mapUdtNameToFieldSettings(
|
||||
distantTableIdColumn.udtName,
|
||||
),
|
||||
},
|
||||
workspaceId: workspaceId,
|
||||
icon: 'IconPlug',
|
||||
isRemote: true,
|
||||
primaryKeyColumnType: distantTableIdColumn.udtName,
|
||||
primaryKeyFieldMetadataSettings: mapUdtNameToFieldSettings(
|
||||
distantTableIdColumn.udtName,
|
||||
),
|
||||
} satisfies CreateObjectInput);
|
||||
});
|
||||
|
||||
for (const column of distantTableColumns) {
|
||||
const columnName = camelCase(column.columnName);
|
||||
@@ -484,8 +484,14 @@ export class RemoteTableService {
|
||||
);
|
||||
|
||||
if (columnName === 'id') {
|
||||
await this.objectMetadataService.updateOne(objectMetadata.id, {
|
||||
labelIdentifierFieldMetadataId: field.id,
|
||||
await this.objectMetadataService.updateOneObject({
|
||||
workspaceId,
|
||||
updateObjectInput: {
|
||||
id: objectMetadata.id,
|
||||
update: {
|
||||
labelIdentifierFieldMetadataId: field.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -589,7 +595,10 @@ export class RemoteTableService {
|
||||
);
|
||||
}
|
||||
|
||||
await this.fieldMetadataService.deleteOne(fieldMetadataToDelete.id);
|
||||
await this.fieldMetadataService.deleteOneField({
|
||||
deleteOneFieldInput: { id: fieldMetadataToDelete.id },
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -598,18 +607,20 @@ export class RemoteTableService {
|
||||
columnName: string,
|
||||
columnType: string,
|
||||
objectMetadataId: string,
|
||||
): Promise<FieldMetadataEntity> {
|
||||
return this.fieldMetadataService.createOne({
|
||||
name: columnName,
|
||||
label: camelToTitleCase(columnName),
|
||||
description: 'Field of remote',
|
||||
type: mapUdtNameToFieldType(columnType),
|
||||
): Promise<FlatFieldMetadata> {
|
||||
return this.fieldMetadataService.createOneField({
|
||||
createFieldInput: {
|
||||
name: columnName,
|
||||
label: camelToTitleCase(columnName),
|
||||
description: 'Field of remote',
|
||||
type: mapUdtNameToFieldType(columnType),
|
||||
objectMetadataId: objectMetadataId,
|
||||
isRemoteCreation: true,
|
||||
isNullable: true,
|
||||
icon: 'IconPlug',
|
||||
settings: mapUdtNameToFieldSettings(columnType),
|
||||
},
|
||||
workspaceId: workspaceId,
|
||||
objectMetadataId: objectMetadataId,
|
||||
isRemoteCreation: true,
|
||||
isNullable: true,
|
||||
icon: 'IconPlug',
|
||||
settings: mapUdtNameToFieldSettings(columnType),
|
||||
} satisfies CreateFieldInput);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
|
||||
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { IndexMetadataModule } from 'src/engine/metadata-modules/index-metadata/index-metadata.module';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { SearchVectorService } from 'src/engine/metadata-modules/search-vector/search-vector.service';
|
||||
import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
NestjsQueryTypeOrmModule.forFeature([
|
||||
ObjectMetadataEntity,
|
||||
FieldMetadataEntity,
|
||||
]),
|
||||
IndexMetadataModule,
|
||||
WorkspaceMigrationModule,
|
||||
],
|
||||
providers: [SearchVectorService],
|
||||
exports: [SearchVectorService],
|
||||
})
|
||||
export class SearchVectorModule {}
|
||||
-191
@@ -1,191 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type QueryRunner, Repository } from 'typeorm';
|
||||
|
||||
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { IndexMetadataService } from 'src/engine/metadata-modules/index-metadata/index-metadata.service';
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
|
||||
import { type CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input';
|
||||
import { DEFAULT_LABEL_IDENTIFIER_FIELD_NAME } from 'src/engine/metadata-modules/object-metadata/object-metadata.constants';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import {
|
||||
TsVectorColumnActionFactory,
|
||||
type TsVectorFieldMetadata,
|
||||
} from 'src/engine/metadata-modules/workspace-migration/factories/ts-vector-column-action.factory';
|
||||
import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
|
||||
import {
|
||||
WorkspaceMigrationColumnActionType,
|
||||
WorkspaceMigrationTableActionType,
|
||||
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
|
||||
import { WorkspaceMigrationFactory } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.factory';
|
||||
import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service';
|
||||
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
|
||||
import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
|
||||
import { CUSTOM_OBJECT_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||
import {
|
||||
type FieldTypeAndNameMetadata,
|
||||
getTsVectorColumnExpressionFromFields,
|
||||
} from 'src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util';
|
||||
import { type SearchableFieldType } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/is-searchable-field.util';
|
||||
|
||||
@Injectable()
|
||||
export class SearchVectorService {
|
||||
constructor(
|
||||
@InjectRepository(ObjectMetadataEntity)
|
||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||
private readonly tsVectorColumnActionFactory: TsVectorColumnActionFactory,
|
||||
private readonly indexMetadataService: IndexMetadataService,
|
||||
@InjectRepository(FieldMetadataEntity)
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
private readonly workspaceMigrationService: WorkspaceMigrationService,
|
||||
private readonly workspaceMigrationFactory: WorkspaceMigrationFactory,
|
||||
) {}
|
||||
|
||||
public async createSearchVectorFieldForObject(
|
||||
objectMetadataInput: CreateObjectInput,
|
||||
createdObjectMetadata: ObjectMetadataEntity,
|
||||
queryRunner?: QueryRunner,
|
||||
) {
|
||||
const repository = queryRunner
|
||||
? queryRunner.manager.getRepository(FieldMetadataEntity)
|
||||
: this.fieldMetadataRepository;
|
||||
|
||||
const searchVectorFieldMetadata = await repository.save({
|
||||
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.searchVector,
|
||||
objectMetadataId: createdObjectMetadata.id,
|
||||
workspaceId: objectMetadataInput.workspaceId,
|
||||
isCustom: false,
|
||||
isActive: false,
|
||||
isSystem: true,
|
||||
type: FieldMetadataType.TS_VECTOR,
|
||||
name: SEARCH_VECTOR_FIELD.name,
|
||||
label: SEARCH_VECTOR_FIELD.label.message ?? '',
|
||||
description: SEARCH_VECTOR_FIELD.description.message ?? '',
|
||||
isNullable: true,
|
||||
});
|
||||
|
||||
if (
|
||||
!isFieldMetadataEntityOfType(
|
||||
searchVectorFieldMetadata,
|
||||
FieldMetadataType.TS_VECTOR,
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
'Should never occur, created searchVectorFieldMetadata is not a TS_VECTOR field',
|
||||
);
|
||||
}
|
||||
|
||||
const searchableFieldForCustomObject =
|
||||
createdObjectMetadata.labelIdentifierFieldMetadataId
|
||||
? createdObjectMetadata.fields.find(
|
||||
(field) =>
|
||||
field.id === createdObjectMetadata.labelIdentifierFieldMetadataId,
|
||||
)
|
||||
: createdObjectMetadata.fields.find(
|
||||
(field) => field.name === DEFAULT_LABEL_IDENTIFIER_FIELD_NAME,
|
||||
);
|
||||
|
||||
if (!isDefined(searchableFieldForCustomObject)) {
|
||||
throw new Error(
|
||||
`No searchable field found for custom object (object name: ${createdObjectMetadata.nameSingular})`,
|
||||
);
|
||||
}
|
||||
|
||||
await this.workspaceMigrationService.createCustomMigration(
|
||||
generateMigrationName(`create-${createdObjectMetadata.nameSingular}`),
|
||||
createdObjectMetadata.workspaceId,
|
||||
[
|
||||
{
|
||||
name: computeTableName(
|
||||
createdObjectMetadata.nameSingular,
|
||||
createdObjectMetadata.isCustom,
|
||||
),
|
||||
action: WorkspaceMigrationTableActionType.ALTER,
|
||||
columns: this.tsVectorColumnActionFactory.handleCreateAction({
|
||||
...searchVectorFieldMetadata,
|
||||
generatedType: 'STORED',
|
||||
asExpression: getTsVectorColumnExpressionFromFields([
|
||||
{
|
||||
type: searchableFieldForCustomObject.type as SearchableFieldType,
|
||||
name: searchableFieldForCustomObject.name,
|
||||
},
|
||||
]),
|
||||
}),
|
||||
},
|
||||
],
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
await this.indexMetadataService.createIndex({
|
||||
workspaceId: objectMetadataInput.workspaceId,
|
||||
objectMetadata: createdObjectMetadata,
|
||||
fieldMetadataToIndex: [searchVectorFieldMetadata],
|
||||
isUnique: false,
|
||||
isCustom: false,
|
||||
indexType: IndexType.GIN,
|
||||
queryRunner,
|
||||
});
|
||||
}
|
||||
|
||||
public async updateSearchVector(
|
||||
objectMetadataId: string,
|
||||
fieldMetadataNameAndTypeForSearch: FieldTypeAndNameMetadata[],
|
||||
workspaceId: string,
|
||||
queryRunner?: QueryRunner,
|
||||
) {
|
||||
const repository = queryRunner
|
||||
? queryRunner.manager.getRepository(ObjectMetadataEntity)
|
||||
: this.objectMetadataRepository;
|
||||
|
||||
const objectMetadata = await repository.findOneByOrFail({
|
||||
id: objectMetadataId,
|
||||
});
|
||||
|
||||
const existingSearchVectorFieldMetadata =
|
||||
await this.fieldMetadataRepository.findOneByOrFail({
|
||||
name: SEARCH_VECTOR_FIELD.name,
|
||||
objectMetadataId,
|
||||
});
|
||||
|
||||
await this.workspaceMigrationService.createCustomMigration(
|
||||
generateMigrationName(`update-${objectMetadata.nameSingular}`),
|
||||
workspaceId,
|
||||
[
|
||||
{
|
||||
name: computeTableName(
|
||||
objectMetadata.nameSingular,
|
||||
objectMetadata.isCustom,
|
||||
),
|
||||
action: WorkspaceMigrationTableActionType.ALTER,
|
||||
columns: this.workspaceMigrationFactory.createColumnActions(
|
||||
WorkspaceMigrationColumnActionType.ALTER,
|
||||
existingSearchVectorFieldMetadata,
|
||||
{
|
||||
...existingSearchVectorFieldMetadata,
|
||||
asExpression: getTsVectorColumnExpressionFromFields(
|
||||
fieldMetadataNameAndTypeForSearch,
|
||||
),
|
||||
generatedType: 'STORED', // Not stored on fieldMetadata
|
||||
options: null,
|
||||
} as TsVectorFieldMetadata,
|
||||
),
|
||||
},
|
||||
],
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
// index needs to be recreated as typeorm deletes then recreates searchVector column at alter
|
||||
await this.indexMetadataService.createIndexCreationMigration({
|
||||
workspaceId,
|
||||
objectMetadata,
|
||||
fieldMetadataToIndex: [existingSearchVectorFieldMetadata],
|
||||
isUnique: false,
|
||||
indexType: IndexType.GIN,
|
||||
queryRunner,
|
||||
});
|
||||
}
|
||||
}
|
||||
+14
-93
@@ -13,8 +13,6 @@ import {
|
||||
|
||||
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 { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
|
||||
@@ -32,7 +30,6 @@ import {
|
||||
} from 'src/engine/metadata-modules/view-field/exceptions/view-field.exception';
|
||||
import { ViewFieldRestApiExceptionFilter } from 'src/engine/metadata-modules/view-field/filters/view-field-rest-api-exception.filter';
|
||||
import { ViewFieldV2Service } from 'src/engine/metadata-modules/view-field/services/view-field-v2.service';
|
||||
import { ViewFieldService } from 'src/engine/metadata-modules/view-field/services/view-field.service';
|
||||
import { CreateViewFieldPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/create-view-field-permission.guard';
|
||||
import { DeleteViewFieldPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/delete-view-field-permission.guard';
|
||||
import { UpdateViewFieldPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/update-view-field-permission.guard';
|
||||
@@ -41,11 +38,7 @@ import { UpdateViewFieldPermissionGuard } from 'src/engine/metadata-modules/view
|
||||
@UseGuards(WorkspaceAuthGuard)
|
||||
@UseFilters(ViewFieldRestApiExceptionFilter)
|
||||
export class ViewFieldController {
|
||||
constructor(
|
||||
private readonly viewFieldService: ViewFieldService,
|
||||
private readonly viewFieldV2Service: ViewFieldV2Service,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
) {}
|
||||
constructor(private readonly viewFieldV2Service: ViewFieldV2Service) {}
|
||||
|
||||
@Get()
|
||||
@UseGuards(NoPermissionGuard)
|
||||
@@ -54,10 +47,10 @@ export class ViewFieldController {
|
||||
@Query('viewId') viewId?: string,
|
||||
): Promise<ViewFieldEntity[]> {
|
||||
if (viewId) {
|
||||
return this.viewFieldService.findByViewId(workspace.id, viewId);
|
||||
return this.viewFieldV2Service.findByViewId(workspace.id, viewId);
|
||||
}
|
||||
|
||||
return this.viewFieldService.findByWorkspaceId(workspace.id);
|
||||
return this.viewFieldV2Service.findByWorkspaceId(workspace.id);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@@ -66,7 +59,7 @@ export class ViewFieldController {
|
||||
@Param('id') id: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<ViewFieldEntity> {
|
||||
const viewField = await this.viewFieldService.findById(id, workspace.id);
|
||||
const viewField = await this.viewFieldV2Service.findById(id, workspace.id);
|
||||
|
||||
if (!isDefined(viewField)) {
|
||||
throw new ViewFieldException(
|
||||
@@ -93,39 +86,10 @@ export class ViewFieldController {
|
||||
@Body() input: UpdateViewFieldInput['update'],
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<ViewFieldDTO> {
|
||||
const isWorkspaceMigrationV2Enabled =
|
||||
await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
if (isWorkspaceMigrationV2Enabled) {
|
||||
return await this.viewFieldV2Service.updateOne({
|
||||
updateViewFieldInput: { id, update: input },
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
}
|
||||
|
||||
const updatedViewField = await this.viewFieldService.update(
|
||||
id,
|
||||
workspace.id,
|
||||
input,
|
||||
);
|
||||
|
||||
// Convert ViewFieldEntity to ViewFieldDTO for consistency
|
||||
return {
|
||||
id: updatedViewField.id,
|
||||
fieldMetadataId: updatedViewField.fieldMetadataId,
|
||||
isVisible: updatedViewField.isVisible,
|
||||
size: updatedViewField.size,
|
||||
position: updatedViewField.position,
|
||||
aggregateOperation: updatedViewField.aggregateOperation,
|
||||
viewId: updatedViewField.viewId,
|
||||
workspaceId: updatedViewField.workspaceId,
|
||||
createdAt: updatedViewField.createdAt,
|
||||
updatedAt: updatedViewField.updatedAt,
|
||||
deletedAt: updatedViewField.deletedAt,
|
||||
};
|
||||
return await this.viewFieldV2Service.updateOne({
|
||||
updateViewFieldInput: { id, update: input },
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
}
|
||||
|
||||
@Post()
|
||||
@@ -134,38 +98,10 @@ export class ViewFieldController {
|
||||
@Body() input: CreateViewFieldInput,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<ViewFieldDTO> {
|
||||
const isWorkspaceMigrationV2Enabled =
|
||||
await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
if (isWorkspaceMigrationV2Enabled) {
|
||||
return await this.viewFieldV2Service.createOne({
|
||||
createViewFieldInput: input,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
}
|
||||
|
||||
const createdViewField = await this.viewFieldService.create({
|
||||
...input,
|
||||
return await this.viewFieldV2Service.createOne({
|
||||
createViewFieldInput: input,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
// Convert ViewFieldEntity to ViewFieldDTO for consistency
|
||||
return {
|
||||
id: createdViewField.id,
|
||||
fieldMetadataId: createdViewField.fieldMetadataId,
|
||||
isVisible: createdViewField.isVisible,
|
||||
size: createdViewField.size,
|
||||
position: createdViewField.position,
|
||||
aggregateOperation: createdViewField.aggregateOperation,
|
||||
viewId: createdViewField.viewId,
|
||||
workspaceId: createdViewField.workspaceId,
|
||||
createdAt: createdViewField.createdAt,
|
||||
updatedAt: createdViewField.updatedAt,
|
||||
deletedAt: createdViewField.deletedAt,
|
||||
};
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@@ -174,25 +110,10 @@ export class ViewFieldController {
|
||||
@Param('id') id: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<{ success: boolean }> {
|
||||
const isWorkspaceMigrationV2Enabled =
|
||||
await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
if (isWorkspaceMigrationV2Enabled) {
|
||||
const deletedViewField = await this.viewFieldV2Service.deleteOne({
|
||||
deleteViewFieldInput: { id },
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
return { success: isDefined(deletedViewField) };
|
||||
}
|
||||
|
||||
const deletedViewField = await this.viewFieldService.delete(
|
||||
id,
|
||||
workspace.id,
|
||||
);
|
||||
const deletedViewField = await this.viewFieldV2Service.deleteOne({
|
||||
deleteViewFieldInput: { id },
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
return { success: isDefined(deletedViewField) };
|
||||
}
|
||||
|
||||
+16
-87
@@ -1,8 +1,6 @@
|
||||
import { UseFilters, UseGuards } from '@nestjs/common';
|
||||
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
||||
|
||||
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 { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
|
||||
@@ -13,12 +11,7 @@ import { DestroyViewFieldInput } from 'src/engine/metadata-modules/view-field/dt
|
||||
import { UpdateViewFieldInput } from 'src/engine/metadata-modules/view-field/dtos/inputs/update-view-field.input';
|
||||
import { ViewFieldDTO } from 'src/engine/metadata-modules/view-field/dtos/view-field.dto';
|
||||
import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
|
||||
import {
|
||||
ViewFieldException,
|
||||
ViewFieldExceptionCode,
|
||||
} from 'src/engine/metadata-modules/view-field/exceptions/view-field.exception';
|
||||
import { ViewFieldV2Service } from 'src/engine/metadata-modules/view-field/services/view-field-v2.service';
|
||||
import { ViewFieldService } from 'src/engine/metadata-modules/view-field/services/view-field.service';
|
||||
import { CreateViewFieldPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/create-view-field-permission.guard';
|
||||
import { DeleteViewFieldPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/delete-view-field-permission.guard';
|
||||
import { DestroyViewFieldPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/destroy-view-field-permission.guard';
|
||||
@@ -29,11 +22,7 @@ import { ViewGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/view/
|
||||
@UseFilters(ViewGraphqlApiExceptionFilter)
|
||||
@UseGuards(WorkspaceAuthGuard)
|
||||
export class ViewFieldResolver {
|
||||
constructor(
|
||||
private readonly viewFieldService: ViewFieldService,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
private readonly viewFieldV2Service: ViewFieldV2Service,
|
||||
) {}
|
||||
constructor(private readonly viewFieldV2Service: ViewFieldV2Service) {}
|
||||
|
||||
@Query(() => [ViewFieldDTO])
|
||||
@UseGuards(NoPermissionGuard)
|
||||
@@ -41,7 +30,7 @@ export class ViewFieldResolver {
|
||||
@Args('viewId', { type: () => String }) viewId: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<ViewFieldEntity[]> {
|
||||
return this.viewFieldService.findByViewId(workspace.id, viewId);
|
||||
return this.viewFieldV2Service.findByViewId(workspace.id, viewId);
|
||||
}
|
||||
|
||||
@Query(() => ViewFieldDTO, { nullable: true })
|
||||
@@ -50,7 +39,7 @@ export class ViewFieldResolver {
|
||||
@Args('id', { type: () => String }) id: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<ViewFieldEntity | null> {
|
||||
return this.viewFieldService.findById(id, workspace.id);
|
||||
return this.viewFieldV2Service.findById(id, workspace.id);
|
||||
}
|
||||
|
||||
@Mutation(() => ViewFieldDTO)
|
||||
@@ -59,24 +48,10 @@ export class ViewFieldResolver {
|
||||
@Args('input') updateViewFieldInput: UpdateViewFieldInput,
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
): Promise<ViewFieldDTO> {
|
||||
const isWorkspaceMigrationV2Enabled =
|
||||
await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (isWorkspaceMigrationV2Enabled) {
|
||||
return await this.viewFieldV2Service.updateOne({
|
||||
updateViewFieldInput,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
return this.viewFieldService.update(
|
||||
updateViewFieldInput.id,
|
||||
return await this.viewFieldV2Service.updateOne({
|
||||
updateViewFieldInput,
|
||||
workspaceId,
|
||||
updateViewFieldInput.update,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@Mutation(() => ViewFieldDTO)
|
||||
@@ -85,21 +60,8 @@ export class ViewFieldResolver {
|
||||
@Args('input') createViewFieldInput: CreateViewFieldInput,
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
): Promise<ViewFieldDTO> {
|
||||
const isWorkspaceMigrationV2Enabled =
|
||||
await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (isWorkspaceMigrationV2Enabled) {
|
||||
return await this.viewFieldV2Service.createOne({
|
||||
createViewFieldInput,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
return this.viewFieldService.create({
|
||||
...createViewFieldInput,
|
||||
return await this.viewFieldV2Service.createOne({
|
||||
createViewFieldInput,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
@@ -111,19 +73,6 @@ export class ViewFieldResolver {
|
||||
createViewFieldInputs: CreateViewFieldInput[],
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
): Promise<ViewFieldDTO[]> {
|
||||
const isWorkspaceMigrationV2Enabled =
|
||||
await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (!isWorkspaceMigrationV2Enabled) {
|
||||
throw new ViewFieldException(
|
||||
'Not implemented in v1, please active IS_WORKSPACE_MIGRATION_V2_ENABLED',
|
||||
ViewFieldExceptionCode.INVALID_VIEW_FIELD_DATA,
|
||||
);
|
||||
}
|
||||
|
||||
return await this.viewFieldV2Service.createMany({
|
||||
createViewFieldInputs,
|
||||
workspaceId,
|
||||
@@ -136,20 +85,10 @@ export class ViewFieldResolver {
|
||||
@Args('input') deleteViewFieldInput: DeleteViewFieldInput,
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
): Promise<ViewFieldDTO> {
|
||||
const isWorkspaceMigrationV2Enabled =
|
||||
await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (isWorkspaceMigrationV2Enabled) {
|
||||
return await this.viewFieldV2Service.deleteOne({
|
||||
deleteViewFieldInput,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
return this.viewFieldService.delete(deleteViewFieldInput.id, workspaceId);
|
||||
return await this.viewFieldV2Service.deleteOne({
|
||||
deleteViewFieldInput,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
@Mutation(() => ViewFieldDTO)
|
||||
@@ -158,19 +97,9 @@ export class ViewFieldResolver {
|
||||
@Args('input') destroyViewFieldInput: DestroyViewFieldInput,
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
): Promise<ViewFieldDTO> {
|
||||
const isWorkspaceMigrationV2Enabled =
|
||||
await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (isWorkspaceMigrationV2Enabled) {
|
||||
return await this.viewFieldV2Service.destroyOne({
|
||||
destroyViewFieldInput,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
return this.viewFieldService.destroy(destroyViewFieldInput.id, workspaceId);
|
||||
return await this.viewFieldV2Service.destroyOne({
|
||||
destroyViewFieldInput,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
-532
@@ -1,532 +0,0 @@
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
import { UserInputError } from 'apollo-server-core';
|
||||
import { type Repository } from 'typeorm';
|
||||
|
||||
import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
|
||||
import {
|
||||
ViewFieldException,
|
||||
ViewFieldExceptionCode,
|
||||
ViewFieldExceptionMessageKey,
|
||||
generateViewFieldExceptionMessage,
|
||||
generateViewFieldUserFriendlyExceptionMessage,
|
||||
} from 'src/engine/metadata-modules/view-field/exceptions/view-field.exception';
|
||||
import { ViewFieldService } from 'src/engine/metadata-modules/view-field/services/view-field.service';
|
||||
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
|
||||
describe('ViewFieldService', () => {
|
||||
let viewFieldService: ViewFieldService;
|
||||
let viewFieldRepository: Repository<ViewFieldEntity>;
|
||||
|
||||
const mockViewField = {
|
||||
id: 'view-field-id',
|
||||
fieldMetadataId: 'field-id',
|
||||
viewId: 'view-id',
|
||||
workspaceId: 'workspace-id',
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
size: 100,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
deletedAt: null,
|
||||
} as ViewFieldEntity;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
ViewFieldService,
|
||||
{
|
||||
provide: getRepositoryToken(ViewFieldEntity),
|
||||
useValue: {
|
||||
find: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
softDelete: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(ViewEntity),
|
||||
useValue: {
|
||||
findOne: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: WorkspaceCacheStorageService,
|
||||
useValue: {
|
||||
flushGraphQLOperation: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
viewFieldService = module.get<ViewFieldService>(ViewFieldService);
|
||||
viewFieldRepository = module.get<Repository<ViewFieldEntity>>(
|
||||
getRepositoryToken(ViewFieldEntity),
|
||||
);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(viewFieldService).toBeDefined();
|
||||
});
|
||||
|
||||
describe('findByWorkspaceId', () => {
|
||||
it('should return view fields for a workspace', async () => {
|
||||
const workspaceId = 'workspace-id';
|
||||
const expectedViewFields = [mockViewField];
|
||||
|
||||
jest
|
||||
.spyOn(viewFieldRepository, 'find')
|
||||
.mockResolvedValue(expectedViewFields);
|
||||
|
||||
const result = await viewFieldService.findByWorkspaceId(workspaceId);
|
||||
|
||||
expect(viewFieldRepository.find).toHaveBeenCalledWith({
|
||||
where: {
|
||||
workspaceId,
|
||||
deletedAt: expect.anything(),
|
||||
},
|
||||
order: { position: 'ASC' },
|
||||
relations: ['workspace', 'view'],
|
||||
});
|
||||
expect(result).toEqual(expectedViewFields);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findByViewId', () => {
|
||||
it('should return view fields for a view', async () => {
|
||||
const workspaceId = 'workspace-id';
|
||||
const viewId = 'view-id';
|
||||
const expectedViewFields = [mockViewField];
|
||||
|
||||
jest
|
||||
.spyOn(viewFieldRepository, 'find')
|
||||
.mockResolvedValue(expectedViewFields);
|
||||
|
||||
const result = await viewFieldService.findByViewId(workspaceId, viewId);
|
||||
|
||||
expect(viewFieldRepository.find).toHaveBeenCalledWith({
|
||||
where: {
|
||||
workspaceId,
|
||||
viewId,
|
||||
deletedAt: expect.anything(),
|
||||
},
|
||||
order: { position: 'ASC' },
|
||||
relations: ['workspace', 'view'],
|
||||
});
|
||||
expect(result).toEqual(expectedViewFields);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findById', () => {
|
||||
it('should return a view field by id', async () => {
|
||||
const id = 'view-field-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
|
||||
jest
|
||||
.spyOn(viewFieldRepository, 'findOne')
|
||||
.mockResolvedValue(mockViewField);
|
||||
|
||||
const result = await viewFieldService.findById(id, workspaceId);
|
||||
|
||||
expect(viewFieldRepository.findOne).toHaveBeenCalledWith({
|
||||
where: {
|
||||
id,
|
||||
workspaceId,
|
||||
deletedAt: expect.anything(),
|
||||
},
|
||||
relations: ['workspace', 'view'],
|
||||
});
|
||||
expect(result).toEqual(mockViewField);
|
||||
});
|
||||
|
||||
it('should return null when view field is not found', async () => {
|
||||
const id = 'non-existent-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
|
||||
jest.spyOn(viewFieldRepository, 'findOne').mockResolvedValue(null);
|
||||
|
||||
const result = await viewFieldService.findById(id, workspaceId);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
const validViewFieldData = {
|
||||
fieldMetadataId: 'field-id',
|
||||
viewId: 'view-id',
|
||||
workspaceId: 'workspace-id',
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
size: 100,
|
||||
};
|
||||
|
||||
it('should throw exception when workspaceId is missing', async () => {
|
||||
const invalidData = { ...validViewFieldData, workspaceId: undefined };
|
||||
|
||||
await expect(viewFieldService.create(invalidData)).rejects.toThrow(
|
||||
new ViewFieldException(
|
||||
generateViewFieldExceptionMessage(
|
||||
ViewFieldExceptionMessageKey.WORKSPACE_ID_REQUIRED,
|
||||
),
|
||||
ViewFieldExceptionCode.INVALID_VIEW_FIELD_DATA,
|
||||
{
|
||||
userFriendlyMessage: generateViewFieldUserFriendlyExceptionMessage(
|
||||
ViewFieldExceptionMessageKey.WORKSPACE_ID_REQUIRED,
|
||||
),
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw exception when viewId is missing', async () => {
|
||||
const invalidData = { ...validViewFieldData, viewId: undefined };
|
||||
|
||||
await expect(viewFieldService.create(invalidData)).rejects.toThrow(
|
||||
new ViewFieldException(
|
||||
generateViewFieldExceptionMessage(
|
||||
ViewFieldExceptionMessageKey.VIEW_ID_REQUIRED,
|
||||
),
|
||||
ViewFieldExceptionCode.INVALID_VIEW_FIELD_DATA,
|
||||
{
|
||||
userFriendlyMessage: generateViewFieldUserFriendlyExceptionMessage(
|
||||
ViewFieldExceptionMessageKey.VIEW_ID_REQUIRED,
|
||||
),
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw exception when fieldMetadataId is missing', async () => {
|
||||
const invalidData = { ...validViewFieldData, fieldMetadataId: undefined };
|
||||
|
||||
await expect(viewFieldService.create(invalidData)).rejects.toThrow(
|
||||
new ViewFieldException(
|
||||
generateViewFieldExceptionMessage(
|
||||
ViewFieldExceptionMessageKey.FIELD_METADATA_ID_REQUIRED,
|
||||
),
|
||||
ViewFieldExceptionCode.INVALID_VIEW_FIELD_DATA,
|
||||
{
|
||||
userFriendlyMessage: generateViewFieldUserFriendlyExceptionMessage(
|
||||
ViewFieldExceptionMessageKey.FIELD_METADATA_ID_REQUIRED,
|
||||
),
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw exception if position is lower than label metadata identifier', async () => {
|
||||
const labelIdentifierFieldMetadataId =
|
||||
'label-identifier-field-matadata-id';
|
||||
const labelIdentifierViewFieldId =
|
||||
'view-field-for-label-metadata-identifier-id';
|
||||
|
||||
const labelIdentifierViewField = {
|
||||
...mockViewField,
|
||||
id: labelIdentifierViewFieldId,
|
||||
fieldMetadataId: labelIdentifierFieldMetadataId,
|
||||
position: 0,
|
||||
};
|
||||
|
||||
const mockView = {
|
||||
id: 'view-id',
|
||||
objectMetadata: {
|
||||
labelIdentifierFieldMetadataId,
|
||||
},
|
||||
viewFields: [
|
||||
labelIdentifierViewField,
|
||||
{ ...mockViewField, position: 1 },
|
||||
],
|
||||
} as ViewEntity;
|
||||
|
||||
jest.spyOn(viewFieldService, 'findById').mockImplementation((id) => {
|
||||
if (id === mockViewField.id) {
|
||||
return Promise.resolve(mockViewField);
|
||||
}
|
||||
|
||||
return Promise.resolve(null);
|
||||
});
|
||||
jest
|
||||
.spyOn(viewFieldService, 'findViewByIdWithRelations')
|
||||
.mockResolvedValue(mockView);
|
||||
|
||||
const invalidData = { ...validViewFieldData, position: -1 };
|
||||
|
||||
await expect(viewFieldService.create(invalidData)).rejects.toThrow(
|
||||
new UserInputError(
|
||||
'Label metadata identifier must keep the minimal position in the view.',
|
||||
{
|
||||
userFriendlyMessage:
|
||||
'Record text must be in first position of the view.',
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update a view field successfully', async () => {
|
||||
const id = 'view-field-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
const updateData = { position: 1 };
|
||||
const updatedViewField = { ...mockViewField, ...updateData };
|
||||
|
||||
const mockView = {
|
||||
id: 'view-id',
|
||||
objectMetadata: {
|
||||
labelIdentifierFieldMetadataId: mockViewField.fieldMetadataId,
|
||||
},
|
||||
viewFields: [mockViewField],
|
||||
} as ViewEntity;
|
||||
|
||||
jest.spyOn(viewFieldService, 'findById').mockResolvedValue(mockViewField);
|
||||
jest
|
||||
.spyOn(viewFieldRepository, 'save')
|
||||
.mockResolvedValue(updatedViewField);
|
||||
jest
|
||||
.spyOn(viewFieldService, 'findViewByIdWithRelations')
|
||||
.mockResolvedValue(mockView);
|
||||
|
||||
const result = await viewFieldService.update(id, workspaceId, updateData);
|
||||
|
||||
expect(viewFieldService.findById).toHaveBeenCalledWith(id, workspaceId);
|
||||
expect(viewFieldRepository.save).toHaveBeenCalledWith({
|
||||
id,
|
||||
...updateData,
|
||||
});
|
||||
expect(result).toEqual({ ...mockViewField, ...updatedViewField });
|
||||
});
|
||||
|
||||
it('should throw exception when view field is not found', async () => {
|
||||
const id = 'non-existent-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
const updateData = { position: 1 };
|
||||
|
||||
jest.spyOn(viewFieldService, 'findById').mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
viewFieldService.update(id, workspaceId, updateData),
|
||||
).rejects.toThrow(
|
||||
new ViewFieldException(
|
||||
generateViewFieldExceptionMessage(
|
||||
ViewFieldExceptionMessageKey.VIEW_FIELD_NOT_FOUND,
|
||||
id,
|
||||
),
|
||||
ViewFieldExceptionCode.VIEW_FIELD_NOT_FOUND,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw exception when label metadata identifier is not in first position (label metadata identifier field update case)', async () => {
|
||||
const workspaceId = 'workspace-id';
|
||||
const updateData = { position: 2 };
|
||||
const labelIdentifierFieldMetadataId =
|
||||
'label-identifier-field-matadata-id';
|
||||
const labelIdentifierViewFieldId =
|
||||
'view-field-for-label-metadata-identifier-id';
|
||||
|
||||
const labelIdentifierViewField = {
|
||||
...mockViewField,
|
||||
id: labelIdentifierViewFieldId,
|
||||
fieldMetadataId: labelIdentifierFieldMetadataId,
|
||||
position: 0,
|
||||
};
|
||||
|
||||
const mockView = {
|
||||
id: 'view-id',
|
||||
objectMetadata: {
|
||||
labelIdentifierFieldMetadataId,
|
||||
},
|
||||
viewFields: [
|
||||
labelIdentifierViewField,
|
||||
{ ...mockViewField, position: 1 },
|
||||
],
|
||||
} as ViewEntity;
|
||||
|
||||
jest.spyOn(viewFieldService, 'findById').mockImplementation((id) => {
|
||||
if (id === labelIdentifierViewFieldId) {
|
||||
return Promise.resolve(labelIdentifierViewField);
|
||||
}
|
||||
|
||||
return Promise.resolve(null);
|
||||
});
|
||||
jest
|
||||
.spyOn(viewFieldService, 'findViewByIdWithRelations')
|
||||
.mockResolvedValue(mockView);
|
||||
|
||||
await expect(
|
||||
viewFieldService.update(
|
||||
labelIdentifierViewFieldId,
|
||||
workspaceId,
|
||||
updateData,
|
||||
),
|
||||
).rejects.toThrow(
|
||||
new UserInputError(
|
||||
'Label metadata identifier must keep the minimal position in the view.',
|
||||
{
|
||||
userFriendlyMessage:
|
||||
'Record text must be in first position of the view.',
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw exception when label metadata identifier is not in first position (regular field update case)', async () => {
|
||||
const workspaceId = 'workspace-id';
|
||||
const updateData = { position: -1 };
|
||||
const labelIdentifierFieldMetadataId =
|
||||
'label-identifier-field-matadata-id';
|
||||
const labelIdentifierViewFieldId =
|
||||
'view-field-for-label-metadata-identifier-id';
|
||||
|
||||
const labelIdentifierViewField = {
|
||||
...mockViewField,
|
||||
id: labelIdentifierViewFieldId,
|
||||
fieldMetadataId: labelIdentifierFieldMetadataId,
|
||||
position: 0,
|
||||
};
|
||||
|
||||
const mockView = {
|
||||
id: 'view-id',
|
||||
objectMetadata: {
|
||||
labelIdentifierFieldMetadataId,
|
||||
},
|
||||
viewFields: [
|
||||
labelIdentifierViewField,
|
||||
{ ...mockViewField, position: 1 },
|
||||
],
|
||||
} as ViewEntity;
|
||||
|
||||
jest.spyOn(viewFieldService, 'findById').mockImplementation((id) => {
|
||||
if (id === mockViewField.id) {
|
||||
return Promise.resolve(mockViewField);
|
||||
}
|
||||
|
||||
return Promise.resolve(null);
|
||||
});
|
||||
jest
|
||||
.spyOn(viewFieldService, 'findViewByIdWithRelations')
|
||||
.mockResolvedValue(mockView);
|
||||
|
||||
await expect(
|
||||
viewFieldService.update(mockViewField.id, workspaceId, updateData),
|
||||
).rejects.toThrow(
|
||||
new UserInputError(
|
||||
'Label metadata identifier must keep the minimal position in the view.',
|
||||
{
|
||||
userFriendlyMessage:
|
||||
'Record text must be in first position of the view.',
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw exception when attempting to make label metadata identifier invisible', async () => {
|
||||
const workspaceId = 'workspace-id';
|
||||
const updateData = { isVisible: false };
|
||||
const labelIdentifierFieldMetadataId =
|
||||
'label-identifier-field-matadata-id';
|
||||
const labelIdentifierViewFieldId =
|
||||
'view-field-for-label-metadata-identifier-id';
|
||||
|
||||
const labelIdentifierViewField = {
|
||||
...mockViewField,
|
||||
id: labelIdentifierViewFieldId,
|
||||
fieldMetadataId: labelIdentifierFieldMetadataId,
|
||||
position: 0,
|
||||
};
|
||||
|
||||
const mockView = {
|
||||
id: 'view-id',
|
||||
objectMetadata: {
|
||||
labelIdentifierFieldMetadataId,
|
||||
},
|
||||
viewFields: [labelIdentifierViewField, mockViewField],
|
||||
} as ViewEntity;
|
||||
|
||||
jest.spyOn(viewFieldService, 'findById').mockImplementation((id) => {
|
||||
if (id === labelIdentifierViewFieldId) {
|
||||
return Promise.resolve(labelIdentifierViewField);
|
||||
}
|
||||
|
||||
return Promise.resolve(null);
|
||||
});
|
||||
jest
|
||||
.spyOn(viewFieldService, 'findViewByIdWithRelations')
|
||||
.mockResolvedValue(mockView);
|
||||
|
||||
await expect(
|
||||
viewFieldService.update(
|
||||
labelIdentifierViewField.id,
|
||||
workspaceId,
|
||||
updateData,
|
||||
),
|
||||
).rejects.toThrow(
|
||||
new UserInputError('Label metadata identifier must stay visible.', {
|
||||
userFriendlyMessage: 'Record text must stay visible.',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('should delete a view field successfully', async () => {
|
||||
const id = 'view-field-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
|
||||
jest.spyOn(viewFieldService, 'findById').mockResolvedValue(mockViewField);
|
||||
jest
|
||||
.spyOn(viewFieldRepository, 'softDelete')
|
||||
.mockResolvedValue({} as any);
|
||||
|
||||
const result = await viewFieldService.delete(id, workspaceId);
|
||||
|
||||
expect(viewFieldService.findById).toHaveBeenCalledWith(id, workspaceId);
|
||||
expect(viewFieldRepository.softDelete).toHaveBeenCalledWith(id);
|
||||
expect(result).toEqual(mockViewField);
|
||||
});
|
||||
|
||||
it('should throw exception when view field is not found', async () => {
|
||||
const id = 'non-existent-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
|
||||
jest.spyOn(viewFieldService, 'findById').mockResolvedValue(null);
|
||||
|
||||
await expect(viewFieldService.delete(id, workspaceId)).rejects.toThrow(
|
||||
new ViewFieldException(
|
||||
generateViewFieldExceptionMessage(
|
||||
ViewFieldExceptionMessageKey.VIEW_FIELD_NOT_FOUND,
|
||||
id,
|
||||
),
|
||||
ViewFieldExceptionCode.VIEW_FIELD_NOT_FOUND,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('destroy', () => {
|
||||
it('should destroy a view field successfully', async () => {
|
||||
const id = 'view-field-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
|
||||
jest
|
||||
.spyOn(viewFieldRepository, 'findOne')
|
||||
.mockResolvedValue(mockViewField);
|
||||
jest.spyOn(viewFieldRepository, 'delete').mockResolvedValue({} as any);
|
||||
|
||||
const result = await viewFieldService.destroy(id, workspaceId);
|
||||
|
||||
expect(viewFieldRepository.findOne).toHaveBeenCalledWith({
|
||||
where: { id, workspaceId },
|
||||
relations: ['workspace', 'view'],
|
||||
withDeleted: true,
|
||||
});
|
||||
expect(viewFieldRepository.delete).toHaveBeenCalledWith(id);
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
+55
-8
@@ -1,6 +1,8 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IsNull, Repository } from 'typeorm';
|
||||
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { computeFlatEntityMapsFromTo } from 'src/engine/metadata-modules/flat-entity/utils/compute-flat-entity-maps-from-to.util';
|
||||
@@ -15,6 +17,7 @@ import { DeleteViewFieldInput } from 'src/engine/metadata-modules/view-field/dto
|
||||
import { DestroyViewFieldInput } from 'src/engine/metadata-modules/view-field/dtos/inputs/destroy-view-field.input';
|
||||
import { UpdateViewFieldInput } from 'src/engine/metadata-modules/view-field/dtos/inputs/update-view-field.input';
|
||||
import { ViewFieldDTO } from 'src/engine/metadata-modules/view-field/dtos/view-field.dto';
|
||||
import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
|
||||
import {
|
||||
ViewFieldException,
|
||||
ViewFieldExceptionCode,
|
||||
@@ -26,7 +29,9 @@ import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspa
|
||||
export class ViewFieldV2Service {
|
||||
constructor(
|
||||
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
||||
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
@InjectRepository(ViewFieldEntity)
|
||||
private readonly viewFieldRepository: Repository<ViewFieldEntity>,
|
||||
) {}
|
||||
|
||||
async createOne({
|
||||
@@ -67,7 +72,7 @@ export class ViewFieldV2Service {
|
||||
flatViewMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
} = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
} = await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: [
|
||||
@@ -118,7 +123,7 @@ export class ViewFieldV2Service {
|
||||
}
|
||||
|
||||
const { flatViewFieldMaps: recomputedExistingFlatViewFieldMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatViewFieldMaps'],
|
||||
@@ -142,7 +147,7 @@ export class ViewFieldV2Service {
|
||||
flatViewFieldMaps: existingFlatViewFieldMaps,
|
||||
flatObjectMetadataMaps,
|
||||
flatViewMaps,
|
||||
} = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
} = await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: [
|
||||
@@ -189,7 +194,7 @@ export class ViewFieldV2Service {
|
||||
}
|
||||
|
||||
const { flatViewFieldMaps: recomputedExistingFlatViewFieldMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatViewFieldMaps'],
|
||||
@@ -213,7 +218,7 @@ export class ViewFieldV2Service {
|
||||
flatViewFieldMaps: existingFlatViewFieldMaps,
|
||||
flatObjectMetadataMaps,
|
||||
flatViewMaps,
|
||||
} = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
} = await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: [
|
||||
@@ -260,7 +265,7 @@ export class ViewFieldV2Service {
|
||||
}
|
||||
|
||||
const { flatViewFieldMaps: recomputedExistingFlatViewFieldMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatViewFieldMaps'],
|
||||
@@ -285,7 +290,7 @@ export class ViewFieldV2Service {
|
||||
flatViewMaps: existingFlatViewMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
} = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
} = await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: [
|
||||
@@ -338,4 +343,46 @@ export class ViewFieldV2Service {
|
||||
|
||||
return existingViewFieldToDelete;
|
||||
}
|
||||
|
||||
async findByWorkspaceId(workspaceId: string): Promise<ViewFieldEntity[]> {
|
||||
return this.viewFieldRepository.find({
|
||||
where: {
|
||||
workspaceId,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
order: { position: 'ASC' },
|
||||
relations: ['workspace', 'view'],
|
||||
});
|
||||
}
|
||||
|
||||
async findByViewId(
|
||||
workspaceId: string,
|
||||
viewId: string,
|
||||
): Promise<ViewFieldEntity[]> {
|
||||
return this.viewFieldRepository.find({
|
||||
where: {
|
||||
workspaceId,
|
||||
viewId,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
order: { position: 'ASC' },
|
||||
relations: ['workspace', 'view'],
|
||||
});
|
||||
}
|
||||
|
||||
async findById(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
): Promise<ViewFieldEntity | null> {
|
||||
const viewField = await this.viewFieldRepository.findOne({
|
||||
where: {
|
||||
id,
|
||||
workspaceId,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
relations: ['workspace', 'view'],
|
||||
});
|
||||
|
||||
return viewField || null;
|
||||
}
|
||||
}
|
||||
|
||||
-469
@@ -1,469 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IsNull, Repository } from 'typeorm';
|
||||
|
||||
import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
|
||||
import {
|
||||
ViewFieldException,
|
||||
ViewFieldExceptionCode,
|
||||
ViewFieldExceptionMessageKey,
|
||||
generateViewFieldExceptionMessage,
|
||||
generateViewFieldUserFriendlyExceptionMessage,
|
||||
} from 'src/engine/metadata-modules/view-field/exceptions/view-field.exception';
|
||||
import { FIND_ALL_CORE_VIEWS_GRAPHQL_OPERATION } from 'src/engine/metadata-modules/view/constants/find-all-core-views-graphql-operation.constant';
|
||||
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
|
||||
@Injectable()
|
||||
export class ViewFieldService {
|
||||
constructor(
|
||||
@InjectRepository(ViewFieldEntity)
|
||||
private readonly viewFieldRepository: Repository<ViewFieldEntity>,
|
||||
@InjectRepository(ViewEntity)
|
||||
private readonly viewRepository: Repository<ViewEntity>,
|
||||
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
|
||||
) {}
|
||||
|
||||
async findByWorkspaceId(workspaceId: string): Promise<ViewFieldEntity[]> {
|
||||
return this.viewFieldRepository.find({
|
||||
where: {
|
||||
workspaceId,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
order: { position: 'ASC' },
|
||||
relations: ['workspace', 'view'],
|
||||
});
|
||||
}
|
||||
|
||||
async findByViewId(
|
||||
workspaceId: string,
|
||||
viewId: string,
|
||||
): Promise<ViewFieldEntity[]> {
|
||||
return this.viewFieldRepository.find({
|
||||
where: {
|
||||
workspaceId,
|
||||
viewId,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
order: { position: 'ASC' },
|
||||
relations: ['workspace', 'view'],
|
||||
});
|
||||
}
|
||||
|
||||
async findById(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
): Promise<ViewFieldEntity | null> {
|
||||
const viewField = await this.viewFieldRepository.findOne({
|
||||
where: {
|
||||
id,
|
||||
workspaceId,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
relations: ['workspace', 'view'],
|
||||
});
|
||||
|
||||
return viewField || null;
|
||||
}
|
||||
|
||||
async create(
|
||||
viewFieldData: Partial<ViewFieldEntity>,
|
||||
): Promise<ViewFieldEntity> {
|
||||
if (this.hasRequiredFields(viewFieldData)) {
|
||||
try {
|
||||
if (isDefined(viewFieldData.position)) {
|
||||
const viewFieldDataWithPosition =
|
||||
viewFieldData as typeof viewFieldData & { position: number };
|
||||
|
||||
await this.verifyLabelMetadataIdentifierIsInFirstPositionOrThrow(
|
||||
viewFieldDataWithPosition,
|
||||
viewFieldData.workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
await this.verifyLabelMetadataIdentifierIsVisibleOrThrow(
|
||||
viewFieldData,
|
||||
viewFieldData.workspaceId,
|
||||
);
|
||||
|
||||
const viewFieldDataWithPosition = await this.formatViewFieldData(
|
||||
viewFieldData,
|
||||
viewFieldData.workspaceId,
|
||||
);
|
||||
|
||||
const viewField = this.viewFieldRepository.create(
|
||||
viewFieldDataWithPosition,
|
||||
);
|
||||
|
||||
const savedViewField = await this.viewFieldRepository.save(viewField);
|
||||
|
||||
await this.flushGraphQLCache(viewFieldData.workspaceId);
|
||||
|
||||
const createdViewField = await this.findById(
|
||||
savedViewField.id,
|
||||
viewFieldData.workspaceId,
|
||||
);
|
||||
|
||||
if (!isDefined(createdViewField)) {
|
||||
throw new ViewFieldException(
|
||||
generateViewFieldExceptionMessage(
|
||||
ViewFieldExceptionMessageKey.VIEW_FIELD_NOT_FOUND,
|
||||
),
|
||||
ViewFieldExceptionCode.VIEW_FIELD_NOT_FOUND,
|
||||
{
|
||||
userFriendlyMessage:
|
||||
generateViewFieldUserFriendlyExceptionMessage(
|
||||
ViewFieldExceptionMessageKey.VIEW_FIELD_NOT_FOUND,
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return createdViewField;
|
||||
} catch (error) {
|
||||
if (
|
||||
error.message.includes(
|
||||
'duplicate key value violates unique constraint',
|
||||
)
|
||||
) {
|
||||
throw new ViewFieldException(
|
||||
generateViewFieldExceptionMessage(
|
||||
ViewFieldExceptionMessageKey.VIEW_FIELD_ALREADY_EXISTS,
|
||||
),
|
||||
ViewFieldExceptionCode.INVALID_VIEW_FIELD_DATA,
|
||||
{
|
||||
userFriendlyMessage:
|
||||
generateViewFieldUserFriendlyExceptionMessage(
|
||||
ViewFieldExceptionMessageKey.VIEW_FIELD_ALREADY_EXISTS,
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
if (!isDefined(viewFieldData.workspaceId)) {
|
||||
throw new ViewFieldException(
|
||||
generateViewFieldExceptionMessage(
|
||||
ViewFieldExceptionMessageKey.WORKSPACE_ID_REQUIRED,
|
||||
),
|
||||
ViewFieldExceptionCode.INVALID_VIEW_FIELD_DATA,
|
||||
{
|
||||
userFriendlyMessage: generateViewFieldUserFriendlyExceptionMessage(
|
||||
ViewFieldExceptionMessageKey.WORKSPACE_ID_REQUIRED,
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (!isDefined(viewFieldData.viewId)) {
|
||||
throw new ViewFieldException(
|
||||
generateViewFieldExceptionMessage(
|
||||
ViewFieldExceptionMessageKey.VIEW_ID_REQUIRED,
|
||||
),
|
||||
ViewFieldExceptionCode.INVALID_VIEW_FIELD_DATA,
|
||||
{
|
||||
userFriendlyMessage: generateViewFieldUserFriendlyExceptionMessage(
|
||||
ViewFieldExceptionMessageKey.VIEW_ID_REQUIRED,
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
throw new ViewFieldException(
|
||||
generateViewFieldExceptionMessage(
|
||||
ViewFieldExceptionMessageKey.FIELD_METADATA_ID_REQUIRED,
|
||||
),
|
||||
ViewFieldExceptionCode.INVALID_VIEW_FIELD_DATA,
|
||||
{
|
||||
userFriendlyMessage: generateViewFieldUserFriendlyExceptionMessage(
|
||||
ViewFieldExceptionMessageKey.FIELD_METADATA_ID_REQUIRED,
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async update(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
updateData: Partial<ViewFieldEntity>,
|
||||
): Promise<ViewFieldEntity> {
|
||||
const existingViewField = await this.findById(id, workspaceId);
|
||||
|
||||
if (!isDefined(existingViewField)) {
|
||||
throw new ViewFieldException(
|
||||
generateViewFieldExceptionMessage(
|
||||
ViewFieldExceptionMessageKey.VIEW_FIELD_NOT_FOUND,
|
||||
id,
|
||||
),
|
||||
ViewFieldExceptionCode.VIEW_FIELD_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const viewId = existingViewField.viewId;
|
||||
|
||||
if (this.updatesPosition(updateData)) {
|
||||
await this.verifyLabelMetadataIdentifierIsInFirstPositionOrThrow(
|
||||
{
|
||||
...updateData,
|
||||
viewId,
|
||||
id,
|
||||
fieldMetadataId: existingViewField.fieldMetadataId,
|
||||
},
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
if (this.disablesVisibility(updateData)) {
|
||||
await this.verifyLabelMetadataIdentifierIsVisibleOrThrow(
|
||||
{ ...updateData, viewId, id },
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
const updatedViewField = await this.viewFieldRepository.save({
|
||||
id,
|
||||
...updateData,
|
||||
});
|
||||
|
||||
await this.flushGraphQLCache(workspaceId);
|
||||
|
||||
return { ...existingViewField, ...updatedViewField };
|
||||
}
|
||||
|
||||
async delete(id: string, workspaceId: string): Promise<ViewFieldEntity> {
|
||||
const viewField = await this.findById(id, workspaceId);
|
||||
|
||||
if (!isDefined(viewField)) {
|
||||
throw new ViewFieldException(
|
||||
generateViewFieldExceptionMessage(
|
||||
ViewFieldExceptionMessageKey.VIEW_FIELD_NOT_FOUND,
|
||||
id,
|
||||
),
|
||||
ViewFieldExceptionCode.VIEW_FIELD_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
await this.viewFieldRepository.softDelete(id);
|
||||
|
||||
await this.flushGraphQLCache(workspaceId);
|
||||
|
||||
return viewField;
|
||||
}
|
||||
|
||||
async destroy(id: string, workspaceId: string): Promise<ViewFieldEntity> {
|
||||
const viewField = await this.viewFieldRepository.findOne({
|
||||
where: {
|
||||
id,
|
||||
workspaceId,
|
||||
},
|
||||
relations: ['workspace', 'view'],
|
||||
withDeleted: true,
|
||||
});
|
||||
|
||||
if (!isDefined(viewField)) {
|
||||
throw new ViewFieldException(
|
||||
generateViewFieldExceptionMessage(
|
||||
ViewFieldExceptionMessageKey.VIEW_FIELD_NOT_FOUND,
|
||||
id,
|
||||
),
|
||||
ViewFieldExceptionCode.VIEW_FIELD_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
await this.viewFieldRepository.delete(id);
|
||||
|
||||
await this.flushGraphQLCache(workspaceId);
|
||||
|
||||
return viewField;
|
||||
}
|
||||
|
||||
private updatesPosition(
|
||||
data: Partial<ViewFieldEntity>,
|
||||
): data is Partial<ViewFieldEntity> & { position: number } {
|
||||
return isDefined(data.position);
|
||||
}
|
||||
|
||||
private disablesVisibility(
|
||||
data: Partial<ViewFieldEntity>,
|
||||
): data is Partial<ViewFieldEntity> & { isVisible: boolean } {
|
||||
return data.isVisible === false;
|
||||
}
|
||||
|
||||
private async verifyLabelMetadataIdentifierIsVisibleOrThrow(
|
||||
newOrUpdatedViewField: Partial<ViewFieldEntity> & {
|
||||
viewId: string;
|
||||
},
|
||||
workspaceId: string,
|
||||
) {
|
||||
const view = await this.findViewByIdWithRelations(
|
||||
newOrUpdatedViewField.viewId,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (!isDefined(view)) {
|
||||
throw new Error(`View not found: ${newOrUpdatedViewField.viewId}`);
|
||||
}
|
||||
|
||||
const labelMetadataIdentifierFieldMetadataId =
|
||||
view.objectMetadata.labelIdentifierFieldMetadataId;
|
||||
|
||||
const labelMetadataIdentifierViewField = view.viewFields.find(
|
||||
(viewField) =>
|
||||
viewField.fieldMetadataId === labelMetadataIdentifierFieldMetadataId,
|
||||
);
|
||||
|
||||
if (
|
||||
!isDefined(labelMetadataIdentifierViewField) ||
|
||||
labelMetadataIdentifierViewField.id !== newOrUpdatedViewField.id
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (newOrUpdatedViewField.isVisible === false) {
|
||||
throw new UserInputError('Label metadata identifier must stay visible.', {
|
||||
userFriendlyMessage: msg`Record text must stay visible.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async verifyLabelMetadataIdentifierIsInFirstPositionOrThrow(
|
||||
newOrUpdatedViewField: Partial<ViewFieldEntity> & { viewId: string } & {
|
||||
position: number;
|
||||
},
|
||||
workspaceId: string,
|
||||
) {
|
||||
const view = await this.findViewByIdWithRelations(
|
||||
newOrUpdatedViewField.viewId,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (!isDefined(view)) {
|
||||
throw new Error(`View not found: ${newOrUpdatedViewField.viewId}`);
|
||||
}
|
||||
|
||||
const viewFieldsWithoutUpdatedViewField = view.viewFields.filter(
|
||||
(viewField) => viewField.id !== newOrUpdatedViewField?.id,
|
||||
);
|
||||
|
||||
if (viewFieldsWithoutUpdatedViewField.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const labelMetadataIdentifierFieldMetadataId =
|
||||
view.objectMetadata.labelIdentifierFieldMetadataId;
|
||||
|
||||
if (
|
||||
labelMetadataIdentifierFieldMetadataId ===
|
||||
newOrUpdatedViewField.fieldMetadataId
|
||||
) {
|
||||
const minPositionInViewWithoutUpdatedViewField =
|
||||
viewFieldsWithoutUpdatedViewField.reduce(
|
||||
(minViewField, viewField) =>
|
||||
viewField.position < minViewField.position
|
||||
? viewField
|
||||
: minViewField,
|
||||
viewFieldsWithoutUpdatedViewField[0],
|
||||
).position;
|
||||
|
||||
if (
|
||||
newOrUpdatedViewField.position >=
|
||||
minPositionInViewWithoutUpdatedViewField
|
||||
) {
|
||||
throw new UserInputError(
|
||||
'Label metadata identifier must keep the minimal position in the view.',
|
||||
{
|
||||
userFriendlyMessage: msg`Record text must be in first position of the view.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const labelMetadataIdentifierViewFieldPosition = view.viewFields.find(
|
||||
(viewField) =>
|
||||
viewField.fieldMetadataId === labelMetadataIdentifierFieldMetadataId,
|
||||
)?.position;
|
||||
|
||||
if (
|
||||
isDefined(labelMetadataIdentifierViewFieldPosition) &&
|
||||
newOrUpdatedViewField.position <=
|
||||
labelMetadataIdentifierViewFieldPosition
|
||||
) {
|
||||
throw new UserInputError(
|
||||
'Label metadata identifier must keep the minimal position in the view.',
|
||||
{
|
||||
userFriendlyMessage: msg`Record text must be in first position of the view.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async formatViewFieldData(
|
||||
viewFieldData: Partial<ViewFieldEntity> & { viewId: string },
|
||||
workspaceId: string,
|
||||
): Promise<Partial<ViewFieldEntity>> {
|
||||
if (!isDefined(viewFieldData.position)) {
|
||||
const view = await this.findViewByIdWithRelations(
|
||||
viewFieldData.viewId,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (!isDefined(view)) {
|
||||
throw new Error(`View not found: ${viewFieldData.viewId}`);
|
||||
}
|
||||
|
||||
const highestPositionInView = view.viewFields.reduce(
|
||||
(maxViewField, viewField) =>
|
||||
viewField.position > maxViewField.position ? viewField : maxViewField,
|
||||
view.viewFields[0],
|
||||
);
|
||||
|
||||
return { ...viewFieldData, position: highestPositionInView.position + 1 };
|
||||
} else {
|
||||
return viewFieldData;
|
||||
}
|
||||
}
|
||||
|
||||
private hasRequiredFields(
|
||||
data: Partial<ViewFieldEntity>,
|
||||
): data is Partial<ViewFieldEntity> & {
|
||||
viewId: string;
|
||||
fieldMetadataId: string;
|
||||
workspaceId: string;
|
||||
} {
|
||||
return (
|
||||
isDefined(data.viewId) &&
|
||||
isDefined(data.fieldMetadataId) &&
|
||||
isDefined(data.workspaceId)
|
||||
);
|
||||
}
|
||||
|
||||
public async findViewByIdWithRelations(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
): Promise<ViewEntity | null> {
|
||||
const view = await this.viewRepository.findOne({
|
||||
where: {
|
||||
id,
|
||||
workspaceId,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
relations: ['workspace', 'objectMetadata', 'viewFields'],
|
||||
});
|
||||
|
||||
return view || null;
|
||||
}
|
||||
|
||||
private async flushGraphQLCache(workspaceId: string): Promise<void> {
|
||||
await this.workspaceCacheStorageService.flushGraphQLOperation({
|
||||
operationName: FIND_ALL_CORE_VIEWS_GRAPHQL_OPERATION,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { ViewFieldController } from 'src/engine/metadata-modules/view-field/controllers/view-field.controller';
|
||||
import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
|
||||
import { ViewFieldResolver } from 'src/engine/metadata-modules/view-field/resolvers/view-field.resolver';
|
||||
import { ViewFieldV2Service } from 'src/engine/metadata-modules/view-field/services/view-field-v2.service';
|
||||
import { ViewFieldService } from 'src/engine/metadata-modules/view-field/services/view-field.service';
|
||||
import { ViewPermissionsModule } from 'src/engine/metadata-modules/view-permissions/view-permissions.module';
|
||||
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
|
||||
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
||||
@@ -18,14 +16,13 @@ import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspa
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([ViewFieldEntity, ViewEntity]),
|
||||
WorkspaceCacheStorageModule,
|
||||
FeatureFlagModule,
|
||||
PermissionsModule,
|
||||
WorkspaceMigrationV2Module,
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||
ViewPermissionsModule,
|
||||
],
|
||||
controllers: [ViewFieldController],
|
||||
providers: [ViewFieldService, ViewFieldResolver, ViewFieldV2Service],
|
||||
exports: [ViewFieldService, ViewFieldV2Service],
|
||||
providers: [ViewFieldResolver, ViewFieldV2Service],
|
||||
exports: [ViewFieldV2Service],
|
||||
})
|
||||
export class ViewFieldModule {}
|
||||
|
||||
@@ -24,9 +24,8 @@ import { CustomPermissionGuard } from 'src/engine/guards/custom-permission.guard
|
||||
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { resolveObjectMetadataStandardOverride } from 'src/engine/metadata-modules/object-metadata/utils/resolve-object-metadata-standard-override.util';
|
||||
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
||||
import { ViewFieldDTO } from 'src/engine/metadata-modules/view-field/dtos/view-field.dto';
|
||||
import { ViewFieldService } from 'src/engine/metadata-modules/view-field/services/view-field.service';
|
||||
import { ViewFieldV2Service } from 'src/engine/metadata-modules/view-field/services/view-field-v2.service';
|
||||
import { ViewFilterGroupDTO } from 'src/engine/metadata-modules/view-filter-group/dtos/view-filter-group.dto';
|
||||
import { ViewFilterGroupService } from 'src/engine/metadata-modules/view-filter-group/services/view-filter-group.service';
|
||||
import { ViewFilterDTO } from 'src/engine/metadata-modules/view-filter/dtos/view-filter.dto';
|
||||
@@ -54,7 +53,6 @@ import { ViewGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/view/
|
||||
export class ViewResolver {
|
||||
constructor(
|
||||
private readonly viewService: ViewService,
|
||||
private readonly viewFieldService: ViewFieldService,
|
||||
private readonly viewFilterService: ViewFilterService,
|
||||
private readonly viewFilterGroupService: ViewFilterGroupService,
|
||||
private readonly viewSortService: ViewSortService,
|
||||
@@ -62,7 +60,8 @@ export class ViewResolver {
|
||||
private readonly i18nService: I18nService,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
private readonly viewV2Service: ViewV2Service,
|
||||
private readonly userRoleService: UserRoleService,
|
||||
|
||||
private readonly viewFieldV2Service: ViewFieldV2Service,
|
||||
) {}
|
||||
|
||||
@ResolveField(() => String)
|
||||
@@ -265,7 +264,7 @@ export class ViewResolver {
|
||||
return view.viewFields;
|
||||
}
|
||||
|
||||
return this.viewFieldService.findByViewId(workspace.id, view.id);
|
||||
return this.viewFieldV2Service.findByViewId(workspace.id, view.id);
|
||||
}
|
||||
|
||||
@ResolveField(() => [ViewFilterDTO])
|
||||
|
||||
@@ -7,11 +7,11 @@ import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata
|
||||
import { FlatViewModule } from 'src/engine/metadata-modules/flat-view/flat-view.module';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
|
||||
import { ViewPermissionsModule } from 'src/engine/metadata-modules/view-permissions/view-permissions.module';
|
||||
import { ViewFieldModule } from 'src/engine/metadata-modules/view-field/view-field.module';
|
||||
import { ViewFilterGroupModule } from 'src/engine/metadata-modules/view-filter-group/view-filter-group.module';
|
||||
import { ViewFilterModule } from 'src/engine/metadata-modules/view-filter/view-filter.module';
|
||||
import { ViewGroupModule } from 'src/engine/metadata-modules/view-group/view-group.module';
|
||||
import { ViewPermissionsModule } from 'src/engine/metadata-modules/view-permissions/view-permissions.module';
|
||||
import { ViewSortModule } from 'src/engine/metadata-modules/view-sort/view-sort.module';
|
||||
import { ViewController } from 'src/engine/metadata-modules/view/controllers/view.controller';
|
||||
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import {
|
||||
ActorMetadata,
|
||||
FieldMetadataType,
|
||||
RelationOnDeleteAction,
|
||||
ActorMetadata,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
|
||||
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants';
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
|
||||
import { DEFAULT_LABEL_IDENTIFIER_FIELD_NAME } from 'src/engine/metadata-modules/object-metadata/object-metadata.constants';
|
||||
import { DEFAULT_LABEL_IDENTIFIER_FIELD_NAME } from 'src/engine/metadata-modules/object-metadata/constants/object-metadata.constants';
|
||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
import { WorkspaceCustomEntity } from 'src/engine/twenty-orm/decorators/workspace-custom-entity.decorator';
|
||||
import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator';
|
||||
|
||||
+4
-9
@@ -13,7 +13,6 @@ import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
||||
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
|
||||
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
@@ -28,13 +27,13 @@ import { WorkspaceSyncMetadataService } from 'src/engine/workspace-manager/works
|
||||
|
||||
describe('WorkspaceManagerService', () => {
|
||||
let service: WorkspaceManagerService;
|
||||
let objectMetadataServiceV2: ObjectMetadataServiceV2;
|
||||
let workspaceMigrationRepository: Repository<WorkspaceMigrationEntity>;
|
||||
let dataSourceRepository: Repository<DataSourceEntity>;
|
||||
let workspaceDataSourceService: WorkspaceDataSourceService;
|
||||
let roleTargetsRepository: Repository<RoleTargetsEntity>;
|
||||
let roleRepository: Repository<RoleEntity>;
|
||||
let mockDataSource: jest.Mocked<DataSource>;
|
||||
let objectMetadataServiceV2: ObjectMetadataServiceV2;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockDataSource = {
|
||||
@@ -102,10 +101,6 @@ describe('WorkspaceManagerService', () => {
|
||||
provide: FeatureFlagService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: ObjectMetadataService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: RoleService,
|
||||
useValue: {},
|
||||
@@ -154,9 +149,6 @@ describe('WorkspaceManagerService', () => {
|
||||
}).compile();
|
||||
|
||||
service = module.get<WorkspaceManagerService>(WorkspaceManagerService);
|
||||
objectMetadataServiceV2 = module.get<ObjectMetadataServiceV2>(
|
||||
ObjectMetadataServiceV2,
|
||||
);
|
||||
workspaceMigrationRepository = module.get<
|
||||
Repository<WorkspaceMigrationEntity>
|
||||
>(getRepositoryToken(WorkspaceMigrationEntity));
|
||||
@@ -172,6 +164,9 @@ describe('WorkspaceManagerService', () => {
|
||||
roleRepository = module.get<Repository<RoleEntity>>(
|
||||
getRepositoryToken(RoleEntity),
|
||||
);
|
||||
objectMetadataServiceV2 = module.get<ObjectMetadataServiceV2>(
|
||||
ObjectMetadataServiceV2,
|
||||
);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
|
||||
+2
-2
@@ -8,8 +8,8 @@ import { DataSource } from 'typeorm';
|
||||
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
|
||||
import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
||||
import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
|
||||
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
|
||||
import {
|
||||
@@ -265,7 +265,7 @@ export class DevSeederDataService {
|
||||
constructor(
|
||||
@InjectDataSource()
|
||||
private readonly coreDataSource: DataSource,
|
||||
private readonly objectMetadataService: ObjectMetadataService,
|
||||
private readonly objectMetadataService: ObjectMetadataServiceV2,
|
||||
private readonly timelineActivitySeederService: TimelineActivitySeederService,
|
||||
private readonly fileStorageService: FileStorageService,
|
||||
) {}
|
||||
|
||||
+4
-2
@@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common';
|
||||
import chunk from 'lodash.chunk';
|
||||
import { ObjectRecord } from 'twenty-shared/types';
|
||||
|
||||
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
||||
import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service';
|
||||
import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
|
||||
import { CALENDAR_EVENT_DATA_SEEDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/calendar-event-data-seeds.constant';
|
||||
import {
|
||||
@@ -107,7 +107,9 @@ export class TimelineActivitySeederService {
|
||||
'message',
|
||||
]);
|
||||
|
||||
constructor(private readonly objectMetadataService: ObjectMetadataService) {}
|
||||
constructor(
|
||||
private readonly objectMetadataService: ObjectMetadataServiceV2,
|
||||
) {}
|
||||
|
||||
private getLinkedActivityName(activityType: string): string {
|
||||
// Notes and tasks use the legacy format: linked-{type}.created
|
||||
|
||||
+5
-7
@@ -8,7 +8,6 @@ import { type DataSourceEntity } from 'src/engine/metadata-modules/data-source/d
|
||||
import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
|
||||
import { FieldMetadataServiceV2 } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service-v2';
|
||||
import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service';
|
||||
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
||||
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
|
||||
import {
|
||||
@@ -30,7 +29,6 @@ import { prefillCoreViews } from 'src/engine/workspace-manager/standard-objects-
|
||||
@Injectable()
|
||||
export class DevSeederMetadataService {
|
||||
constructor(
|
||||
private readonly objectMetadataService: ObjectMetadataService,
|
||||
private readonly objectMetadataServiceV2: ObjectMetadataServiceV2,
|
||||
private readonly fieldMetadataServiceV2: FieldMetadataServiceV2,
|
||||
private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService,
|
||||
@@ -140,7 +138,7 @@ export class DevSeederMetadataService {
|
||||
workspaceId: string;
|
||||
objectMetadataSeed: ObjectMetadataSeed;
|
||||
}): Promise<void> {
|
||||
await this.objectMetadataServiceV2.createOne({
|
||||
await this.objectMetadataServiceV2.createOneObject({
|
||||
createObjectInput: {
|
||||
...objectMetadataSeed,
|
||||
dataSourceId,
|
||||
@@ -159,7 +157,7 @@ export class DevSeederMetadataService {
|
||||
fieldMetadataSeeds: FieldMetadataSeed[];
|
||||
}): Promise<void> {
|
||||
const objectMetadata =
|
||||
await this.objectMetadataService.findOneWithinWorkspace(workspaceId, {
|
||||
await this.objectMetadataServiceV2.findOneWithinWorkspace(workspaceId, {
|
||||
where: { nameSingular: objectMetadataNameSingular },
|
||||
});
|
||||
|
||||
@@ -173,7 +171,7 @@ export class DevSeederMetadataService {
|
||||
objectMetadataId: objectMetadata.id,
|
||||
}));
|
||||
|
||||
await this.fieldMetadataServiceV2.createMany({
|
||||
await this.fieldMetadataServiceV2.createManyFields({
|
||||
createFieldInputs,
|
||||
workspaceId,
|
||||
});
|
||||
@@ -189,7 +187,7 @@ export class DevSeederMetadataService {
|
||||
featureFlags?: Record<string, boolean>;
|
||||
}): Promise<void> {
|
||||
const createdObjectMetadata =
|
||||
await this.objectMetadataService.findManyWithinWorkspace(workspaceId);
|
||||
await this.objectMetadataServiceV2.findManyWithinWorkspace(workspaceId);
|
||||
|
||||
await prefillCoreViews({
|
||||
coreDataSource: this.coreDataSource,
|
||||
@@ -243,7 +241,7 @@ export class DevSeederMetadataService {
|
||||
objectMetadataMaps,
|
||||
});
|
||||
|
||||
await this.fieldMetadataServiceV2.createMany({
|
||||
await this.fieldMetadataServiceV2.createManyFields({
|
||||
createFieldInputs,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
+2
-2
@@ -11,7 +11,7 @@ import {
|
||||
} from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-health-options.interface';
|
||||
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
||||
import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service';
|
||||
import { WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
|
||||
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
||||
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
|
||||
@@ -29,7 +29,7 @@ export class WorkspaceHealthService {
|
||||
@InjectDataSource()
|
||||
private readonly coreDataSource: DataSource,
|
||||
private readonly dataSourceService: DataSourceService,
|
||||
private readonly objectMetadataService: ObjectMetadataService,
|
||||
private readonly objectMetadataService: ObjectMetadataServiceV2,
|
||||
private readonly databaseStructureService: DatabaseStructureService,
|
||||
private readonly objectMetadataHealthService: ObjectMetadataHealthService,
|
||||
private readonly fieldMetadataHealthService: FieldMetadataHealthService,
|
||||
|
||||
@@ -10,7 +10,6 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent
|
||||
import { type DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service';
|
||||
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
||||
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { RoleService } from 'src/engine/metadata-modules/role/role.service';
|
||||
@@ -32,7 +31,6 @@ export class WorkspaceManagerService {
|
||||
private readonly coreDataSource: DataSource,
|
||||
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
||||
private readonly workspaceMigrationService: WorkspaceMigrationService,
|
||||
private readonly objectMetadataService: ObjectMetadataService,
|
||||
private readonly objectMetadataServiceV2: ObjectMetadataServiceV2,
|
||||
private readonly dataSourceService: DataSourceService,
|
||||
private readonly workspaceSyncMetadataService: WorkspaceSyncMetadataService,
|
||||
@@ -121,7 +119,7 @@ export class WorkspaceManagerService {
|
||||
featureFlags: Record<string, boolean>,
|
||||
) {
|
||||
const createdObjectMetadata =
|
||||
await this.objectMetadataService.findManyWithinWorkspace(workspaceId);
|
||||
await this.objectMetadataServiceV2.findManyWithinWorkspace(workspaceId);
|
||||
|
||||
await standardObjectsPrefillData(
|
||||
this.coreDataSource,
|
||||
|
||||
-359
@@ -1,359 +0,0 @@
|
||||
import { findViewFieldsOperationFactory } from 'test/integration/graphql/utils/find-view-fields-operation-factory.util';
|
||||
import {
|
||||
assertGraphQLErrorResponse,
|
||||
assertGraphQLSuccessfulResponse,
|
||||
} from 'test/integration/graphql/utils/graphql-test-assertions.util';
|
||||
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
|
||||
import {
|
||||
createViewFieldData,
|
||||
updateViewFieldData,
|
||||
} from 'test/integration/graphql/utils/view-data-factory.util';
|
||||
import { createTestViewWithGraphQL } from 'test/integration/graphql/utils/view-graphql.util';
|
||||
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-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 { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
|
||||
import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util';
|
||||
import { createOneCoreViewField } from 'test/integration/metadata/suites/view-field/utils/create-one-core-view-field.util';
|
||||
import { deleteOneCoreViewField } from 'test/integration/metadata/suites/view-field/utils/delete-one-core-view-field.util';
|
||||
import { destroyOneCoreViewField } from 'test/integration/metadata/suites/view-field/utils/destroy-one-core-view-field.util';
|
||||
import { updateOneCoreViewField } from 'test/integration/metadata/suites/view-field/utils/update-one-core-view-field.util';
|
||||
import { destroyOneCoreView } from 'test/integration/metadata/suites/view/utils/destroy-one-core-view.util';
|
||||
import { assertViewFieldStructure } from 'test/integration/utils/view-test.util';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { type CreateViewFieldInput } from 'src/engine/metadata-modules/view-field/dtos/inputs/create-view-field.input';
|
||||
import {
|
||||
generateViewFieldExceptionMessage,
|
||||
ViewFieldExceptionMessageKey,
|
||||
} from 'src/engine/metadata-modules/view-field/exceptions/view-field.exception';
|
||||
|
||||
const TEST_NOT_EXISTING_VIEW_FIELD_ID = '20202020-0000-4000-8000-000000000001';
|
||||
|
||||
describe('View Field Resolver', () => {
|
||||
let testViewId: string;
|
||||
let testObjectMetadataId: string;
|
||||
let testFieldMetadataId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
await updateFeatureFlag({
|
||||
expectToFail: false,
|
||||
featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
||||
value: false,
|
||||
});
|
||||
});
|
||||
|
||||
beforeAll(async () => {
|
||||
const {
|
||||
data: {
|
||||
createOneObject: { id: objectMetadataId },
|
||||
},
|
||||
} = await createOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
nameSingular: 'myFieldTestObject',
|
||||
namePlural: 'myFieldTestObjects',
|
||||
labelSingular: 'My Field Test Object',
|
||||
labelPlural: 'My Field Test Objects',
|
||||
icon: 'Icon123',
|
||||
},
|
||||
});
|
||||
|
||||
testObjectMetadataId = objectMetadataId;
|
||||
|
||||
const createFieldInput = {
|
||||
name: 'testField',
|
||||
label: 'Test Field',
|
||||
type: FieldMetadataType.TEXT,
|
||||
objectMetadataId: testObjectMetadataId,
|
||||
isLabelSyncedWithName: true,
|
||||
};
|
||||
|
||||
const {
|
||||
data: {
|
||||
createOneField: { id: fieldMetadataId },
|
||||
},
|
||||
} = await createOneFieldMetadata({
|
||||
expectToFail: false,
|
||||
input: createFieldInput,
|
||||
gqlFields: `
|
||||
id
|
||||
name
|
||||
label
|
||||
isLabelSyncedWithName
|
||||
`,
|
||||
});
|
||||
|
||||
testFieldMetadataId = fieldMetadataId;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await updateOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
idToUpdate: testObjectMetadataId,
|
||||
updatePayload: {
|
||||
isActive: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
await deleteOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: { idToDelete: testObjectMetadataId },
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await updateFeatureFlag({
|
||||
expectToFail: false,
|
||||
featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
||||
value: true,
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
const view = await createTestViewWithGraphQL({
|
||||
name: 'Test View for Fields',
|
||||
objectMetadataId: testObjectMetadataId,
|
||||
});
|
||||
|
||||
testViewId = view.id;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await destroyOneCoreView({
|
||||
viewId: testViewId,
|
||||
expectToFail: false,
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCoreViewFields', () => {
|
||||
it('should return empty array when no view fields exist', async () => {
|
||||
const operation = findViewFieldsOperationFactory({ viewId: testViewId });
|
||||
const response = await makeGraphqlAPIRequest(operation);
|
||||
|
||||
assertGraphQLSuccessfulResponse(response);
|
||||
expect(response.body.data.getCoreViewFields).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return view fields for a specific view', async () => {
|
||||
const fieldData = createViewFieldData(testViewId, {
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
fieldMetadataId: testFieldMetadataId,
|
||||
});
|
||||
|
||||
await createOneCoreViewField({
|
||||
input: fieldData as CreateViewFieldInput,
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const getOperation = findViewFieldsOperationFactory({
|
||||
viewId: testViewId,
|
||||
});
|
||||
const response = await makeGraphqlAPIRequest(getOperation);
|
||||
|
||||
assertGraphQLSuccessfulResponse(response);
|
||||
expect(response.body.data.getCoreViewFields).toHaveLength(1);
|
||||
assertViewFieldStructure(response.body.data.getCoreViewFields[0], {
|
||||
fieldMetadataId: testFieldMetadataId,
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
viewId: testViewId,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createCoreViewField', () => {
|
||||
it('should create a new view field', async () => {
|
||||
const fieldData = createViewFieldData(testViewId, {
|
||||
position: 1,
|
||||
isVisible: true,
|
||||
size: 200,
|
||||
fieldMetadataId: testFieldMetadataId,
|
||||
});
|
||||
|
||||
const response = await createOneCoreViewField({
|
||||
input: fieldData as CreateViewFieldInput,
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
assertGraphQLSuccessfulResponse({ body: response, status: 200 });
|
||||
assertViewFieldStructure(response.data.createCoreViewField, {
|
||||
fieldMetadataId: testFieldMetadataId,
|
||||
position: 1,
|
||||
isVisible: true,
|
||||
size: 200,
|
||||
viewId: testViewId,
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a hidden view field', async () => {
|
||||
const fieldData = {
|
||||
fieldMetadataId: testFieldMetadataId,
|
||||
position: 2,
|
||||
isVisible: false,
|
||||
size: 100,
|
||||
viewId: testViewId,
|
||||
};
|
||||
|
||||
const response = await createOneCoreViewField({
|
||||
input: fieldData,
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
assertGraphQLSuccessfulResponse({ body: response, status: 200 });
|
||||
assertViewFieldStructure(response.data.createCoreViewField, {
|
||||
fieldMetadataId: testFieldMetadataId,
|
||||
position: 2,
|
||||
isVisible: false,
|
||||
size: 100,
|
||||
viewId: testViewId,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateCoreViewField', () => {
|
||||
it('should update an existing view field', async () => {
|
||||
const fieldData = createViewFieldData(testViewId, {
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
fieldMetadataId: testFieldMetadataId,
|
||||
});
|
||||
const createResponse = await createOneCoreViewField({
|
||||
input: fieldData as CreateViewFieldInput,
|
||||
expectToFail: false,
|
||||
});
|
||||
const viewField = createResponse.data.createCoreViewField;
|
||||
|
||||
const updateInput = updateViewFieldData({
|
||||
position: 5,
|
||||
isVisible: false,
|
||||
size: 300,
|
||||
});
|
||||
const response = await updateOneCoreViewField({
|
||||
input: {
|
||||
id: viewField.id,
|
||||
update: updateInput,
|
||||
},
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
assertGraphQLSuccessfulResponse({ body: response, status: 200 });
|
||||
expect(response.data.updateCoreViewField).toMatchObject({
|
||||
id: viewField.id,
|
||||
position: 5,
|
||||
isVisible: false,
|
||||
size: 300,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error when updating non-existent view field', async () => {
|
||||
const response = await updateOneCoreViewField({
|
||||
input: {
|
||||
id: TEST_NOT_EXISTING_VIEW_FIELD_ID,
|
||||
update: {
|
||||
position: 1,
|
||||
},
|
||||
},
|
||||
expectToFail: true,
|
||||
});
|
||||
|
||||
assertGraphQLErrorResponse(
|
||||
{ body: response, status: 200 },
|
||||
ErrorCode.NOT_FOUND,
|
||||
generateViewFieldExceptionMessage(
|
||||
ViewFieldExceptionMessageKey.VIEW_FIELD_NOT_FOUND,
|
||||
TEST_NOT_EXISTING_VIEW_FIELD_ID,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteCoreViewField', () => {
|
||||
it('should delete an existing view field', async () => {
|
||||
const fieldData = createViewFieldData(testViewId, {
|
||||
fieldMetadataId: testFieldMetadataId,
|
||||
});
|
||||
const createResponse = await createOneCoreViewField({
|
||||
input: fieldData as CreateViewFieldInput,
|
||||
expectToFail: false,
|
||||
});
|
||||
const viewField = createResponse.data.createCoreViewField;
|
||||
|
||||
const response = await deleteOneCoreViewField({
|
||||
input: { id: viewField.id },
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
assertGraphQLSuccessfulResponse({ body: response, status: 200 });
|
||||
expect(response.data.deleteCoreViewField).toMatchObject({
|
||||
id: viewField.id,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error when deleting non-existent view field', async () => {
|
||||
const response = await deleteOneCoreViewField({
|
||||
input: { id: TEST_NOT_EXISTING_VIEW_FIELD_ID },
|
||||
expectToFail: true,
|
||||
});
|
||||
|
||||
assertGraphQLErrorResponse(
|
||||
{ body: response, status: 200 },
|
||||
ErrorCode.NOT_FOUND,
|
||||
generateViewFieldExceptionMessage(
|
||||
ViewFieldExceptionMessageKey.VIEW_FIELD_NOT_FOUND,
|
||||
TEST_NOT_EXISTING_VIEW_FIELD_ID,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('destroyCoreViewField', () => {
|
||||
it('should destroy an existing view field', async () => {
|
||||
const fieldData = createViewFieldData(testViewId, {
|
||||
fieldMetadataId: testFieldMetadataId,
|
||||
});
|
||||
const createResponse = await createOneCoreViewField({
|
||||
input: fieldData as CreateViewFieldInput,
|
||||
expectToFail: false,
|
||||
});
|
||||
const viewField = createResponse.data.createCoreViewField;
|
||||
|
||||
const response = await destroyOneCoreViewField({
|
||||
input: {
|
||||
id: viewField.id,
|
||||
},
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
assertGraphQLSuccessfulResponse({ body: response, status: 200 });
|
||||
expect(response.data.destroyCoreViewField).toMatchObject({
|
||||
id: viewField.id,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error when destroying non-existent view field', async () => {
|
||||
const response = await destroyOneCoreViewField({
|
||||
input: {
|
||||
id: TEST_NOT_EXISTING_VIEW_FIELD_ID,
|
||||
},
|
||||
expectToFail: true,
|
||||
});
|
||||
|
||||
assertGraphQLErrorResponse(
|
||||
{ body: response, status: 200 },
|
||||
ErrorCode.NOT_FOUND,
|
||||
generateViewFieldExceptionMessage(
|
||||
ViewFieldExceptionMessageKey.VIEW_FIELD_NOT_FOUND,
|
||||
TEST_NOT_EXISTING_VIEW_FIELD_ID,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
+5
-5
@@ -20,8 +20,8 @@ import { AgentHandoffService } from 'src/engine/metadata-modules/agent/agent-han
|
||||
import { AgentToolGeneratorService } from 'src/engine/metadata-modules/agent/agent-tool-generator.service';
|
||||
import { AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity';
|
||||
import { AgentService } from 'src/engine/metadata-modules/agent/agent.service';
|
||||
import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service';
|
||||
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
||||
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
|
||||
@@ -36,7 +36,7 @@ export interface AgentToolTestContext {
|
||||
module: TestingModule;
|
||||
agentToolService: AgentToolGeneratorService;
|
||||
agentService: AgentService;
|
||||
objectMetadataService: ObjectMetadataService;
|
||||
objectMetadataService: ObjectMetadataServiceV2;
|
||||
roleRepository: Repository<RoleEntity>;
|
||||
workspacePermissionsCacheService: WorkspacePermissionsCacheService;
|
||||
twentyORMGlobalManager: TwentyORMGlobalManager;
|
||||
@@ -78,7 +78,7 @@ export const createAgentToolTestModule =
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: ObjectMetadataService,
|
||||
provide: ObjectMetadataServiceV2,
|
||||
useValue: {
|
||||
findManyWithinWorkspace: jest.fn(),
|
||||
findOneWithinWorkspace: jest.fn(),
|
||||
@@ -215,8 +215,8 @@ export const createAgentToolTestModule =
|
||||
AgentToolGeneratorService,
|
||||
);
|
||||
const agentService = module.get<AgentService>(AgentService);
|
||||
const objectMetadataService = module.get<ObjectMetadataService>(
|
||||
ObjectMetadataService,
|
||||
const objectMetadataService = module.get<ObjectMetadataServiceV2>(
|
||||
ObjectMetadataServiceV2,
|
||||
);
|
||||
const roleRepository = module.get<Repository<RoleEntity>>(
|
||||
getRepositoryToken(RoleEntity),
|
||||
|
||||
-221
@@ -1,221 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`update-one-field-metadata-related-record MULTI_SELECT should handle adding new options while maintaining existing view filter 1`] = `
|
||||
{
|
||||
"operand": "IS",
|
||||
"positionInViewFilterGroup": null,
|
||||
"subFieldName": null,
|
||||
"value": [
|
||||
"OPTION_0",
|
||||
"OPTION_1",
|
||||
],
|
||||
"viewFilterGroupId": null,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`update-one-field-metadata-related-record MULTI_SELECT should handle no changes update of options while maintaining existing view filter values 1`] = `
|
||||
{
|
||||
"operand": "IS",
|
||||
"positionInViewFilterGroup": null,
|
||||
"subFieldName": null,
|
||||
"value": [
|
||||
"OPTION_0",
|
||||
"OPTION_1",
|
||||
"OPTION_2",
|
||||
"OPTION_3",
|
||||
"OPTION_4",
|
||||
"OPTION_5",
|
||||
"OPTION_6",
|
||||
"OPTION_7",
|
||||
"OPTION_8",
|
||||
"OPTION_9",
|
||||
],
|
||||
"viewFilterGroupId": null,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`update-one-field-metadata-related-record MULTI_SELECT should handle partial deletion of selected options in view filter 1`] = `
|
||||
{
|
||||
"operand": "IS",
|
||||
"positionInViewFilterGroup": null,
|
||||
"subFieldName": null,
|
||||
"value": [
|
||||
"OPTION_4",
|
||||
"OPTION_5",
|
||||
"OPTION_6",
|
||||
"OPTION_7",
|
||||
"OPTION_8",
|
||||
"OPTION_9",
|
||||
],
|
||||
"viewFilterGroupId": null,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`update-one-field-metadata-related-record MULTI_SELECT should handle reordering of options while maintaining view filter values 1`] = `
|
||||
{
|
||||
"operand": "IS",
|
||||
"positionInViewFilterGroup": null,
|
||||
"subFieldName": null,
|
||||
"value": [
|
||||
"OPTION_0",
|
||||
"OPTION_1",
|
||||
],
|
||||
"viewFilterGroupId": null,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`update-one-field-metadata-related-record MULTI_SELECT should throw error if view filter value is not a stringified JSON array 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "INTERNAL_SERVER_ERROR",
|
||||
"exceptionEventId": "mocked-exception-id",
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
},
|
||||
"message": "Unexpected invalid view filter value for filter 20202020-e3b5-4fa7-85aa-9b1950fc7bf5",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`update-one-field-metadata-related-record MULTI_SELECT should update related multi selected options view filter 1`] = `
|
||||
{
|
||||
"operand": "IS",
|
||||
"positionInViewFilterGroup": null,
|
||||
"subFieldName": null,
|
||||
"value": [
|
||||
"OPTION_0_UPDATED",
|
||||
"OPTION_1",
|
||||
"OPTION_2_UPDATED",
|
||||
"OPTION_3",
|
||||
"OPTION_4_UPDATED",
|
||||
"OPTION_5",
|
||||
"OPTION_6_UPDATED",
|
||||
"OPTION_7",
|
||||
"OPTION_8_UPDATED",
|
||||
"OPTION_9",
|
||||
],
|
||||
"viewFilterGroupId": null,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`update-one-field-metadata-related-record MULTI_SELECT should update related solo selected option view filter 1`] = `
|
||||
{
|
||||
"operand": "IS",
|
||||
"positionInViewFilterGroup": null,
|
||||
"subFieldName": null,
|
||||
"value": [
|
||||
"OPTION_5_UPDATED",
|
||||
],
|
||||
"viewFilterGroupId": null,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`update-one-field-metadata-related-record SELECT should handle adding new options while maintaining existing view filter 1`] = `
|
||||
{
|
||||
"operand": "IS",
|
||||
"positionInViewFilterGroup": null,
|
||||
"subFieldName": null,
|
||||
"value": [
|
||||
"OPTION_0",
|
||||
"OPTION_1",
|
||||
],
|
||||
"viewFilterGroupId": null,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`update-one-field-metadata-related-record SELECT should handle no changes update of options while maintaining existing view filter values 1`] = `
|
||||
{
|
||||
"operand": "IS",
|
||||
"positionInViewFilterGroup": null,
|
||||
"subFieldName": null,
|
||||
"value": [
|
||||
"OPTION_0",
|
||||
"OPTION_1",
|
||||
"OPTION_2",
|
||||
"OPTION_3",
|
||||
"OPTION_4",
|
||||
"OPTION_5",
|
||||
"OPTION_6",
|
||||
"OPTION_7",
|
||||
"OPTION_8",
|
||||
"OPTION_9",
|
||||
],
|
||||
"viewFilterGroupId": null,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`update-one-field-metadata-related-record SELECT should handle partial deletion of selected options in view filter 1`] = `
|
||||
{
|
||||
"operand": "IS",
|
||||
"positionInViewFilterGroup": null,
|
||||
"subFieldName": null,
|
||||
"value": [
|
||||
"OPTION_4",
|
||||
"OPTION_5",
|
||||
"OPTION_6",
|
||||
"OPTION_7",
|
||||
"OPTION_8",
|
||||
"OPTION_9",
|
||||
],
|
||||
"viewFilterGroupId": null,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`update-one-field-metadata-related-record SELECT should handle reordering of options while maintaining view filter values 1`] = `
|
||||
{
|
||||
"operand": "IS",
|
||||
"positionInViewFilterGroup": null,
|
||||
"subFieldName": null,
|
||||
"value": [
|
||||
"OPTION_0",
|
||||
"OPTION_1",
|
||||
],
|
||||
"viewFilterGroupId": null,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`update-one-field-metadata-related-record SELECT should throw error if view filter value is not a stringified JSON array 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "INTERNAL_SERVER_ERROR",
|
||||
"exceptionEventId": "mocked-exception-id",
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
},
|
||||
"message": "Unexpected invalid view filter value for filter 20202020-e3b5-4fa7-85aa-9b1950fc7bf5",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`update-one-field-metadata-related-record SELECT should update related multi selected options view filter 1`] = `
|
||||
{
|
||||
"operand": "IS",
|
||||
"positionInViewFilterGroup": null,
|
||||
"subFieldName": null,
|
||||
"value": [
|
||||
"OPTION_0_UPDATED",
|
||||
"OPTION_1",
|
||||
"OPTION_2_UPDATED",
|
||||
"OPTION_3",
|
||||
"OPTION_4_UPDATED",
|
||||
"OPTION_5",
|
||||
"OPTION_6_UPDATED",
|
||||
"OPTION_7",
|
||||
"OPTION_8_UPDATED",
|
||||
"OPTION_9",
|
||||
],
|
||||
"viewFilterGroupId": null,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`update-one-field-metadata-related-record SELECT should update related solo selected option view filter 1`] = `
|
||||
{
|
||||
"operand": "IS",
|
||||
"positionInViewFilterGroup": null,
|
||||
"subFieldName": null,
|
||||
"value": [
|
||||
"OPTION_5_UPDATED",
|
||||
],
|
||||
"viewFilterGroupId": null,
|
||||
}
|
||||
`;
|
||||
-136
@@ -1,136 +0,0 @@
|
||||
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
|
||||
import { deleteOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/delete-one-field-metadata.util';
|
||||
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 { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
|
||||
import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util';
|
||||
import {
|
||||
createTestViewWithRestApi,
|
||||
findViewByIdWithRestApi,
|
||||
} from 'test/integration/rest/utils/view-rest-api.util';
|
||||
import { generateRecordName } from 'test/integration/utils/generate-record-name';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { ViewType } from 'src/engine/metadata-modules/view/enums/view-type.enum';
|
||||
|
||||
describe('deleteOne', () => {
|
||||
beforeAll(async () => {
|
||||
await updateFeatureFlag({
|
||||
expectToFail: false,
|
||||
featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
||||
value: false,
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await updateFeatureFlag({
|
||||
expectToFail: false,
|
||||
featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
||||
value: true,
|
||||
});
|
||||
});
|
||||
describe('Kanban aggregate operation', () => {
|
||||
let listingObjectId = '';
|
||||
let testFieldId = '';
|
||||
let viewId = '';
|
||||
|
||||
beforeEach(async () => {
|
||||
const { data } = await createOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
nameSingular: 'listingv2',
|
||||
namePlural: 'listingsv2',
|
||||
labelSingular: 'Listing',
|
||||
labelPlural: 'Listings',
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
},
|
||||
});
|
||||
|
||||
listingObjectId = data.createOneObject.id;
|
||||
const { data: createdFieldData } = await createOneFieldMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
name: 'house',
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'House',
|
||||
objectMetadataId: listingObjectId,
|
||||
},
|
||||
});
|
||||
|
||||
testFieldId = createdFieldData.createOneField.id;
|
||||
|
||||
const createdView = await createTestViewWithRestApi({
|
||||
name: generateRecordName('By Type'),
|
||||
objectMetadataId: listingObjectId,
|
||||
type: ViewType.KANBAN,
|
||||
kanbanAggregateOperationFieldMetadataId: testFieldId,
|
||||
kanbanAggregateOperation: AggregateOperations.MAX,
|
||||
icon: 'IconLayoutKanban',
|
||||
});
|
||||
|
||||
viewId = createdView.id;
|
||||
});
|
||||
afterEach(async () => {
|
||||
await updateOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
idToUpdate: listingObjectId,
|
||||
updatePayload: {
|
||||
isActive: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
await deleteOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: { idToDelete: listingObjectId },
|
||||
});
|
||||
});
|
||||
|
||||
it('should delete kanban aggregate operation when deleting a field used as kanbanAggregateOperationFieldMetadataId', async () => {
|
||||
const viewThatShouldBeUpdated = await findViewByIdWithRestApi(viewId);
|
||||
|
||||
if (!isDefined(viewThatShouldBeUpdated)) {
|
||||
throw new Error('View not found, this should not happen');
|
||||
}
|
||||
|
||||
expect(
|
||||
viewThatShouldBeUpdated.kanbanAggregateOperationFieldMetadataId,
|
||||
).toBe(testFieldId);
|
||||
expect(viewThatShouldBeUpdated.kanbanAggregateOperation).toBe('MAX');
|
||||
|
||||
await updateOneFieldMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
idToUpdate: testFieldId,
|
||||
updatePayload: { isActive: false },
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
isActive
|
||||
`,
|
||||
});
|
||||
|
||||
const { data } = await deleteOneFieldMetadata({
|
||||
expectToFail: false,
|
||||
input: { idToDelete: testFieldId },
|
||||
});
|
||||
|
||||
expect(data.deleteOneField.id).toBe(testFieldId);
|
||||
|
||||
const updatedViewResponse = await findViewByIdWithRestApi(viewId);
|
||||
|
||||
if (!isDefined(updatedViewResponse)) {
|
||||
throw new Error('View not found, this should not happen');
|
||||
}
|
||||
|
||||
expect(
|
||||
updatedViewResponse.kanbanAggregateOperationFieldMetadataId,
|
||||
).toBeNull();
|
||||
expect(updatedViewResponse.kanbanAggregateOperation).toBe(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
-518
@@ -1,518 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with an invalid default value 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
},
|
||||
"message": "Default value for multi-select must be an array",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with an unknown default value 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
},
|
||||
"message": "Default value for multi-select must be an array",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with comma in option label 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Label must not contain a comma",
|
||||
},
|
||||
"message": "Label must not contain a comma",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with duplicated option ids 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Duplicated option id",
|
||||
},
|
||||
"message": "Duplicated option {field}",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with duplicated option positions 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Duplicated option position",
|
||||
},
|
||||
"message": "Duplicated option {field}",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with duplicated option values 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Duplicated option value",
|
||||
},
|
||||
"message": "Duplicated option {field}",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with duplicated trimmed option values 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Duplicated option value",
|
||||
},
|
||||
"message": "Duplicated option {field}",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with empty multi-select default values array 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "If defined default value must contain at least one value",
|
||||
},
|
||||
"message": "If defined default value must contain at least one value",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with empty options 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
},
|
||||
"message": "Options are required for enum fields",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with empty string defaultValue 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Default value should be as quoted string",
|
||||
},
|
||||
"message": "Default value should be as quoted string",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with empty string id 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option id is invalid",
|
||||
},
|
||||
"message": "Option id is invalid",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with empty string label 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option label "" is beneath 1 character",
|
||||
},
|
||||
"message": "Option label "{sanitizedLabel}" is beneath 1 character",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with empty string value 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option value "" is beneath 1 character",
|
||||
},
|
||||
"message": "Option value "{sanitizedValue}" is beneath 1 character",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with invalid multi-select default value format 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Default value should be as quoted string",
|
||||
},
|
||||
"message": "Default value should be as quoted string",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with invalid option id 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option id is invalid",
|
||||
},
|
||||
"message": "Option id is invalid",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with invalid option value format 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Value must be in UPPER_CASE and follow snake_case "Option 1 and some other things, /"",
|
||||
},
|
||||
"message": "Value must be in UPPER_CASE and follow snake_case "{sanitizedValue}"",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with non stringified array default value 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
},
|
||||
"message": "Default value for multi-select must be an array",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with not a string defaultValue 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
},
|
||||
"message": "Default value for multi-select must be a stringified array",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with not a string id 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option id is invalid",
|
||||
},
|
||||
"message": "Option id is invalid",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with not a string label 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "INTERNAL_SERVER_ERROR",
|
||||
"exceptionEventId": "mocked-exception-id",
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
},
|
||||
"message": "label.includes is not a function",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with not a string value 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Value must be in UPPER_CASE and follow snake_case "22222"",
|
||||
},
|
||||
"message": "Value must be in UPPER_CASE and follow snake_case "{sanitizedValue}"",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with null id 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option id is required",
|
||||
},
|
||||
"message": "Option id is required",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with null label 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option label is required",
|
||||
},
|
||||
"message": "Option label is required",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with null options 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
},
|
||||
"message": "Options are required for enum fields",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with null value 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option value is required",
|
||||
},
|
||||
"message": "Option value is required",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with only white spaces defaultValue 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Default value should be as quoted string",
|
||||
},
|
||||
"message": "Default value should be as quoted string",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with only white spaces id 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option id is invalid",
|
||||
},
|
||||
"message": "Option id is invalid",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with only white spaces label 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option label "" is beneath 1 character",
|
||||
},
|
||||
"message": "Option label "{sanitizedLabel}" is beneath 1 character",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with only white spaces value 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option value "" is beneath 1 character",
|
||||
},
|
||||
"message": "Option value "{sanitizedValue}" is beneath 1 character",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with too long defaultValue 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Default value should be as quoted string",
|
||||
},
|
||||
"message": "Default value should be as quoted string",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with too long id 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option id is invalid",
|
||||
},
|
||||
"message": "Option id is invalid",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with too long label 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option label exceeds 63 characters",
|
||||
},
|
||||
"message": "Option label exceeds 63 characters",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with too long value 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option value exceeds 63 characters",
|
||||
},
|
||||
"message": "Option value exceeds 63 characters",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with undefined option label 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option label is required",
|
||||
},
|
||||
"message": "Option label is required",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with undefined option value 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option value is required",
|
||||
},
|
||||
"message": "Option value is required",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with undefined options 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
},
|
||||
"message": "Options are required for enum fields",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with unknown multi-select default values 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Default value "'UNKNOWN_OPTION'" must be one of the option values",
|
||||
},
|
||||
"message": "Default value "{defaultValue}" must be one of the option values",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
-462
@@ -1,462 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Failing create field metadata RATING tests suite Create should fail with an invalid default value 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Default value should be as quoted string",
|
||||
},
|
||||
"message": "Default value should be as quoted string",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata RATING tests suite Create should fail with an unknown default value 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Default value "'OPTION_424242'" must be one of the option values",
|
||||
},
|
||||
"message": "Default value "{defaultValue}" must be one of the option values",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata RATING tests suite Create should fail with comma in option label 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Label must not contain a comma",
|
||||
},
|
||||
"message": "Label must not contain a comma",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata RATING tests suite Create should fail with duplicated option ids 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Duplicated option id",
|
||||
},
|
||||
"message": "Duplicated option {field}",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata RATING tests suite Create should fail with duplicated option positions 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Duplicated option position",
|
||||
},
|
||||
"message": "Duplicated option {field}",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata RATING tests suite Create should fail with duplicated option values 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Duplicated option value",
|
||||
},
|
||||
"message": "Duplicated option {field}",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata RATING tests suite Create should fail with duplicated trimmed option values 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Duplicated option value",
|
||||
},
|
||||
"message": "Duplicated option {field}",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata RATING tests suite Create should fail with empty options 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
},
|
||||
"message": "Options are required for enum fields",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata RATING tests suite Create should fail with empty string defaultValue 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Default value should be as quoted string",
|
||||
},
|
||||
"message": "Default value should be as quoted string",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata RATING tests suite Create should fail with empty string id 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option id is invalid",
|
||||
},
|
||||
"message": "Option id is invalid",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata RATING tests suite Create should fail with empty string label 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option label "" is beneath 1 character",
|
||||
},
|
||||
"message": "Option label "{sanitizedLabel}" is beneath 1 character",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata RATING tests suite Create should fail with empty string value 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option value "" is beneath 1 character",
|
||||
},
|
||||
"message": "Option value "{sanitizedValue}" is beneath 1 character",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata RATING tests suite Create should fail with invalid option id 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option id is invalid",
|
||||
},
|
||||
"message": "Option id is invalid",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata RATING tests suite Create should fail with invalid option value format 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Value must be in UPPER_CASE and follow snake_case "Option 1 and some other things, /"",
|
||||
},
|
||||
"message": "Value must be in UPPER_CASE and follow snake_case "{sanitizedValue}"",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata RATING tests suite Create should fail with not a string defaultValue 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
},
|
||||
"message": "Default value for multi-select must be a stringified array",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata RATING tests suite Create should fail with not a string id 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option id is invalid",
|
||||
},
|
||||
"message": "Option id is invalid",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata RATING tests suite Create should fail with not a string label 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "INTERNAL_SERVER_ERROR",
|
||||
"exceptionEventId": "mocked-exception-id",
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
},
|
||||
"message": "label.includes is not a function",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata RATING tests suite Create should fail with not a string value 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Value must be in UPPER_CASE and follow snake_case "22222"",
|
||||
},
|
||||
"message": "Value must be in UPPER_CASE and follow snake_case "{sanitizedValue}"",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata RATING tests suite Create should fail with null id 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option id is required",
|
||||
},
|
||||
"message": "Option id is required",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata RATING tests suite Create should fail with null label 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option label is required",
|
||||
},
|
||||
"message": "Option label is required",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata RATING tests suite Create should fail with null options 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
},
|
||||
"message": "Options are required for enum fields",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata RATING tests suite Create should fail with null value 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option value is required",
|
||||
},
|
||||
"message": "Option value is required",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata RATING tests suite Create should fail with only white spaces defaultValue 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Default value should be as quoted string",
|
||||
},
|
||||
"message": "Default value should be as quoted string",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata RATING tests suite Create should fail with only white spaces id 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option id is invalid",
|
||||
},
|
||||
"message": "Option id is invalid",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata RATING tests suite Create should fail with only white spaces label 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option label "" is beneath 1 character",
|
||||
},
|
||||
"message": "Option label "{sanitizedLabel}" is beneath 1 character",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata RATING tests suite Create should fail with only white spaces value 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option value "" is beneath 1 character",
|
||||
},
|
||||
"message": "Option value "{sanitizedValue}" is beneath 1 character",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata RATING tests suite Create should fail with too long defaultValue 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Default value should be as quoted string",
|
||||
},
|
||||
"message": "Default value should be as quoted string",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata RATING tests suite Create should fail with too long id 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option id is invalid",
|
||||
},
|
||||
"message": "Option id is invalid",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata RATING tests suite Create should fail with too long label 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option label exceeds 63 characters",
|
||||
},
|
||||
"message": "Option label exceeds 63 characters",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata RATING tests suite Create should fail with too long value 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option value exceeds 63 characters",
|
||||
},
|
||||
"message": "Option value exceeds 63 characters",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata RATING tests suite Create should fail with undefined option label 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option label is required",
|
||||
},
|
||||
"message": "Option label is required",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata RATING tests suite Create should fail with undefined option value 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option value is required",
|
||||
},
|
||||
"message": "Option value is required",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing create field metadata RATING tests suite Create should fail with undefined options 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
},
|
||||
"message": "Options are required for enum fields",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
-951
@@ -1,951 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with an invalid default value 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
},
|
||||
"message": "Default value for multi-select must be an array",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with an unknown default value 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
},
|
||||
"message": "Default value for multi-select must be an array",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with comma in option label 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Label must not contain a comma",
|
||||
},
|
||||
"message": "Label must not contain a comma",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with duplicated option ids 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Duplicated option id",
|
||||
},
|
||||
"message": "Duplicated option {field}",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with duplicated option positions 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Duplicated option position",
|
||||
},
|
||||
"message": "Duplicated option {field}",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with duplicated option values 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Duplicated option value",
|
||||
},
|
||||
"message": "Duplicated option {field}",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with duplicated trimmed option values 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Duplicated option value",
|
||||
},
|
||||
"message": "Duplicated option {field}",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with empty multi-select default values array 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "If defined default value must contain at least one value",
|
||||
},
|
||||
"message": "If defined default value must contain at least one value",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with empty options 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
},
|
||||
"message": "Options are required for enum fields",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with empty string defaultValue 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Default value should be as quoted string",
|
||||
},
|
||||
"message": "Default value should be as quoted string",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with empty string id 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option id is invalid",
|
||||
},
|
||||
"message": "Option id is invalid",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with empty string label 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option label "" is beneath 1 character",
|
||||
},
|
||||
"message": "Option label "{sanitizedLabel}" is beneath 1 character",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with empty string value 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option value "" is beneath 1 character",
|
||||
},
|
||||
"message": "Option value "{sanitizedValue}" is beneath 1 character",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with invalid multi-select default value format 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Default value should be as quoted string",
|
||||
},
|
||||
"message": "Default value should be as quoted string",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with invalid option id 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option id is invalid",
|
||||
},
|
||||
"message": "Option id is invalid",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with invalid option value format 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Value must be in UPPER_CASE and follow snake_case "Option 1 and some other things, /"",
|
||||
},
|
||||
"message": "Value must be in UPPER_CASE and follow snake_case "{sanitizedValue}"",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with non stringified array default value 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
},
|
||||
"message": "Default value for multi-select must be an array",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with not a string defaultValue 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
},
|
||||
"message": "Default value for multi-select must be a stringified array",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with not a string id 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option id is invalid",
|
||||
},
|
||||
"message": "Option id is invalid",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with not a string label 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "INTERNAL_SERVER_ERROR",
|
||||
"exceptionEventId": "mocked-exception-id",
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
},
|
||||
"message": "label.includes is not a function",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with not a string value 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Value must be in UPPER_CASE and follow snake_case "22222"",
|
||||
},
|
||||
"message": "Value must be in UPPER_CASE and follow snake_case "{sanitizedValue}"",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with null id 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option id is required",
|
||||
},
|
||||
"message": "Option id is required",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with null label 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option label is required",
|
||||
},
|
||||
"message": "Option label is required",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with null value 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option value is required",
|
||||
},
|
||||
"message": "Option value is required",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with only white spaces defaultValue 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Default value should be as quoted string",
|
||||
},
|
||||
"message": "Default value should be as quoted string",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with only white spaces id 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option id is invalid",
|
||||
},
|
||||
"message": "Option id is invalid",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with only white spaces label 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option label "" is beneath 1 character",
|
||||
},
|
||||
"message": "Option label "{sanitizedLabel}" is beneath 1 character",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with only white spaces value 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option value "" is beneath 1 character",
|
||||
},
|
||||
"message": "Option value "{sanitizedValue}" is beneath 1 character",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with too long defaultValue 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Default value should be as quoted string",
|
||||
},
|
||||
"message": "Default value should be as quoted string",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with too long id 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option id is invalid",
|
||||
},
|
||||
"message": "Option id is invalid",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with too long label 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option label exceeds 63 characters",
|
||||
},
|
||||
"message": "Option label exceeds 63 characters",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with too long value 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option value exceeds 63 characters",
|
||||
},
|
||||
"message": "Option value exceeds 63 characters",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with undefined option label 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option label is required",
|
||||
},
|
||||
"message": "Option label is required",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with undefined option value 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option value is required",
|
||||
},
|
||||
"message": "Option value is required",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with unknown default value and no options 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Default value "'OPTION_42'" must be one of the option values",
|
||||
},
|
||||
"message": "Default value "{defaultValue}" must be one of the option values",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with unknown multi-select default values 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Default value "'UNKNOWN_OPTION'" must be one of the option values",
|
||||
},
|
||||
"message": "Default value "{defaultValue}" must be one of the option values",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata SELECT tests suite Update should fail with an invalid default value 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Default value should be as quoted string",
|
||||
},
|
||||
"message": "Default value should be as quoted string",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata SELECT tests suite Update should fail with an unknown default value 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Default value "'OPTION_424242'" must be one of the option values",
|
||||
},
|
||||
"message": "Default value "{defaultValue}" must be one of the option values",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata SELECT tests suite Update should fail with comma in option label 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Label must not contain a comma",
|
||||
},
|
||||
"message": "Label must not contain a comma",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata SELECT tests suite Update should fail with duplicated option ids 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Duplicated option id",
|
||||
},
|
||||
"message": "Duplicated option {field}",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata SELECT tests suite Update should fail with duplicated option positions 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Duplicated option position",
|
||||
},
|
||||
"message": "Duplicated option {field}",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata SELECT tests suite Update should fail with duplicated option values 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Duplicated option value",
|
||||
},
|
||||
"message": "Duplicated option {field}",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata SELECT tests suite Update should fail with duplicated trimmed option values 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Duplicated option value",
|
||||
},
|
||||
"message": "Duplicated option {field}",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata SELECT tests suite Update should fail with empty options 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
},
|
||||
"message": "Options are required for enum fields",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata SELECT tests suite Update should fail with empty string defaultValue 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Default value should be as quoted string",
|
||||
},
|
||||
"message": "Default value should be as quoted string",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata SELECT tests suite Update should fail with empty string id 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option id is invalid",
|
||||
},
|
||||
"message": "Option id is invalid",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata SELECT tests suite Update should fail with empty string label 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option label "" is beneath 1 character",
|
||||
},
|
||||
"message": "Option label "{sanitizedLabel}" is beneath 1 character",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata SELECT tests suite Update should fail with empty string value 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option value "" is beneath 1 character",
|
||||
},
|
||||
"message": "Option value "{sanitizedValue}" is beneath 1 character",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata SELECT tests suite Update should fail with invalid option id 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option id is invalid",
|
||||
},
|
||||
"message": "Option id is invalid",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata SELECT tests suite Update should fail with invalid option value format 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Value must be in UPPER_CASE and follow snake_case "Option 1 and some other things, /"",
|
||||
},
|
||||
"message": "Value must be in UPPER_CASE and follow snake_case "{sanitizedValue}"",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata SELECT tests suite Update should fail with not a string defaultValue 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
},
|
||||
"message": "Default value for multi-select must be a stringified array",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata SELECT tests suite Update should fail with not a string id 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option id is invalid",
|
||||
},
|
||||
"message": "Option id is invalid",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata SELECT tests suite Update should fail with not a string label 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "INTERNAL_SERVER_ERROR",
|
||||
"exceptionEventId": "mocked-exception-id",
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
},
|
||||
"message": "label.includes is not a function",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata SELECT tests suite Update should fail with not a string value 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Value must be in UPPER_CASE and follow snake_case "22222"",
|
||||
},
|
||||
"message": "Value must be in UPPER_CASE and follow snake_case "{sanitizedValue}"",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata SELECT tests suite Update should fail with null id 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option id is required",
|
||||
},
|
||||
"message": "Option id is required",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata SELECT tests suite Update should fail with null label 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option label is required",
|
||||
},
|
||||
"message": "Option label is required",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata SELECT tests suite Update should fail with null value 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option value is required",
|
||||
},
|
||||
"message": "Option value is required",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata SELECT tests suite Update should fail with only white spaces defaultValue 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Default value should be as quoted string",
|
||||
},
|
||||
"message": "Default value should be as quoted string",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata SELECT tests suite Update should fail with only white spaces id 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option id is invalid",
|
||||
},
|
||||
"message": "Option id is invalid",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata SELECT tests suite Update should fail with only white spaces label 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option label "" is beneath 1 character",
|
||||
},
|
||||
"message": "Option label "{sanitizedLabel}" is beneath 1 character",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata SELECT tests suite Update should fail with only white spaces value 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option value "" is beneath 1 character",
|
||||
},
|
||||
"message": "Option value "{sanitizedValue}" is beneath 1 character",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata SELECT tests suite Update should fail with too long defaultValue 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Default value should be as quoted string",
|
||||
},
|
||||
"message": "Default value should be as quoted string",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata SELECT tests suite Update should fail with too long id 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option id is invalid",
|
||||
},
|
||||
"message": "Option id is invalid",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata SELECT tests suite Update should fail with too long label 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option label exceeds 63 characters",
|
||||
},
|
||||
"message": "Option label exceeds 63 characters",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata SELECT tests suite Update should fail with too long value 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option value exceeds 63 characters",
|
||||
},
|
||||
"message": "Option value exceeds 63 characters",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata SELECT tests suite Update should fail with undefined option label 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option label is required",
|
||||
},
|
||||
"message": "Option label is required",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata SELECT tests suite Update should fail with undefined option value 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Option value is required",
|
||||
},
|
||||
"message": "Option value is required",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Failing update field metadata SELECT tests suite Update should fail with unknown default value and no options 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_FIELD_INPUT",
|
||||
"userFriendlyMessage": "Default value "'OPTION_42'" must be one of the option values",
|
||||
},
|
||||
"message": "Default value "{defaultValue}" must be one of the option values",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
-85
@@ -1,85 +0,0 @@
|
||||
import { CREATE_ENUM_FIELD_METADATA_TEST_CASES } from 'test/integration/metadata/suites/field-metadata/enum/common/create-enum-field-metadata-test-cases';
|
||||
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-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 { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util';
|
||||
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';
|
||||
|
||||
describe('Failing create field metadata MULTI_SELECT tests suite', () => {
|
||||
let createdObjectMetadataId: string;
|
||||
const testCases =
|
||||
CREATE_ENUM_FIELD_METADATA_TEST_CASES[FieldMetadataType.MULTI_SELECT];
|
||||
|
||||
if (!isDefined(testCases)) {
|
||||
return;
|
||||
}
|
||||
const { failing: failingTestCases } = testCases;
|
||||
|
||||
beforeAll(async () => {
|
||||
await updateFeatureFlag({
|
||||
expectToFail: false,
|
||||
featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
||||
value: false,
|
||||
});
|
||||
});
|
||||
|
||||
beforeAll(async () => {
|
||||
const { data } = await createOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
labelSingular: 'Enum testing v1',
|
||||
labelPlural: 'Enums testings v1',
|
||||
nameSingular: 'enumTestingV1',
|
||||
namePlural: 'enumsTestingsV1',
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
isLabelSyncedWithName: false,
|
||||
},
|
||||
});
|
||||
|
||||
createdObjectMetadataId = data.createOneObject.id;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await deleteOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: { idToDelete: createdObjectMetadataId },
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await updateFeatureFlag({
|
||||
expectToFail: false,
|
||||
featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
||||
value: true,
|
||||
});
|
||||
});
|
||||
|
||||
test.each(failingTestCases)(
|
||||
'Create $title',
|
||||
async ({ context: { input } }) => {
|
||||
const { data, errors } = await createOneFieldMetadata({
|
||||
expectToFail: true,
|
||||
input: {
|
||||
objectMetadataId: createdObjectMetadataId,
|
||||
type: FieldMetadataType.MULTI_SELECT,
|
||||
name: 'testField',
|
||||
label: 'Test Field',
|
||||
isLabelSyncedWithName: false,
|
||||
...input,
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
options
|
||||
defaultValue
|
||||
`,
|
||||
});
|
||||
|
||||
expect(data).toBeNull();
|
||||
expect(errors).toBeDefined();
|
||||
expect(errors).toMatchSnapshot();
|
||||
},
|
||||
);
|
||||
});
|
||||
-85
@@ -1,85 +0,0 @@
|
||||
import { CREATE_ENUM_FIELD_METADATA_TEST_CASES } from 'test/integration/metadata/suites/field-metadata/enum/common/create-enum-field-metadata-test-cases';
|
||||
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
|
||||
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
|
||||
import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
|
||||
describe('Failing create field metadata RATING tests suite', () => {
|
||||
let createdObjectMetadataId: string;
|
||||
const testCases =
|
||||
CREATE_ENUM_FIELD_METADATA_TEST_CASES[FieldMetadataType.SELECT];
|
||||
|
||||
if (!isDefined(testCases)) {
|
||||
return;
|
||||
}
|
||||
const { failing: failingTestCases } = testCases;
|
||||
|
||||
beforeAll(async () => {
|
||||
await updateFeatureFlag({
|
||||
expectToFail: false,
|
||||
featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
||||
value: false,
|
||||
});
|
||||
});
|
||||
|
||||
beforeAll(async () => {
|
||||
const { data } = await createOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
labelSingular: 'Enum testing v1',
|
||||
labelPlural: 'Enums testings v1',
|
||||
nameSingular: 'enumTestingV1',
|
||||
namePlural: 'enumsTestingsV1',
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
isLabelSyncedWithName: false,
|
||||
},
|
||||
});
|
||||
|
||||
createdObjectMetadataId = data.createOneObject.id;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await deleteOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: { idToDelete: createdObjectMetadataId },
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await updateFeatureFlag({
|
||||
expectToFail: false,
|
||||
featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
||||
value: true,
|
||||
});
|
||||
});
|
||||
|
||||
test.each(failingTestCases)(
|
||||
'Create $title',
|
||||
async ({ context: { input } }) => {
|
||||
const { data, errors } = await createOneFieldMetadata({
|
||||
expectToFail: true,
|
||||
input: {
|
||||
objectMetadataId: createdObjectMetadataId,
|
||||
type: FieldMetadataType.SELECT,
|
||||
name: 'testField',
|
||||
label: 'Test Field',
|
||||
isLabelSyncedWithName: false,
|
||||
...input,
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
options
|
||||
defaultValue
|
||||
`,
|
||||
});
|
||||
|
||||
expect(data).toBeNull();
|
||||
expect(errors).toBeDefined();
|
||||
expect(errors).toMatchSnapshot();
|
||||
},
|
||||
);
|
||||
});
|
||||
-124
@@ -1,124 +0,0 @@
|
||||
import { UPDATE_ENUM_FIELD_METADATA_TEST_CASES } from 'test/integration/metadata/suites/field-metadata/enum/common/update-enum-field-metadata-test-cases';
|
||||
import { type CreateOneFieldFactoryInput } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata-query-factory.util';
|
||||
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
|
||||
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 { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { fieldMetadataEnumTypes } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util';
|
||||
|
||||
describe.each(fieldMetadataEnumTypes)(
|
||||
'Failing update field metadata %s tests suite',
|
||||
(testedFieldMetadataType) => {
|
||||
let createdObjectMetadataId: string;
|
||||
let createdFieldMetadataId: string;
|
||||
const testCases =
|
||||
UPDATE_ENUM_FIELD_METADATA_TEST_CASES[testedFieldMetadataType];
|
||||
|
||||
if (!isDefined(testCases)) {
|
||||
return;
|
||||
}
|
||||
const { failing: failingTestCases } = testCases;
|
||||
|
||||
beforeAll(async () => {
|
||||
await updateFeatureFlag({
|
||||
expectToFail: false,
|
||||
featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
||||
value: false,
|
||||
});
|
||||
});
|
||||
|
||||
const initialOptions: CreateOneFieldFactoryInput['options'] = [
|
||||
{
|
||||
label: 'Option 1',
|
||||
value: 'OPTION_1',
|
||||
color: 'green',
|
||||
position: 1,
|
||||
},
|
||||
{
|
||||
label: 'Option 2',
|
||||
value: 'OPTION_2',
|
||||
color: 'green',
|
||||
position: 2,
|
||||
},
|
||||
];
|
||||
|
||||
beforeAll(async () => {
|
||||
const { data } = await createOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
labelSingular: 'failingUpdateEnumField',
|
||||
labelPlural: 'failingUpdateEnumFields',
|
||||
nameSingular: 'failingUpdateEnumField',
|
||||
namePlural: 'failingUpdateEnumFields',
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
isLabelSyncedWithName: false,
|
||||
},
|
||||
});
|
||||
|
||||
createdObjectMetadataId = data.createOneObject.id;
|
||||
|
||||
const {
|
||||
data: { createOneField },
|
||||
} = await createOneFieldMetadata({
|
||||
input: {
|
||||
objectMetadataId: createdObjectMetadataId,
|
||||
type: testedFieldMetadataType,
|
||||
name: 'testField',
|
||||
label: 'Test Field',
|
||||
isLabelSyncedWithName: false,
|
||||
options: initialOptions,
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
type
|
||||
`,
|
||||
});
|
||||
|
||||
createdFieldMetadataId = createOneField.id;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await deleteOneObjectMetadata({
|
||||
input: {
|
||||
idToDelete: createdObjectMetadataId,
|
||||
},
|
||||
expectToFail: false,
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await updateFeatureFlag({
|
||||
expectToFail: false,
|
||||
featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
||||
value: true,
|
||||
});
|
||||
});
|
||||
|
||||
test.each(failingTestCases)(
|
||||
'Update $title',
|
||||
async ({ context: { input } }) => {
|
||||
const { ...updatePayload } = input;
|
||||
|
||||
const { data, errors } = await updateOneFieldMetadata({
|
||||
expectToFail: true,
|
||||
input: {
|
||||
idToUpdate: createdFieldMetadataId,
|
||||
updatePayload,
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
options
|
||||
`,
|
||||
});
|
||||
|
||||
expect(data).toBeNull();
|
||||
expect(errors).toBeDefined();
|
||||
expect(errors).toMatchSnapshot();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
-112
@@ -1,112 +0,0 @@
|
||||
import { CREATE_ENUM_FIELD_METADATA_TEST_CASES } from 'test/integration/metadata/suites/field-metadata/enum/common/create-enum-field-metadata-test-cases';
|
||||
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-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 { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
|
||||
import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { fieldMetadataEnumTypes } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util';
|
||||
|
||||
describe.each(fieldMetadataEnumTypes)(
|
||||
'Successful create field metadata %s tests suite',
|
||||
(testedFieldMetadataType) => {
|
||||
let createdObjectMetadataId: string;
|
||||
const testCases =
|
||||
CREATE_ENUM_FIELD_METADATA_TEST_CASES[testedFieldMetadataType];
|
||||
|
||||
if (!isDefined(testCases)) {
|
||||
return;
|
||||
}
|
||||
const { successful: successfulTestCases } = testCases;
|
||||
|
||||
beforeAll(async () => {
|
||||
await updateFeatureFlag({
|
||||
expectToFail: false,
|
||||
featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
||||
value: false,
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await updateFeatureFlag({
|
||||
expectToFail: false,
|
||||
featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
||||
value: true,
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
const { data } = await createOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
labelSingular: 'successCreateEnumField',
|
||||
labelPlural: 'successCreateEnumFields',
|
||||
nameSingular: 'successCreateEnumField',
|
||||
namePlural: 'successCreateEnumFields',
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
isLabelSyncedWithName: false,
|
||||
},
|
||||
});
|
||||
|
||||
createdObjectMetadataId = data.createOneObject.id;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await updateOneObjectMetadata({
|
||||
input: {
|
||||
idToUpdate: createdObjectMetadataId,
|
||||
updatePayload: {
|
||||
isActive: false,
|
||||
},
|
||||
},
|
||||
expectToFail: false,
|
||||
});
|
||||
await deleteOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: { idToDelete: createdObjectMetadataId },
|
||||
});
|
||||
});
|
||||
|
||||
test.each(successfulTestCases)(
|
||||
'Create $title',
|
||||
async ({ context: { input, expectedOptions } }) => {
|
||||
const { data, errors } = await createOneFieldMetadata<
|
||||
typeof testedFieldMetadataType
|
||||
>({
|
||||
input: {
|
||||
objectMetadataId: createdObjectMetadataId,
|
||||
type: testedFieldMetadataType,
|
||||
name: 'testField',
|
||||
label: 'Test Field',
|
||||
isLabelSyncedWithName: false,
|
||||
...input,
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
options
|
||||
defaultValue
|
||||
type
|
||||
`,
|
||||
});
|
||||
|
||||
expect(data).not.toBeNull();
|
||||
expect(data.createOneField).toBeDefined();
|
||||
expect(data.createOneField.type).toEqual(testedFieldMetadataType);
|
||||
|
||||
const createdOptions = data.createOneField.options;
|
||||
const optionsToCompare = expectedOptions ?? input.options ?? [];
|
||||
|
||||
expect(errors).toBeUndefined();
|
||||
expect(createdOptions?.length).toBe(optionsToCompare.length);
|
||||
createdOptions?.forEach((option) => expect(option.id).toBeDefined());
|
||||
expect(createdOptions).toMatchObject(optionsToCompare);
|
||||
|
||||
if (isDefined(input.defaultValue)) {
|
||||
expect(data.createOneField.defaultValue).toEqual(input.defaultValue);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
-204
@@ -1,204 +0,0 @@
|
||||
import { UPDATE_ENUM_FIELD_METADATA_TEST_CASES } from 'test/integration/metadata/suites/field-metadata/enum/common/update-enum-field-metadata-test-cases';
|
||||
import { type CreateOneFieldFactoryInput } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata-query-factory.util';
|
||||
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
|
||||
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 { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
|
||||
import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util';
|
||||
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 {
|
||||
type FieldMetadataComplexOption,
|
||||
type FieldMetadataDefaultOption,
|
||||
} from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
|
||||
import { fieldMetadataEnumTypes } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util';
|
||||
|
||||
describe.each(fieldMetadataEnumTypes)(
|
||||
'Successful update field metadata %s tests suite',
|
||||
(testedFieldMetadataType) => {
|
||||
let createdObjectMetadataId: string;
|
||||
let createdFieldMetadataId: string;
|
||||
const testCases =
|
||||
UPDATE_ENUM_FIELD_METADATA_TEST_CASES[testedFieldMetadataType];
|
||||
|
||||
if (!isDefined(testCases)) {
|
||||
return;
|
||||
}
|
||||
const { successful: successfulTestCases } = testCases;
|
||||
|
||||
beforeAll(async () => {
|
||||
await updateFeatureFlag({
|
||||
expectToFail: false,
|
||||
featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
||||
value: false,
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await updateFeatureFlag({
|
||||
expectToFail: false,
|
||||
featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
||||
value: true,
|
||||
});
|
||||
});
|
||||
|
||||
const initialOptions: CreateOneFieldFactoryInput['options'] = [
|
||||
{
|
||||
label: 'Option 1',
|
||||
value: 'OPTION_1',
|
||||
color: 'green',
|
||||
position: 1,
|
||||
},
|
||||
{
|
||||
label: 'Option 2',
|
||||
value: 'OPTION_2',
|
||||
color: 'green',
|
||||
position: 2,
|
||||
},
|
||||
];
|
||||
|
||||
beforeEach(async () => {
|
||||
const { data } = await createOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
labelSingular: 'successUpdateEnumField',
|
||||
labelPlural: 'successUpdateEnumFields',
|
||||
nameSingular: 'successUpdateEnumField',
|
||||
namePlural: 'successUpdateEnumFields',
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
isLabelSyncedWithName: false,
|
||||
},
|
||||
});
|
||||
|
||||
createdObjectMetadataId = data.createOneObject.id;
|
||||
|
||||
const {
|
||||
data: { createOneField },
|
||||
} = await createOneFieldMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
objectMetadataId: createdObjectMetadataId,
|
||||
type: testedFieldMetadataType,
|
||||
name: 'testField',
|
||||
label: 'Test Field',
|
||||
isLabelSyncedWithName: false,
|
||||
options: initialOptions,
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
type
|
||||
`,
|
||||
});
|
||||
|
||||
createdFieldMetadataId = createOneField.id;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await updateOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
idToUpdate: createdObjectMetadataId,
|
||||
updatePayload: {
|
||||
isActive: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
await deleteOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: { idToDelete: createdObjectMetadataId },
|
||||
});
|
||||
});
|
||||
|
||||
it('Should update default value to null even if it was set before', async () => {
|
||||
const isMultiSelect =
|
||||
testedFieldMetadataType === FieldMetadataType.MULTI_SELECT;
|
||||
const rawDefaultValue = `'${initialOptions[0].value}'`;
|
||||
const expectedDefaultValue = isMultiSelect
|
||||
? [rawDefaultValue]
|
||||
: rawDefaultValue;
|
||||
|
||||
const { data: firstUpdate } = await updateOneFieldMetadata({
|
||||
input: {
|
||||
idToUpdate: createdFieldMetadataId,
|
||||
updatePayload: {
|
||||
defaultValue: expectedDefaultValue,
|
||||
},
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
defaultValue
|
||||
`,
|
||||
});
|
||||
|
||||
expect(firstUpdate.updateOneField.defaultValue).toEqual(
|
||||
expectedDefaultValue,
|
||||
);
|
||||
|
||||
const updatedOptions = initialOptions.slice(1);
|
||||
const { data: secondUpdate, errors } = await updateOneFieldMetadata({
|
||||
input: {
|
||||
idToUpdate: createdFieldMetadataId,
|
||||
updatePayload: {
|
||||
defaultValue: null,
|
||||
options: updatedOptions,
|
||||
},
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
options
|
||||
defaultValue
|
||||
`,
|
||||
});
|
||||
|
||||
expect(errors).toBeUndefined();
|
||||
expect(secondUpdate.updateOneField.defaultValue).toBeNull();
|
||||
expect(secondUpdate.updateOneField.options).toMatchObject(updatedOptions);
|
||||
});
|
||||
|
||||
test.each(successfulTestCases)(
|
||||
'Update $title',
|
||||
async ({ context: { input, expectedOptions } }) => {
|
||||
const { ...updatePayload } = input;
|
||||
|
||||
const { data, errors } = await updateOneFieldMetadata({
|
||||
input: {
|
||||
idToUpdate: createdFieldMetadataId,
|
||||
updatePayload,
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
options
|
||||
defaultValue
|
||||
`,
|
||||
});
|
||||
|
||||
expect(data.updateOneField).toBeDefined();
|
||||
const updatedOptions:
|
||||
| FieldMetadataComplexOption[]
|
||||
| FieldMetadataDefaultOption[]
|
||||
| null = data.updateOneField.options;
|
||||
|
||||
expect(updatedOptions).toBeDefined();
|
||||
if (!isDefined(updatedOptions))
|
||||
throw new Error(
|
||||
'Should never occur, type invariant post test assertion',
|
||||
);
|
||||
|
||||
expect(errors).toBeUndefined();
|
||||
updatedOptions.forEach((option) => expect(option.id).toBeDefined());
|
||||
|
||||
const optionsToCompare = expectedOptions ?? input.options ?? [];
|
||||
|
||||
expect(updatedOptions.length).toBe(optionsToCompare.length);
|
||||
expect(updatedOptions).toMatchObject(optionsToCompare);
|
||||
|
||||
if (isDefined(input.defaultValue)) {
|
||||
expect(data.updateOneField.defaultValue).toEqual(input.defaultValue);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
-191
@@ -1,191 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Field metadata morph relation creation should fail relation MANY_TO_ONE when targetFieldLabel conflicts with an existing field on target object metadata id 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "Name not available",
|
||||
"userFriendlyMessage": "This name is not available as it is already used by another field.",
|
||||
},
|
||||
"message": "Name "collisionfieldlabel" is not available as it is already used by another field",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Field metadata morph relation creation should fail relation MANY_TO_ONE when targetFieldLabel contains only whitespace 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "Invalid label",
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
},
|
||||
"message": "Invalid label: " "",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Field metadata morph relation creation should fail relation MANY_TO_ONE when targetFieldLabel exceeds maximum length 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "Exceeds max length",
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
},
|
||||
"message": "Name is too long: it exceeds the 63 characters limit.",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Field metadata morph relation creation should fail relation MANY_TO_ONE when targetFieldLabel is empty 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "Input too short",
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
},
|
||||
"message": "Input is too short: """,
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Field metadata morph relation creation should fail relation MANY_TO_ONE when targetObjectMetadataId is unknown 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "INTERNAL_SERVER_ERROR",
|
||||
"exceptionEventId": "mocked-exception-id",
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
},
|
||||
"message": "Target object metadata does not exist in the object metadata maps",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Field metadata morph relation creation should fail relation MANY_TO_ONE when type is a wrong value 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "INTERNAL_SERVER_ERROR",
|
||||
"exceptionEventId": "mocked-exception-id",
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
},
|
||||
"message": "Invalid relation type (wrong) for field owner on firstTargetRelation",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Field metadata morph relation creation should fail relation MANY_TO_ONE when type is not provided 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "INTERNAL_SERVER_ERROR",
|
||||
"exceptionEventId": "mocked-exception-id",
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
},
|
||||
"message": "Invalid relation type (undefined) for field owner on firstTargetRelation",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Field metadata morph relation creation should fail relation ONE_TO_MANY when targetFieldLabel conflicts with an existing field on target object metadata id 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "Name not available",
|
||||
"userFriendlyMessage": "This name is not available as it is already used by another field.",
|
||||
},
|
||||
"message": "Name "collisionfieldlabel" is not available as it is already used by another field",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Field metadata morph relation creation should fail relation ONE_TO_MANY when targetFieldLabel contains only whitespace 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "Invalid label",
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
},
|
||||
"message": "Invalid label: " "",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Field metadata morph relation creation should fail relation ONE_TO_MANY when targetFieldLabel exceeds maximum length 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "Exceeds max length",
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
},
|
||||
"message": "Name is too long: it exceeds the 63 characters limit.",
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Field metadata morph relation creation should fail relation ONE_TO_MANY when targetFieldLabel is empty 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "Input too short",
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
},
|
||||
"message": "Input is too short: """,
|
||||
"name": "UserInputError",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Field metadata morph relation creation should fail relation ONE_TO_MANY when targetObjectMetadataId is unknown 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "INTERNAL_SERVER_ERROR",
|
||||
"exceptionEventId": "mocked-exception-id",
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
},
|
||||
"message": "Target object metadata does not exist in the object metadata maps",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Field metadata morph relation creation should fail relation ONE_TO_MANY when type is a wrong value 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "INTERNAL_SERVER_ERROR",
|
||||
"exceptionEventId": "mocked-exception-id",
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
},
|
||||
"message": "Invalid relation type (wrong) for field owner on firstTargetRelation",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Field metadata morph relation creation should fail relation ONE_TO_MANY when type is not provided 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "INTERNAL_SERVER_ERROR",
|
||||
"exceptionEventId": "mocked-exception-id",
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
},
|
||||
"message": "Invalid relation type (undefined) for field owner on firstTargetRelation",
|
||||
},
|
||||
]
|
||||
`;
|
||||
-247
@@ -1,247 +0,0 @@
|
||||
import { faker } from '@faker-js/faker/.';
|
||||
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-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 { getMockCreateObjectInput } from 'test/integration/metadata/suites/object-metadata/utils/generate-mock-create-object-metadata-input';
|
||||
import { type EachTestingContext } from 'twenty-shared/testing';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util';
|
||||
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
|
||||
|
||||
type GlobalTestContext = {
|
||||
objectMetadataIds: {
|
||||
firstTargetObjectId: string;
|
||||
secondTargetObjectId: string;
|
||||
sourceObjectId: string;
|
||||
};
|
||||
targetFieldLabel: string;
|
||||
type: FieldMetadataType;
|
||||
targetFieldIcon: string;
|
||||
collisionFieldLabel: string;
|
||||
};
|
||||
const globalTestContext: GlobalTestContext = {
|
||||
objectMetadataIds: {
|
||||
firstTargetObjectId: '',
|
||||
secondTargetObjectId: '',
|
||||
sourceObjectId: '',
|
||||
},
|
||||
targetFieldLabel: 'defaultTargetFieldLabel',
|
||||
type: FieldMetadataType.MORPH_RELATION,
|
||||
targetFieldIcon: 'IconBuildingSkyscraper',
|
||||
collisionFieldLabel: 'collisionfieldlabel',
|
||||
};
|
||||
|
||||
type TestedRelationCreationPayload = Partial<
|
||||
NonNullable<CreateFieldInput['relationCreationPayload']>
|
||||
>;
|
||||
|
||||
type CreateOneObjectMetadataItemTestingContext = EachTestingContext<
|
||||
| TestedRelationCreationPayload
|
||||
| ((context: GlobalTestContext) => TestedRelationCreationPayload)
|
||||
>[];
|
||||
describe('Field metadata morph relation creation should fail', () => {
|
||||
beforeAll(async () => {
|
||||
await updateFeatureFlag({
|
||||
expectToFail: false,
|
||||
featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
||||
value: false,
|
||||
});
|
||||
});
|
||||
|
||||
const failingLabelsCreationTestsUseCase: CreateOneObjectMetadataItemTestingContext =
|
||||
[
|
||||
{
|
||||
title: 'when targetFieldLabel is empty',
|
||||
context: { targetFieldLabel: '' },
|
||||
},
|
||||
{
|
||||
title: 'when targetFieldLabel exceeds maximum length',
|
||||
context: {
|
||||
targetFieldLabel: 'A'.repeat(64),
|
||||
},
|
||||
},
|
||||
{
|
||||
// Not handled gracefully should be refactored
|
||||
title: 'when targetObjectMetadataId is unknown',
|
||||
context: {
|
||||
targetObjectMetadataId: faker.string.uuid(),
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'when targetFieldLabel contains only whitespace',
|
||||
context: { targetFieldLabel: ' ' },
|
||||
},
|
||||
{
|
||||
title:
|
||||
'when targetFieldLabel conflicts with an existing field on target object metadata id',
|
||||
context: ({ collisionFieldLabel }) => ({
|
||||
targetFieldLabel: collisionFieldLabel,
|
||||
}),
|
||||
},
|
||||
{
|
||||
title: 'when type is not provided',
|
||||
context: { type: undefined },
|
||||
},
|
||||
{
|
||||
title: 'when type is a wrong value',
|
||||
context: { type: 'wrong' as RelationType },
|
||||
},
|
||||
];
|
||||
|
||||
beforeAll(async () => {
|
||||
const {
|
||||
data: {
|
||||
createOneObject: { id: sourceObjectId },
|
||||
},
|
||||
} = await createOneObjectMetadata({
|
||||
input: getMockCreateObjectInput({
|
||||
namePlural: 'sourceRelations',
|
||||
nameSingular: 'sourceRelation',
|
||||
}),
|
||||
});
|
||||
|
||||
const {
|
||||
data: {
|
||||
createOneObject: { id: firstTargetObjectId },
|
||||
},
|
||||
} = await createOneObjectMetadata({
|
||||
input: getMockCreateObjectInput({
|
||||
namePlural: 'firstTargetRelations',
|
||||
nameSingular: 'firstTargetRelation',
|
||||
}),
|
||||
});
|
||||
|
||||
const {
|
||||
data: {
|
||||
createOneObject: { id: secondTargetObjectId },
|
||||
},
|
||||
} = await createOneObjectMetadata({
|
||||
input: getMockCreateObjectInput({
|
||||
namePlural: 'secondTargetRelations',
|
||||
nameSingular: 'secondTargetRelation',
|
||||
}),
|
||||
});
|
||||
|
||||
globalTestContext.objectMetadataIds = {
|
||||
sourceObjectId,
|
||||
firstTargetObjectId,
|
||||
secondTargetObjectId,
|
||||
};
|
||||
|
||||
const { data } = await createOneFieldMetadata({
|
||||
input: {
|
||||
objectMetadataId: firstTargetObjectId,
|
||||
name: globalTestContext.collisionFieldLabel,
|
||||
label: 'LabelThatCouldBeAnything',
|
||||
isLabelSyncedWithName: false,
|
||||
type: FieldMetadataType.TEXT,
|
||||
},
|
||||
});
|
||||
|
||||
expect(data).toBeDefined();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
for (const objectMetadataId of Object.values(
|
||||
globalTestContext.objectMetadataIds,
|
||||
)) {
|
||||
await deleteOneObjectMetadata({
|
||||
input: {
|
||||
idToDelete: objectMetadataId,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await updateFeatureFlag({
|
||||
expectToFail: false,
|
||||
featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
||||
value: true,
|
||||
});
|
||||
});
|
||||
|
||||
it.each(failingLabelsCreationTestsUseCase)(
|
||||
'relation ONE_TO_MANY $title',
|
||||
async ({ context }) => {
|
||||
const computedContext =
|
||||
typeof context === 'function' ? context(globalTestContext) : context;
|
||||
|
||||
const { errors } = await createOneFieldMetadata({
|
||||
expectToFail: true,
|
||||
input: {
|
||||
objectMetadataId: globalTestContext.objectMetadataIds.sourceObjectId,
|
||||
name: 'owner',
|
||||
label: 'owner field',
|
||||
isLabelSyncedWithName: false,
|
||||
type: FieldMetadataType.MORPH_RELATION,
|
||||
morphRelationsCreationPayload: [
|
||||
{
|
||||
targetFieldLabel: 'defaultFirstTargetFieldLabel',
|
||||
type: RelationType.ONE_TO_MANY,
|
||||
targetObjectMetadataId:
|
||||
globalTestContext.objectMetadataIds.firstTargetObjectId,
|
||||
targetFieldIcon: 'IconBuildingSkyscraper',
|
||||
...computedContext,
|
||||
},
|
||||
{
|
||||
targetFieldLabel: 'defaultSecondTargetFieldLabel',
|
||||
type: RelationType.ONE_TO_MANY,
|
||||
targetObjectMetadataId:
|
||||
globalTestContext.objectMetadataIds.secondTargetObjectId,
|
||||
targetFieldIcon: 'IconBuildingSkyscraper',
|
||||
...computedContext,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(errors).toBeDefined();
|
||||
expect(errors).toMatchSnapshot();
|
||||
},
|
||||
);
|
||||
|
||||
it.each(failingLabelsCreationTestsUseCase)(
|
||||
'relation MANY_TO_ONE $title',
|
||||
async ({ context }) => {
|
||||
const computedContext =
|
||||
typeof context === 'function' ? context(globalTestContext) : context;
|
||||
|
||||
const { errors } = await createOneFieldMetadata({
|
||||
expectToFail: true,
|
||||
input: {
|
||||
objectMetadataId: globalTestContext.objectMetadataIds.sourceObjectId,
|
||||
name: 'owner',
|
||||
label: 'owner field',
|
||||
isLabelSyncedWithName: false,
|
||||
type: FieldMetadataType.MORPH_RELATION,
|
||||
morphRelationsCreationPayload: [
|
||||
{
|
||||
targetFieldLabel: 'defaultFirstTargetFieldLabel',
|
||||
type: RelationType.MANY_TO_ONE,
|
||||
targetObjectMetadataId:
|
||||
globalTestContext.objectMetadataIds.firstTargetObjectId,
|
||||
targetFieldIcon: 'IconBuildingSkyscraper',
|
||||
...computedContext,
|
||||
},
|
||||
{
|
||||
targetFieldLabel: 'defaultSecondTargetFieldLabel',
|
||||
type: RelationType.MANY_TO_ONE,
|
||||
targetObjectMetadataId:
|
||||
globalTestContext.objectMetadataIds.secondTargetObjectId,
|
||||
targetFieldIcon: 'IconBuildingSkyscraper',
|
||||
...computedContext,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(errors).toBeDefined();
|
||||
expect(errors).toMatchSnapshot();
|
||||
},
|
||||
);
|
||||
});
|
||||
-226
@@ -1,226 +0,0 @@
|
||||
import gql from 'graphql-tag';
|
||||
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
|
||||
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
|
||||
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 { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
|
||||
import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
|
||||
const createRecordQuery = gql`
|
||||
mutation CreateFirstRecord($data: TestRecordObjectCreateInput!) {
|
||||
createTestRecordObject(data: $data) {
|
||||
id
|
||||
uniqueTestField
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
describe('create records with custom unique fields', () => {
|
||||
let createdObjectMetadataId = '';
|
||||
let uniqueFieldId = '';
|
||||
|
||||
beforeAll(async () => {
|
||||
await updateFeatureFlag({
|
||||
expectToFail: false,
|
||||
featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
||||
value: false,
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await updateFeatureFlag({
|
||||
expectToFail: false,
|
||||
featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
||||
value: true,
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
const {
|
||||
data: {
|
||||
createOneObject: { id: objectMetadataId },
|
||||
},
|
||||
} = await createOneObjectMetadata({
|
||||
input: {
|
||||
nameSingular: 'testRecordObject',
|
||||
namePlural: 'testRecordObjects',
|
||||
labelSingular: 'Test Record Object',
|
||||
labelPlural: 'Test Record Objects',
|
||||
icon: 'IconTestRecord',
|
||||
},
|
||||
});
|
||||
|
||||
createdObjectMetadataId = objectMetadataId;
|
||||
|
||||
const { data: createdField } = await createOneFieldMetadata({
|
||||
input: {
|
||||
name: 'uniqueTestField',
|
||||
label: 'Unique Test Field',
|
||||
type: FieldMetadataType.TEXT,
|
||||
objectMetadataId: createdObjectMetadataId,
|
||||
isUnique: true,
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
name
|
||||
label
|
||||
type
|
||||
isUnique
|
||||
`,
|
||||
});
|
||||
|
||||
uniqueFieldId = createdField.createOneField.id;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await updateOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
idToUpdate: createdObjectMetadataId,
|
||||
updatePayload: {
|
||||
isActive: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
await deleteOneObjectMetadata({
|
||||
input: { idToDelete: createdObjectMetadataId },
|
||||
});
|
||||
});
|
||||
|
||||
it('should not create the second record with same value on unique field', async () => {
|
||||
const firstRecordResponse = await makeGraphqlAPIRequest({
|
||||
query: createRecordQuery,
|
||||
variables: {
|
||||
data: {
|
||||
uniqueTestField: 'uniqueValue123',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(firstRecordResponse.body.errors).toBeUndefined();
|
||||
expect(firstRecordResponse.body.data.createTestRecordObject).toBeDefined();
|
||||
expect(
|
||||
firstRecordResponse.body.data.createTestRecordObject.uniqueTestField,
|
||||
).toBe('uniqueValue123');
|
||||
|
||||
const secondRecordResponse = await makeGraphqlAPIRequest({
|
||||
query: createRecordQuery,
|
||||
variables: {
|
||||
data: {
|
||||
uniqueTestField: 'uniqueValue123',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(secondRecordResponse.body.errors).toBeDefined();
|
||||
expect(secondRecordResponse.body.errors[0].message).toContain(
|
||||
'Duplicate Unique Test Field with value uniqueValue123. Please set a unique one.',
|
||||
);
|
||||
expect(secondRecordResponse.body.data.createTestRecordObject).toBeNull();
|
||||
});
|
||||
|
||||
it('should create the second record with same value on updated field (previously unique)', async () => {
|
||||
const firstRecordResponse = await makeGraphqlAPIRequest({
|
||||
query: createRecordQuery,
|
||||
variables: {
|
||||
data: {
|
||||
uniqueTestField: 'duplicateValue',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(firstRecordResponse.body.errors).toBeUndefined();
|
||||
expect(
|
||||
firstRecordResponse.body.data.createTestRecordObject.uniqueTestField,
|
||||
).toBe('duplicateValue');
|
||||
|
||||
const { data, errors } = await updateOneFieldMetadata({
|
||||
input: {
|
||||
idToUpdate: uniqueFieldId,
|
||||
updatePayload: { isUnique: false },
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
name
|
||||
isUnique
|
||||
`,
|
||||
});
|
||||
|
||||
expect(errors).toBeUndefined();
|
||||
expect(data.updateOneField.isUnique).toBe(false);
|
||||
|
||||
const secondRecordResponse = await makeGraphqlAPIRequest({
|
||||
query: createRecordQuery,
|
||||
variables: {
|
||||
data: {
|
||||
uniqueTestField: 'duplicateValue',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(secondRecordResponse.body.errors).toBeUndefined();
|
||||
expect(secondRecordResponse.body.data.createTestRecordObject).toBeDefined();
|
||||
expect(
|
||||
secondRecordResponse.body.data.createTestRecordObject.uniqueTestField,
|
||||
).toBe('duplicateValue');
|
||||
});
|
||||
|
||||
it('should not create a unique index on field if records with same value already exist', async () => {
|
||||
await updateOneFieldMetadata({
|
||||
input: {
|
||||
idToUpdate: uniqueFieldId,
|
||||
updatePayload: { isUnique: false },
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
name
|
||||
isUnique
|
||||
`,
|
||||
});
|
||||
|
||||
const firstRecordResponse = await makeGraphqlAPIRequest({
|
||||
query: createRecordQuery,
|
||||
variables: {
|
||||
data: {
|
||||
uniqueTestField: 'duplicatedValue',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(firstRecordResponse.body.errors).toBeUndefined();
|
||||
|
||||
const secondRecordResponse = await makeGraphqlAPIRequest({
|
||||
query: createRecordQuery,
|
||||
variables: {
|
||||
data: {
|
||||
uniqueTestField: 'duplicatedValue',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(secondRecordResponse.body.errors).toBeUndefined();
|
||||
|
||||
const { data, errors } = await updateOneFieldMetadata({
|
||||
input: {
|
||||
idToUpdate: uniqueFieldId,
|
||||
updatePayload: { isUnique: true },
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
name
|
||||
isUnique
|
||||
`,
|
||||
expectToFail: true,
|
||||
});
|
||||
|
||||
expect(data).toBeNull();
|
||||
expect(errors).toBeDefined();
|
||||
expect(errors[0].message).toContain(
|
||||
'Unique index creation failed because of unique constraint violation',
|
||||
);
|
||||
});
|
||||
});
|
||||
-380
@@ -1,380 +0,0 @@
|
||||
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
|
||||
import { deleteOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/delete-one-field-metadata.util';
|
||||
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 { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
|
||||
import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
|
||||
describe('update one unique field metadata', () => {
|
||||
let createdObjectId = '';
|
||||
|
||||
beforeAll(async () => {
|
||||
await updateFeatureFlag({
|
||||
expectToFail: false,
|
||||
featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
||||
value: false,
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await updateFeatureFlag({
|
||||
expectToFail: false,
|
||||
featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
||||
value: true,
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
const {
|
||||
data: {
|
||||
createOneObject: { id: objectMetadataId },
|
||||
},
|
||||
} = await createOneObjectMetadata({
|
||||
input: {
|
||||
nameSingular: 'testObjectToTestUniqueField',
|
||||
namePlural: 'testObjectsToTestUniqueFields',
|
||||
labelSingular: 'Test Object To Test Unique Field',
|
||||
labelPlural: 'Test Objects To Test Unique Fields',
|
||||
icon: 'IconTest',
|
||||
},
|
||||
});
|
||||
|
||||
createdObjectId = objectMetadataId;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await updateOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
idToUpdate: createdObjectId,
|
||||
updatePayload: {
|
||||
isActive: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
await deleteOneObjectMetadata({
|
||||
input: { idToDelete: createdObjectId },
|
||||
});
|
||||
});
|
||||
|
||||
it('should update non unique field metadata to unique', async () => {
|
||||
const { data: createdField } = await createOneFieldMetadata({
|
||||
input: {
|
||||
name: 'testField',
|
||||
label: 'Test Field',
|
||||
type: FieldMetadataType.TEXT,
|
||||
objectMetadataId: createdObjectId,
|
||||
isUnique: false,
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
name
|
||||
label
|
||||
type
|
||||
isUnique
|
||||
`,
|
||||
});
|
||||
|
||||
const { data, errors } = await updateOneFieldMetadata({
|
||||
input: {
|
||||
idToUpdate: createdField.createOneField.id,
|
||||
updatePayload: { isUnique: true },
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
name
|
||||
label
|
||||
type
|
||||
isUnique
|
||||
`,
|
||||
});
|
||||
|
||||
expect(errors).toBeUndefined();
|
||||
expect(data).not.toBeNull();
|
||||
expect(data.updateOneField).toBeDefined();
|
||||
expect(data.updateOneField.isUnique).toBe(true);
|
||||
});
|
||||
|
||||
it('should update unique field metadata to non unique', async () => {
|
||||
const { data: createdField } = await createOneFieldMetadata({
|
||||
input: {
|
||||
name: 'uniqueTestField',
|
||||
label: 'Unique Test Field',
|
||||
type: FieldMetadataType.TEXT,
|
||||
objectMetadataId: createdObjectId,
|
||||
isUnique: true,
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
name
|
||||
label
|
||||
type
|
||||
isUnique
|
||||
`,
|
||||
});
|
||||
|
||||
const { data, errors } = await updateOneFieldMetadata({
|
||||
input: {
|
||||
idToUpdate: createdField.createOneField.id,
|
||||
updatePayload: { isUnique: false },
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
name
|
||||
label
|
||||
type
|
||||
isUnique
|
||||
`,
|
||||
});
|
||||
|
||||
expect(errors).toBeUndefined();
|
||||
expect(data).not.toBeNull();
|
||||
expect(data.updateOneField).toBeDefined();
|
||||
expect(data.updateOneField.isUnique).toBe(false);
|
||||
});
|
||||
|
||||
it('should update unique index name if field metadata name is updated', async () => {
|
||||
const { data: createdField } = await createOneFieldMetadata({
|
||||
input: {
|
||||
name: 'uniqueTestField',
|
||||
label: 'Unique Test Field',
|
||||
type: FieldMetadataType.TEXT,
|
||||
objectMetadataId: createdObjectId,
|
||||
isUnique: true,
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
name
|
||||
label
|
||||
type
|
||||
isUnique
|
||||
`,
|
||||
});
|
||||
|
||||
const { data, errors } = await updateOneFieldMetadata({
|
||||
input: {
|
||||
idToUpdate: createdField.createOneField.id,
|
||||
updatePayload: {
|
||||
name: 'renamedUniqueField',
|
||||
label: 'Renamed Unique Field',
|
||||
},
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
name
|
||||
label
|
||||
type
|
||||
isUnique
|
||||
`,
|
||||
});
|
||||
|
||||
expect(errors).toBeUndefined();
|
||||
expect(data).not.toBeNull();
|
||||
expect(data.updateOneField).toBeDefined();
|
||||
expect(data.updateOneField.name).toBe('renamedUniqueField');
|
||||
expect(data.updateOneField.isUnique).toBe(true);
|
||||
|
||||
await updateOneFieldMetadata({
|
||||
input: {
|
||||
idToUpdate: createdField.createOneField.id,
|
||||
updatePayload: {
|
||||
isActive: false,
|
||||
},
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
`,
|
||||
});
|
||||
|
||||
const { data: deletedField, errors: deletedFieldErrors } =
|
||||
await deleteOneFieldMetadata({
|
||||
input: {
|
||||
idToDelete: createdField.createOneField.id,
|
||||
},
|
||||
});
|
||||
|
||||
expect(deletedField).not.toBeNull();
|
||||
expect(deletedFieldErrors).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not update to unique field metadata if it has custom default value', async () => {
|
||||
const { data: createdField } = await createOneFieldMetadata({
|
||||
input: {
|
||||
name: 'fieldWithDefault',
|
||||
label: 'Field With Default',
|
||||
type: FieldMetadataType.TEXT,
|
||||
objectMetadataId: createdObjectId,
|
||||
defaultValue: "'defaultValue'",
|
||||
isUnique: false,
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
name
|
||||
label
|
||||
type
|
||||
isUnique
|
||||
`,
|
||||
});
|
||||
|
||||
const { data, errors } = await updateOneFieldMetadata({
|
||||
input: {
|
||||
idToUpdate: createdField.createOneField.id,
|
||||
updatePayload: { isUnique: true },
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
name
|
||||
label
|
||||
type
|
||||
isUnique
|
||||
`,
|
||||
expectToFail: true,
|
||||
});
|
||||
|
||||
expect(data).toBeNull();
|
||||
expect(errors).toBeDefined();
|
||||
expect(errors[0].message).toBe('Unique field cannot have a default value');
|
||||
});
|
||||
|
||||
it('should not update with custom default value if unique field metadata', async () => {
|
||||
const { data: createdField } = await createOneFieldMetadata({
|
||||
input: {
|
||||
name: 'uniqueTestField',
|
||||
label: 'Unique Test Field',
|
||||
type: FieldMetadataType.TEXT,
|
||||
objectMetadataId: createdObjectId,
|
||||
isUnique: true,
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
name
|
||||
label
|
||||
type
|
||||
isUnique
|
||||
`,
|
||||
});
|
||||
|
||||
const { data, errors } = await updateOneFieldMetadata({
|
||||
input: {
|
||||
idToUpdate: createdField.createOneField.id,
|
||||
updatePayload: { defaultValue: "'defaultValue'" },
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
name
|
||||
label
|
||||
type
|
||||
isUnique
|
||||
`,
|
||||
expectToFail: true,
|
||||
});
|
||||
|
||||
expect(data).toBeNull();
|
||||
expect(errors).toBeDefined();
|
||||
expect(errors[0].message).toBe('Unique field cannot have a default value');
|
||||
});
|
||||
|
||||
it('should not update to unique field metadata if it has composite type without unique subfields', async () => {
|
||||
const { data: createdField } = await createOneFieldMetadata({
|
||||
input: {
|
||||
name: 'fullNameField',
|
||||
label: 'Full Name Field',
|
||||
type: FieldMetadataType.FULL_NAME,
|
||||
objectMetadataId: createdObjectId,
|
||||
isUnique: false,
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
name
|
||||
label
|
||||
type
|
||||
isUnique
|
||||
`,
|
||||
});
|
||||
|
||||
const { data, errors } = await updateOneFieldMetadata({
|
||||
input: {
|
||||
idToUpdate: createdField.createOneField.id,
|
||||
updatePayload: { isUnique: true },
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
name
|
||||
label
|
||||
type
|
||||
isUnique
|
||||
`,
|
||||
expectToFail: true,
|
||||
});
|
||||
|
||||
expect(data).toBeNull();
|
||||
expect(errors).toBeDefined();
|
||||
expect(errors[0].message).toContain(
|
||||
'Unique index cannot be created for field fullNameField of type FULL_NAME',
|
||||
);
|
||||
});
|
||||
|
||||
it('should update a standard field to unique field metadata if it has not standard unique index', async () => {
|
||||
const { data: createdField } = await createOneFieldMetadata({
|
||||
input: {
|
||||
name: 'testField',
|
||||
label: 'Test Field',
|
||||
type: FieldMetadataType.TEXT,
|
||||
objectMetadataId: createdObjectId,
|
||||
isUnique: false,
|
||||
isCustom: false,
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
name
|
||||
label
|
||||
type
|
||||
isUnique
|
||||
`,
|
||||
});
|
||||
|
||||
const { data: addUniqueData, errors: addUniqueErrors } =
|
||||
await updateOneFieldMetadata({
|
||||
input: {
|
||||
idToUpdate: createdField.createOneField.id,
|
||||
updatePayload: { isUnique: true },
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
name
|
||||
label
|
||||
type
|
||||
isUnique
|
||||
`,
|
||||
});
|
||||
|
||||
expect(addUniqueErrors).toBeUndefined();
|
||||
expect(addUniqueData).not.toBeNull();
|
||||
expect(addUniqueData.updateOneField).toBeDefined();
|
||||
expect(addUniqueData.updateOneField.isUnique).toBe(true);
|
||||
|
||||
const { data: removeUniqueData, errors: removeUniqueErrors } =
|
||||
await updateOneFieldMetadata({
|
||||
input: {
|
||||
idToUpdate: createdField.createOneField.id,
|
||||
updatePayload: { isUnique: false },
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
name
|
||||
label
|
||||
type
|
||||
isUnique
|
||||
`,
|
||||
});
|
||||
|
||||
expect(removeUniqueErrors).toBeUndefined();
|
||||
expect(removeUniqueData).not.toBeNull();
|
||||
expect(removeUniqueData.updateOneField).toBeDefined();
|
||||
expect(removeUniqueData.updateOneField.isUnique).toBe(false);
|
||||
});
|
||||
});
|
||||
-366
@@ -1,366 +0,0 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
|
||||
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 { getMockCreateObjectInput } from 'test/integration/metadata/suites/object-metadata/utils/generate-mock-create-object-metadata-input';
|
||||
import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
|
||||
import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util';
|
||||
import {
|
||||
createTestViewFilterWithRestApi,
|
||||
createTestViewWithRestApi,
|
||||
findViewFilterWithRestApi,
|
||||
} from 'test/integration/rest/utils/view-rest-api.util';
|
||||
import { type EachTestingContext } from 'twenty-shared/testing';
|
||||
import {
|
||||
type EnumFieldMetadataType,
|
||||
FieldMetadataType,
|
||||
ViewFilterOperand,
|
||||
} 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 {
|
||||
type FieldMetadataComplexOption,
|
||||
type FieldMetadataDefaultOption,
|
||||
} from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
|
||||
import { type ViewFilterValue } from 'src/engine/metadata-modules/view-filter/types/view-filter-value.type';
|
||||
import { ViewType } from 'src/engine/metadata-modules/view/enums/view-type.enum';
|
||||
|
||||
type Option = FieldMetadataDefaultOption | FieldMetadataComplexOption;
|
||||
|
||||
const generateOption = (index: number): Option => ({
|
||||
label: `Option ${index}`,
|
||||
value: `OPTION_${index}`,
|
||||
color: 'green',
|
||||
position: index,
|
||||
});
|
||||
const generateOptions = (length: number) =>
|
||||
Array.from({ length }, (_value, index) => generateOption(index));
|
||||
const fakeOptionUpdate = ({ value, label, ...option }: Option) => ({
|
||||
...option,
|
||||
value: `${value}_UPDATED`,
|
||||
label: `${label} updated`,
|
||||
});
|
||||
|
||||
const ALL_OPTIONS = generateOptions(10);
|
||||
|
||||
const isEven = (_value: unknown, index: number) => index % 2 === 0;
|
||||
|
||||
type ViewFilterUpdate = {
|
||||
value: string[];
|
||||
};
|
||||
|
||||
type FieldMetadataOptionsAndType = {
|
||||
options: Option[];
|
||||
type: EnumFieldMetadataType;
|
||||
};
|
||||
|
||||
type TestCase = EachTestingContext<{
|
||||
fieldMetadata?: FieldMetadataOptionsAndType;
|
||||
createViewFilter?: ViewFilterUpdate;
|
||||
updateOptions: (
|
||||
options: FieldMetadataDefaultOption[] | FieldMetadataComplexOption[],
|
||||
) => FieldMetadataDefaultOption[] | FieldMetadataComplexOption[];
|
||||
expected?: null;
|
||||
}>;
|
||||
const testFieldMetadataType: EnumFieldMetadataType[] = [
|
||||
FieldMetadataType.SELECT,
|
||||
FieldMetadataType.MULTI_SELECT,
|
||||
];
|
||||
|
||||
describe('update-one-field-metadata-related-record', () => {
|
||||
let idToDelete: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
await updateFeatureFlag({
|
||||
expectToFail: false,
|
||||
featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
||||
value: false,
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await updateFeatureFlag({
|
||||
expectToFail: false,
|
||||
featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
||||
value: true,
|
||||
});
|
||||
});
|
||||
|
||||
const createObjectSelectFieldAndView = async ({
|
||||
options,
|
||||
type: fieldMetadataType,
|
||||
}: FieldMetadataOptionsAndType) => {
|
||||
const singular = faker.lorem.words();
|
||||
const plural = singular + faker.lorem.word();
|
||||
const {
|
||||
data: { createOneObject },
|
||||
} = await createOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: getMockCreateObjectInput({
|
||||
labelSingular: singular,
|
||||
labelPlural: plural,
|
||||
nameSingular: singular.split(' ').join(''),
|
||||
namePlural: plural.split(' ').join(''),
|
||||
isLabelSyncedWithName: false,
|
||||
}),
|
||||
});
|
||||
|
||||
idToDelete = createOneObject.id;
|
||||
|
||||
const {
|
||||
data: { createOneField },
|
||||
} = await createOneFieldMetadata<typeof fieldMetadataType>({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
objectMetadataId: createOneObject.id,
|
||||
type: fieldMetadataType,
|
||||
name: 'testName',
|
||||
label: 'Test name',
|
||||
isLabelSyncedWithName: true,
|
||||
options,
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
options
|
||||
`,
|
||||
});
|
||||
|
||||
const createdView = await createTestViewWithRestApi({
|
||||
id: faker.string.uuid(),
|
||||
name: 'Test View',
|
||||
objectMetadataId: createOneObject.id,
|
||||
type: ViewType.TABLE,
|
||||
});
|
||||
|
||||
return { createOneObject, createOneField, createdView };
|
||||
};
|
||||
|
||||
afterEach(async () => {
|
||||
if (isDefined(idToDelete)) {
|
||||
await updateOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
idToUpdate: idToDelete,
|
||||
updatePayload: {
|
||||
isActive: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
await deleteOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: { idToDelete: idToDelete },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe.each(testFieldMetadataType)('%s', (fieldType) => {
|
||||
const testCases: TestCase[] = [
|
||||
{
|
||||
title:
|
||||
'should delete related view filter if all select field options got deleted',
|
||||
context: {
|
||||
updateOptions: () => generateOptions(3),
|
||||
expected: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should update related multi selected options view filter',
|
||||
context: {
|
||||
updateOptions: (options) =>
|
||||
options.map((option, index) =>
|
||||
isEven(option, index) ? fakeOptionUpdate(option) : option,
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should update related solo selected option view filter',
|
||||
context: {
|
||||
createViewFilter: {
|
||||
value: [ALL_OPTIONS[5].value],
|
||||
},
|
||||
updateOptions: (options) => [fakeOptionUpdate(options[5])],
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'should handle partial deletion of selected options in view filter',
|
||||
context: {
|
||||
updateOptions: (options) => options.slice(4),
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'should handle reordering of options while maintaining view filter values',
|
||||
context: {
|
||||
createViewFilter: {
|
||||
value: ALL_OPTIONS.slice(0, 2).map((option) => option.value),
|
||||
},
|
||||
updateOptions: (options) => [...options].reverse(),
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'should handle no changes update of options while maintaining existing view filter values',
|
||||
context: {
|
||||
updateOptions: (options) => options,
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'should handle adding new options while maintaining existing view filter',
|
||||
context: {
|
||||
fieldMetadata: {
|
||||
options: ALL_OPTIONS.slice(0, 5),
|
||||
type: fieldType,
|
||||
},
|
||||
createViewFilter: {
|
||||
value: ALL_OPTIONS.slice(0, 2).map((option) => option.value),
|
||||
},
|
||||
updateOptions: (options) => [
|
||||
...options,
|
||||
...generateOptions(6).slice(5),
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
test.each(testCases)(
|
||||
'$title',
|
||||
async ({
|
||||
context: {
|
||||
expected,
|
||||
createViewFilter = {
|
||||
value: ALL_OPTIONS.map((option) => option.value),
|
||||
},
|
||||
fieldMetadata = { options: ALL_OPTIONS, type: fieldType },
|
||||
updateOptions,
|
||||
},
|
||||
}) => {
|
||||
const { createOneField, createdView } =
|
||||
await createObjectSelectFieldAndView(fieldMetadata);
|
||||
|
||||
const createdViewFilter = await createTestViewFilterWithRestApi({
|
||||
viewId: createdView.id,
|
||||
fieldMetadataId: createOneField.id,
|
||||
operand: ViewFilterOperand.IS,
|
||||
value: createViewFilter.value,
|
||||
});
|
||||
|
||||
const optionsWithIds = createOneField.options;
|
||||
|
||||
if (!isDefined(optionsWithIds)) {
|
||||
throw new Error('optionsWithIds is not defined');
|
||||
}
|
||||
const updatedOptions = updateOptions(optionsWithIds);
|
||||
|
||||
await updateOneFieldMetadata({
|
||||
input: {
|
||||
idToUpdate: createOneField.id,
|
||||
updatePayload: {
|
||||
options: updatedOptions,
|
||||
},
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
options
|
||||
`,
|
||||
});
|
||||
|
||||
const updatedViewFilter = await findViewFilterWithRestApi(
|
||||
createdViewFilter.id,
|
||||
);
|
||||
|
||||
if (expected !== undefined) {
|
||||
expect(updatedViewFilter).toBe(expected);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isDefined(updatedViewFilter)) {
|
||||
throw new Error(
|
||||
'updatedViewFilter is not defined but should be at this point',
|
||||
);
|
||||
}
|
||||
|
||||
expect(updatedViewFilter.value).not.toBeNull();
|
||||
if (updatedViewFilter.value === null) {
|
||||
throw new Error('Invariant parsedValue should not be null');
|
||||
}
|
||||
expect(updatedOptions.map((option) => option.value)).toEqual(
|
||||
expect.arrayContaining(updatedViewFilter.value as string[]),
|
||||
);
|
||||
|
||||
expect({
|
||||
value: updatedViewFilter.value,
|
||||
operand: updatedViewFilter.operand,
|
||||
viewFilterGroupId: updatedViewFilter.viewFilterGroupId,
|
||||
positionInViewFilterGroup:
|
||||
updatedViewFilter.positionInViewFilterGroup,
|
||||
subFieldName: updatedViewFilter.subFieldName,
|
||||
}).toMatchSnapshot();
|
||||
},
|
||||
);
|
||||
|
||||
// Note these test exists only because we do not validate the view filter value on creation/update
|
||||
// Should be removed after https://github.com/twentyhq/core-team-issues/issues/1009 completion
|
||||
const failingTestCases: EachTestingContext<{
|
||||
createViewFilterValue: unknown;
|
||||
}>[] = [
|
||||
{
|
||||
title:
|
||||
'should throw error if view filter value is not a stringified JSON array',
|
||||
context: {
|
||||
createViewFilterValue: JSON.stringify(
|
||||
'not an array stringified json',
|
||||
),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
test.each(failingTestCases)(
|
||||
'$title',
|
||||
async ({ context: { createViewFilterValue } }) => {
|
||||
const { createOneField, createdView } =
|
||||
await createObjectSelectFieldAndView({
|
||||
options: ALL_OPTIONS,
|
||||
type: fieldType,
|
||||
});
|
||||
|
||||
const viewFilterId = '20202020-e3b5-4fa7-85aa-9b1950fc7bf5';
|
||||
|
||||
await createTestViewFilterWithRestApi({
|
||||
id: viewFilterId,
|
||||
viewId: createdView.id,
|
||||
fieldMetadataId: createOneField.id,
|
||||
operand: ViewFilterOperand.IS,
|
||||
value: createViewFilterValue as unknown as ViewFilterValue,
|
||||
});
|
||||
|
||||
const optionsWithIds = createOneField.options;
|
||||
|
||||
if (!isDefined(optionsWithIds)) {
|
||||
throw new Error('optionsWithIds is not defined');
|
||||
}
|
||||
const updatePayload = {
|
||||
options: optionsWithIds.map((option) => fakeOptionUpdate(option)),
|
||||
};
|
||||
const { errors, data } = await updateOneFieldMetadata({
|
||||
input: {
|
||||
idToUpdate: createOneField.id,
|
||||
updatePayload,
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
options
|
||||
`,
|
||||
});
|
||||
|
||||
expect(data).toBeNull();
|
||||
expect(errors).toBeDefined();
|
||||
expect(errors).toMatchSnapshot();
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
+16
-2494
File diff suppressed because it is too large
Load Diff
-43
@@ -1,43 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Object metadata creation should fail when labelPlural contains only whitespace 1`] = `"Input is too short: """`;
|
||||
|
||||
exports[`Object metadata creation should fail when labelPlural exceeds maximum length 1`] = `"Name is too long: it exceeds the 63 characters limit."`;
|
||||
|
||||
exports[`Object metadata creation should fail when labelPlural is empty 1`] = `"Input is too short: """`;
|
||||
|
||||
exports[`Object metadata creation should fail when labelSingular contains only whitespace 1`] = `"Input is too short: """`;
|
||||
|
||||
exports[`Object metadata creation should fail when labelSingular exceeds maximum length 1`] = `"Name is too long: it exceeds the 63 characters limit."`;
|
||||
|
||||
exports[`Object metadata creation should fail when labelSingular is empty 1`] = `"Input is too short: """`;
|
||||
|
||||
exports[`Object metadata creation should fail when labels are identical 1`] = `"The singular and plural labels cannot be the same for an object"`;
|
||||
|
||||
exports[`Object metadata creation should fail when labels with whitespaces result to be identical 1`] = `"The singular and plural labels cannot be the same for an object"`;
|
||||
|
||||
exports[`Object metadata creation should fail when name exceeds maximum length 1`] = `"Name is too long: it exceeds the 63 characters limit."`;
|
||||
|
||||
exports[`Object metadata creation should fail when namePlural has invalid characters 1`] = `"String "μ" is not valid: must start with lowercase letter and contain only alphanumeric letters"`;
|
||||
|
||||
exports[`Object metadata creation should fail when namePlural is a reserved keyword 1`] = `"The name "users" is not available"`;
|
||||
|
||||
exports[`Object metadata creation should fail when namePlural is an empty string 1`] = `"Input is too short: """`;
|
||||
|
||||
exports[`Object metadata creation should fail when namePlural is not camelCased 1`] = `"Not_Camel_Case should be in camelCase"`;
|
||||
|
||||
exports[`Object metadata creation should fail when nameSingular contains only one char and whitespaces 1`] = `"a a should be in camelCase"`;
|
||||
|
||||
exports[`Object metadata creation should fail when nameSingular contains only whitespaces 1`] = `"should be in camelCase"`;
|
||||
|
||||
exports[`Object metadata creation should fail when nameSingular has invalid characters 1`] = `"String "μ" is not valid: must start with lowercase letter and contain only alphanumeric letters"`;
|
||||
|
||||
exports[`Object metadata creation should fail when nameSingular is a reserved keyword 1`] = `"The name "user" is not available"`;
|
||||
|
||||
exports[`Object metadata creation should fail when nameSingular is an empty string 1`] = `"Input is too short: """`;
|
||||
|
||||
exports[`Object metadata creation should fail when nameSingular is not camelCased 1`] = `"Not_Camel_Case should be in camelCase"`;
|
||||
|
||||
exports[`Object metadata creation should fail when names are identical 1`] = `"The singular and plural names cannot be the same for an object"`;
|
||||
|
||||
exports[`Object metadata creation should fail when names with whitespaces result to be identical 1`] = `"fooBar should be in camelCase"`;
|
||||
-47
@@ -1,47 +0,0 @@
|
||||
import { OBJECT_METADATA_LABEL_FAILING_TEST_CASES } from 'test/integration/metadata/suites/object-metadata/common/object-metadata-label-failing-tests-cases';
|
||||
import { OBJECT_METADATA_NAMES_FAILING_TEST_CASES } from 'test/integration/metadata/suites/object-metadata/common/object-metadata-names-failing-tests-cases';
|
||||
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
|
||||
import { getMockCreateObjectInput } from 'test/integration/metadata/suites/object-metadata/utils/generate-mock-create-object-metadata-input';
|
||||
import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util';
|
||||
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { SEED_APPLE_WORKSPACE_ID } from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant';
|
||||
|
||||
const allTestsUseCases = [
|
||||
...OBJECT_METADATA_NAMES_FAILING_TEST_CASES,
|
||||
...OBJECT_METADATA_LABEL_FAILING_TEST_CASES,
|
||||
];
|
||||
|
||||
describe('Object metadata creation should fail', () => {
|
||||
beforeAll(async () => {
|
||||
await updateFeatureFlag({
|
||||
expectToFail: false,
|
||||
featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
||||
value: false,
|
||||
workspaceId: SEED_APPLE_WORKSPACE_ID,
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await updateFeatureFlag({
|
||||
expectToFail: false,
|
||||
featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
||||
value: true,
|
||||
workspaceId: SEED_APPLE_WORKSPACE_ID,
|
||||
});
|
||||
});
|
||||
|
||||
it.each(allTestsUseCases)('$title', async ({ context }) => {
|
||||
const { errors } = await createOneObjectMetadata({
|
||||
input: getMockCreateObjectInput(context),
|
||||
expectToFail: true,
|
||||
});
|
||||
|
||||
expect(errors.length).toBe(1);
|
||||
const firstError = errors[0];
|
||||
|
||||
expect(firstError.extensions.code).toBe(ErrorCode.BAD_USER_INPUT);
|
||||
expect(firstError.message).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user