import PayloadValidator from '../validation/PayloadValidator.js'; import QueryBuilder from './QueryBuilder.js'; export default class SchemaBuilder { constructor() { this.errorSchemas = new Map(); } requestSchema(s) { this.reqSchema = s; return this; } responseSchema(s) { this.resSchema = s; return this; } requestTranslator(f) { this.reqT = f; return this; } responseTranslator(f) { this.resT = f; return this; } preProcess(f) { this.pre = f; return this; } postProcess(f) { this.post = f; return this; } onError(c, s) { this.errorSchemas.set(c, s); return this; } defaultError(s) { this.defErr = s; return this; } pathSchema(schema) { this._pathSchema = schema; return this; } querySchema(schema) { this._querySchema = schema; return this; } headerSchema(schema) { this._headerSchema = schema; return this; } getErrorSchema(statusCode) { return this.errorSchemas.has(statusCode) ? this.errorSchemas.get(statusCode) : this.defErr; } log(req, level, message, meta = {}) { const requestId = req?.requestId || 'unknown'; console[level]( `[SchemaBuilder] [${level.toUpperCase()}] [req:${requestId}] ${message}`, Object.keys(meta).length ? meta : '' ); } async execute(req, res, dbClient) { const start = Date.now(); try { this.log(req, 'info', 'Execution started'); // ----------------------------- // PATH PARAM VALIDATION // ----------------------------- if (this._pathSchema) { this.log(req, 'debug', 'Validating path params'); const vPath = PayloadValidator.validate(this._pathSchema, req.params); if (!vPath.valid) { this.log(req, 'warn', 'Path validation failed', vPath); return res.status(400).json(vPath); } } // ----------------------------- // QUERY PARAM VALIDATION // ----------------------------- if (this._querySchema) { this.log(req, 'debug', 'Validating query params'); const vQuery = PayloadValidator.validate(this._querySchema, req.query); if (!vQuery.valid) { this.log(req, 'warn', 'Query validation failed', vQuery); return res.status(400).json(vQuery); } } // ----------------------------- // HEADER VALIDATION // ----------------------------- if (this._headerSchema) { this.log(req, 'debug', 'Validating headers'); const vHeader = PayloadValidator.validate(this._headerSchema, req.headers); if (!vHeader.valid) { this.log(req, 'warn', 'Header validation failed', vHeader); return res.status(400).json(vHeader); } } // ----------------------------- // BODY VALIDATION // ----------------------------- if (this.reqSchema) { this.log(req, 'debug', 'Validating request body'); const vReq = PayloadValidator.validate(this.reqSchema, req.body); if (!vReq.valid) { this.log(req, 'warn', 'Body validation failed', vReq); return res.status(400).json(vReq); } } // ----------------------------- // PRE-PROCESS // ----------------------------- if (this.pre) { this.log(req, 'debug', 'Running pre-process hook'); const c = await this.pre(req.headers, req.params, req.query, req.body); if (c !== 200) { this.log(req, 'warn', 'Pre-process failed', { status: c }); return res.status(c).json(this.getErrorSchema(c)); } } // ----------------------------- // REQUEST TRANSLATION // ----------------------------- this.log(req, 'debug', 'Translating request to query'); const q = this.reqT(req.headers, req.params, req.query, req.body); // ----------------------------- // DB EXECUTION // ----------------------------- this.log(req, 'info', 'Executing DB query', { type: q?.type }); const qb = new QueryBuilder().fromJSON(q); const dbResp = await qb.execute(dbClient); // ----------------------------- // RESPONSE TRANSLATION // ----------------------------- this.log(req, 'debug', 'Translating DB response'); const resp = this.resT(dbResp, this.resSchema); // ----------------------------- // RESPONSE VALIDATION // ----------------------------- this.log(req, 'debug', 'Validating response'); const vResp = PayloadValidator.validate(this.resSchema, resp); if (!vResp.valid) { this.log(req, 'error', 'Response validation failed', vResp); return res.status(500).json(vResp); } // ----------------------------- // POST-PROCESS // ----------------------------- if (this.post) { this.log(req, 'debug', 'Running post-process hook'); const c = await this.post(req, resp); if (c !== 200) { this.log(req, 'warn', 'Post-process failed', { status: c }); return res.status(c).json(this.getErrorSchema(c)); } } const duration = Date.now() - start; this.log(req, 'info', 'Execution completed', { durationMs: duration }); res.json(resp); } catch (e) { this.log(req, 'error', 'Unhandled exception', { message: e.message, stack: e.stack }); res.status(500).json(this.defErr); } } }