TASK22102: slice1 api contract helper + email/admin pilot
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
## Current development focus (from recent commits)
|
||||
|
||||
- API contract consistency rollout (TASK22102), starting with helper-based response normalization in selected email/admin handlers.
|
||||
- Search/case navigation correctness, especially breadcrumb and back-link behavior.
|
||||
- My Portal "view all" and DNS application path handling.
|
||||
- Welsh/English email behavior for specific notification templates.
|
||||
|
||||
@@ -1302,3 +1302,40 @@ Validation:
|
||||
Follow-ups:
|
||||
|
||||
- Continue same small-batch consistency pattern for remaining endpoint handlers where required-input/logging drift is clear.
|
||||
|
||||
---
|
||||
|
||||
### CL-035: TASK22102 API contract consistency — Slice 1 (response helper + email/admin pilot)
|
||||
|
||||
date: 2026-03-17
|
||||
author: Cline
|
||||
scope: `pages/api/middleware/apiResponse.js`, `pages/api/email/{notify,getmailinglist}.js`, `pages/api/admin/{getnewappeals_api,getlatestdocuments_api}.js`, `memory-bank/*`
|
||||
type: change
|
||||
rationale: Start API contract-consistency program with a low-risk pilot slice introducing shared response helpers and replacing raw error passthrough in selected email/admin handlers.
|
||||
impact: Improves response contract consistency and prevents raw error payload leakage in pilot handlers while preserving existing success-body compatibility.
|
||||
status: completed
|
||||
|
||||
Summary:
|
||||
|
||||
- Added shared API response helper module:
|
||||
- `pages/api/middleware/apiResponse.js`
|
||||
- `respondSuccess(res, data, status)`
|
||||
- `respondError(res, { status, code, message, details })`
|
||||
- Migrated first slice handlers to helper-based responses:
|
||||
- `pages/api/email/notify.js`
|
||||
- `pages/api/email/getmailinglist.js`
|
||||
- `pages/api/admin/getnewappeals_api.js`
|
||||
- `pages/api/admin/getlatestdocuments_api.js`
|
||||
- Contract updates in slice:
|
||||
- replaced `res.status(400).json(error)` with structured error envelope
|
||||
- replaced direct `res.status(200).json(...)` with `respondSuccess(...)`
|
||||
- kept success payload shape as existing data object for non-breaking rollout
|
||||
|
||||
Validation:
|
||||
|
||||
- `npx next lint --file pages/api/middleware/apiResponse.js --file pages/api/email/notify.js --file pages/api/email/getmailinglist.js --file pages/api/admin/getnewappeals_api.js --file pages/api/admin/getlatestdocuments_api.js` -> pass (no warnings/errors)
|
||||
|
||||
Follow-ups:
|
||||
|
||||
- Slice 2 should migrate remaining email handlers (`getcaseref`, `getall`, `getevents`, `getdocuments`) to helper responses.
|
||||
- After email/admin completion, expand to selected high-risk file handlers using same non-breaking helper model.
|
||||
|
||||
@@ -22,6 +22,7 @@ import { azureHeadersPagedCustom } from "../../../actions/core/headers";
|
||||
import { consoleLogger } from "../../../actions/core/logger";
|
||||
import { getToken } from "../../../actions/core/token";
|
||||
import { hashAPIPath } from "../../../actions/core/hash";
|
||||
import { respondError, respondSuccess } from "../middleware/apiResponse";
|
||||
const WORDKEY = process.env.HASHKEY;
|
||||
|
||||
const WEBAPI_URL =
|
||||
@@ -154,11 +155,15 @@ export default async function ApiProxy(req, res) {
|
||||
_.has(data, "@odata.nextLink") == true &&
|
||||
((dataStr = JSON.stringify(data["@odata.nextLink"])),
|
||||
(data["@odata.nextLink"] = dataStr.split("/v8.2/")[1]));
|
||||
res.status(200).json(data);
|
||||
return respondSuccess(res, data);
|
||||
})
|
||||
.catch((error) => {
|
||||
consoleLogger(error);
|
||||
res.status(400).json(error);
|
||||
return respondError(res, {
|
||||
status: 400,
|
||||
code: "ADMIN_LATEST_DOCS_FETCH_FAILED",
|
||||
message: "Failed to fetch latest documents"
|
||||
});
|
||||
});
|
||||
|
||||
return apiResponse;
|
||||
|
||||
@@ -22,6 +22,7 @@ import { azureHeadersPagedCustom } from "../../../actions/core/headers";
|
||||
import { getToken } from "../../../actions/core/token";
|
||||
import { consoleLogger } from "../../../actions/core/logger";
|
||||
import { hashAPIPath } from "../../../actions/core/hash";
|
||||
import { respondError, respondSuccess } from "../middleware/apiResponse";
|
||||
|
||||
const WEBAPI_URL =
|
||||
process.env.RELAY_ROOT ||
|
||||
@@ -64,11 +65,15 @@ export default async function ApiProxy(req, res) {
|
||||
_.has(data, "@odata.nextLink") == true &&
|
||||
((dataStr = JSON.stringify(data["@odata.nextLink"])),
|
||||
(data["@odata.nextLink"] = dataStr.split("/v8.2/")[1]));
|
||||
res.status(200).json(data);
|
||||
return respondSuccess(res, data);
|
||||
})
|
||||
.catch((error) => {
|
||||
consoleLogger(error);
|
||||
res.status(400).json(error);
|
||||
return respondError(res, {
|
||||
status: 400,
|
||||
code: "ADMIN_NEW_APPEALS_FETCH_FAILED",
|
||||
message: "Failed to fetch new appeals"
|
||||
});
|
||||
});
|
||||
|
||||
return apiResponse;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { azureHeaders } from "../../../actions/core/headers";
|
||||
import { consoleLogger } from "../../../actions/core/logger";
|
||||
import { getToken } from "../../../actions/core/token";
|
||||
import { hashAPIPath } from "../../../actions/core/hash";
|
||||
import { respondError, respondSuccess } from "../middleware/apiResponse";
|
||||
|
||||
const WORDKEY = process.env.HASHKEY;
|
||||
|
||||
@@ -40,10 +41,14 @@ export default async function ApiProxy(req, res) {
|
||||
|
||||
const flattenedResults = data.value.map(flattenWatchlistEntry);
|
||||
|
||||
res.status(200).json(flattenedResults);
|
||||
return respondSuccess(res, flattenedResults);
|
||||
})
|
||||
.catch((error) => {
|
||||
consoleLogger(error);
|
||||
res.status(400).json(error);
|
||||
return respondError(res, {
|
||||
status: 400,
|
||||
code: "MAILING_LIST_FETCH_FAILED",
|
||||
message: "Failed to fetch mailing list"
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -28,13 +28,18 @@
|
||||
import { consoleLogger, redactSensitive } from "../../../actions/core/logger";
|
||||
import { isNonEmptyString, sanitizeString } from "../../../actions/core/guards";
|
||||
import { getPreferredLanguage } from "../../../actions/services/accountService";
|
||||
import { respondError, respondSuccess } from "../middleware/apiResponse";
|
||||
|
||||
export default async function ApiProxy(req, res) {
|
||||
var data = req.body;
|
||||
const emailAddress = sanitizeString(data?.emailAddress);
|
||||
|
||||
if (!isNonEmptyString(emailAddress)) {
|
||||
return res.status(400).json({ error: "emailAddress is required" });
|
||||
return respondError(res, {
|
||||
status: 400,
|
||||
code: "EMAIL_ADDRESS_REQUIRED",
|
||||
message: "emailAddress is required"
|
||||
});
|
||||
}
|
||||
|
||||
data.emailAddress = emailAddress;
|
||||
@@ -71,11 +76,15 @@ export default async function ApiProxy(req, res) {
|
||||
})
|
||||
.then((response) => {
|
||||
//console.log("thisis the response", response);
|
||||
return res.status(200).json(data);
|
||||
return respondSuccess(res, data);
|
||||
})
|
||||
.catch((error) => {
|
||||
consoleLogger(error);
|
||||
return res.status(400).json(error);
|
||||
return respondError(res, {
|
||||
status: 400,
|
||||
code: "EMAIL_NOTIFY_FAILED",
|
||||
message: "Failed to send notify email"
|
||||
});
|
||||
});
|
||||
//return res.status(200).json(data);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
export const respondSuccess = (res, data, status = 200) => {
|
||||
return res.status(status).json(data);
|
||||
};
|
||||
|
||||
export const respondError = (
|
||||
res,
|
||||
{
|
||||
status = 500,
|
||||
code = "INTERNAL_SERVER_ERROR",
|
||||
message = "Request failed",
|
||||
details
|
||||
} = {}
|
||||
) => {
|
||||
const payload = {
|
||||
success: false,
|
||||
error: {
|
||||
code,
|
||||
message
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof details !== "undefined") {
|
||||
payload.error.details = details;
|
||||
}
|
||||
|
||||
return res.status(status).json(payload);
|
||||
};
|
||||
Reference in New Issue
Block a user