TASK22102: slice6 blob crud and retrieval contract cleanup

This commit is contained in:
2026-03-17 13:23:01 +00:00
parent 3048df3cd9
commit 98184b65df
8 changed files with 145 additions and 52 deletions
+36
View File
@@ -1472,3 +1472,39 @@ Validation:
Follow-ups: Follow-ups:
- Continue next larger slice on remaining file handlers with empty 400 responses to complete contract-consistency rollout. - 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.
@@ -1,5 +1,6 @@
import { hashAPIPath } from "../../../actions/core/hash"; import { hashAPIPath } from "../../../actions/core/hash";
import { deleteBlob } from "../../../actions/azurestorage"; import { deleteBlob } from "../../../actions/azurestorage";
import { respondError, respondSuccess } from "../middleware/apiResponse";
import nextConnect from "next-connect"; import nextConnect from "next-connect";
import middleware from "../middleware/middleware"; import middleware from "../middleware/middleware";
@@ -23,7 +24,11 @@ ApiProxy.get(async (req, res) => {
typeof checkHash === "undefined" || typeof checkHash === "undefined" ||
checkHash.length === 0 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 = var checkquerypath =
@@ -35,12 +40,16 @@ ApiProxy.get(async (req, res) => {
blobName; blobName;
if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { 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( await deleteBlob(containerName, casefolderID + "/files/" + blobName).then(
(data) => { (data) => {
return res.status(200).json({ data: data }); return respondSuccess(res, { data: data });
} }
); );
}); });
+12 -3
View File
@@ -1,5 +1,6 @@
import { hashAPIPath } from "../../../actions/core/hash"; import { hashAPIPath } from "../../../actions/core/hash";
import { deleteBlob } from "../../../actions/azurestorage"; import { deleteBlob } from "../../../actions/azurestorage";
import { respondError, respondSuccess } from "../middleware/apiResponse";
import nextConnect from "next-connect"; import nextConnect from "next-connect";
import middleware from "../middleware/middleware"; import middleware from "../middleware/middleware";
@@ -23,7 +24,11 @@ ApiProxy.get(async (req, res) => {
typeof checkHash === "undefined" || typeof checkHash === "undefined" ||
checkHash.length === 0 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 = var checkquerypath =
@@ -35,12 +40,16 @@ ApiProxy.get(async (req, res) => {
encodeURIComponent(blobName); encodeURIComponent(blobName);
if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { 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( await deleteBlob(containerName, casefolderID + "/files/" + blobName).then(
(data) => { (data) => {
return res.status(200).json({ data: data }); return respondSuccess(res, { data: data });
} }
); );
}); });
+14 -7
View File
@@ -1,5 +1,6 @@
import { hashAPIPath } from "../../../actions/core/hash"; import { hashAPIPath } from "../../../actions/core/hash";
import { deleteBlobCase } from "../../../actions/azurestorage"; import { deleteBlobCase } from "../../../actions/azurestorage";
import { respondError, respondSuccess } from "../middleware/apiResponse";
import nextConnect from "next-connect"; import nextConnect from "next-connect";
import middleware from "../middleware/middleware"; import middleware from "../middleware/middleware";
@@ -20,7 +21,11 @@ ApiProxy.get(async (req, res) => {
typeof checkHash === "undefined" || typeof checkHash === "undefined" ||
checkHash.length === 0 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 = var checkquerypath =
@@ -30,14 +35,16 @@ ApiProxy.get(async (req, res) => {
casefolderID; casefolderID;
if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) {
return res.status(400).json(); return respondError(res, {
} status: 400,
code: "INVALID_HASH",
if (hashAPIPath(checkquerypath) == "&hash=" + checkHash) { message: "Invalid hash"
await deleteBlobCase(containerName, casefolderID).then((data) => {
return res.status(200).json({ data: data });
}); });
} }
await deleteBlobCase(containerName, casefolderID).then((data) => {
return respondSuccess(res, { data: data });
});
}); });
export const config = { export const config = {
+14 -7
View File
@@ -1,5 +1,6 @@
import { hashAPIPath } from "../../../actions/core/hash"; import { hashAPIPath } from "../../../actions/core/hash";
import { deleteBlobRep } from "../../../actions/azurestorage"; import { deleteBlobRep } from "../../../actions/azurestorage";
import { respondError, respondSuccess } from "../middleware/apiResponse";
import nextConnect from "next-connect"; import nextConnect from "next-connect";
import middleware from "../middleware/middleware"; import middleware from "../middleware/middleware";
@@ -23,7 +24,11 @@ ApiProxy.get(async (req, res) => {
typeof checkHash === "undefined" || typeof checkHash === "undefined" ||
checkHash.length === 0 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 = var checkquerypath =
@@ -35,16 +40,18 @@ ApiProxy.get(async (req, res) => {
repfile; repfile;
if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) {
return res.status(400).json(); return respondError(res, {
status: 400,
code: "INVALID_HASH",
message: "Invalid hash"
});
} }
casefolderID = casefolderID + "/" + repfile; casefolderID = casefolderID + "/" + repfile;
if (hashAPIPath(checkquerypath) == "&hash=" + checkHash) { await deleteBlobRep(containerName, casefolderID).then((data) => {
await deleteBlobRep(containerName, casefolderID).then((data) => { return respondSuccess(res, { data: data });
return res.status(200).json({ data: data }); });
});
}
}); });
export const config = { export const config = {
+18 -11
View File
@@ -6,6 +6,7 @@ import _ from "lodash";
import nextConnect from "next-connect"; import nextConnect from "next-connect";
import middleware from "../middleware/middleware"; import middleware from "../middleware/middleware";
import { hashAPIPath } from "../../../actions/core/hash"; import { hashAPIPath } from "../../../actions/core/hash";
import { respondError, respondSuccess } from "../middleware/apiResponse";
const ApiProxy = nextConnect(); const ApiProxy = nextConnect();
ApiProxy.use(middleware); ApiProxy.use(middleware);
@@ -20,25 +21,31 @@ ApiProxy.get(async (req, res) => {
typeof checkHash === "undefined" || typeof checkHash === "undefined" ||
checkHash.length === 0 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 = var checkquerypath =
"/api/file/getawaitingsubmissionfromblob?container=" + containerName; "/api/file/getawaitingsubmissionfromblob?container=" + containerName;
if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { 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)
const blobObj = await getAllProgressBlobs(containerName) .then((data) => {
.then((data) => { return downloadAllProgressFiles(containerName, data);
return downloadAllProgressFiles(containerName, data); })
}) .then((data) => {
.then((data) => { return respondSuccess(res, data);
return res.status(200).json(data); });
});
}
}); });
export const config = { export const config = {
+12 -3
View File
@@ -6,6 +6,7 @@ import _ from "lodash";
import nextConnect from "next-connect"; import nextConnect from "next-connect";
import middleware from "../middleware/middleware"; import middleware from "../middleware/middleware";
import { hashAPIPath } from "../../../actions/core/hash"; import { hashAPIPath } from "../../../actions/core/hash";
import { respondError, respondSuccess } from "../middleware/apiResponse";
const ApiProxy = nextConnect(); const ApiProxy = nextConnect();
ApiProxy.use(middleware); ApiProxy.use(middleware);
@@ -24,7 +25,11 @@ ApiProxy.get(async (req, res) => {
typeof checkHash === "undefined" || typeof checkHash === "undefined" ||
checkHash.length === 0 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 = var checkquerypath =
@@ -34,7 +39,11 @@ ApiProxy.get(async (req, res) => {
casefolderID; casefolderID;
if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { 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) await getProgressBlobs(containerName, casefolderID)
@@ -42,7 +51,7 @@ ApiProxy.get(async (req, res) => {
return downloadProgressFile(containerName, data.path, casefolderID); return downloadProgressFile(containerName, data.path, casefolderID);
}) })
.then((data) => { .then((data) => {
return res.status(200).json(data); return respondSuccess(res, data);
}); });
}); });
+27 -18
View File
@@ -38,6 +38,7 @@ import nextConnect from "next-connect";
import middleware from "../middleware/middleware"; import middleware from "../middleware/middleware";
import { hashAPIPath } from "../../../actions/core/hash"; import { hashAPIPath } from "../../../actions/core/hash";
import { consoleLogger } from "../../../actions/core/logger"; import { consoleLogger } from "../../../actions/core/logger";
import { respondError, respondSuccess } from "../middleware/apiResponse";
const ApiProxy = nextConnect(); const ApiProxy = nextConnect();
ApiProxy.use(middleware); ApiProxy.use(middleware);
@@ -54,32 +55,40 @@ ApiProxy.get(async (req, res) => {
typeof checkHash === "undefined" || typeof checkHash === "undefined" ||
checkHash.length === 0 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; var checkquerypath = "/api/file/getrepsblob?container=" + containerName;
if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { 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)
const blobObj = await getRepsBlobs(containerName) .then(async (data) => {
.then(async (data) => { return data;
return data; })
}) .then(async (data) => {
.then(async (data) => { let result = await downloadAllRepsFiles(containerName, data);
let result = await downloadAllRepsFiles(containerName, data); res.setHeader("Cache-Control", "no-store");
return respondSuccess(res, result);
return res })
.setHeader("Cache-Control", "no-store") .catch((error) => {
.status(200) consoleLogger(error);
.json(result); return respondError(res, {
}) status: 400,
.catch((error) => { code: "GET_REPS_BLOB_FAILED",
consoleLogger(error); message: "Failed to retrieve representation blobs"
}); });
} });
}); });
export default ApiProxy; export default ApiProxy;