Generic API builder

This commit is contained in:
SaiD 2025-12-21 22:06:51 +05:30
parent 8e6f6d32d4
commit 0353d4c642
25 changed files with 893 additions and 2 deletions

View File

@ -0,0 +1,17 @@
export default class ApiBuilder {
constructor(common){
this.app=common.app;
this.middlewares=[...common.middlewares];
}
method(m){ this.m=m.toLowerCase(); return this; }
url(u){ this.path=u.build(); return this; }
schema(s){ this.schemaBuilder=s; return this; }
sync(){ return this; }
build(dbClient){
this.app[this.m](
this.path,
...this.middlewares,
(req,res)=>this.schemaBuilder.execute(req,res,dbClient)
);
}
}

View File

@ -0,0 +1,5 @@
export default class CommonApiBuilder {
constructor(app){ this.app=app; this.middlewares=[]; }
use(mw){ this.middlewares.push(mw.middleware()); return this; }
build(){ return { app:this.app, middlewares:[...this.middlewares] }; }
}

View File

@ -0,0 +1,9 @@
export default class QueryBuilder {
constructor(){ this.query={}; }
fromJSON(q){ this.query=q; return this; }
select(cfg){ this.query={ op:'SELECT', ...cfg }; return this; }
insert(cfg){ this.query={ op:'INSERT', ...cfg }; return this; }
update(cfg){ this.query={ op:'UPDATE', ...cfg }; return this; }
delete(cfg){ this.query={ op:'DELETE', ...cfg }; return this; }
async execute(dbClient){ return dbClient.execute(this.query); }
}

View File

@ -0,0 +1,156 @@
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);
}
}
}

View File

@ -0,0 +1,33 @@
export default class UrlBuilder {
constructor(baseUrl) {
this.baseUrl = baseUrl;
this.pathParams = {};
this.queryParams = {};
this.headerParams = {};
}
withPathParams(params = {}) {
this.pathParams = params;
return this;
}
withQueryParams(params = {}) {
this.queryParams = params;
return this;
}
withHeaderParams(params = {}) {
this.headerParams = params;
return this;
}
build() {
let path = this.baseUrl;
for (const key of Object.keys(this.pathParams)) {
path += `/:${key}`;
}
return path;
}
}

70
core/db/client.js Normal file
View File

@ -0,0 +1,70 @@
import knex from 'knex';
import 'dotenv/config';
import executeJsonQuery from './jsonQueryExecutor.js';
/**
* Knex configuration
* Uses environment variables with sane defaults
*/
const config = {
client: 'pg',
connection: {
host: process.env.PGHOST || '127.0.0.1',
port: Number(process.env.PGPORT || 5432),
user: process.env.PGUSER || 'postgres',
password: process.env.PGPASSWORD || 'postgres',
database: process.env.PGDATABASE || 'postgres',
},
pool: {
min: 2,
max: 10,
},
};
const knexInstance = knex(config);
const dbClient = {
async execute(query) {
if (!query || typeof query !== 'object') {
throw new Error('Invalid query object');
}
switch (query.type) {
case 'json': {
return await executeJsonQuery(knexInstance, query);
}
case 'raw-builder': {
if (typeof query.handler !== 'function') {
throw new Error('raw-builder requires a handler function');
}
return await query.handler(knexInstance);
}
case 'transaction': {
if (typeof query.handler !== 'function') {
throw new Error('transaction requires a handler function');
}
return await knexInstance.transaction(async (trx) => {
return await query.handler(trx);
});
}
default:
throw new Error(`Unsupported query type: ${query.type}`);
}
},
getKnex() {
return knexInstance;
},
async destroy() {
await knexInstance.destroy();
}
};
export default dbClient;

View File

