ledgerflow-backend/shared/index.js
Adam Moussa 59127d5ab8 Initial commit — LedgerFlow backend
Lambda-based serverless backend with Google SSO, purchase orders,
invoices, and X12 810 EDI generation for Amazon Payee Central.
Includes bill-to/ship-to address support from Coupa purchase-orders table.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 18:19:38 -04:00

223 lines
7 KiB
JavaScript

// @ledgerflow/shared/index.js
// Shared utilities for all Lambda functions
const { DynamoDBClient } = require("@aws-sdk/client-dynamodb");
const { DynamoDBDocumentClient } = require("@aws-sdk/lib-dynamodb");
// ─── HTTP Response Helpers ───────────────────────────────────────────────────
const ALLOWED_ORIGINS = [
process.env.ALLOWED_ORIGIN || "*",
"http://localhost:3000",
];
function getCorsHeaders(event) {
const origin = event?.headers?.origin || event?.headers?.Origin || "";
const matched = ALLOWED_ORIGINS.includes(origin) ? origin : ALLOWED_ORIGINS[0];
return {
"Access-Control-Allow-Origin": matched,
"Access-Control-Allow-Headers": "Content-Type,Authorization",
"Access-Control-Allow-Methods": "GET,POST,PUT,PATCH,DELETE,OPTIONS",
"Content-Type": "application/json",
};
}
// Static fallback for responses that don't have event context
const CORS_HEADERS = {
"Access-Control-Allow-Origin": process.env.ALLOWED_ORIGIN || "*",
"Access-Control-Allow-Headers": "Content-Type,Authorization",
"Access-Control-Allow-Methods": "GET,POST,PUT,PATCH,DELETE,OPTIONS",
"Content-Type": "application/json",
};
function ok(body, statusCode = 200) {
return {
statusCode,
headers: CORS_HEADERS,
body: JSON.stringify(body),
};
}
function created(body) {
return ok(body, 201);
}
function noContent() {
return { statusCode: 204, headers: CORS_HEADERS, body: "" };
}
function badRequest(message, details = null) {
return {
statusCode: 400,
headers: CORS_HEADERS,
body: JSON.stringify({ error: "Bad Request", message, details }),
};
}
function unauthorized(message = "Unauthorized") {
return {
statusCode: 401,
headers: CORS_HEADERS,
body: JSON.stringify({ error: "Unauthorized", message }),
};
}
function forbidden(message = "Forbidden") {
return {
statusCode: 403,
headers: CORS_HEADERS,
body: JSON.stringify({ error: "Forbidden", message }),
};
}
function notFound(resource = "Resource") {
return {
statusCode: 404,
headers: CORS_HEADERS,
body: JSON.stringify({ error: "Not Found", message: `${resource} not found` }),
};
}
function conflict(message) {
return {
statusCode: 409,
headers: CORS_HEADERS,
body: JSON.stringify({ error: "Conflict", message }),
};
}
function serverError(message = "Internal server error", err = null) {
if (err) console.error("[ERROR]", err);
return {
statusCode: 500,
headers: CORS_HEADERS,
body: JSON.stringify({ error: "Internal Server Error", message }),
};
}
// ─── DynamoDB Client ─────────────────────────────────────────────────────────
let _docClient = null;
function getDocClient() {
if (!_docClient) {
const base = new DynamoDBClient({ region: process.env.AWS_REGION || "us-east-1" });
_docClient = DynamoDBDocumentClient.from(base, {
marshallOptions: { removeUndefinedValues: true },
});
}
return _docClient;
}
// ─── Table Names ─────────────────────────────────────────────────────────────
const TABLES = {
POS: process.env.POS_TABLE || "ledgerflow-pos",
INVOICES: process.env.INVOICES_TABLE || "ledgerflow-invoices",
EDI_TX: process.env.EDI_TX_TABLE || "ledgerflow-edi-transactions",
SESSIONS: process.env.SESSIONS_TABLE || "ledgerflow-sessions",
SETTINGS: process.env.SETTINGS_TABLE || "ledgerflow-settings",
};
// ─── Pagination Helper ───────────────────────────────────────────────────────
function parsePagination(queryParams = {}) {
const limit = Math.min(parseInt(queryParams.limit || "50", 10), 200);
const cursor = queryParams.cursor || null;
return { limit, cursor };
}
function paginatedResponse(items, lastKey, total = null) {
return {
items,
cursor: lastKey ? Buffer.from(JSON.stringify(lastKey)).toString("base64") : null,
...(total !== null && { total }),
};
}
function decodeCursor(cursor) {
if (!cursor) return undefined;
try {
return JSON.parse(Buffer.from(cursor, "base64").toString("utf-8"));
} catch {
return undefined;
}
}
// ─── ID Generation ───────────────────────────────────────────────────────────
function genId(prefix = "") {
const ts = Date.now().toString(36);
const rand = Math.random().toString(36).slice(2, 8);
return `${prefix}${ts}${rand}`.toUpperCase();
}
// ─── Input Validation ────────────────────────────────────────────────────────
function parseBody(event) {
if (!event.body) return {};
try {
return JSON.parse(event.body);
} catch {
throw new ValidationError("Invalid JSON body");
}
}
class ValidationError extends Error {
constructor(message, details = null) {
super(message);
this.name = "ValidationError";
this.details = details;
}
}
function require_fields(obj, fields) {
const missing = fields.filter((f) => !obj[f] && obj[f] !== 0);
if (missing.length) {
throw new ValidationError(`Missing required fields: ${missing.join(", ")}`);
}
}
// ─── Lambda Handler Wrapper ──────────────────────────────────────────────────
// Catches errors, handles OPTIONS preflight, extracts auth context
function handler(fn) {
return async (event, context) => {
const corsHeaders = getCorsHeaders(event);
// CORS preflight
if (event.requestContext?.http?.method === "OPTIONS" || event.httpMethod === "OPTIONS") {
return { statusCode: 200, headers: corsHeaders, body: "" };
}
// Attach authenticated user from authorizer context
const user = event.requestContext?.authorizer?.lambda || null;
try {
const response = await fn(event, context, user);
// Override CORS origin with the matched request origin
if (response?.headers) response.headers["Access-Control-Allow-Origin"] = corsHeaders["Access-Control-Allow-Origin"];
return response;
} catch (err) {
if (err instanceof ValidationError) {
return badRequest(err.message, err.details);
}
console.error("[UNHANDLED]", err);
return serverError("An unexpected error occurred", err);
}
};
}
module.exports = {
// Response
ok, created, noContent, badRequest, unauthorized, forbidden,
notFound, conflict, serverError,
CORS_HEADERS,
// DynamoDB
getDocClient, TABLES,
// Pagination
parsePagination, paginatedResponse, decodeCursor,
// Util
genId, parseBody, require_fields, handler,
ValidationError,
};