Skip to content

Commit 27f6671

Browse files
committed
feat(V2): block deletes when local inbound references remain
Reference checks run on committed rows and staged INSERT/UPDATE; remove ?force=true bypass. Order live DELETE phase by dependency; add coverage.
1 parent 6e1636c commit 27f6671

27 files changed

Lines changed: 507 additions & 118 deletions

docs/cadt_rpc_api_v2.md

Lines changed: 16 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1890,7 +1890,7 @@ Response
18901890

18911891
**Note**: The ID in the URL path is the `cadTrustMethodologyId`.
18921892

1893-
**Referential integrity**: If any committed or staged (`INSERT`/`UPDATE`) `project_methodology` records reference this methodology, the request returns `409 Conflict`. Pass `?force=true` to bypass the guard and stage the delete anyway.
1893+
**Referential integrity**: If any committed or staged (`INSERT`/`UPDATE`) `project_methodology` records reference this methodology, the request returns `409 Conflict` until those references are removed. References from any synced registry count the same.
18941894

18951895
Request
18961896
```shell
@@ -1910,18 +1910,12 @@ Response (409 — references exist)
19101910
```json
19111911
{
19121912
"success": false,
1913-
"message": "Cannot delete methodology: referenced by 2 project-methodology links",
1914-
"references": [{ "table": "project_methodology", "count": 2 }],
1915-
"hint": "Remove all references first, or use ?force=true to delete anyway"
1913+
"message": "Cannot delete methodology: it is still referenced by 2 project-methodology links. Remove those references before deleting this methodology.",
1914+
"error": "Referenced records must be removed before deletion",
1915+
"references": [{ "table": "project_methodology", "count": 2 }]
19161916
}
19171917
```
19181918

1919-
Force delete (bypass guard)
1920-
```shell
1921-
curl --location --request DELETE 'localhost:31310/v2/methodology/9b9bb857-c71b-4649-b805-a289db27dc1c?force=true' \
1922-
--header 'Content-Type: application/json'
1923-
```
1924-
19251919
---
19261920

19271921
## `program`
@@ -2076,7 +2070,7 @@ Response
20762070

20772071
**Note**: The ID in the URL path is the `cadTrustProgramId`.
20782072

2079-
**Referential integrity**: If any committed or staged (`INSERT`/`UPDATE`) `project` records reference this program via `cadTrustProgramId`, the request returns `409 Conflict`. Pass `?force=true` to bypass the guard and stage the delete anyway.
2073+
**Referential integrity**: If any committed or staged (`INSERT`/`UPDATE`) `project` records reference this program via `cadTrustProgramId`, the request returns `409 Conflict` until those references are removed.
20802074

20812075
Request
20822076
```shell
@@ -2096,18 +2090,12 @@ Response (409 — references exist)
20962090
```json
20972091
{
20982092
"success": false,
2099-
"message": "Cannot delete program: referenced by 3 projects",
2100-
"references": [{ "table": "project", "count": 3 }],
2101-
"hint": "Remove all references first, or use ?force=true to delete anyway"
2093+
"message": "Cannot delete program: it is still referenced by 3 projects. Remove those references before deleting this program.",
2094+
"error": "Referenced records must be removed before deletion",
2095+
"references": [{ "table": "project", "count": 3 }]
21022096
}
21032097
```
21042098

2105-
Force delete (bypass guard)
2106-
```shell
2107-
curl --location --request DELETE 'localhost:31310/v2/program/51ca9638-22b0-4e14-ae7a-c09d23b37b58?force=true' \
2108-
--header 'Content-Type: application/json'
2109-
```
2110-
21112099
---
21122100

21132101
## `project`
@@ -4671,7 +4659,7 @@ Response
46714659

46724660
**Note**: The ID in the URL path is the `cadTrustStakeholderId`.
46734661

4674-
**Referential integrity**: If any committed or staged (`INSERT`/`UPDATE`) `stakeholder_projects` records reference this stakeholder, the request returns `409 Conflict`. Pass `?force=true` to bypass the guard and stage the delete anyway.
4662+
**Referential integrity**: If any committed or staged (`INSERT`/`UPDATE`) `stakeholder_projects` records reference this stakeholder, the request returns `409 Conflict` until those references are removed.
46754663