@ -0,0 +1,107 @@
/**
* Execute a JSON-based query using Knex
*
* Supported ops:
* SELECT | INSERT | UPDATE | DELETE
*/
export default function executeJsonQuery(knex, query) {
const {
op,
table,
columns,
values,
where,
orderBy,
limit,
offset
} = query;
if (!op || !table) {
throw new Error('JSON query must include "op" and "table"');
}
let qb;
switch (op) {
// ----------------------------------
// SELECT
// ----------------------------------
case 'SELECT': {
qb = knex(table);
if (Array.isArray(columns) && columns.length > 0) {
qb.select(columns);
} else {
qb.select('*');
}
if (where && typeof where === 'object') {
qb.where(where);
}
if (Array.isArray(orderBy)) {
for (const { column, direction } of orderBy) {
qb.orderBy(column, direction || 'asc');
}
}
if (Number.isInteger(limit)) {
qb.limit(limit);
}
if (Number.isInteger(offset)) {
qb.offset(offset);
}
return qb;
}
// ----------------------------------
// INSERT
// ----------------------------------
case 'INSERT': {
if (!values || typeof values !== 'object') {
throw new Error('INSERT requires "values"');
}
return knex(table)
.insert(values)
.returning(columns || '*');
}
// ----------------------------------
// UPDATE
// ----------------------------------
case 'UPDATE': {
if (!values || typeof values !== 'object') {
throw new Error('UPDATE requires "values"');
}
if (!where || typeof where !== 'object') {
throw new Error('UPDATE without WHERE is not allowed');
}
return knex(table)
.where(where)
.update(values)
.returning(columns || '*');
}
// ----------------------------------
// DELETE
// ----------------------------------
case 'DELETE': {
if (!where || typeof where !== 'object') {
throw new Error('DELETE without WHERE is not allowed');
}
return knex(table)
.where(where)
.del();
}
default:
throw new Error(`Unsupported JSON op: ${op}`);
}
}

View File

@ -0,0 +1,3 @@
export default class BaseMiddleware {
middleware(){ throw new Error('middleware() not implemented'); }
}

View File

@ -0,0 +1,11 @@
import BaseMiddleware from './BaseMiddleware.js';
export default class CoarseAuthMiddleware extends BaseMiddleware {
constructor(roles=[]){ super(); this.roles=roles; }
middleware(){
return (req,res,next)=>{
if(!this.roles.includes(req.user?.role))
return res.status(403).json({ error:'FORBIDDEN' });
next();
};
}
}

View File

@ -0,0 +1,11 @@
import BaseMiddleware from './BaseMiddleware.js';
export default class FineAuthMiddleware extends BaseMiddleware {
constructor({ getResourceOwnerId }){ super(); this.getResourceOwnerId=getResourceOwnerId; }
middleware(){
return (req,res,next)=>{
if(req.user.role==='ADMIN') return next();
if(req.user.userId===this.getResourceOwnerId(req)) return next();
res.status(403).json({ error:'FORBIDDEN' });
};
}
}

View File

@ -0,0 +1,22 @@
import axios from 'axios';
import BaseMiddleware from './BaseMiddleware.js';
export default class JwtAuthMiddleware extends BaseMiddleware {
constructor(options={}){
super();
this.authServiceUrl=options.authServiceUrl||'http://auth-service:3000/auth/validate-token';
}
middleware(){
return async (req,res,next)=>{
const h=req.headers.authorization;
if(!h) return res.status(401).json({ error:'UNAUTHORIZED' });
try{
const token=h.replace('Bearer ','');
const r=await axios.post(this.authServiceUrl,{ token });
req.user=r.data;
next();
}catch{
res.status(401).json({ error:'INVALID_TOKEN' });
}
};
}
}

View File

@ -0,0 +1,5 @@
import BaseMiddleware from './BaseMiddleware.js';
export default class RateLimiterMiddleware extends BaseMiddleware {
constructor(createLimiterFn){ super(); this.createLimiterFn=createLimiterFn; }
middleware(){ return this.createLimiterFn(); }
}

View File

@ -0,0 +1,7 @@
export default {
STRING: 'string',
NUMBER: 'number',
ARRAY: 'Array',
JSON_OBJECT: 'JSONObject',
JSON_ARRAY: 'JSONArray'
};

View File

