Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2f0064dd7 |
+20
-2
@@ -1,4 +1,4 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
|
||||
import { QUERY_MAX_RECORDS_FROM_RELATION } from 'twenty-shared/constants';
|
||||
import { ObjectRecord } from 'twenty-shared/types';
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
CommonQueryRunnerExceptionCode,
|
||||
} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
|
||||
import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant';
|
||||
import { RelationOnDeleteService } from 'src/engine/api/common/common-query-runners/services/relation-on-delete.service';
|
||||
import { CommonBaseQueryRunnerContext } from 'src/engine/api/common/types/common-base-query-runner-context.type';
|
||||
import { CommonExtendedQueryRunnerContext } from 'src/engine/api/common/types/common-extended-query-runner-context.type';
|
||||
import {
|
||||
@@ -32,6 +33,9 @@ export class CommonDeleteManyQueryRunnerService extends CommonBaseQueryRunnerSer
|
||||
DeleteManyQueryArgs,
|
||||
ObjectRecord[]
|
||||
> {
|
||||
@Inject()
|
||||
private readonly relationOnDeleteService: RelationOnDeleteService;
|
||||
|
||||
protected readonly operationName = CommonQueryNames.DELETE_MANY;
|
||||
|
||||
async run(
|
||||
@@ -60,7 +64,7 @@ export class CommonDeleteManyQueryRunnerService extends CommonBaseQueryRunnerSer
|
||||
);
|
||||
|
||||
const columnsToReturn = buildColumnsToReturn({
|
||||
select: args.selectedFieldsResult.select,
|
||||
select: { ...args.selectedFieldsResult.select, id: true },
|
||||
relations: args.selectedFieldsResult.relations,
|
||||
flatObjectMetadata,
|
||||
flatObjectMetadataMaps,
|
||||
@@ -74,6 +78,20 @@ export class CommonDeleteManyQueryRunnerService extends CommonBaseQueryRunnerSer
|
||||
|
||||
const deletedRecords = deletedObjectRecords.generatedMaps as ObjectRecord[];
|
||||
|
||||
const deletedRecordIds = deletedRecords
|
||||
.map((record) => record.id)
|
||||
.filter(isDefined);
|
||||
|
||||
if (deletedRecordIds.length > 0) {
|
||||
await this.relationOnDeleteService.nullifyForeignKeysOnSoftDelete({
|
||||
deletedRecordIds,
|
||||
deletedObjectMetadata: flatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
workspaceDataSource,
|
||||
});
|
||||
}
|
||||
|
||||
if (isDefined(args.selectedFieldsResult.relations)) {
|
||||
await this.processNestedRelationsHelper.processNestedRelations({
|
||||
flatObjectMetadataMaps,
|
||||
|
||||
+2
@@ -11,6 +11,7 @@ import { CommonGroupByQueryRunnerService } from 'src/engine/api/common/common-qu
|
||||
import { CommonMergeManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-merge-many-query-runner.service';
|
||||
import { CommonRestoreManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-restore-many-query-runner.service';
|
||||
import { CommonRestoreOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-restore-one-query-runner.service';
|
||||
import { RelationOnDeleteService } from 'src/engine/api/common/common-query-runners/services/relation-on-delete.service';
|
||||
import { CommonUpdateManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-update-many-query-runner.service';
|
||||
import { CommonUpdateOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-update-one-query-runner.service';
|
||||
|
||||
@@ -30,4 +31,5 @@ export const CommonQueryRunners = [
|
||||
CommonRestoreManyQueryRunnerService,
|
||||
CommonRestoreOneQueryRunnerService,
|
||||
CommonMergeManyQueryRunnerService,
|
||||
RelationOnDeleteService,
|
||||
];
|
||||
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
import {
|
||||
FieldMetadataType,
|
||||
RelationOnDeleteAction,
|
||||
RelationType,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import { RelationOnDeleteService } from 'src/engine/api/common/common-query-runners/services/relation-on-delete.service';
|
||||
import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
|
||||
describe('RelationOnDeleteService', () => {
|
||||
let service: RelationOnDeleteService;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new RelationOnDeleteService();
|
||||
});
|
||||
|
||||
describe('nullifyForeignKeysOnSoftDelete', () => {
|
||||
it('should not run any queries when deletedRecordIds is empty', async () => {
|
||||
const mockWorkspaceDataSource = {
|
||||
getRepository: jest.fn(),
|
||||
};
|
||||
|
||||
await service.nullifyForeignKeysOnSoftDelete({
|
||||
deletedRecordIds: [],
|
||||
deletedObjectMetadata: { id: 'obj-1' } as FlatObjectMetadata,
|
||||
flatFieldMetadataMaps: {
|
||||
byUniversalIdentifier: {},
|
||||
universalIdentifierById: {},
|
||||
universalIdentifiersByApplicationId: {},
|
||||
} as FlatEntityMaps<FlatFieldMetadata>,
|
||||
flatObjectMetadataMaps: {
|
||||
byUniversalIdentifier: {},
|
||||
universalIdentifierById: {},
|
||||
universalIdentifiersByApplicationId: {},
|
||||
} as FlatEntityMaps<FlatObjectMetadata>,
|
||||
// oxlint-disable-next-line @typescripttypescript/no-explicit-any
|
||||
workspaceDataSource: mockWorkspaceDataSource as any,
|
||||
});
|
||||
|
||||
expect(mockWorkspaceDataSource.getRepository).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should nullify FKs for SET_NULL MANY_TO_ONE relations targeting the deleted object', async () => {
|
||||
const executeMock = jest.fn().mockResolvedValue({ affected: 1 });
|
||||
const whereMock = jest.fn().mockReturnValue({ execute: executeMock });
|
||||
const setMock = jest.fn().mockReturnValue({ where: whereMock });
|
||||
const updateMock = jest.fn().mockReturnValue({ set: setMock });
|
||||
const createQueryBuilderMock = jest
|
||||
.fn()
|
||||
.mockReturnValue({ update: updateMock });
|
||||
const mockRepository = {
|
||||
createQueryBuilder: createQueryBuilderMock,
|
||||
};
|
||||
const mockWorkspaceDataSource = {
|
||||
getRepository: jest.fn().mockReturnValue(mockRepository),
|
||||
};
|
||||
|
||||
const deletedObjectId = 'company-object-id';
|
||||
const referencingObjectId = 'person-object-id';
|
||||
|
||||
const flatFieldMetadataMaps = {
|
||||
byUniversalIdentifier: {
|
||||
'person-company-field': {
|
||||
id: 'field-1',
|
||||
name: 'company',
|
||||
type: FieldMetadataType.RELATION,
|
||||
objectMetadataId: referencingObjectId,
|
||||
relationTargetObjectMetadataId: deletedObjectId,
|
||||
settings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: RelationOnDeleteAction.SET_NULL,
|
||||
joinColumnName: 'companyId',
|
||||
},
|
||||
} as unknown as FlatFieldMetadata,
|
||||
},
|
||||
universalIdentifierById: {
|
||||
'field-1': 'person-company-field',
|
||||
},
|
||||
universalIdentifiersByApplicationId: {},
|
||||
} as unknown as FlatEntityMaps<FlatFieldMetadata>;
|
||||
|
||||
const flatObjectMetadataMaps = {
|
||||
byUniversalIdentifier: {
|
||||
'person-object': {
|
||||
id: referencingObjectId,
|
||||
nameSingular: 'person',
|
||||
} as unknown as FlatObjectMetadata,
|
||||
},
|
||||
universalIdentifierById: {
|
||||
[referencingObjectId]: 'person-object',
|
||||
},
|
||||
universalIdentifiersByApplicationId: {},
|
||||
} as unknown as FlatEntityMaps<FlatObjectMetadata>;
|
||||
|
||||
await service.nullifyForeignKeysOnSoftDelete({
|
||||
deletedRecordIds: ['company-record-1', 'company-record-2'],
|
||||
deletedObjectMetadata: {
|
||||
id: deletedObjectId,
|
||||
} as FlatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
// oxlint-disable-next-line @typescripttypescript/no-explicit-any
|
||||
workspaceDataSource: mockWorkspaceDataSource as any,
|
||||
});
|
||||
|
||||
expect(mockWorkspaceDataSource.getRepository).toHaveBeenCalledWith(
|
||||
'person',
|
||||
);
|
||||
expect(setMock).toHaveBeenCalledWith({ companyId: null });
|
||||
expect(whereMock).toHaveBeenCalledWith(
|
||||
'"companyId" IN (:...ids)',
|
||||
{ ids: ['company-record-1', 'company-record-2'] },
|
||||
);
|
||||
});
|
||||
|
||||
it('should NOT nullify FKs for CASCADE relations', async () => {
|
||||
const mockWorkspaceDataSource = {
|
||||
getRepository: jest.fn(),
|
||||
};
|
||||
|
||||
const deletedObjectId = 'task-object-id';
|
||||
|
||||
const flatFieldMetadataMaps = {
|
||||
byUniversalIdentifier: {
|
||||
'tasktarget-task-field': {
|
||||
id: 'field-2',
|
||||
name: 'task',
|
||||
type: FieldMetadataType.RELATION,
|
||||
objectMetadataId: 'tasktarget-object-id',
|
||||
relationTargetObjectMetadataId: deletedObjectId,
|
||||
settings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
joinColumnName: 'taskId',
|
||||
},
|
||||
} as unknown as FlatFieldMetadata,
|
||||
},
|
||||
universalIdentifierById: {
|
||||
'field-2': 'tasktarget-task-field',
|
||||
},
|
||||
universalIdentifiersByApplicationId: {},
|
||||
} as unknown as FlatEntityMaps<FlatFieldMetadata>;
|
||||
|
||||
const flatObjectMetadataMaps = {
|
||||
byUniversalIdentifier: {},
|
||||
universalIdentifierById: {},
|
||||
universalIdentifiersByApplicationId: {},
|
||||
} as unknown as FlatEntityMaps<FlatObjectMetadata>;
|
||||
|
||||
await service.nullifyForeignKeysOnSoftDelete({
|
||||
deletedRecordIds: ['task-record-1'],
|
||||
deletedObjectMetadata: {
|
||||
id: deletedObjectId,
|
||||
} as FlatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
// oxlint-disable-next-line @typescripttypescript/no-explicit-any
|
||||
workspaceDataSource: mockWorkspaceDataSource as any,
|
||||
});
|
||||
|
||||
expect(mockWorkspaceDataSource.getRepository).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should NOT nullify FKs for ONE_TO_MANY relations', async () => {
|
||||
const mockWorkspaceDataSource = {
|
||||
getRepository: jest.fn(),
|
||||
};
|
||||
|
||||
const deletedObjectId = 'company-object-id';
|
||||
|
||||
const flatFieldMetadataMaps = {
|
||||
byUniversalIdentifier: {
|
||||
'company-people-field': {
|
||||
id: 'field-3',
|
||||
name: 'people',
|
||||
type: FieldMetadataType.RELATION,
|
||||
objectMetadataId: deletedObjectId,
|
||||
relationTargetObjectMetadataId: 'person-object-id',
|
||||
settings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
onDelete: RelationOnDeleteAction.SET_NULL,
|
||||
},
|
||||
} as unknown as FlatFieldMetadata,
|
||||
},
|
||||
universalIdentifierById: {
|
||||
'field-3': 'company-people-field',
|
||||
},
|
||||
universalIdentifiersByApplicationId: {},
|
||||
} as unknown as FlatEntityMaps<FlatFieldMetadata>;
|
||||
|
||||
const flatObjectMetadataMaps = {
|
||||
byUniversalIdentifier: {},
|
||||
universalIdentifierById: {},
|
||||
universalIdentifiersByApplicationId: {},
|
||||
} as unknown as FlatEntityMaps<FlatObjectMetadata>;
|
||||
|
||||
await service.nullifyForeignKeysOnSoftDelete({
|
||||
deletedRecordIds: ['company-record-1'],
|
||||
deletedObjectMetadata: {
|
||||
id: deletedObjectId,
|
||||
} as FlatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
// oxlint-disable-next-line @typescripttypescript/no-explicit-any
|
||||
workspaceDataSource: mockWorkspaceDataSource as any,
|
||||
});
|
||||
|
||||
expect(mockWorkspaceDataSource.getRepository).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
FieldMetadataType,
|
||||
RelationOnDeleteAction,
|
||||
RelationType,
|
||||
} from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { GlobalWorkspaceDataSource } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource';
|
||||
|
||||
type RelationSettings = {
|
||||
relationType?: string;
|
||||
onDelete?: string;
|
||||
joinColumnName?: string | null;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class RelationOnDeleteService {
|
||||
async nullifyForeignKeysOnSoftDelete({
|
||||
deletedRecordIds,
|
||||
deletedObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
workspaceDataSource,
|
||||
}: {
|
||||
deletedRecordIds: string[];
|
||||
deletedObjectMetadata: FlatObjectMetadata;
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
|
||||
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
|
||||
workspaceDataSource: GlobalWorkspaceDataSource;
|
||||
}): Promise<void> {
|
||||
if (deletedRecordIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const incomingSetNullFields = this.findIncomingSetNullRelations(
|
||||
deletedObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
);
|
||||
|
||||
const nullifyTasks = incomingSetNullFields.map((field) =>
|
||||
this.nullifyForeignKeyColumn({
|
||||
field,
|
||||
deletedRecordIds,
|
||||
flatObjectMetadataMaps,
|
||||
workspaceDataSource,
|
||||
}),
|
||||
);
|
||||
|
||||
await Promise.all(nullifyTasks);
|
||||
}
|
||||
|
||||
private findIncomingSetNullRelations(
|
||||
deletedObjectMetadata: FlatObjectMetadata,
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
|
||||
): FlatFieldMetadata[] {
|
||||
return Object.values(flatFieldMetadataMaps.byUniversalIdentifier)
|
||||
.filter(isDefined)
|
||||
.filter((field) => {
|
||||
if (field.type !== FieldMetadataType.RELATION) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (field.relationTargetObjectMetadataId !== deletedObjectMetadata.id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const settings = field.settings as unknown as
|
||||
| RelationSettings
|
||||
| undefined;
|
||||
|
||||
return (
|
||||
settings?.relationType === RelationType.MANY_TO_ONE &&
|
||||
settings?.onDelete === RelationOnDeleteAction.SET_NULL
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private async nullifyForeignKeyColumn({
|
||||
field,
|
||||
deletedRecordIds,
|
||||
flatObjectMetadataMaps,
|
||||
workspaceDataSource,
|
||||
}: {
|
||||
field: FlatFieldMetadata;
|
||||
deletedRecordIds: string[];
|
||||
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
|
||||
workspaceDataSource: GlobalWorkspaceDataSource;
|
||||
}): Promise<void> {
|
||||
const referencingObjectMetadata = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: field.objectMetadataId,
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
});
|
||||
|
||||
if (!referencingObjectMetadata) {
|
||||
return;
|
||||
}
|
||||
|
||||
const settings = field.settings as unknown as
|
||||
| RelationSettings
|
||||
| undefined;
|
||||
|
||||
const joinColumnName = settings?.joinColumnName ?? `${field.name}Id`;
|
||||
|
||||
const repository = workspaceDataSource.getRepository(
|
||||
referencingObjectMetadata.nameSingular,
|
||||
);
|
||||
|
||||
await repository
|
||||
.createQueryBuilder(referencingObjectMetadata.nameSingular)
|
||||
.update()
|
||||
.set({ [joinColumnName]: null } as Record<string, null>)
|
||||
.where(`"${joinColumnName}" IN (:...ids)`, { ids: deletedRecordIds })
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import { COMPANY_GQL_FIELDS } from 'test/integration/constants/company-gql-fields.constants';
|
||||
import { createOneOperationFactory } from 'test/integration/graphql/utils/create-one-operation-factory.util';
|
||||
import { deleteOneOperationFactory } from 'test/integration/graphql/utils/delete-one-operation-factory.util';
|
||||
import { findOneOperationFactory } from 'test/integration/graphql/utils/find-one-operation-factory.util';
|
||||
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
|
||||
|
||||
const PERSON_WITH_COMPANY_GQL_FIELDS = `
|
||||
id
|
||||
companyId
|
||||
name {
|
||||
firstName
|
||||
lastName
|
||||
}
|
||||
`;
|
||||
|
||||
describe('relationOnDeleteNullifyFkOnSoftDelete', () => {
|
||||
it('should nullify person.companyId when the related company is soft-deleted', async () => {
|
||||
const companyId = randomUUID();
|
||||
const personId = randomUUID();
|
||||
|
||||
const createCompanyOperation = createOneOperationFactory({
|
||||
objectMetadataSingularName: 'company',
|
||||
gqlFields: COMPANY_GQL_FIELDS,
|
||||
data: {
|
||||
id: companyId,
|
||||
name: 'Test Company For Soft Delete',
|
||||
},
|
||||
});
|
||||
|
||||
await makeGraphqlAPIRequest(createCompanyOperation);
|
||||
|
||||
const createPersonOperation = createOneOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
gqlFields: PERSON_WITH_COMPANY_GQL_FIELDS,
|
||||
data: {
|
||||
id: personId,
|
||||
companyId,
|
||||
name: { firstName: 'Test', lastName: 'Person' },
|
||||
},
|
||||
});
|
||||
|
||||
const createPersonResponse =
|
||||
await makeGraphqlAPIRequest(createPersonOperation);
|
||||
|
||||
expect(createPersonResponse.body.data.createPerson.companyId).toBe(
|
||||
companyId,
|
||||
);
|
||||
|
||||
const deleteCompanyOperation = deleteOneOperationFactory({
|
||||
objectMetadataSingularName: 'company',
|
||||
gqlFields: COMPANY_GQL_FIELDS,
|
||||
recordId: companyId,
|
||||
});
|
||||
|
||||
await makeGraphqlAPIRequest(deleteCompanyOperation);
|
||||
|
||||
const findPersonOperation = findOneOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
gqlFields: PERSON_WITH_COMPANY_GQL_FIELDS,
|
||||
filter: { id: { eq: personId } },
|
||||
});
|
||||
|
||||
const findPersonResponse =
|
||||
await makeGraphqlAPIRequest(findPersonOperation);
|
||||
|
||||
expect(findPersonResponse.body.data.person.companyId).toBeNull();
|
||||
});
|
||||
|
||||
it('should nullify opportunity.companyId when the related company is soft-deleted', async () => {
|
||||
const companyId = randomUUID();
|
||||
const opportunityId = randomUUID();
|
||||
|
||||
const createCompanyOperation = createOneOperationFactory({
|
||||
objectMetadataSingularName: 'company',
|
||||
gqlFields: COMPANY_GQL_FIELDS,
|
||||
data: {
|
||||
id: companyId,
|
||||
name: 'Test Company For Opportunity',
|
||||
},
|
||||
});
|
||||
|
||||
await makeGraphqlAPIRequest(createCompanyOperation);
|
||||
|
||||
const createOpportunityOperation = createOneOperationFactory({
|
||||
objectMetadataSingularName: 'opportunity',
|
||||
gqlFields: `id companyId`,
|
||||
data: {
|
||||
id: opportunityId,
|
||||
companyId,
|
||||
},
|
||||
});
|
||||
|
||||
const createOpportunityResponse = await makeGraphqlAPIRequest(
|
||||
createOpportunityOperation,
|
||||
);
|
||||
|
||||
expect(
|
||||
createOpportunityResponse.body.data.createOpportunity.companyId,
|
||||
).toBe(companyId);
|
||||
|
||||
const deleteCompanyOperation = deleteOneOperationFactory({
|
||||
objectMetadataSingularName: 'company',
|
||||
gqlFields: COMPANY_GQL_FIELDS,
|
||||
recordId: companyId,
|
||||
});
|
||||
|
||||
await makeGraphqlAPIRequest(deleteCompanyOperation);
|
||||
|
||||
const findOpportunityOperation = findOneOperationFactory({
|
||||
objectMetadataSingularName: 'opportunity',
|
||||
gqlFields: `id companyId`,
|
||||
filter: { id: { eq: opportunityId } },
|
||||
});
|
||||
|
||||
const findOpportunityResponse = await makeGraphqlAPIRequest(
|
||||
findOpportunityOperation,
|
||||
);
|
||||
|
||||
expect(
|
||||
findOpportunityResponse.body.data.opportunity.companyId,
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('should NOT nullify FK when relation has CASCADE onDelete (e.g., taskTarget.taskId)', async () => {
|
||||
const taskId = randomUUID();
|
||||
const taskTargetId = randomUUID();
|
||||
const personId = randomUUID();
|
||||
|
||||
const createPersonOperation = createOneOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
gqlFields: `id`,
|
||||
data: { id: personId, name: { firstName: 'FK', lastName: 'Test' } },
|
||||
});
|
||||
|
||||
await makeGraphqlAPIRequest(createPersonOperation);
|
||||
|
||||
const createTaskOperation = createOneOperationFactory({
|
||||
objectMetadataSingularName: 'task',
|
||||
gqlFields: `id`,
|
||||
data: { id: taskId },
|
||||
});
|
||||
|
||||
await makeGraphqlAPIRequest(createTaskOperation);
|
||||
|
||||
const createTaskTargetOperation = createOneOperationFactory({
|
||||
objectMetadataSingularName: 'taskTarget',
|
||||
gqlFields: `id taskId personId`,
|
||||
data: {
|
||||
id: taskTargetId,
|
||||
taskId,
|
||||
personId,
|
||||
},
|
||||
});
|
||||
|
||||
const createTaskTargetResponse = await makeGraphqlAPIRequest(
|
||||
createTaskTargetOperation,
|
||||
);
|
||||
|
||||
expect(
|
||||
createTaskTargetResponse.body.data.createTaskTarget.taskId,
|
||||
).toBe(taskId);
|
||||
|
||||
const deleteTaskOperation = deleteOneOperationFactory({
|
||||
objectMetadataSingularName: 'task',
|
||||
gqlFields: `id`,
|
||||
recordId: taskId,
|
||||
});
|
||||
|
||||
await makeGraphqlAPIRequest(deleteTaskOperation);
|
||||
|
||||
// taskTarget has CASCADE onDelete for taskId, so the FK should remain
|
||||
// (CASCADE would hard-delete the child, but soft-delete doesn't cascade)
|
||||
const findTaskTargetOperation = findOneOperationFactory({
|
||||
objectMetadataSingularName: 'taskTarget',
|
||||
gqlFields: `id taskId`,
|
||||
filter: { id: { eq: taskTargetId } },
|
||||
});
|
||||
|
||||
const findTaskTargetResponse = await makeGraphqlAPIRequest(
|
||||
findTaskTargetOperation,
|
||||
);
|
||||
|
||||
expect(findTaskTargetResponse.body.data.taskTarget.taskId).toBe(
|
||||
taskId,
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user