diff --git a/.cursor/rules/erp-backend-core.mdc b/.cursor/rules/erp-backend-core.mdc index 1634fdf..3a04d25 100644 --- a/.cursor/rules/erp-backend-core.mdc +++ b/.cursor/rules/erp-backend-core.mdc @@ -34,6 +34,6 @@ Stack: Node.js 20+ · Express (CommonJS) · PostgreSQL 15+ · Prisma · Joi · W ## API -- Routes under `/api/v1/`; pagination `?page=1&limit=20` (max 100); dates ISO 8601 UTC +- Routes under `/api/v1/`; pagination `?page=1&limit=20` (no upper cap on limit); dates ISO 8601 UTC - RBAC actions: `view`, `create`, `edit`, `delete`, `approve`, `export` - Reference: `BACKEND_SETUP.md`, task checklist: `BACKEND_TASKS.md` diff --git a/BACKEND_TASKS.md b/BACKEND_TASKS.md index badf481..33b66ab 100644 --- a/BACKEND_TASKS.md +++ b/BACKEND_TASKS.md @@ -407,7 +407,7 @@ Each sub-master supports: `GET /` (list), `GET /:id`, `POST /`, `PUT /:id`, `DEL ## 7. API Conventions - Versioning: `/api/v1/...` -- Pagination: `?page=1&limit=20` (cap limit at 100); response `meta: { page, limit, total }` +- Pagination: `?page=1&limit=20` (limit accepted as sent by FE, min 1, default 20); response `meta: { page, limit, total }` - Filtering: `?status=...&search=...` per module - Sorting: `?sort=-created_at` (`-` = descending) - IDs: `BigInt` serialized as strings in JSON diff --git a/src/docs/audit-logs-routes.yaml b/src/docs/audit-logs-routes.yaml index 14a0681..a14217c 100644 --- a/src/docs/audit-logs-routes.yaml +++ b/src/docs/audit-logs-routes.yaml @@ -113,7 +113,7 @@ paths: schema: { type: integer, default: 1 } - in: query name: limit - schema: { type: integer, default: 20, maximum: 100 } + schema: { type: integer, default: 20, minimum: 1 } - in: query name: table_name schema: { type: string } diff --git a/src/docs/completed-routes.yaml b/src/docs/completed-routes.yaml index 9349cb1..5074136 100644 --- a/src/docs/completed-routes.yaml +++ b/src/docs/completed-routes.yaml @@ -690,7 +690,7 @@ paths: schema: { type: integer, default: 1 } - in: query name: limit - schema: { type: integer, default: 20, maximum: 100 } + schema: { type: integer, default: 20, minimum: 1 } - in: query name: search schema: { type: string } @@ -759,7 +759,7 @@ paths: schema: { type: integer, default: 1 } - in: query name: limit - schema: { type: integer, default: 20, maximum: 100 } + schema: { type: integer, default: 20, minimum: 1 } - in: query name: search schema: { type: string } @@ -833,7 +833,7 @@ paths: schema: { type: integer, default: 1 } - in: query name: limit - schema: { type: integer, default: 20, maximum: 100 } + schema: { type: integer, default: 20, minimum: 1 } - in: query name: search schema: { type: string } @@ -922,7 +922,7 @@ paths: schema: { type: integer, default: 1 } - in: query name: limit - schema: { type: integer, default: 20, maximum: 100 } + schema: { type: integer, default: 20, minimum: 1 } - in: query name: search schema: { type: string } @@ -991,7 +991,7 @@ paths: schema: { type: integer, default: 1 } - in: query name: limit - schema: { type: integer, default: 20, maximum: 100 } + schema: { type: integer, default: 20, minimum: 1 } - in: query name: search schema: { type: string } @@ -1060,7 +1060,7 @@ paths: schema: { type: integer, default: 1 } - in: query name: limit - schema: { type: integer, default: 20, maximum: 100 } + schema: { type: integer, default: 20, minimum: 1 } - in: query name: search schema: { type: string, description: Search by code or description } @@ -1129,7 +1129,7 @@ paths: schema: { type: integer, default: 1 } - in: query name: limit - schema: { type: integer, default: 20, maximum: 100 } + schema: { type: integer, default: 20, minimum: 1 } - in: query name: search schema: { type: string } @@ -1223,7 +1223,7 @@ paths: schema: { type: integer, default: 1 } - in: query name: limit - schema: { type: integer, default: 20, maximum: 100 } + schema: { type: integer, default: 20, minimum: 1 } - in: query name: type schema: { type: string, enum: [PO, INVOICE] } @@ -1295,7 +1295,7 @@ paths: schema: { type: integer, default: 1 } - in: query name: limit - schema: { type: integer, default: 20, maximum: 100 } + schema: { type: integer, default: 20, minimum: 1 } - in: query name: search schema: { type: string } @@ -1364,7 +1364,7 @@ paths: schema: { type: integer, default: 1 } - in: query name: limit - schema: { type: integer, default: 20, maximum: 100 } + schema: { type: integer, default: 20, minimum: 1 } - in: query name: search schema: { type: string } @@ -1433,7 +1433,7 @@ paths: schema: { type: integer, default: 1 } - in: query name: limit - schema: { type: integer, default: 20, maximum: 100 } + schema: { type: integer, default: 20, minimum: 1 } - in: query name: search schema: { type: string } @@ -1513,7 +1513,7 @@ paths: schema: { type: integer, default: 1 } - in: query name: limit - schema: { type: integer, default: 20, maximum: 100 } + schema: { type: integer, default: 20, minimum: 1 } - in: query name: search schema: { type: string } @@ -1585,7 +1585,7 @@ paths: schema: { type: integer, default: 1 } - in: query name: limit - schema: { type: integer, default: 20, maximum: 100 } + schema: { type: integer, default: 20, minimum: 1 } - in: query name: search schema: { type: string } @@ -1654,7 +1654,7 @@ paths: schema: { type: integer, default: 1 } - in: query name: limit - schema: { type: integer, default: 20, maximum: 100 } + schema: { type: integer, default: 20, minimum: 1 } - in: query name: search schema: { type: string } @@ -1723,7 +1723,7 @@ paths: schema: { type: integer, default: 1 } - in: query name: limit - schema: { type: integer, default: 20, maximum: 100 } + schema: { type: integer, default: 20, minimum: 1 } - in: query name: search schema: { type: string } @@ -1834,7 +1834,7 @@ paths: schema: { type: integer, default: 1 } - in: query name: limit - schema: { type: integer, default: 20, maximum: 100 } + schema: { type: integer, default: 20, minimum: 1 } - in: query name: search schema: { type: string } @@ -1929,7 +1929,7 @@ paths: schema: { type: integer, default: 1 } - in: query name: limit - schema: { type: integer, default: 20, maximum: 100 } + schema: { type: integer, default: 20, minimum: 1 } - in: query name: search schema: { type: string } diff --git a/src/docs/reports-routes.yaml b/src/docs/reports-routes.yaml index e7c554e..4cb2f06 100644 --- a/src/docs/reports-routes.yaml +++ b/src/docs/reports-routes.yaml @@ -187,7 +187,7 @@ paths: schema: { type: integer, default: 1 } - in: query name: limit - schema: { type: integer, default: 20, maximum: 100 } + schema: { type: integer, default: 20, minimum: 1 } - in: query name: search schema: { type: string } diff --git a/src/docs/vendors-routes.yaml b/src/docs/vendors-routes.yaml index a8f2972..dd78c16 100644 --- a/src/docs/vendors-routes.yaml +++ b/src/docs/vendors-routes.yaml @@ -190,7 +190,7 @@ paths: schema: { type: integer, default: 1 } - in: query name: limit - schema: { type: integer, default: 20, maximum: 100 } + schema: { type: integer, default: 20, minimum: 1 } - in: query name: search schema: { type: string } diff --git a/src/modules/assets/assets.validation.js b/src/modules/assets/assets.validation.js index 026414c..f7700af 100644 --- a/src/modules/assets/assets.validation.js +++ b/src/modules/assets/assets.validation.js @@ -1,4 +1,5 @@ const Joi = require('joi'); +const { pageQuery, limitQuery } = require('../../utils/pagination.validation'); const { ASSET_CONDITIONS, ASSET_STATUSES, @@ -116,8 +117,8 @@ const updateAssetSchema = Joi.object({ }).min(1); const listAssetsQuerySchema = Joi.object({ - page: Joi.number().integer().min(1).default(1), - limit: Joi.number().integer().min(1).max(100).default(20), + page: pageQuery(), + limit: limitQuery(), search: Joi.string().allow('').optional(), status: Joi.string() .valid(...ASSET_STATUSES) @@ -315,8 +316,8 @@ const depreciationCalculateSchema = Joi.object({ }); const myMaintenanceAssetsQuerySchema = Joi.object({ - page: Joi.number().integer().min(1).default(1), - limit: Joi.number().integer().min(1).max(100).default(20), + page: pageQuery(), + limit: limitQuery(), search: Joi.string().allow('').optional(), due_only: Joi.boolean().optional(), status: Joi.string() @@ -334,13 +335,13 @@ const createMaintenanceLogSchema = Joi.object({ }); const listMaintenanceLogsQuerySchema = Joi.object({ - page: Joi.number().integer().min(1).default(1), - limit: Joi.number().integer().min(1).max(100).default(20), + page: pageQuery(), + limit: limitQuery(), }); const expiryAlertsQuerySchema = Joi.object({ - page: Joi.number().integer().min(1).default(1), - limit: Joi.number().integer().min(1).max(100).default(20), + page: pageQuery(), + limit: limitQuery(), days: Joi.number().integer().min(1).max(365).default(90), type: Joi.string() .valid(...ALERT_TYPES) @@ -348,8 +349,8 @@ const expiryAlertsQuerySchema = Joi.object({ }); const serviceAlertsQuerySchema = Joi.object({ - page: Joi.number().integer().min(1).default(1), - limit: Joi.number().integer().min(1).max(100).default(20), + page: pageQuery(), + limit: limitQuery(), status: Joi.string() .valid(...SERVICE_ALERT_STATUSES) .optional(), diff --git a/src/modules/audit-logs/audit-logs.validation.js b/src/modules/audit-logs/audit-logs.validation.js index 2a57cdb..9ad9177 100644 --- a/src/modules/audit-logs/audit-logs.validation.js +++ b/src/modules/audit-logs/audit-logs.validation.js @@ -1,8 +1,9 @@ const Joi = require('joi'); +const { pageQuery, limitQuery } = require('../../utils/pagination.validation'); const listAuditLogsQuerySchema = Joi.object({ - page: Joi.number().integer().min(1).default(1), - limit: Joi.number().integer().min(1).max(100).default(20), + page: pageQuery(), + limit: limitQuery(), table_name: Joi.string().max(100).trim().optional(), record_id: Joi.number().integer().positive().optional(), action: Joi.string().max(50).trim().optional(), diff --git a/src/modules/grn/grn.validation.js b/src/modules/grn/grn.validation.js index 3454597..05d9c4c 100644 --- a/src/modules/grn/grn.validation.js +++ b/src/modules/grn/grn.validation.js @@ -1,4 +1,5 @@ const Joi = require('joi'); +const { pageQuery, limitQuery } = require('../../utils/pagination.validation'); const { GRN_STATUSES } = require('./grn.constants'); const grnItemSchema = Joi.object({ @@ -47,8 +48,8 @@ const updateGrnSchema = Joi.object({ }).min(1); const listGrnQuerySchema = Joi.object({ - page: Joi.number().integer().min(1).default(1), - limit: Joi.number().integer().min(1).max(100).default(20), + page: pageQuery(), + limit: limitQuery(), search: Joi.string().allow('').optional(), status: Joi.string() .valid(...GRN_STATUSES) diff --git a/src/modules/masters/_shared/masters.validation.js b/src/modules/masters/_shared/masters.validation.js index 4787093..26ec82e 100644 --- a/src/modules/masters/_shared/masters.validation.js +++ b/src/modules/masters/_shared/masters.validation.js @@ -1,4 +1,5 @@ const Joi = require('joi'); +const { pageQuery, limitQuery } = require('../../../utils/pagination.validation'); const MASTER_CODE_REGEX = /^[A-Za-z0-9\-_/]+$/; const MASTER_NAME_REGEX = /^[A-Za-z0-9 \-_/&.()]+$/; @@ -63,8 +64,8 @@ const masterName = ({ max = 150, required = false } = {}) => { }; const listQuerySchema = Joi.object({ - page: Joi.number().integer().min(1).default(1), - limit: Joi.number().integer().min(1).max(100).default(20), + page: pageQuery(), + limit: limitQuery(), search: Joi.string().allow('').optional(), is_active: Joi.boolean().optional(), }); diff --git a/src/modules/purchase-orders/purchase-orders.validation.js b/src/modules/purchase-orders/purchase-orders.validation.js index 82a714e..48a67aa 100644 --- a/src/modules/purchase-orders/purchase-orders.validation.js +++ b/src/modules/purchase-orders/purchase-orders.validation.js @@ -1,4 +1,5 @@ const Joi = require('joi'); +const { pageQuery, limitQuery } = require('../../utils/pagination.validation'); const { PO_STATUSES, VENDOR_TYPES } = require('./purchase-orders.constants'); const poItemSchema = Joi.object({ @@ -84,8 +85,8 @@ const amendPurchaseOrderSchema = Joi.object({ }); const listPurchaseOrdersQuerySchema = Joi.object({ - page: Joi.number().integer().min(1).default(1), - limit: Joi.number().integer().min(1).max(100).default(20), + page: pageQuery(), + limit: limitQuery(), search: Joi.string().allow('').optional(), status: Joi.string() .valid(...PO_STATUSES) diff --git a/src/modules/reports/reports.validation.js b/src/modules/reports/reports.validation.js index c636184..d21d189 100644 --- a/src/modules/reports/reports.validation.js +++ b/src/modules/reports/reports.validation.js @@ -1,9 +1,10 @@ const Joi = require('joi'); +const { pageQuery, limitQuery } = require('../../utils/pagination.validation'); const { ASSET_STATUSES } = require('../assets/assets.constants'); const depreciationReportQuerySchema = Joi.object({ - page: Joi.number().integer().min(1).default(1), - limit: Joi.number().integer().min(1).max(100).default(20), + page: pageQuery(), + limit: limitQuery(), search: Joi.string().max(100).trim().allow('').optional(), status: Joi.string() .valid(...ASSET_STATUSES) diff --git a/src/modules/roles/roles.validation.js b/src/modules/roles/roles.validation.js index 2db0676..bada494 100644 --- a/src/modules/roles/roles.validation.js +++ b/src/modules/roles/roles.validation.js @@ -1,4 +1,5 @@ const Joi = require('joi'); +const { pageQuery, limitQuery } = require('../../utils/pagination.validation'); const createRoleSchema = Joi.object({ name: Joi.string().max(100).required(), @@ -38,8 +39,8 @@ const permissionMatrixSchema = Joi.object({ }); const listRolesQuerySchema = Joi.object({ - page: Joi.number().integer().min(1).default(1), - limit: Joi.number().integer().min(1).max(100).default(20), + page: pageQuery(), + limit: limitQuery(), search: Joi.string().allow('').optional(), is_active: Joi.boolean().optional(), }); diff --git a/src/modules/users/users.validation.js b/src/modules/users/users.validation.js index 039fb0c..d5a3dd5 100644 --- a/src/modules/users/users.validation.js +++ b/src/modules/users/users.validation.js @@ -1,4 +1,5 @@ const Joi = require('joi'); +const { pageQuery, limitQuery } = require('../../utils/pagination.validation'); const roleIdsSchema = Joi.array().items(Joi.number().integer().positive()).min(1); @@ -33,8 +34,8 @@ const updateUserSchema = Joi.object({ }).min(1); const listUsersQuerySchema = Joi.object({ - page: Joi.number().integer().min(1).default(1), - limit: Joi.number().integer().min(1).max(100).default(20), + page: pageQuery(), + limit: limitQuery(), search: Joi.string().allow('').optional(), status: Joi.string().valid('active', 'inactive', 'locked').optional(), is_active: Joi.boolean().optional(), diff --git a/src/modules/vendors/vendors.validation.js b/src/modules/vendors/vendors.validation.js index ab3b48e..afb28e3 100644 --- a/src/modules/vendors/vendors.validation.js +++ b/src/modules/vendors/vendors.validation.js @@ -1,4 +1,5 @@ const Joi = require('joi'); +const { pageQuery, limitQuery } = require('../../utils/pagination.validation'); const { GST_TREATMENT_VALUES, SOURCE_OF_SUPPLY_VALUES, @@ -58,8 +59,8 @@ const vendorStatusSchema = Joi.object({ }); const listVendorsQuerySchema = Joi.object({ - page: Joi.number().integer().min(1).default(1), - limit: Joi.number().integer().min(1).max(100).default(20), + page: pageQuery(), + limit: limitQuery(), search: Joi.string().allow('').optional(), status: Joi.string() .valid(...vendorStatuses) diff --git a/src/utils/pagination.js b/src/utils/pagination.js index e33ed44..8c8e305 100644 --- a/src/utils/pagination.js +++ b/src/utils/pagination.js @@ -1,6 +1,7 @@ const getPagination = (query) => { const page = Math.max(Number(query.page) || 1, 1); - const limit = Math.min(Math.max(Number(query.limit) || 20, 1), 100); + // Accept whatever positive limit FE sends (no upper cap). + const limit = Math.max(Number(query.limit) || 20, 1); const skip = (page - 1) * limit; return { page, limit, skip }; }; diff --git a/src/utils/pagination.validation.js b/src/utils/pagination.validation.js new file mode 100644 index 0000000..a9a2334 --- /dev/null +++ b/src/utils/pagination.validation.js @@ -0,0 +1,14 @@ +const Joi = require('joi'); + +/** Empty / missing page → 1. */ +const pageQuery = () => + Joi.number().integer().min(1).empty(['', null]).default(1); + +/** + * Empty / missing limit → 20. + * When FE sends a number, accept it as-is (no upper cap). + */ +const limitQuery = () => + Joi.number().integer().min(1).empty(['', null]).default(20); + +module.exports = { pageQuery, limitQuery };