@ -0,0 +1,74 @@
import fs from 'fs';
import path from 'path';
export default class SchemaFileLoader {
/**
* Load a schema JSON file and resolve all nested json_dtype references
*
* @param {string} filePath - Absolute or relative path to schema file
* @param {Set<string>} visited - Internal circular reference protection
* @returns {object} Fully resolved schema JSON
*/
static load(filePath, visited = new Set()) {
const absolutePath = path.resolve(filePath);
if (visited.has(absolutePath)) {
throw new Error(`Circular schema reference detected: ${absolutePath}`);
}
visited.add(absolutePath);
if (!fs.existsSync(absolutePath)) {
throw new Error(`Schema file not found: ${absolutePath}`);
}
const raw = fs.readFileSync(absolutePath, 'utf-8');
const schema = JSON.parse(raw);
const baseDir = path.dirname(absolutePath);
return this._resolveSchema(schema, baseDir, visited);
}
/**
* Recursively resolve json_dtype references
*/
static _resolveSchema(schema, baseDir, visited) {
if (typeof schema !== 'object' || schema === null) return schema;
for (const field in schema) {
const rule = schema[field];
if (!rule || typeof rule !== 'object') continue;
// Resolve nested JSON object schema
if (
rule.type === 'JSONObject' ||
rule.type === 'JSONArray'
) {
if (typeof rule.json_dtype === 'string') {
const nestedPath = path.join(baseDir, rule.json_dtype);
rule.json_dtype = this.load(nestedPath, visited);
} else if (typeof rule.json_dtype === 'object') {
rule.json_dtype = this._resolveSchema(
rule.json_dtype,
baseDir,
visited
);
}
}
// Resolve array of JSON objects
if (
rule.type === 'Array' &&
rule.array_dtype === 'JSONObject'
) {
if (typeof rule.json_dtype === 'string') {
const nestedPath = path.join(baseDir, rule.json_dtype);
rule.json_dtype = this.load(nestedPath, visited);
}
}
}
return schema;
}
}

View File

@ -0,0 +1,39 @@
import PayloadTypes from '../types/PayloadTypes.js';
export default class PayloadValidator {
static validate(schema, payload, path = '') {
for (const field in schema) {
const rule = schema[field];
const value = payload[field];
const p = path ? `${path}.${field}` : field;
if (rule.is_mandatory_field && value === undefined) {
return { valid:false, error:`Missing field ${p}` };
}
if (value === undefined) continue;
switch (rule.type) {
case PayloadTypes.STRING:
case PayloadTypes.NUMBER:
if (typeof value !== rule.type) return { valid:false, error:`Invalid type ${p}` };
break;
case PayloadTypes.ARRAY:
case PayloadTypes.JSON_ARRAY:
if (!Array.isArray(value)) return { valid:false, error:`Invalid array ${p}` };
if (rule.array_dtype) {
for (const v of value) {
if (typeof v !== rule.array_dtype)
return { valid:false, error:`Invalid array dtype ${p}` };
}
}
break;
case PayloadTypes.JSON_OBJECT:
if (typeof value !== 'object' || Array.isArray(value))
return { valid:false, error:`Invalid object ${p}` };
if (rule.json_dtype) {
const nested = this.validate(rule.json_dtype, value, p);
if (!nested.valid) return nested;
}
break;
}
}
return { valid:true };
}
}

View File

@ -0,0 +1,89 @@
export default function listingRequestTranslator(
headerParams,
pathParams,
queryParams,
body
) {
const {
id
} = pathParams || {};
const {
limit = 20,
offset = 0,
species_id,
breed_id,
sex,
min_price,
max_price,
status
} = queryParams || {};
// -----------------------------
// BASE WHERE CLAUSE
// -----------------------------
const where = {
deleted: false
};
// -----------------------------
// PATH FILTER (single listing)
// -----------------------------
if (id) {
where.id = id;
}
// -----------------------------
// FILTERS (denormalized columns)
// -----------------------------
if (species_id) {
where.filter_species_id = species_id;
}
if (breed_id) {
where.filter_breed_id = breed_id;
}
if (sex) {
where.filter_sex = sex;
}
if (status) {
where.status = status;
}
// -----------------------------
// BUILD QUERY JSON
// -----------------------------
const query = {
type: 'json',
op: 'SELECT',
table: 'listings',
where,
orderBy: [
{ column: 'created_at', direction: 'desc' }
],
limit: Math.min(Number(limit), 100),
offset: Number(offset)
};
// -----------------------------
// PRICE RANGE (handled separately)
// -----------------------------
// JSON executor extension point:
// price range is encoded as a special where clause
if (min_price !== undefined || max_price !== undefined) {
query.whereRange = {
column: 'price',
min: min_price !== undefined ? Number(min_price) : undefined,
max: max_price !== undefined ? Number(max_price) : undefined
};
}
return query;
}

