Skip to content

Commit 9492108

Browse files
committed
fix(V2): account for staged deletes in reference guards
1 parent 66197b4 commit 9492108

2 files changed

Lines changed: 90 additions & 1 deletion

File tree

src/utils/v2-reference-guards.js

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
} from '../models/v2/index.js';
2626
import { Op } from 'sequelize';
2727
import { toSnakeCase } from './v2-camel-to-snake.js';
28+
import { getV2PrimaryKeyField } from './v2-primary-key-utils.js';
2829

2930
const REFERENCE_ERROR_CODE = 'Referenced records must be removed before deletion';
3031

@@ -172,6 +173,34 @@ const countStagedReferences = async (table, snakeFkField, recordId, matchPredica
172173
return count;
173174
};
174175

176+
const findPendingStagedDeleteIds = async (table, primaryKeyField) => {
177+
const stagedRows = await StagingV2.findAll({
178+
where: {
179+
table,
180+
action: 'DELETE',
181+
committed: false,
182+
failed_commit: false,
183+
},
184+
raw: true,
185+
});
186+
187+
const deletedIds = new Set();
188+
for (const stagedRow of stagedRows) {
189+
try {
190+
const parsedData = JSON.parse(stagedRow.data);
191+
const records = Array.isArray(parsedData) ? parsedData : [parsedData];
192+
for (const record of records) {
193+
if (record?.[primaryKeyField]) {
194+
deletedIds.add(record[primaryKeyField]);
195+
}
196+
}
197+
} catch {
198+
continue;
199+
}
200+
}
201+
return deletedIds;
202+
};
203+
175204
/**
176205
* Check whether any local records reference the given record (committed + staged).
177206
*
@@ -191,7 +220,14 @@ export const checkReferences = async (table, recordId) => {
191220
for (const def of definitions) {
192221
const { model, fkField, table: refTable, label, buildWhere, stagedMatch } = def;
193222
const where = buildWhere ? buildWhere(recordId) : { [fkField]: recordId };
194-
const mainCount = await model.count({ where });
223+
const mainRecords = await model.findAll({ where, raw: true });
224+
const primaryKeyAttr = model.primaryKeyAttribute;
225+
const primaryKeyField = getV2PrimaryKeyField(refTable) || toSnakeCase(primaryKeyAttr);
226+
const pendingDeleteIds = await findPendingStagedDeleteIds(refTable, primaryKeyField);
227+
const mainCount = mainRecords.filter((row) => {
228+
const rowId = row?.[primaryKeyAttr] ?? row?.[primaryKeyField];
229+
return !pendingDeleteIds.has(rowId);
230+
}).length;
195231

196232
const snakeFk = toSnakeCase(fkField);
197233
const stagedCount = stagedMatch

tests/v2/integration/methodology-v2.spec.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,59 @@ describe('V2 Methodology API - Basic CRUD Tests', function () {
476476
expect(response.body.references[0].count).to.equal(1);
477477
});
478478

479+
it('should allow delete when committed references are already staged for deletion', async function () {
480+
const methodology = await MethodologyV2.create({
481+
cadTrustMethodologyId: uuidv4(),
482+
methodologyCode: 'STAGED-DELETE-METHOD-001',
483+
methodologyName: 'Methodology With Deleted Reference',
484+
});
485+
486+
const program = await ProgramV2.create({
487+
programName: 'Staged Delete Program',
488+
programRegistry: 'Test Registry',
489+
programRegistryActivityId: 'STAGED-DELETE-001',
490+
});
491+
492+
const project = await ProjectV2.create({
493+
cadTrustProjectId: uuidv4(),
494+
orgUid: 'test-home-org-v2',
495+
projectRegistryName: 'Test Registry',
496+
projectId: 'STAGED-DELETE-PROJ-001',
497+
projectName: 'Staged Delete Project',
498+
cadTrustProgramId: program.cadTrustProgramId,
499+
});
500+
501+
const projectMethodology = await ProjectMethodologyV2.create({
502+
cadTrustProjectMethodologyId: uuidv4(),
503+
cadTrustProjectId: project.cadTrustProjectId,
504+
cadTrustMethodologyId: methodology.cadTrustMethodologyId,
505+
});
506+
507+
await StagingV2.create({
508+
uuid: uuidv4(),
509+
table: 'project_methodology',
510+
action: 'DELETE',
511+
data: JSON.stringify([{
512+
cad_trust_project_methodology_id: projectMethodology.cadTrustProjectMethodologyId,
513+
}]),
514+
committed: false,
515+
failed_commit: false,
516+
is_transfer: false,
517+
});
518+
519+
const response = await supertest(app)
520+
.delete(`/v2/methodology/${methodology.cadTrustMethodologyId}`)
521+
.expect(200);
522+
523+
expect(response.body.success).to.be.true;
524+
expect(response.body.message).to.equal('Methodology delete staged successfully');
525+
526+
const stagingRecord = await StagingV2.findOne({
527+
where: { table: 'methodology', action: 'DELETE' },
528+
});
529+
expect(stagingRecord).to.exist;
530+
});
531+
479532
it('should return 409 with ?force=true when references still exist', async function () {
480533
const methodology = await MethodologyV2.create({
481534
cadTrustMethodologyId: uuidv4(),

0 commit comments

Comments
 (0)