46764664
Request
46774665
```shell
@@ -4691,18 +4679,12 @@ Response (409 — references exist)
46914679
```json
46924680
{
46934681
"success": false,
4694-
"message": "Cannot delete stakeholder: referenced by 1 stakeholder-project links",
4695-
"references": [{ "table": "stakeholder_projects", "count": 1 }],
4696-
"hint": "Remove all references first, or use ?force=true to delete anyway"
4682+
"message": "Cannot delete stakeholder: it is still referenced by 1 stakeholder-project links. Remove those references before deleting this stakeholder.",
4683+
"error": "Referenced records must be removed before deletion",
4684+
"references": [{ "table": "stakeholder_projects", "count": 1 }]
46974685
}
46984686
```
46994687

4700-
Force delete (bypass guard)
4701-
```shell
4702-
curl --location --request DELETE 'localhost:31310/v2/stakeholder/e880047e-cdf4-45bb-a9df-e706fa427713?force=true' \
4703-
--header 'Content-Type: application/json'
4704-
```
4705-
47064688
---
47074689

47084690
## `stakeholder-projects`
@@ -4999,7 +4981,7 @@ Response
49994981

50004982
**Note**: The ID in the URL path is the `cadTrustLabelId`.
50014983

5002-
**Referential integrity**: If any committed or staged (`INSERT`/`UPDATE`) `unit_label` records reference this label, the request returns `409 Conflict`. Pass `?force=true` to bypass the guard and stage the delete anyway.
4984+
**Referential integrity**: If any committed or staged (`INSERT`/`UPDATE`) `unit_label` records reference this label, the request returns `409 Conflict` until those references are removed.
50034985

50044986
Request
50054987
```shell
@@ -5019,18 +5001,12 @@ Response (409 — references exist)
50195001
```json
50205002
{
50215003
"success": false,
5022-
"message": "Cannot delete label: referenced by 4 unit-label links",
5023-
"references": [{ "table": "unit_label", "count": 4 }],
5024-
"hint": "Remove all references first, or use ?force=true to delete anyway"
5004+
"message": "Cannot delete label: it is still referenced by 4 unit-label links. Remove those references before deleting this label.",
5005+
"error": "Referenced records must be removed before deletion",
5006+
"references": [{ "table": "unit_label", "count": 4 }]
50255007
}
50265008
```
50275009

5028-
Force delete (bypass guard)
5029-
```shell
5030-
curl --location --request DELETE 'localhost:31310/v2/label/dcacd68e-1cfb-4f06-9798-efa0aacda42c?force=true' \
5031-
--header 'Content-Type: application/json'
5032-
```
5033-
50345010
---
50355011

50365012
## `unit-label`

src/controllers/v2/label-v2.controller.js

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -272,12 +272,9 @@ export const deleteLabelV2 = async (req, res) => {
272272
});
273273
}
274274

275-
const force = req.query.force === 'true';
276-
if (!force) {
277-
const refResult = await checkReferences('label', id);
278-
if (refResult.hasReferences) {
279-
return res.status(409).json(buildReferenceConflictBody('label', refResult));
280-
}
275+
const refResult = await checkReferences('label', id);
276+
if (refResult.hasReferences) {
277+
return res.status(409).json(buildReferenceConflictBody('label', refResult));
281278
}
282279

283280
await StagingV2.create({

src/controllers/v2/location-v2.controller.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
import { resolveOrgUid } from '../../utils/owner-utils.js';
1313
import { paginationParams, optionallyPaginatedResponse } from '../../utils/helpers.js';
1414
import { loggerV2 } from '../../config/logger.js';
15+
import { checkReferences, buildReferenceConflictBody } from '../../utils/v2-reference-guards.js';
1516

1617
// Generic CRUD controller factory for LocationV2
1718
const createLocationController = (Model, ModelMirror, schema) => {
@@ -261,6 +262,11 @@ const createLocationController = (Model, ModelMirror, schema) => {
261262
});
262263
}
263264

265+
const refResult = await checkReferences('location', id);
266+
if (refResult.hasReferences) {
267+
return res.status(409).json(buildReferenceConflictBody('location', refResult));
268+
}
269+
264270
// Generate UUID for staging
265271
const uuid = uuidv4();
266272

src/controllers/v2/methodology-v2.controller.js

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -276,12 +276,9 @@ export const destroy = async (req, res) => {
276276
});
277277
}
278278

