TASK22102: slice1 api contract helper + email/admin pilot
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
## Current development focus (from recent commits)
|
## 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.
|
- Search/case navigation correctness, especially breadcrumb and back-link behavior.
|
||||||
- My Portal "view all" and DNS application path handling.
|
- My Portal "view all" and DNS application path handling.
|
||||||
- Welsh/English email behavior for specific notification templates.
|
- Welsh/English email behavior for specific notification templates.
|
||||||
|
|||||||
@@ -1302,3 +1302,40 @@ Validation:
|
|||||||
Follow-ups:
|
Follow-ups:
|
||||||
|
|
||||||
- Continue same small-batch consistency pattern for remaining endpoint handlers where required-input/logging drift is clear.
|
- 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 { consoleLogger } from "../../../actions/core/logger";
|
||||||
import { getToken } from "../../../actions/core/token";
|
import { getToken } from "../../../actions/core/token";
|
||||||
import { hashAPIPath } from "../../../actions/core/hash";
|
import { hashAPIPath } from "../../../actions/core/hash";
|
||||||
|
import { respondError, respondSuccess } from "../middleware/apiResponse";
|
||||||
const WORDKEY = process.env.HASHKEY;
|
const WORDKEY = process.env.HASHKEY;
|
||||||
|
|
||||||
const WEBAPI_URL =
|
const WEBAPI_URL =
|
||||||
@@ -154,11 +155,15 @@ export default async function ApiProxy(req, res) {
|
|||||||
_.has(data, "@odata.nextLink") == true &&
|
_.has(data, "@odata.nextLink") == true &&
|
||||||
((dataStr = JSON.stringify(data["@odata.nextLink"])),
|
((dataStr = JSON.stringify(data["@odata.nextLink"])),
|
||||||
(data["@odata.nextLink"] = dataStr.split("/v8.2/")[1]));
|
(data["@odata.nextLink"] = dataStr.split("/v8.2/")[1]));
|
||||||
res.status(200).json(data);
|
return respondSuccess(res, data);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
consoleLogger(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;
|
return apiResponse;
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { azureHeadersPagedCustom } from "../../../actions/core/headers";
|
|||||||
import { getToken } from "../../../actions/core/token";
|
import { getToken } from "../../../actions/core/token";
|
||||||
import { consoleLogger } from "../../../actions/core/logger";
|
import { consoleLogger } from "../../../actions/core/logger";
|
||||||
import { hashAPIPath } from "../../../actions/core/hash";
|
import { hashAPIPath } from "../../../actions/core/hash";
|
||||||
|
import { respondError, respondSuccess } from "../middleware/apiResponse";
|
||||||
|
|
||||||
const WEBAPI_URL =
|
const WEBAPI_URL =
|
||||||
process.env.RELAY_ROOT ||
|
process.env.RELAY_ROOT ||
|
||||||
@@ -64,11 +65,15 @@ export default async function ApiProxy(req, res) {
|
|||||||
_.has(data, "@odata.nextLink") == true &&
|
_.has(data, "@odata.nextLink") == true &&
|
||||||
((dataStr = JSON.stringify(data["@odata.nextLink"])),
|
((dataStr = JSON.stringify(data["@odata.nextLink"])),
|
||||||
(data["@odata.nextLink"] = dataStr.split("/v8.2/")[1]));
|
(data["@odata.nextLink"] = dataStr.split("/v8.2/")[1]));
|
||||||
res.status(200).json(data);
|
return respondSuccess(res, data);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
consoleLogger(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;
|
return apiResponse;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { azureHeaders } from "../../../actions/core/headers";
|
|||||||
import { consoleLogger } from "../../../actions/core/logger";
|
import { consoleLogger } from "../../../actions/core/logger";
|
||||||
import { getToken } from "../../../actions/core/token";
|
import { getToken } from "../../../actions/core/token";
|
||||||
import { hashAPIPath } from "../../../actions/core/hash";
|
import { hashAPIPath } from "../../../actions/core/hash";
|
||||||
|
import { respondError, respondSuccess } from "../middleware/apiResponse";
|
||||||
|
|
||||||
const WORDKEY = process.env.HASHKEY;
|
const WORDKEY = process.env.HASHKEY;
|
||||||
|
|
||||||
@@ -40,10 +41,14 @@ export default async function ApiProxy(req, res) {
|
|||||||
|
|
||||||
const flattenedResults = data.value.map(flattenWatchlistEntry);
|
const flattenedResults = data.value.map(flattenWatchlistEntry);
|
||||||
|
|
||||||
res.status(200).json(flattenedResults);
|
return respondSuccess(res, flattenedResults);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
consoleLogger(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 { consoleLogger, redactSensitive } from "../../../actions/core/logger";
|
||||||
import { isNonEmptyString, sanitizeString } from "../../../actions/core/guards";
|
import { isNonEmptyString, sanitizeString } from "../../../actions/core/guards";
|
||||||
import { getPreferredLanguage } from "../../../actions/services/accountService";
|
import { getPreferredLanguage } from "../../../actions/services/accountService";
|
||||||
|
import { respondError, respondSuccess } from "../middleware/apiResponse";
|
||||||
|
|
||||||
export default async function ApiProxy(req, res) {
|
export default async function ApiProxy(req, res) {
|
||||||
var data = req.body;
|
var data = req.body;
|
||||||
const emailAddress = sanitizeString(data?.emailAddress);
|
const emailAddress = sanitizeString(data?.emailAddress);
|
||||||
|
|
||||||
if (!isNonEmptyString(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;
|
data.emailAddress = emailAddress;
|
||||||
@@ -71,11 +76,15 @@ export default async function ApiProxy(req, res) {
|
|||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
//console.log("thisis the response", response);
|
//console.log("thisis the response", response);
|
||||||
return res.status(200).json(data);
|
return respondSuccess(res, data);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
consoleLogger(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);
|
//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