View File

@ -0,0 +1,53 @@
export default function listingResponseTranslator(dbResp, respSchema) {
// -----------------------------
// Normalize DB response
// -----------------------------
const rows = Array.isArray(dbResp)
? dbResp
: dbResp
? [dbResp]
: [];
// -----------------------------
// Pick only fields defined in response schema
// -----------------------------
const allowedFields = respSchema
? Object.keys(respSchema)
: null;
const normalizeRow = (row) => {
if (!row || typeof row !== 'object') return null;
// If schema exists, filter fields
if (allowedFields) {
const filtered = {};
for (const key of allowedFields) {
if (row[key] !== undefined) {
filtered[key] = row[key];
}
}
return filtered;
}
// Fallback (should not happen normally)
return row;
};
// -----------------------------
// Map rows
// -----------------------------
const result = rows
.map(normalizeRow)
.filter(Boolean);
// -----------------------------
// Return shape
// -----------------------------
// If schema represents a single object, return object
if (!Array.isArray(respSchema)) {
return result.length === 1 ? result[0] : result;
}
return result;
}

View File

@ -0,0 +1,6 @@
{
"listingId": {
"is_mandatory_field": true,
"type": "number"
}
}

View File

@ -0,0 +1,46 @@
{
"seller_id": {
"is_mandatory_field": true,
"type": "string"
},
"animal_id": {
"is_mandatory_field": true,
"type": "string"
},
"title": {
"is_mandatory_field": true,
"type": "string"
},
"price": {
"is_mandatory_field": false,
"type": "number"
},
"currency": {
"is_mandatory_field": false,
"type": "string"
},
"is_negotiable": {
"is_mandatory_field": false,
"type": "boolean"
},
"listing_type": {
"is_mandatory_field": false,
"type": "string"
},
"status": {
"is_mandatory_field": false,
"type": "string"
},
"thumbnail_url": {
"is_mandatory_field": false,
"type": "string"
}
}

View File

@ -0,0 +1,10 @@
{
"phone": {
"is_mandatory_field": true,
"type": "string"
},
"email": {
"is_mandatory_field": false,
"type": "string"
}
}

View File

@ -0,0 +1,15 @@
{
"id": {
"is_mandatory_field": true,
"type": "number"
},
"name": {
"is_mandatory_field": true,
"type": "string"
},
"contacts": {
"is_mandatory_field": false,
"type": "JSONArray",
"json_dtype": "user_contacts.json"
}
}

View File

@ -0,0 +1,15 @@
{
"id": {
"is_mandatory_field": true,
"type": "number"
},
"name": {
"is_mandatory_field": true,
"type": "string"
},
"contacts": {
"is_mandatory_field": false,
"type": "JSONArray",
"json_dtype": "user_contacts.json"
}
}

2
package-lock.json generated
View File

