[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:
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user