From 98184b65df794c9e5164cd95de3f97d77ea9a0d6 Mon Sep 17 00:00:00 2001 From: robbond Date: Tue, 17 Mar 2026 13:23:01 +0000 Subject: [PATCH] TASK22102: slice6 blob crud and retrieval contract cleanup --- memory-bank/change-log.md | 36 +++++++++++++++ .../file/deleteawaitingsubmissionfromblob.js | 15 +++++-- pages/api/file/deleteblob.js | 15 +++++-- pages/api/file/deleteblobcase.js | 21 ++++++--- pages/api/file/deleteblobrep.js | 21 ++++++--- .../api/file/getawaitingsubmissionfromblob.js | 29 +++++++----- pages/api/file/getprogressobjblob.js | 15 +++++-- pages/api/file/getrepsblob.js | 45 +++++++++++-------- 8 files changed, 145 insertions(+), 52 deletions(-) diff --git a/memory-bank/change-log.md b/memory-bank/change-log.md index 79e65e3d..7b86df07 100644 --- a/memory-bank/change-log.md +++ b/memory-bank/change-log.md @@ -1472,3 +1472,39 @@ Validation: Follow-ups: - Continue next larger slice on remaining file handlers with empty 400 responses to complete contract-consistency rollout. + +--- + +### CL-040: TASK22102 API contract consistency — Slice 6 (CRUD/blob retrieval group) + +date: 2026-03-17 +author: Cline +scope: `pages/api/file/{deleteblob,deleteblobcase,deleteblobrep,deleteawaitingsubmissionfromblob,getprogressobjblob,getawaitingsubmissionfromblob,getrepsblob}.js` +type: change +rationale: Deliver the first of two remaining larger slices by standardizing response contracts across blob CRUD and retrieval handlers still using direct/empty response patterns. +impact: Improves consistency and safety of error responses in selected blob handlers while preserving existing success payload behavior and cache-control semantics. +status: completed + +Summary: + +- Migrated 7 blob CRUD/retrieval handlers to `respondSuccess`/`respondError`: + - `deleteblob` + - `deleteblobcase` + - `deleteblobrep` + - `deleteawaitingsubmissionfromblob` + - `getprogressobjblob` + - `getawaitingsubmissionfromblob` + - `getrepsblob` +- Replaced empty `400` and direct status responses with structured error envelopes including explicit codes/messages. +- Removed redundant post-hash equality branches where equivalent logic was already guaranteed after early guard checks. +- Preserved response behavior: + - existing success data payload shapes retained (`{ data: ... }` or direct data payloads) + - `getrepsblob` retains `Cache-Control: no-store` header. + +Validation: + +- `npx next lint --file pages/api/file/deleteblob.js --file pages/api/file/deleteblobcase.js --file pages/api/file/deleteblobrep.js --file pages/api/file/deleteawaitingsubmissionfromblob.js --file pages/api/file/getprogressobjblob.js --file pages/api/file/getawaitingsubmissionfromblob.js --file pages/api/file/getrepsblob.js` -> pass (no warnings/errors) + +Follow-ups: + +- Execute final Slice 7 for remaining complex handlers (`upload`, `getbloblist`, `downloadblob`, `generateappealpdf`) to complete the 2-slice finish plan. diff --git a/pages/api/file/deleteawaitingsubmissionfromblob.js b/pages/api/file/deleteawaitingsubmissionfromblob.js index cc99f15e..a1cdbe53 100644 --- a/pages/api/file/deleteawaitingsubmissionfromblob.js +++ b/pages/api/file/deleteawaitingsubmissionfromblob.js @@ -1,5 +1,6 @@ import { hashAPIPath } from "../../../actions/core/hash"; import { deleteBlob } from "../../../actions/azurestorage"; +import { respondError, respondSuccess } from "../middleware/apiResponse"; import nextConnect from "next-connect"; import middleware from "../middleware/middleware"; @@ -23,7 +24,11 @@ ApiProxy.get(async (req, res) => { typeof checkHash === "undefined" || checkHash.length === 0 ) { - return res.status(400).json(); + return respondError(res, { + status: 400, + code: "MISSING_REQUIRED_QUERY", + message: "container, casefolderID, blobname and hash are required" + }); } var checkquerypath = @@ -35,12 +40,16 @@ ApiProxy.get(async (req, res) => { blobName; if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { - return res.status(400).json(); + return respondError(res, { + status: 400, + code: "INVALID_HASH", + message: "Invalid hash" + }); } await deleteBlob(containerName, casefolderID + "/files/" + blobName).then( (data) => { - return res.status(200).json({ data: data }); + return respondSuccess(res, { data: data }); } ); }); diff --git a/pages/api/file/deleteblob.js b/pages/api/file/deleteblob.js index 7ff99c4f..5c1d52fe 100644 --- a/pages/api/file/deleteblob.js +++ b/pages/api/file/deleteblob.js @@ -1,5 +1,6 @@ import { hashAPIPath } from "../../../actions/core/hash"; import { deleteBlob } from "../../../actions/azurestorage"; +import { respondError, respondSuccess } from "../middleware/apiResponse"; import nextConnect from "next-connect"; import middleware from "../middleware/middleware"; @@ -23,7 +24,11 @@ ApiProxy.get(async (req, res) => { typeof checkHash === "undefined" || checkHash.length === 0 ) { - return res.status(400).json(); + return respondError(res, { + status: 400, + code: "MISSING_REQUIRED_QUERY", + message: "container, casefolderID, blobname and hash are required" + }); } var checkquerypath = @@ -35,12 +40,16 @@ ApiProxy.get(async (req, res) => { encodeURIComponent(blobName); if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { - return res.status(400).json(); + return respondError(res, { + status: 400, + code: "INVALID_HASH", + message: "Invalid hash" + }); } await deleteBlob(containerName, casefolderID + "/files/" + blobName).then( (data) => { - return res.status(200).json({ data: data }); + return respondSuccess(res, { data: data }); } ); }); diff --git a/pages/api/file/deleteblobcase.js b/pages/api/file/deleteblobcase.js index 39e2cfde..ba48e9d5 100644 --- a/pages/api/file/deleteblobcase.js +++ b/pages/api/file/deleteblobcase.js @@ -1,5 +1,6 @@ import { hashAPIPath } from "../../../actions/core/hash"; import { deleteBlobCase } from "../../../actions/azurestorage"; +import { respondError, respondSuccess } from "../middleware/apiResponse"; import nextConnect from "next-connect"; import middleware from "../middleware/middleware"; @@ -20,7 +21,11 @@ ApiProxy.get(async (req, res) => { typeof checkHash === "undefined" || checkHash.length === 0 ) { - return res.status(400).json(); + return respondError(res, { + status: 400, + code: "MISSING_REQUIRED_QUERY", + message: "container, casefolderID and hash are required" + }); } var checkquerypath = @@ -30,14 +35,16 @@ ApiProxy.get(async (req, res) => { casefolderID; if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { - return res.status(400).json(); - } - - if (hashAPIPath(checkquerypath) == "&hash=" + checkHash) { - await deleteBlobCase(containerName, casefolderID).then((data) => { - return res.status(200).json({ data: data }); + return respondError(res, { + status: 400, + code: "INVALID_HASH", + message: "Invalid hash" }); } + + await deleteBlobCase(containerName, casefolderID).then((data) => { + return respondSuccess(res, { data: data }); + }); }); export const config = { diff --git a/pages/api/file/deleteblobrep.js b/pages/api/file/deleteblobrep.js index 5d55a259..fd80f28b 100644 --- a/pages/api/file/deleteblobrep.js +++ b/pages/api/file/deleteblobrep.js @@ -1,5 +1,6 @@ import { hashAPIPath } from "../../../actions/core/hash"; import { deleteBlobRep } from "../../../actions/azurestorage"; +import { respondError, respondSuccess } from "../middleware/apiResponse"; import nextConnect from "next-connect"; import middleware from "../middleware/middleware"; @@ -23,7 +24,11 @@ ApiProxy.get(async (req, res) => { typeof checkHash === "undefined" || checkHash.length === 0 ) { - return res.status(400).json(); + return respondError(res, { + status: 400, + code: "MISSING_REQUIRED_QUERY", + message: "container, casefolderID, repfile and hash are required" + }); } var checkquerypath = @@ -35,16 +40,18 @@ ApiProxy.get(async (req, res) => { repfile; if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { - return res.status(400).json(); + return respondError(res, { + status: 400, + code: "INVALID_HASH", + message: "Invalid hash" + }); } casefolderID = casefolderID + "/" + repfile; - if (hashAPIPath(checkquerypath) == "&hash=" + checkHash) { - await deleteBlobRep(containerName, casefolderID).then((data) => { - return res.status(200).json({ data: data }); - }); - } + await deleteBlobRep(containerName, casefolderID).then((data) => { + return respondSuccess(res, { data: data }); + }); }); export const config = { diff --git a/pages/api/file/getawaitingsubmissionfromblob.js b/pages/api/file/getawaitingsubmissionfromblob.js index c1949040..d8ca4315 100644 --- a/pages/api/file/getawaitingsubmissionfromblob.js +++ b/pages/api/file/getawaitingsubmissionfromblob.js @@ -6,6 +6,7 @@ import _ from "lodash"; import nextConnect from "next-connect"; import middleware from "../middleware/middleware"; import { hashAPIPath } from "../../../actions/core/hash"; +import { respondError, respondSuccess } from "../middleware/apiResponse"; const ApiProxy = nextConnect(); ApiProxy.use(middleware); @@ -20,25 +21,31 @@ ApiProxy.get(async (req, res) => { typeof checkHash === "undefined" || checkHash.length === 0 ) { - return res.status(400).json(); + return respondError(res, { + status: 400, + code: "MISSING_REQUIRED_QUERY", + message: "container and hash are required" + }); } var checkquerypath = "/api/file/getawaitingsubmissionfromblob?container=" + containerName; if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { - return res.status(400).json(); + return respondError(res, { + status: 400, + code: "INVALID_HASH", + message: "Invalid hash" + }); } - if (hashAPIPath(checkquerypath) == "&hash=" + checkHash) { - const blobObj = await getAllProgressBlobs(containerName) - .then((data) => { - return downloadAllProgressFiles(containerName, data); - }) - .then((data) => { - return res.status(200).json(data); - }); - } + const blobObj = await getAllProgressBlobs(containerName) + .then((data) => { + return downloadAllProgressFiles(containerName, data); + }) + .then((data) => { + return respondSuccess(res, data); + }); }); export const config = { diff --git a/pages/api/file/getprogressobjblob.js b/pages/api/file/getprogressobjblob.js index 6df0a3ac..a9481aa4 100644 --- a/pages/api/file/getprogressobjblob.js +++ b/pages/api/file/getprogressobjblob.js @@ -6,6 +6,7 @@ import _ from "lodash"; import nextConnect from "next-connect"; import middleware from "../middleware/middleware"; import { hashAPIPath } from "../../../actions/core/hash"; +import { respondError, respondSuccess } from "../middleware/apiResponse"; const ApiProxy = nextConnect(); ApiProxy.use(middleware); @@ -24,7 +25,11 @@ ApiProxy.get(async (req, res) => { typeof checkHash === "undefined" || checkHash.length === 0 ) { - return res.status(400).json(); + return respondError(res, { + status: 400, + code: "MISSING_REQUIRED_QUERY", + message: "container, casefolderID and hash are required" + }); } var checkquerypath = @@ -34,7 +39,11 @@ ApiProxy.get(async (req, res) => { casefolderID; if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { - return res.status(400).json(); + return respondError(res, { + status: 400, + code: "INVALID_HASH", + message: "Invalid hash" + }); } await getProgressBlobs(containerName, casefolderID) @@ -42,7 +51,7 @@ ApiProxy.get(async (req, res) => { return downloadProgressFile(containerName, data.path, casefolderID); }) .then((data) => { - return res.status(200).json(data); + return respondSuccess(res, data); }); }); diff --git a/pages/api/file/getrepsblob.js b/pages/api/file/getrepsblob.js index a7177949..a059f5a2 100644 --- a/pages/api/file/getrepsblob.js +++ b/pages/api/file/getrepsblob.js @@ -38,6 +38,7 @@ import nextConnect from "next-connect"; import middleware from "../middleware/middleware"; import { hashAPIPath } from "../../../actions/core/hash"; import { consoleLogger } from "../../../actions/core/logger"; +import { respondError, respondSuccess } from "../middleware/apiResponse"; const ApiProxy = nextConnect(); ApiProxy.use(middleware); @@ -54,32 +55,40 @@ ApiProxy.get(async (req, res) => { typeof checkHash === "undefined" || checkHash.length === 0 ) { - return res.status(400).json(); + return respondError(res, { + status: 400, + code: "MISSING_REQUIRED_QUERY", + message: "container and hash are required" + }); } var checkquerypath = "/api/file/getrepsblob?container=" + containerName; if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { - return res.status(400).json(); + return respondError(res, { + status: 400, + code: "INVALID_HASH", + message: "Invalid hash" + }); } - if (hashAPIPath(checkquerypath) == "&hash=" + checkHash) { - const blobObj = await getRepsBlobs(containerName) - .then(async (data) => { - return data; - }) - .then(async (data) => { - let result = await downloadAllRepsFiles(containerName, data); - - return res - .setHeader("Cache-Control", "no-store") - .status(200) - .json(result); - }) - .catch((error) => { - consoleLogger(error); + const blobObj = await getRepsBlobs(containerName) + .then(async (data) => { + return data; + }) + .then(async (data) => { + let result = await downloadAllRepsFiles(containerName, data); + res.setHeader("Cache-Control", "no-store"); + return respondSuccess(res, result); + }) + .catch((error) => { + consoleLogger(error); + return respondError(res, { + status: 400, + code: "GET_REPS_BLOB_FAILED", + message: "Failed to retrieve representation blobs" }); - } + }); }); export default ApiProxy;