279-
const force = req.query.force === 'true';
280-
if (!force) {
281-
const refResult = await checkReferences('methodology', id);
282-
if (refResult.hasReferences) {
283-
return res.status(409).json(buildReferenceConflictBody('methodology', refResult));
284-
}
279+
const refResult = await checkReferences('methodology', id);
280+
if (refResult.hasReferences) {
281+
return res.status(409).json(buildReferenceConflictBody('methodology', refResult));
285282
}
286283

287284
await StagingV2.create({

src/controllers/v2/program-v2.controller.js

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -265,12 +265,9 @@ export const destroy = async (req, res) => {
265265
});
266266
}
267267

268-
const force = req.query.force === 'true';
269-
if (!force) {
270-
const refResult = await checkReferences('program', id);
271-
if (refResult.hasReferences) {
272-
return res.status(409).json(buildReferenceConflictBody('program', refResult));
273-
}
268+
const refResult = await checkReferences('program', id);
269+
if (refResult.hasReferences) {
270+
return res.status(409).json(buildReferenceConflictBody('program', refResult));
274271
}
275272

276273
await StagingV2.create({

src/controllers/v2/project-methodology-v2.controller.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
import { resolveOrgUid } from '../../utils/owner-utils.js';
1515
import { paginationParams, optionallyPaginatedResponse } from '../../utils/helpers.js';
1616
import { loggerV2 } from '../../config/logger.js';
17+
import { checkReferences, buildReferenceConflictBody } from '../../utils/v2-reference-guards.js';
1718

1819
export const createProjectMethodologyV2 = async (req, res) => {
1920
try {
@@ -368,6 +369,13 @@ export const deleteProjectMethodologyV2 = async (req, res) => {
368369
});
369370
}
370371

372+
const refResult = await checkReferences('project_methodology', cadTrustProjectMethodologyId);
373+
if (refResult.hasReferences) {
374+
return res.status(409).json(
375+
buildReferenceConflictBody('project-methodology relationship', refResult),
376+
);
377+
}
378+
371379
// Stage the delete
372380
await StagingV2.create({
373381
uuid: cadTrustProjectMethodologyId,

src/controllers/v2/project-v2.controller.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { projectV2Schema } from '../../validations/v2/project-v2.validations.js'
3636
import { formatModelAssociationName } from '../../utils/model-utils.js';
3737
import { resolveOrgUid } from '../../utils/owner-utils.js';
3838
import { stageProjectChildDeletes } from '../../utils/v2-cascade-delete.js';
39+
import { checkReferences, buildReferenceConflictBody } from '../../utils/v2-reference-guards.js';
3940

4041
export const create = async (req, res) => {
4142
try {
@@ -819,6 +820,11 @@ export const destroy = async (req, res) => {
819820
});
820821
}
821822

823+
const refResult = await checkReferences('project', id);
824+
if (refResult.hasReferences) {
825+
return res.status(409).json(buildReferenceConflictBody('project', refResult));
826+
}
827+
822828
const releaseTransactionMutex =
823829
await processingSyncRegistriesTransactionMutexV2.acquire();
824830
let stagedChildDeletes;

src/controllers/v2/stakeholder-v2.controller.js

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -263,12 +263,9 @@ export const deleteStakeholderV2 = async (req, res) => {
263263
});
264264
}
265265

266-
const force = req.query.force === 'true';
267-
if (!force) {
268-
const refResult = await checkReferences('stakeholder', id);
269-
if (refResult.hasReferences) {
270-
return res.status(409).json(buildReferenceConflictBody('stakeholder', refResult));
271-
}
266+
const refResult = await checkReferences('stakeholder', id);
267+
if (refResult.hasReferences) {
268+
return res.status(409).json(buildReferenceConflictBody('stakeholder', refResult));
272269
}
273270

274271
await StagingV2.create({

src/controllers/v2/unit-v2.controller.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { unitV2Schema } from '../../validations/v2/unit-v2.validations.js';
3131
import { genericSortColumnRegex } from '../../utils/string-utils.js';
3232
import { resolveOrgUid } from '../../utils/owner-utils.js';
3333
import { stageUnitChildDeletes } from '../../utils/v2-cascade-delete.js';
34+
import { checkReferences, buildReferenceConflictBody } from '../../utils/v2-reference-guards.js';
3435

3536
// Regex patterns for query parsing
3637
const genericFilterRegex = /^(\w+):(.+):(\w+)$/;
@@ -800,6 +801,11 @@ export const destroy = async (req, res) => {
800801
});
801802
}
802803

804+
const refResult = await checkReferences('unit', id);
805+
if (refResult.hasReferences) {
806+
return res.status(409).json(buildReferenceConflictBody('unit', refResult));
807+
}
808+
803809
const releaseTransactionMutex =
804810
await processingSyncRegistriesTransactionMutexV2.acquire();
805811
let stagedChildDeletes;

src/controllers/v2/validation-v2.controller.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { resolveOrgUid } from '../../utils/owner-utils.js';
2121

2222
import { loggerV2 } from '../../config/logger.js';
2323
import { validationV2Schema } from '../../validations/v2/validation-v2.validations.js';
24+
import { checkReferences, buildReferenceConflictBody } from '../../utils/v2-reference-guards.js';
2425

2526
export const create = async (req, res) => {
2627
try {
@@ -313,6 +314,11 @@ export const destroy = async (req, res) => {
313314
});
314315
}
315316

317+
const refResult = await checkReferences('validation', id);
318+
if (refResult.hasReferences) {
319+
return res.status(409).json(buildReferenceConflictBody('validation', refResult));
320+
}
321+
316322
// Stage the delete
317323
await StagingV2.create({
318324
uuid: uuidv4(),

0 commit comments

Comments
 (0)