@ -346,6 +346,7 @@
"resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz", "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz",
"integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==", "integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"cluster-key-slot": "1.1.2", "cluster-key-slot": "1.1.2",
"generic-pool": "3.9.0", "generic-pool": "3.9.0",
@ -2203,6 +2204,7 @@
"version": "8.16.3", "version": "8.16.3",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz",
"integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==",
"peer": true,
"dependencies": { "dependencies": {
"pg-connection-string": "^2.9.1", "pg-connection-string": "^2.9.1",
"pg-pool": "^3.10.1", "pg-pool": "^3.10.1",

70
routes/listings.route.js Normal file
View File

@ -0,0 +1,70 @@
import ApiBuilder from '../core/builders/ApiBuilder.js';
import UrlBuilder from '../core/builders/UrlBuilder.js';
import SchemaBuilder from '../core/builders/SchemaBuilder.js';
import SchemaFileLoader from '../core/utils/SchemaFileLoader.js';
import listingRequestTranslator from '../logic/listings/listingRequestTranslator.js';
import listingResponseTranslator from '../logic/listings/listingResponseTranslator.js';
export default function registerListingsApi(baseUrl, common, dbClient) {
// GET /listings (feed)
new ApiBuilder(common)
.method('get')
.url(new UrlBuilder(baseUrl))
.schema(
new SchemaBuilder()
.responseSchema(
SchemaFileLoader.load('./models/listings/listings_response.json')
)
.requestTranslator(listingRequestTranslator)
.responseTranslator(listingResponseTranslator)
.defaultError({ error: 'INTERNAL_ERROR' })
)
.sync()
.build(dbClient);
// GET /listings/:id
new ApiBuilder(common)
.method('get')
.url(new UrlBuilder(baseUrl).withPathParams(SchemaFileLoader.load('./models/listings/listing_request_path_param.json')))
.schema(
new SchemaBuilder()
.responseSchema(
SchemaFileLoader.load('./models/listings/listings_response.json')
)
.requestTranslator(listingRequestTranslator)
.responseTranslator(listingResponseTranslator)
.defaultError({ error: 'NOT_FOUND' })
)
.sync()
.build(dbClient);
// POST /listings (create)
new ApiBuilder(common)
.method('post')
.url(new UrlBuilder(baseUrl))
.schema(
new SchemaBuilder()
.requestSchema(
SchemaFileLoader.load('./models/listings/listings_response.json')
)
.responseSchema(
SchemaFileLoader.load('./models/listings/listings_response.json')
)
.requestTranslator(req => ({
type: 'transaction',
handler: async (trx) => {
const [listing] = await trx('listings')
.insert(req.body)
.returning('*');
return listing;
}
}))
.responseTranslator(result => result)
.defaultError({ error: 'CREATE_FAILED' })
)
.sync()
.build(dbClient);
};

View File

@ -4,7 +4,7 @@ import { fileURLToPath } from 'url';
import { dirname, join } from 'path'; import { dirname, join } from 'path';
import http from "http"; import http from "http";
import dotenv from "dotenv"; import dotenv from "dotenv";
import listingRoutes from "./routes/listingRoutes.js"; // import listingRoutes from "./routes/listingRoutes.js";
import locationRoutes from "./routes/locationRoutes.js"; import locationRoutes from "./routes/locationRoutes.js";
import chatRoutes from "./routes/chatRoutes.js"; import chatRoutes from "./routes/chatRoutes.js";
import userRoutes from "./routes/userRoutes.js"; import userRoutes from "./routes/userRoutes.js";
@ -15,6 +15,13 @@ import { startExpirationJob } from "./jobs/expirationJob.js";
import requestContext from "./middleware/requestContext.js"; import requestContext from "./middleware/requestContext.js";
import { auditLoggerMiddleware } from "./services/auditLogger.js"; import { auditLoggerMiddleware } from "./services/auditLogger.js";
import registerListingsApi from "./routes/listings.route.js";
import CommonApiBuilder from "./core/builders/CommonApiBuilder.js";
// import JwtAuthMiddleware from "./core/middleware/JwtAuthMiddleware.js";
// import RateLimiterMiddleware from "./core/middleware/RateLimiterMiddleware.js";
import dbClient from "./core/db/client.js";
// Load environment variables // Load environment variables
dotenv.config(); dotenv.config();
@ -44,8 +51,17 @@ app.use(express.static(join(__dirname, 'public')));
const PORT = process.env.PORT || 3200; const PORT = process.env.PORT || 3200;
const common = new CommonApiBuilder(app)
// .use(new JwtAuthMiddleware())
// .use(new RateLimiterMiddleware(rateLimiterRead))
// .use(new CoarseAuthMiddleware(['USER', 'ADMIN']))
.build();
// register ALL listing APIs
registerListingsApi("/listings", common, dbClient);
// Add routes here // Add routes here
app.use("/listings", listingRoutes);
app.use("/locations", locationRoutes); app.use("/locations", locationRoutes);
app.use("/chat", chatRoutes); app.use("/chat", chatRoutes);
app.use("/users", userRoutes); app.use("/users", userRoutes);