[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:
+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,
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user