fastify-auto-sqlapi 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/AGENTS_BACKEND.md +6 -1
  2. package/AGENTS_FRONTEND.md +7 -5
  3. package/README.md +40 -1
  4. package/dist/lib/engine/bulk/bulk-upsert.d.ts.map +1 -1
  5. package/dist/lib/engine/bulk/bulk-upsert.js +3 -1
  6. package/dist/lib/engine/rest/insert.d.ts.map +1 -1
  7. package/dist/lib/engine/rest/insert.js +3 -1
  8. package/dist/lib/engine/rest/update.d.ts.map +1 -1
  9. package/dist/lib/engine/rest/update.js +5 -2
  10. package/dist/lib/engine/search/search.d.ts.map +1 -1
  11. package/dist/lib/engine/search/search.js +45 -15
  12. package/dist/lib/schema/bulk-delete.d.ts +1 -1
  13. package/dist/lib/schema/bulk-delete.d.ts.map +1 -1
  14. package/dist/lib/schema/bulk-delete.js +2 -2
  15. package/dist/lib/schema/bulk-upsert.d.ts +1 -1
  16. package/dist/lib/schema/bulk-upsert.d.ts.map +1 -1
  17. package/dist/lib/schema/bulk-upsert.js +5 -3
  18. package/dist/lib/schema/helpers.d.ts +2 -0
  19. package/dist/lib/schema/helpers.d.ts.map +1 -1
  20. package/dist/lib/schema/helpers.js +3 -2
  21. package/dist/lib/schema/insert.d.ts.map +1 -1
  22. package/dist/lib/schema/insert.js +4 -1
  23. package/dist/lib/schema/update.d.ts.map +1 -1
  24. package/dist/lib/schema/update.js +4 -1
  25. package/dist/lib/sql-api.d.ts +2 -0
  26. package/dist/lib/sql-api.d.ts.map +1 -1
  27. package/dist/lib/sql-api.js +1 -0
  28. package/dist/lib/tenant.d.ts +22 -0
  29. package/dist/lib/tenant.d.ts.map +1 -1
  30. package/dist/lib/tenant.js +0 -0
  31. package/dist/routes/auto/bulk-delete.routes.d.ts.map +1 -1
  32. package/dist/routes/auto/bulk-delete.routes.js +2 -1
  33. package/dist/routes/auto/bulk-upsert.routes.d.ts.map +1 -1
  34. package/dist/routes/auto/bulk-upsert.routes.js +2 -1
  35. package/dist/routes/auto/search.routes.d.ts.map +1 -1
  36. package/dist/routes/auto/search.routes.js +8 -0
  37. package/dist/types/plugin.d.ts +16 -0
  38. package/dist/types/plugin.d.ts.map +1 -1
  39. package/dist/types/plugin.js +4 -1
  40. package/dist/types/search.d.ts +7 -0
  41. package/dist/types/search.d.ts.map +1 -1
  42. package/package.json +2 -2
package/AGENTS_BACKEND.md CHANGED
@@ -126,6 +126,8 @@ await app.register(fastifyAutoSqlApi, {
126
126
  prefix: '/auto', // optional — standard Fastify prefix
127
127
  onRequests: [], // optional — global auth hooks (run on every route)
128
128
  getTenantId: (request) => request.user?.organizationId ?? null, // optional — multi-tenant
129
+ maxItemsPerPage: 1000, // optional — cap on search page size / no-paginator LIMIT (default 1000)
130
+ maxBulkItems: 1000, // optional — cap on bulk array length (default 1000)
129
131
  debug: true, // optional — log all SQL queries and params to console
130
132
  });
131
133
 
@@ -698,6 +700,7 @@ Behavior:
698
700
  - **Insert**: auto-injects `organization_id` if single tenant; validates if already present (403 if mismatch); 400 if multi-tenant without explicit value
699
701
  - **Update**: strips `organization_id` from SET (can't change tenant), adds to WHERE condition
700
702
  - **Bulk upsert**: auto-injects on all records
703
+ - **Upsert conflict guard** (`upsertMap` set): before the `ON CONFLICT DO UPDATE`, a probe rejects (403) any incoming conflict key that matches a row owned by another tenant — an upsert cannot overwrite or re-assign a foreign tenant's row.
701
704
 
702
705
  ### Indirect tenant (via JOIN to parent table)
703
706
 
@@ -719,7 +722,7 @@ const TableOrder = defineTable({
719
722
  Behavior:
720
723
  - **Read**: INNER JOIN to parent table + WHERE on parent's tenant column
721
724
  - **Insert/Bulk upsert**: validates FK references belong to tenant (1 batch query)
722
- - **Update**: pre-check via SELECT with INNER JOIN (404 if not found)
725
+ - **Update**: pre-check via SELECT with INNER JOIN (404 if not found). If the payload also changes the through-FK (`localField`), the new value is re-validated against the tenant — moving a record into another tenant's scope is rejected (403).
723
726
  - **Delete**: subquery `DELETE WHERE pk IN (SELECT ... INNER JOIN ... WHERE tenant IN (...))`
724
727
 
725
728
  ### Multi-tenant users
@@ -760,6 +763,8 @@ Tables without `tenantScope` are unaffected — no filtering regardless of `getT
760
763
  - **`validateBulk` replaces `validate` in bulk**: when `validateBulk` is defined, it is called once with all items and per-item `validate` is skipped. This allows optimized batch queries instead of N individual checks. When only `validate` is defined, it runs per-item as fallback.
761
764
  - **All hooks and validators receive camelCase records**: `validate` and the whole hook matrix (`beforeInsert`/`afterInsert`, `beforeUpdate`/`afterUpdate`, `beforeDelete`/`afterDelete`, `beforeBulkDelete`/`afterBulkDelete`) get records keyed by schema field names (camelCase). Mutations propagate to the SQL (plugin converts to DB column format via `colMap` after the hook). The engine internally uses `snakecaseRecord(..., schema)` after user mutations to map field names to actual DB columns.
762
765
  - **Filters validation**: TypeBox schemas use `additionalProperties: false`. By default Fastify strips unknown fields silently. For 400 errors on unknown filters: `Fastify({ ajv: { customOptions: { removeAdditional: false } } })`.
766
+ - **Write-body whitelist**: insert/update/bulk-upsert bodies (`main` + secondaries items) are `additionalProperties: false`. Unknown properties are rejected with 400 — only columns in the generated Schema (as narrowed by `schemaOverrides`/`excludeFromCreation`) can be written, so no mass assignment of unexposed columns. Trim the Schema to keep sensitive columns unwritable.
767
+ - **Request limits**: `maxItemsPerPage` (default 1000) caps the search page size and is applied as the `LIMIT` even when no paginator is sent (so an empty-body search can't dump a whole table); over-limit `itemsPerPage` → 400. `maxBulkItems` (default 1000) caps the bulk array length via schema `maxItems` → 400. Programmatic `sqlApi.*` calls are uncapped.
763
768
  - **Tenant filtering**: when `tenantScope` is set on a table and `getTenantId` is provided in plugin options, all CRUD operations are automatically scoped to the tenant. `getTenantId` returning `null`/`undefined` = admin (no filter). Returning an array = multi-tenant user (IN clause).
764
769
 
765
770
  ---
@@ -35,10 +35,10 @@ Without prefix, routes are at root: `/search/customer`, `/rest/customer/:id`, et
35
35
  ```
36
36
 
37
37
  - `orderBy` — SQL ORDER BY clause (default: `defaultOrder` from table config). Supports dotted notations for joins, see below.
38
- - `page` + `itemsPerPage` — pagination (when either is present, response includes `pagination` object; `page` defaults to 1, `itemsPerPage` defaults to 500)
38
+ - `page` + `itemsPerPage` — pagination (when either is present, response includes `pagination` object; `page` defaults to 1, `itemsPerPage` defaults to 500). `itemsPerPage` is capped server-side at `maxItemsPerPage` (default 1000); sending a larger value returns 400.
39
39
  - `computeMin`, `computeMax`, `computeSum`, `computeAvg` — aggregate a column, returned in `pagination.computed`
40
40
 
41
- ### Body (all fields optional, empty `{}` returns all records)
41
+ ### Body (all fields optional, empty `{}` returns up to `maxItemsPerPage` records)
42
42
 
43
43
  ```json
44
44
  {
@@ -350,7 +350,7 @@ Add `page` or `itemsPerPage` to the query string to enable pagination:
350
350
  ?itemsPerPage=10 # page defaults to 1
351
351
  ```
352
352
 
353
- When either is present, the response includes a `pagination` object with total count, total pages, and the paginator values. `page` defaults to 1, `itemsPerPage` defaults to 500.
353
+ When either is present, the response includes a `pagination` object with total count, total pages, and the paginator values. `page` defaults to 1, `itemsPerPage` defaults to 500. `itemsPerPage` is capped at `maxItemsPerPage` (default 1000) — a larger value returns 400. Even without pagination, a search returns at most `maxItemsPerPage` rows.
354
354
 
355
355
  ### Computed values (aggregations on main table)
356
356
 
@@ -503,6 +503,8 @@ Insert or update multiple records in a single request.
503
503
 
504
504
  All main records are inserted/upserted in a single SQL query. Secondaries and deletions are processed per-item. `secondaries`/`deletions` keys are **aliases**.
505
505
 
506
+ The array is capped at `maxBulkItems` (default 1000); a longer array returns 400 — chunk client-side if you have more.
507
+
506
508
  **Response (200):** Array of `{ main (PK-only), secondaries? (PK-only), deletions? }`.
507
509
 
508
510
  ---
@@ -517,7 +519,7 @@ Delete multiple records by PK.
517
519
  [{ "id": 1 }, { "id": 2 }, { "id": 3 }]
518
520
  ```
519
521
 
520
- Each object must contain the PK field. All deletions execute as a single `DELETE WHERE pk IN (...)`.
522
+ Each object must contain the PK field. All deletions execute as a single `DELETE WHERE pk IN (...)`. The array is capped at `maxBulkItems` (default 1000) — a longer array returns 400.
521
523
 
522
524
  **Response (200):** Array of PK-only objects.
523
525
 
@@ -531,7 +533,7 @@ Each object must contain the PK field. All deletions execute as a single `DELETE
531
533
 
532
534
  Insert, update, and bulk-upsert endpoints may return **structured field-level validation errors** (400). Two sources:
533
535
 
534
- 1. **Schema validation** (TypeBox/Ajv) — type mismatches, missing required fields
536
+ 1. **Schema validation** (TypeBox/Ajv) — type mismatches, missing required fields, and **unknown properties** (write bodies reject any field not in the table Schema — send only real columns, don't include extra keys)
535
537
  2. **Custom validation** (`validate` / `validateBulk` on the table) — business rules, cross-entity checks
536
538
 
537
539
  Both return the same response format:
package/README.md CHANGED
@@ -238,6 +238,8 @@ await app.register(fastifyAutoSqlApi, {
238
238
  swagger: true, // Optional — enable Swagger UI (or pass SwaggerOptions)
239
239
  onRequests: [authMiddleware], // Optional — global hooks applied to every route
240
240
  getTenantId: (req) => id, // Optional — multi-tenant function
241
+ maxItemsPerPage: 1000, // Optional — cap on search page size (default: 1000)
242
+ maxBulkItems: 1000, // Optional — cap on bulk array length (default: 1000)
241
243
  debug: true, // Optional — log all SQL queries and params
242
244
  });
243
245
  ```
@@ -250,6 +252,8 @@ await app.register(fastifyAutoSqlApi, {
250
252
  | `swagger` | `boolean \| SwaggerOptions` | No | Enable Swagger UI |
251
253
  | `onRequests` | `Function[]` | No | Global auth/middleware hooks |
252
254
  | `getTenantId` | `(req) => id \| null` | No | Tenant resolver for multi-tenant |
255
+ | `maxItemsPerPage` | `number` | No | Max search page size, and the row `LIMIT` applied when no paginator is sent (default: `1000`) |
256
+ | `maxBulkItems` | `number` | No | Max number of items accepted by the bulk endpoints (default: `1000`) |
253
257
  | `debug` | `boolean` | No | Log all SQL queries to console |
254
258
 
255
259
  For MySQL/MariaDB, register `mysql2/promise` pool instead of `@fastify/postgres` and pass `dialect: 'mysql'` or `'mariadb'`.
@@ -294,6 +298,28 @@ are always available to your own code.
294
298
  **3. Multi-tenancy** — `getTenantId` + `tenantScope` filter every query by tenant (see
295
299
  [Multi-Tenant](#multi-tenant)).
296
300
 
301
+ ### Request limits
302
+
303
+ Two hard caps bound how much work a single request can trigger (defense against accidental or
304
+ malicious resource exhaustion — relevant precisely because the API is open by default):
305
+
306
+ - **Search result size** — `itemsPerPage` above `maxItemsPerPage` (default `1000`) is rejected
307
+ with `400`. When a search omits pagination entirely, the same value is applied as a `LIMIT`, so
308
+ an empty-body `POST /search/:table` can never dump an entire table.
309
+ - **Bulk array size** — the `PUT /bulk/:table` and `POST /bulk/:table/delete` bodies reject arrays
310
+ longer than `maxBulkItems` (default `1000`) at schema validation.
311
+
312
+ Both are configurable in the plugin options. The programmatic `fastify.sqlApi.*` methods are not
313
+ capped — they run your own trusted code.
314
+
315
+ ### Write whitelist
316
+
317
+ Insert/update/bulk-upsert bodies reject **unknown properties** (`additionalProperties: false`):
318
+ only columns present in the generated Schema (as narrowed by `schemaOverrides` /
319
+ `excludeFromCreation`) can be written. An unexpected field is a `400`, never a silently-written
320
+ column — the Schema is the write whitelist. Trim the generated Schema (or use `excludeFromCreation`)
321
+ to keep sensitive columns out of it.
322
+
297
323
  ## API Reference
298
324
 
299
325
  ### POST /search/{table}
@@ -358,6 +384,8 @@ Search with filters, advanced conditions, pagination, ordering, joins, and aggre
358
384
 
359
385
  **Querystring** (optional): `orderBy`, `page`, `itemsPerPage`, `computeMin`, `computeMax`, `computeSum`, `computeAvg`
360
386
 
387
+ > `itemsPerPage` is capped at `maxItemsPerPage` (default `1000`); a larger value returns `400`. A search with no `page`/`itemsPerPage` returns at most that many rows (no full-table dumps). See [Request limits](#request-limits).
388
+
361
389
  **Response:**
362
390
 
363
391
  ```json
@@ -501,6 +529,17 @@ defineTable({
501
529
 
502
530
  Tables without `tenantScope` are unaffected.
503
531
 
532
+ ### Isolation guarantees on writes
533
+
534
+ Tenant isolation is enforced on write paths, not just reads:
535
+
536
+ - **Upsert conflicts are ownership-checked.** On a tenant-scoped table with an `upsertMap`, an
537
+ upsert (`POST /rest/:table` or `PUT /bulk/:table`) whose conflict key matches a row owned by
538
+ another tenant is rejected with `403` — it cannot overwrite or re-assign that row.
539
+ - **The tenant link cannot be changed to another tenant.** For direct scopes the tenant column is
540
+ stripped from update payloads. For indirect scopes, changing the through-FK (`localField`) is
541
+ allowed only to a value the caller owns; otherwise the update returns `403`.
542
+
504
543
  ## Swagger
505
544
 
506
545
  Enabled by passing `swagger: true` (or a config object) to the plugin options. Requires `@fastify/swagger` and `@fastify/swagger-ui` as peer dependencies.
@@ -610,7 +649,7 @@ After registering the plugin, `app.sqlApi` is decorated on the Fastify instance
610
649
  ## Requirements
611
650
 
612
651
  - Node.js >= 18
613
- - Fastify >= 4 (peer dependency)
652
+ - Fastify >= 5 (peer dependency)
614
653
  - One of:
615
654
  - **PostgreSQL** + `pg` + `@fastify/postgres`
616
655
  - **MySQL** / **MariaDB** + `mysql2`
@@ -1 +1 @@
1
- {"version":3,"file":"bulk-upsert.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/bulk/bulk-upsert.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,gBAAgB,EAChB,gBAAgB,EAEjB,MAAM,mBAAmB,CAAC;AAE3B,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAkF5F"}
1
+ {"version":3,"file":"bulk-upsert.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/bulk/bulk-upsert.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,gBAAgB,EAChB,gBAAgB,EAEjB,MAAM,mBAAmB,CAAC;AAE3B,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAoF5F"}
@@ -1,6 +1,6 @@
1
1
  import { camelcaseObject } from '../../naming.js';
2
2
  import { processSecondaries, processDeletions, prepareInsertRecord } from '../write-helpers.js';
3
- import { enforceTenantOnWrites } from '../../tenant.js';
3
+ import { enforceTenantOnWrites, assertTenantOwnsConflicts } from '../../tenant.js';
4
4
  import { runBulkValidation } from '../validate.js';
5
5
  import { primaryAsCols } from '../../../types.js';
6
6
  export async function bulkUpsertEngine(params) {
@@ -26,6 +26,8 @@ export async function bulkUpsertEngine(params) {
26
26
  let pkRows;
27
27
  if (upsertKeys) {
28
28
  const conflictCols = upsertKeys.map((k) => schema.col(k));
29
+ // Tenant isolation: a conflicting upsert must not overwrite rows owned by another tenant.
30
+ await assertTenantOwnsConflicts(db, tenant, schema.tableName, conflictCols, preparedMains);
29
31
  pkRows = await db.bulkInsertOrUpdate(schema.tableName, preparedMains, conflictCols, pkCol);
30
32
  }
31
33
  else {
@@ -1 +1 @@
1
- {"version":3,"file":"insert.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/insert.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,YAAY,EACZ,YAAY,EAEb,MAAM,mBAAmB,CAAC;AAE3B,wBAAsB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CA+D9E"}
1
+ {"version":3,"file":"insert.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/insert.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,YAAY,EACZ,YAAY,EAEb,MAAM,mBAAmB,CAAC;AAE3B,wBAAsB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAiE9E"}
@@ -1,6 +1,6 @@
1
1
  import { camelcaseObject } from '../../naming.js';
2
2
  import { processSecondaries, prepareInsertRecord } from '../write-helpers.js';
3
- import { enforceTenantOnWrites } from '../../tenant.js';
3
+ import { enforceTenantOnWrites, assertTenantOwnsConflicts } from '../../tenant.js';
4
4
  import { primaryAsCols } from '../../../types.js';
5
5
  export async function insertEngine(params) {
6
6
  const { db, tableConf, dbTables, request, record, secondaries, tenant } = params;
@@ -20,6 +20,8 @@ export async function insertEngine(params) {
20
20
  const upsertKeys = tableConf.upsertMap?.get(schema);
21
21
  if (upsertKeys) {
22
22
  const conflictCols = upsertKeys.map((k) => schema.col(k));
23
+ // Tenant isolation: a conflicting upsert must not overwrite a row owned by another tenant.
24
+ await assertTenantOwnsConflicts(tx, tenant, schema.tableName, conflictCols, [mainRecord]);
23
25
  pkResult = await tx.insertOrUpdate(schema.tableName, mainRecord, conflictCols, pkCol);
24
26
  }
25
27
  else {
@@ -1 +1 @@
1
- {"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/update.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EACV,YAAY,EACZ,YAAY,EAEb,MAAM,mBAAmB,CAAC;AAE3B,wBAAsB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAqH9E"}
1
+ {"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/update.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EACV,YAAY,EACZ,YAAY,EAEb,MAAM,mBAAmB,CAAC;AAE3B,wBAAsB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAwH9E"}
@@ -1,6 +1,6 @@
1
1
  import { snakecaseRecord } from '../../naming.js';
2
2
  import { processSecondaries, processDeletions } from '../write-helpers.js';
3
- import { stripTenantColumn, assertTenantOwnership, buildTenantUpdateExtra, } from '../../tenant.js';
3
+ import { stripTenantColumn, assertTenantOwnership, buildTenantUpdateExtra, enforceTenantFKOnUpdate, } from '../../tenant.js';
4
4
  import { runValidation } from '../validate.js';
5
5
  import { httpError } from '../../errors.js';
6
6
  export async function updateEngine(params) {
@@ -29,9 +29,12 @@ export async function updateEngine(params) {
29
29
  const updateFields = { ...snaked };
30
30
  for (const c of pkCols)
31
31
  delete updateFields[c];
32
- // 6. Strip tenant column (user cannot change tenant of an existing record)
32
+ // 6. Strip tenant column (user cannot change tenant of an existing record).
33
+ // For indirect scopes the tenant link is the through-FK (localField): it may be changed,
34
+ // but only to another value the caller owns — re-validate the new FK against the tenant.
33
35
  if (tenant) {
34
36
  stripTenantColumn(updateFields, tenant.scope);
37
+ await enforceTenantFKOnUpdate(db, tenant, updateFields);
35
38
  }
36
39
  // Steps 7-9 are atomic: a failure in secondaries or deletions rolls back the main
37
40
  // update too. Degrades to non-transactional when the adapter has no connect().
@@ -1 +1 @@
1
- {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/search/search.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,QAAQ,EACR,YAAY,EACZ,YAAY,EAcb,MAAM,mBAAmB,CAAC;AAmiC3B,wBAAsB,YAAY,CAChC,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,YAAY,CAAC,CAoIvB"}
1
+ {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/search/search.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,QAAQ,EACR,YAAY,EACZ,YAAY,EAcb,MAAM,mBAAmB,CAAC;AA4jC3B,wBAAsB,YAAY,CAChC,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,YAAY,CAAC,CA2IvB"}
@@ -171,6 +171,28 @@ function buildAggOrderExpr(db, dbTables, tableConf, alias, fn, field, joinGroup,
171
171
  const expr = `COALESCE((SELECT ${aggExpr(aggFn, qualifiedCol)} FROM ${refs.joinTable} WHERE ${refs.joinTable}.${refs.fkCol} = ${refs.mainTable}.${refs.mainCol}${filterWhere}), 0)`;
172
172
  return { expr, values: filterVals };
173
173
  }
174
+ /**
175
+ * Pre-scan `orderBy` for the joinLeft aliases referenced in 2-parti notation (`<alias>.<field>`).
176
+ * Needed BEFORE building the LEFT JOIN clauses (which must know their aliases) while the actual
177
+ * orderBy SQL — with its parameter placeholders — is only baked later, once the LEFT JOIN value
178
+ * count is known. Validates each alias against `allowedReadJoins` (throws 400), mirroring
179
+ * `validateOrderBy`; 3-parti aggregation entries are ignored (they use joinGroup, not a LEFT JOIN).
180
+ */
181
+ function collectOrderByLeftAliases(orderBy, tableConf) {
182
+ const aliases = new Set();
183
+ for (const part of orderBy.split(',')) {
184
+ const trimmed = part.trim();
185
+ if (/^(\w+)\.(\w+)\.(\w+)(?:\s+(ASC|DESC))?$/i.test(trimmed))
186
+ continue; // 3-parti aggregation
187
+ const m = trimmed.match(/^(\w+)\.(\w+)(?:\s+(ASC|DESC))?$/i);
188
+ if (m) {
189
+ const alias = m[1];
190
+ requireJoin(tableConf, alias, true);
191
+ aliases.add(alias);
192
+ }
193
+ }
194
+ return aliases;
195
+ }
174
196
  function validateOrderBy(orderBy, tableConf, db, dbTables, joinGroup, startIdx) {
175
197
  const parts = orderBy.split(',');
176
198
  const outParts = [];
@@ -281,12 +303,14 @@ function buildLeftJoinClauses(db, dbTables, tableConf, aliasesNeedingJoin, joinL
281
303
  return { joinClauses, whereExtras, values };
282
304
  }
283
305
  // ─── Main query execution ───────────────────────────────────
284
- async function executeMainQuery(db, tableConf, where, values, orderBy, paginator, extraJoins = [], selectComputed) {
306
+ async function executeMainQuery(db, tableConf, where, values, orderBy, paginator, extraJoins = [], selectComputed, maxRows) {
285
307
  const tableName = tableConf.Schema.tableName;
286
308
  const order = orderBy || tableConf.defaultOrder || primaryAsString(tableConf.primary);
309
+ // With a paginator, the page size governs the LIMIT. Without one, apply `maxRows` (if set) so an
310
+ // unbounded search cannot dump the whole table; a plain integer is a safe, non-injectable LIMIT.
287
311
  const limit = paginator
288
312
  ? `${paginator.itemsPerPage} OFFSET ${(paginator.page - 1) * paginator.itemsPerPage}`
289
- : null;
313
+ : (maxRows != null ? String(maxRows) : null);
290
314
  // Optional computed projections — bound values, if any, are NOT yet supported
291
315
  // here (would require placeholder-aware composition with WHERE values).
292
316
  let columns;
@@ -794,27 +818,23 @@ export async function searchEngine(dbTables, params) {
794
818
  ({ where, values } = appendAggConditions(where, values, conditions, db, dbTables, tableConf, joinGroup));
795
819
  }
796
820
  }
797
- // orderBy parsing — collects 2-parti aliases that need LEFT JOIN on the main query
798
- let safeOrderBy;
799
- let orderByValues = [];
800
- let orderByLeftAliases = new Set();
801
- if (orderBy) {
802
- const obResult = validateOrderBy(orderBy, tableConf, db, dbTables, joinGroup, values.length + 1);
803
- safeOrderBy = obResult.sql;
804
- orderByValues = obResult.values;
805
- orderByLeftAliases = obResult.leftJoinAliases;
806
- }
807
821
  // Determine which joinLeft aliases need a LEFT JOIN on the main query:
808
822
  // - any alias used in 2-parti orderBy
809
823
  // - any alias in joinLeft body that has filters or conditions on the parent
810
- const aliasesNeedingLeftJoin = new Set(orderByLeftAliases);
824
+ const aliasesNeedingLeftJoin = new Set();
825
+ if (orderBy) {
826
+ for (const a of collectOrderByLeftAliases(orderBy, tableConf))
827
+ aliasesNeedingLeftJoin.add(a);
828
+ }
811
829
  if (joinLeft) {
812
830
  for (const [alias, ref] of Object.entries(joinLeft)) {
813
831
  if (ref?.filters || ref?.conditions?.length)
814
832
  aliasesNeedingLeftJoin.add(alias);
815
833
  }
816
834
  }
817
- // Build LEFT JOIN clauses + extra WHERE for filtered parents
835
+ // Build LEFT JOIN clauses + extra WHERE for filtered parents. Their placeholders start right
836
+ // after the WHERE values; the same clause SQL is reused by the pagination COUNT/compute queries,
837
+ // whose value array is [...WHERE, ...leftJoin] — so this base MUST stay `values.length + 1`.
818
838
  const extraJoinClauses = [...tenantJoins];
819
839
  let leftJoinValues = [];
820
840
  if (aliasesNeedingLeftJoin.size > 0) {
@@ -824,11 +844,21 @@ export async function searchEngine(dbTables, params) {
824
844
  for (const w of lj.whereExtras)
825
845
  where += ` AND ${w}`;
826
846
  }
847
+ // orderBy is parsed AFTER the LEFT JOIN values so aggregation-orderBy placeholders start past
848
+ // them (main query binds [...WHERE, ...leftJoin, ...orderBy]). Parsing it earlier with base
849
+ // `values.length + 1` misbinds when BOTH an aggregation orderBy and a filtered joinLeft exist.
850
+ let safeOrderBy;
851
+ let orderByValues = [];
852
+ if (orderBy) {
853
+ const obResult = validateOrderBy(orderBy, tableConf, db, dbTables, joinGroup, values.length + leftJoinValues.length + 1);
854
+ safeOrderBy = obResult.sql;
855
+ orderByValues = obResult.values;
856
+ }
827
857
  // Bind WHERE values + LEFT JOIN values + orderBy aggregation values for main query.
828
858
  // Pagination COUNT/compute include WHERE + LEFT JOIN values (no orderBy).
829
859
  const whereAndJoinValues = [...values, ...leftJoinValues];
830
860
  const mainValues = [...whereAndJoinValues, ...orderByValues];
831
- const main = await executeMainQuery(db, tableConf, where, mainValues, safeOrderBy, paginator, extraJoinClauses, params.selectComputed);
861
+ const main = await executeMainQuery(db, tableConf, where, mainValues, safeOrderBy, paginator, extraJoinClauses, params.selectComputed, params.maxRows);
832
862
  let pagination;
833
863
  if (paginator) {
834
864
  pagination = await buildPagination(db, tableConf, where, whereAndJoinValues, paginator, extraJoinClauses, computeMin, computeMax, computeSum, computeAvg);
@@ -1,5 +1,5 @@
1
1
  import type { DbTables } from '../../types.js';
2
- export declare function BulkDeleteTableBody(dbTables: DbTables, tableName: string): import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
2
+ export declare function BulkDeleteTableBody(dbTables: DbTables, tableName: string, maxItems?: number): import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
3
3
  [x: string]: any;
4
4
  }>>;
5
5
  export declare function BulkDeleteTableResponse(dbTables: DbTables, tableName: string): import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
@@ -1 +1 @@
1
- {"version":3,"file":"bulk-delete.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/bulk-delete.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE/C,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM;;IAMxE;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM;;;;IAU5E"}
1
+ {"version":3,"file":"bulk-delete.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/bulk-delete.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE/C,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM;;IAS3F;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM;;;;IAU5E"}
@@ -1,10 +1,10 @@
1
1
  import { Type } from '@sinclair/typebox';
2
2
  import { primaryAsString } from '../../types.js';
3
- export function BulkDeleteTableBody(dbTables, tableName) {
3
+ export function BulkDeleteTableBody(dbTables, tableName, maxItems) {
4
4
  const tableConf = dbTables[tableName];
5
5
  const pkField = primaryAsString(tableConf.primary);
6
6
  const pkType = tableConf.Schema.fields[pkField];
7
- return Type.Array(Type.Object({ [pkField]: pkType }));
7
+ return Type.Array(Type.Object({ [pkField]: pkType }, { additionalProperties: false }), maxItems != null ? { maxItems } : {});
8
8
  }
9
9
  export function BulkDeleteTableResponse(dbTables, tableName) {
10
10
  const tableConf = dbTables[tableName];
@@ -1,5 +1,5 @@
1
1
  import { type TSchema } from '@sinclair/typebox';
2
2
  import type { DbTables } from '../../types.js';
3
- export declare function BulkUpsertTableBody(dbTables: DbTables, tableName: string): import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<Record<string, TSchema>>>;
3
+ export declare function BulkUpsertTableBody(dbTables: DbTables, tableName: string, maxItems?: number): import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<Record<string, TSchema>>>;
4
4
  export declare function BulkUpsertTableResponse(dbTables: DbTables, tableName: string): import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<Record<string, TSchema>>>;
5
5
  //# sourceMappingURL=bulk-upsert.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"bulk-upsert.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/bulk-upsert.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAEvD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAS/C,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,oGAgBxE;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,oGAa5E"}
1
+ {"version":3,"file":"bulk-upsert.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/bulk-upsert.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAEvD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAS/C,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,oGAoB3F;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,oGAa5E"}
@@ -1,18 +1,20 @@
1
1
  import { Type } from '@sinclair/typebox';
2
2
  import { primaryAsString } from '../../types.js';
3
3
  import { pkSchema, applySchemaOverrides, attachWriteJoinSections, writeJoinBodyFields, writeJoinResponseFields, } from './helpers.js';
4
- export function BulkUpsertTableBody(dbTables, tableName) {
4
+ export function BulkUpsertTableBody(dbTables, tableName, maxItems) {
5
5
  const tableConf = dbTables[tableName];
6
6
  const schema = tableConf.Schema;
7
- const mainSchema = Type.Partial(Type.Object(applySchemaOverrides({ ...schema.fields }, tableConf)));
7
+ // additionalProperties:false makes the schema the real write whitelist (no mass assignment).
8
+ const mainSchema = Type.Partial(Type.Object(applySchemaOverrides({ ...schema.fields }, tableConf), { additionalProperties: false }));
8
9
  const itemProperties = {
9
10
  main: mainSchema,
10
11
  };
11
12
  attachWriteJoinSections(itemProperties, tableConf, dbTables, {
12
13
  withDeletions: true,
13
14
  secondaryFields: writeJoinBodyFields,
15
+ strictItems: true,
14
16
  });
15
- return Type.Array(Type.Object(itemProperties));
17
+ return Type.Array(Type.Object(itemProperties), maxItems != null ? { maxItems } : {});
16
18
  }
17
19
  export function BulkUpsertTableResponse(dbTables, tableName) {
18
20
  const tableConf = dbTables[tableName];
@@ -24,6 +24,7 @@ type JoinFieldsBuilder = (join: JoinDefinition, secondaryConf: ITable | undefine
24
24
  */
25
25
  export declare function buildJoinAliasMap(tableConf: ITable, dbTables: DbTables, build: JoinFieldsBuilder, options?: {
26
26
  itemsPartial?: boolean;
27
+ strictItems?: boolean;
27
28
  }): Record<string, TSchema>;
28
29
  /**
29
30
  * Attach `secondaries` (and optionally `deletions`) sections to the given target object,
@@ -37,6 +38,7 @@ export declare function buildJoinAliasMap(tableConf: ITable, dbTables: DbTables,
37
38
  export declare function attachWriteJoinSections(target: Record<string, TSchema>, tableConf: ITable, dbTables: DbTables, options: {
38
39
  withDeletions: boolean;
39
40
  secondaryFields: JoinFieldsBuilder;
41
+ strictItems?: boolean;
40
42
  }): void;
41
43
  /** JoinFieldsBuilder for body schemas: includes all join fields with FK + excluded made Optional. */
42
44
  export declare const writeJoinBodyFields: JoinFieldsBuilder;
@@ -1 +1 @@
1
- {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,OAAO,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAGzF;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE,MAAM,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAQ3H;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,gBAAgB,EAC5B,SAAS,EAAE,MAAM,EACjB,kBAAkB,EAAE,MAAM,GAAG,SAAS,GACrC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAgBzB;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,SAAS,EAAE,MAAM,GAChB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CASzB;AAED,KAAK,iBAAiB,GAAG,CACvB,IAAI,EAAE,cAAc,EACpB,aAAa,EAAE,MAAM,GAAG,SAAS,KAC9B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE7B;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,QAAQ,EAClB,KAAK,EAAE,iBAAiB,EACxB,OAAO,GAAE;IAAE,YAAY,CAAC,EAAE,OAAO,CAAA;CAAO,GACvC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CASzB;AAED;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE;IAAE,aAAa,EAAE,OAAO,CAAC;IAAC,eAAe,EAAE,iBAAiB,CAAA;CAAE,GACtE,IAAI,CAmBN;AAED,qGAAqG;AACrG,eAAO,MAAM,mBAAmB,EAAE,iBACmB,CAAC;AAEtD,uDAAuD;AACvD,eAAO,MAAM,uBAAuB,EAAE,iBACG,CAAC"}
1
+ {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,OAAO,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAGzF;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE,MAAM,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAQ3H;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,gBAAgB,EAC5B,SAAS,EAAE,MAAM,EACjB,kBAAkB,EAAE,MAAM,GAAG,SAAS,GACrC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAgBzB;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,SAAS,EAAE,MAAM,GAChB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CASzB;AAED,KAAK,iBAAiB,GAAG,CACvB,IAAI,EAAE,cAAc,EACpB,aAAa,EAAE,MAAM,GAAG,SAAS,KAC9B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE7B;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,QAAQ,EAClB,KAAK,EAAE,iBAAiB,EACxB,OAAO,GAAE;IAAE,YAAY,CAAC,EAAE,OAAO,CAAC;IAAC,WAAW,CAAC,EAAE,OAAO,CAAA;CAAO,GAC9D,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAUzB;AAED;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE;IAAE,aAAa,EAAE,OAAO,CAAC;IAAC,eAAe,EAAE,iBAAiB,CAAC;IAAC,WAAW,CAAC,EAAE,OAAO,CAAA;CAAE,GAC7F,IAAI,CAmBN;AAED,qGAAqG;AACrG,eAAO,MAAM,mBAAmB,EAAE,iBACmB,CAAC;AAEtD,uDAAuD;AACvD,eAAO,MAAM,uBAAuB,EAAE,iBACG,CAAC"}
@@ -56,9 +56,10 @@ export function buildJoinAliasMap(tableConf, dbTables, build, options = {}) {
56
56
  const out = {};
57
57
  if (!tableConf.allowedWriteJoins?.length)
58
58
  return out;
59
+ const objOpts = options.strictItems ? { additionalProperties: false } : {};
59
60
  for (const j of tableConf.allowedWriteJoins) {
60
61
  const sc = findSecondaryTableConf(dbTables, j.joinSchema.tableName);
61
- const item = Type.Object(build(j, sc));
62
+ const item = Type.Object(build(j, sc), objOpts);
62
63
  out[j.alias] = Type.Array(options.itemsPartial ? Type.Partial(item) : item);
63
64
  }
64
65
  return out;
@@ -75,7 +76,7 @@ export function buildJoinAliasMap(tableConf, dbTables, build, options = {}) {
75
76
  export function attachWriteJoinSections(target, tableConf, dbTables, options) {
76
77
  if (!tableConf.allowedWriteJoins?.length)
77
78
  return;
78
- target.secondaries = Type.Optional(Type.Partial(Type.Object(buildJoinAliasMap(tableConf, dbTables, options.secondaryFields))));
79
+ target.secondaries = Type.Optional(Type.Partial(Type.Object(buildJoinAliasMap(tableConf, dbTables, options.secondaryFields, { strictItems: options.strictItems }))));
79
80
  if (options.withDeletions) {
80
81
  target.deletions = Type.Optional(Type.Partial(Type.Object(buildJoinAliasMap(tableConf, dbTables, ({ joinSchema }) => joinSchema.fields, { itemsPartial: true }))));
81
82
  }
@@ -1 +1 @@
1
- {"version":3,"file":"insert.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/insert.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,OAAO,EAAgB,MAAM,mBAAmB,CAAC;AAErE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAS/C,wBAAgB,eAAe,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAsB9E;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAalF"}
1
+ {"version":3,"file":"insert.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/insert.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,OAAO,EAAgB,MAAM,mBAAmB,CAAC;AAErE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAS/C,wBAAgB,eAAe,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAyB9E;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAalF"}
@@ -12,11 +12,14 @@ export function InsertTableBody(dbTables, tableName) {
12
12
  }
13
13
  }
14
14
  const bodyProperties = {
15
- main: Type.Object(mainFields),
15
+ // additionalProperties:false makes the schema the real write whitelist — unknown keys are
16
+ // rejected (400) instead of silently reaching the INSERT as columns (mass assignment).
17
+ main: Type.Object(mainFields, { additionalProperties: false }),
16
18
  };
17
19
  attachWriteJoinSections(bodyProperties, tableConf, dbTables, {
18
20
  withDeletions: false,
19
21
  secondaryFields: writeJoinBodyFields,
22
+ strictItems: true,
20
23
  });
21
24
  return Type.Object(bodyProperties);
22
25
  }
@@ -1 +1 @@
1
- {"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/update.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,OAAO,EAAgB,MAAM,mBAAmB,CAAC;AAErE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAS/C,wBAAgB,eAAe,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAqB9E;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAalF"}
1
+ {"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/update.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,OAAO,EAAgB,MAAM,mBAAmB,CAAC;AAErE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAS/C,wBAAgB,eAAe,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAwB9E;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAalF"}
@@ -11,11 +11,14 @@ export function UpdateTableBody(dbTables, tableName) {
11
11
  mainFields[key] = key === primaryAsString(tableConf.primary) ? value : Type.Optional(value);
12
12
  }
13
13
  const bodyProperties = {
14
- main: Type.Object(mainFields),
14
+ // additionalProperties:false makes the schema the real write whitelist — unknown keys are
15
+ // rejected (400) instead of silently reaching the UPDATE as columns (mass assignment).
16
+ main: Type.Object(mainFields, { additionalProperties: false }),
15
17
  };
16
18
  attachWriteJoinSections(bodyProperties, tableConf, dbTables, {
17
19
  withDeletions: true,
18
20
  secondaryFields: writeJoinBodyFields,
21
+ strictItems: true,
19
22
  });
20
23
  return Type.Object(bodyProperties);
21
24
  }
@@ -16,6 +16,8 @@ export interface SqlApiSearchParams {
16
16
  computeSum?: string;
17
17
  computeAvg?: string;
18
18
  selectComputed?: string[];
19
+ /** Row cap for the main query when no paginator is provided (see SearchParams.maxRows). */
20
+ maxRows?: number;
19
21
  }
20
22
  export interface SqlApiInsertParams {
21
23
  record: Record<string, unknown>;
@@ -1 +1 @@
1
- {"version":3,"file":"sql-api.d.ts","sourceRoot":"","sources":["../../src/lib/sql-api.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"sql-api.d.ts","sourceRoot":"","sources":["../../src/lib/sql-api.ts"],"names":[],"mappings":"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"}
@@ -61,6 +61,7 @@ export class SqlApi {
61
61
  computeSum: params.computeSum,
62
62
  computeAvg: params.computeAvg,
63
63
  selectComputed: params.selectComputed,
64
+ maxRows: params.maxRows,
64
65
  tenant,
65
66
  });
66
67
  }
@@ -15,6 +15,28 @@ export declare function buildTenantDeleteWhere(db: QueryClient, tableName: strin
15
15
  where: string;
16
16
  values: unknown[];
17
17
  };
18
+ /**
19
+ * Guard the conflict target of an UPSERT (INSERT … ON CONFLICT / ON DUPLICATE KEY) on a
20
+ * tenant-scoped table. Without this, an upsert whose conflict key matches a row owned by
21
+ * ANOTHER tenant would silently UPDATE (and, for direct scopes, re-assign) that row, because
22
+ * the conflict is matched by the unique key alone — the tenant is never part of the ON CONFLICT
23
+ * predicate. This pre-checks that no existing row matching the incoming conflict keys belongs to
24
+ * a different tenant, throwing 403 if one does. No-op when `tenant` is undefined.
25
+ *
26
+ * `conflictCols` and the keys of each record are DB column names (snake_case). Records missing
27
+ * any conflict column are skipped (they can only INSERT, never hit a conflict).
28
+ */
29
+ export declare function assertTenantOwnsConflicts(db: QueryClient, tenant: TenantContext | undefined, tableName: string, conflictCols: string[], records: Record<string, unknown>[]): Promise<void>;
30
+ /**
31
+ * For an UPDATE on an indirect-tenant table: the record's ownership is verified against its
32
+ * CURRENT through-FK (`assertTenantOwnership`), but if the payload also changes that FK
33
+ * (`through.localField`), the NEW value must be re-validated — otherwise a caller could move
34
+ * their own record into another tenant's scope. Validates the new FK value against the tenant,
35
+ * throwing 403 on mismatch. No-op for direct/no-tenant or when the FK is not being changed.
36
+ *
37
+ * `updateFields` keys are DB column names (snake_case), matching `through.localField`.
38
+ */
39
+ export declare function enforceTenantFKOnUpdate(db: QueryClient, tenant: TenantContext | undefined, updateFields: Record<string, unknown>): Promise<void>;
18
40
  /**
19
41
  * Mutates `fields` in-place: removes the tenant column from a SET payload (direct scopes only).
20
42
  * No-op for indirect or no scope.
@@ -1 +1 @@
1
- {"version":3,"file":"tenant.d.ts","sourceRoot":"","sources":["../../src/lib/tenant.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,EAAE,gBAAgB,EAAE,KAAK,cAAc,EAAE,KAAK,yBAAyB,EAAE,MAAM,wBAAwB,CAAC;AAE/G,OAAO,KAAK,EACV,MAAM,EACN,mBAAmB,EACnB,QAAQ,EACR,WAAW,EACX,mBAAmB,EACnB,aAAa,EACd,MAAM,aAAa,CAAC;AAMrB,wBAAsB,aAAa,CACjC,OAAO,EAAE,mBAAmB,EAC5B,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CASpC;AAED,wBAAgB,oBAAoB,CAClC,EAAE,EAAE,WAAW,EACf,KAAK,EAAE,WAAW,EAClB,SAAS,EAAE,QAAQ,EAAE,GACpB,gBAAgB,CAclB;AAED,wBAAgB,eAAe,CAC7B,EAAE,EAAE,WAAW,EACf,KAAK,EAAE,mBAAmB,EAC1B,aAAa,EAAE,MAAM,GACpB,MAAM,CAMR;AAED,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,KAAK,EAAE,WAAW,EAClB,SAAS,EAAE,QAAQ,EAAE,GACpB,IAAI,CAiBN;AAED,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,WAAW,EACf,KAAK,EAAE,mBAAmB,EAC1B,SAAS,EAAE,QAAQ,EAAE,EACrB,QAAQ,EAAE,OAAO,EAAE,GAClB,OAAO,CAAC,IAAI,CAAC,CAyBf;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,WAAW,EACf,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,yBAAyB,GAAG,cAAc,EAAE,EACrD,MAAM,EAAE,aAAa,GACpB;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,EAAE,CAAA;CAAE,CAsBtC;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,KAAK,EAAE,WAAW,GACjB,IAAI,CAGN;AAED;;;;;GAKG;AACH,wBAAsB,qBAAqB,CACzC,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,aAAa,GAAG,SAAS,EACjC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GACjC,OAAO,CAAC,IAAI,CAAC,CAQf;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,aAAa,GAAG,SAAS,GAChC,gBAAgB,GAAG,SAAS,CAM9B;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,CACzC,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,aAAa,GAAG,SAAS,EACjC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,EACxB,OAAO,EAAE,cAAc,GAAG,cAAc,EAAE,GACzC,OAAO,CAAC,IAAI,CAAC,CAYf;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CACvC,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,aAAa,GAAG,SAAS,EACjC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,cAAc,EAAE,GACpB,OAAO,CAAC,IAAI,CAAC,CAgBf"}
1
+ {"version":3,"file":"tenant.d.ts","sourceRoot":"","sources":["../../src/lib/tenant.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,EAAE,gBAAgB,EAAE,KAAK,cAAc,EAAE,KAAK,yBAAyB,EAAE,MAAM,wBAAwB,CAAC;AAE/G,OAAO,KAAK,EACV,MAAM,EACN,mBAAmB,EACnB,QAAQ,EACR,WAAW,EACX,mBAAmB,EACnB,aAAa,EACd,MAAM,aAAa,CAAC;AAMrB,wBAAsB,aAAa,CACjC,OAAO,EAAE,mBAAmB,EAC5B,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CASpC;AAED,wBAAgB,oBAAoB,CAClC,EAAE,EAAE,WAAW,EACf,KAAK,EAAE,WAAW,EAClB,SAAS,EAAE,QAAQ,EAAE,GACpB,gBAAgB,CAclB;AAED,wBAAgB,eAAe,CAC7B,EAAE,EAAE,WAAW,EACf,KAAK,EAAE,mBAAmB,EAC1B,aAAa,EAAE,MAAM,GACpB,MAAM,CAMR;AAED,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,KAAK,EAAE,WAAW,EAClB,SAAS,EAAE,QAAQ,EAAE,GACpB,IAAI,CAiBN;AAED,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,WAAW,EACf,KAAK,EAAE,mBAAmB,EAC1B,SAAS,EAAE,QAAQ,EAAE,EACrB,QAAQ,EAAE,OAAO,EAAE,GAClB,OAAO,CAAC,IAAI,CAAC,CAyBf;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,WAAW,EACf,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,yBAAyB,GAAG,cAAc,EAAE,EACrD,MAAM,EAAE,aAAa,GACpB;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,EAAE,CAAA;CAAE,CAsBtC;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,yBAAyB,CAC7C,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,aAAa,GAAG,SAAS,EACjC,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,EAAE,EACtB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GACjC,OAAO,CAAC,IAAI,CAAC,CA2Df;AAED;;;;;;;;GAQG;AACH,wBAAsB,uBAAuB,CAC3C,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,aAAa,GAAG,SAAS,EACjC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACpC,OAAO,CAAC,IAAI,CAAC,CAOf;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,KAAK,EAAE,WAAW,GACjB,IAAI,CAGN;AAED;;;;;GAKG;AACH,wBAAsB,qBAAqB,CACzC,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,aAAa,GAAG,SAAS,EACjC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GACjC,OAAO,CAAC,IAAI,CAAC,CAQf;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,aAAa,GAAG,SAAS,GAChC,gBAAgB,GAAG,SAAS,CAM9B;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,CACzC,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,aAAa,GAAG,SAAS,EACjC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,EACxB,OAAO,EAAE,cAAc,GAAG,cAAc,EAAE,GACzC,OAAO,CAAC,IAAI,CAAC,CAYf;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CACvC,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,aAAa,GAAG,SAAS,EACjC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,cAAc,EAAE,GACpB,OAAO,CAAC,IAAI,CAAC,CAgBf"}
Binary file
@@ -1 +1 @@
1
- {"version":3,"file":"bulk-delete.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/bulk-delete.routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAI/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,wBAA8B,gBAAgB,CAC5C,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAkBf"}
1
+ {"version":3,"file":"bulk-delete.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/bulk-delete.routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAK/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,wBAA8B,gBAAgB,CAC5C,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAkBf"}
@@ -1,6 +1,7 @@
1
1
  import { BulkDeleteTableBody, BulkDeleteTableResponse } from '../../lib/schema/bulk-delete.js';
2
2
  import { primaryAsString } from '../../types.js';
3
3
  import { registerForAllTables } from './route-helpers.js';
4
+ import { DEFAULT_MAX_BULK_ITEMS } from '../../types.js';
4
5
  export default async function bulkDeleteRoutes(fastify, options) {
5
6
  await registerForAllTables(fastify, options, {
6
7
  operation: 'bulkDelete',
@@ -8,7 +9,7 @@ export default async function bulkDeleteRoutes(fastify, options) {
8
9
  url: (tc) => `/bulk/${tc.Schema.tableName}/delete`,
9
10
  successStatus: 200,
10
11
  schemas: (db, table) => ({
11
- body: BulkDeleteTableBody(db, table),
12
+ body: BulkDeleteTableBody(db, table, options.maxBulkItems ?? DEFAULT_MAX_BULK_ITEMS),
12
13
  response: BulkDeleteTableResponse(db, table),
13
14
  }),
14
15
  summary: 'Bulk delete',
@@ -1 +1 @@
1
- {"version":3,"file":"bulk-upsert.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/bulk-upsert.routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAG/C,OAAO,KAAK,EAAE,mBAAmB,EAAkB,MAAM,gBAAgB,CAAC;AAE1E,wBAA8B,gBAAgB,CAC5C,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAiBf"}
1
+ {"version":3,"file":"bulk-upsert.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/bulk-upsert.routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAI/C,OAAO,KAAK,EAAE,mBAAmB,EAAkB,MAAM,gBAAgB,CAAC;AAE1E,wBAA8B,gBAAgB,CAC5C,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAiBf"}
@@ -1,5 +1,6 @@
1
1
  import { BulkUpsertTableBody, BulkUpsertTableResponse } from '../../lib/schema/bulk-upsert.js';
2
2
  import { registerForAllTables, buildWriteDescription } from './route-helpers.js';
3
+ import { DEFAULT_MAX_BULK_ITEMS } from '../../types.js';
3
4
  export default async function bulkUpsertRoutes(fastify, options) {
4
5
  await registerForAllTables(fastify, options, {
5
6
  operation: 'bulkUpsert',
@@ -7,7 +8,7 @@ export default async function bulkUpsertRoutes(fastify, options) {
7
8
  url: (tc) => `/bulk/${tc.Schema.tableName}`,
8
9
  successStatus: 200,
9
10
  schemas: (db, table) => ({
10
- body: BulkUpsertTableBody(db, table),
11
+ body: BulkUpsertTableBody(db, table, options.maxBulkItems ?? DEFAULT_MAX_BULK_ITEMS),
11
12
  response: BulkUpsertTableResponse(db, table),
12
13
  }),
13
14
  summary: 'Bulk upsert',
@@ -1 +1 @@
1
- {"version":3,"file":"search.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/search.routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAG/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,wBAA8B,YAAY,CACxC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAkDf"}
1
+ {"version":3,"file":"search.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/search.routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAK/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,wBAA8B,YAAY,CACxC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAyDf"}
@@ -1,5 +1,7 @@
1
1
  import { SearchTableBodyPost, SearchTableQueryString, SearchTableResponse } from '../../lib/schema/search.js';
2
2
  import { registerForAllTables } from './route-helpers.js';
3
+ import { httpError } from '../../lib/errors.js';
4
+ import { DEFAULT_MAX_ITEMS_PER_PAGE } from '../../types.js';
3
5
  export default async function searchRoutes(fastify, options) {
4
6
  await registerForAllTables(fastify, options, {
5
7
  operation: 'search',
@@ -28,6 +30,11 @@ export default async function searchRoutes(fastify, options) {
28
30
  const body = request.body;
29
31
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
30
32
  const query = request.query;
33
+ // DoS guard: bound the page size and the no-paginator LIMIT.
34
+ const maxRows = options.maxItemsPerPage ?? DEFAULT_MAX_ITEMS_PER_PAGE;
35
+ if (query.itemsPerPage != null && query.itemsPerPage > maxRows) {
36
+ throw httpError(400, `itemsPerPage exceeds the maximum of ${maxRows}`);
37
+ }
31
38
  const result = await fastify.sqlApi.search(tableName, {
32
39
  filters: body.filters,
33
40
  conditions: body.conditions,
@@ -44,6 +51,7 @@ export default async function searchRoutes(fastify, options) {
44
51
  computeMax: query.computeMax,
45
52
  computeSum: query.computeSum,
46
53
  computeAvg: query.computeAvg,
54
+ maxRows,
47
55
  }, request);
48
56
  return { table: tc.Schema.tableName, ...result };
49
57
  },
@@ -16,5 +16,21 @@ export interface SqlApiPluginOptions {
16
16
  dialect?: DialectName;
17
17
  getTenantId?: (request: FastifyRequest) => TenantId | TenantId[] | null | undefined | Promise<TenantId | TenantId[] | null | undefined>;
18
18
  debug?: boolean;
19
+ /**
20
+ * Hard cap on the search page size (the `itemsPerPage` query param), and the row LIMIT applied
21
+ * even when no paginator is supplied — so an unbounded `POST /search/:table` cannot dump an
22
+ * entire table. Requests exceeding it get a 400. Defaults to {@link DEFAULT_MAX_ITEMS_PER_PAGE}.
23
+ */
24
+ maxItemsPerPage?: number;
25
+ /**
26
+ * Hard cap on the number of items accepted by the bulk endpoints (`PUT /bulk/:table`,
27
+ * `POST /bulk/:table/delete`). Larger arrays are rejected at schema validation (400).
28
+ * Defaults to {@link DEFAULT_MAX_BULK_ITEMS}.
29
+ */
30
+ maxBulkItems?: number;
19
31
  }
32
+ /** Default row cap for search (page size and no-paginator LIMIT). Override via `maxItemsPerPage`. */
33
+ export declare const DEFAULT_MAX_ITEMS_PER_PAGE = 1000;
34
+ /** Default cap on bulk endpoint array length. Override via `maxBulkItems`. */
35
+ export declare const DEFAULT_MAX_BULK_ITEMS = 1000;
20
36
  //# sourceMappingURL=plugin.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../src/types/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC5D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,QAAQ,CAAC;IACnB,UAAU,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,GAAG,YAAY,CAAC,CAAC,EAAE,CAAC;IAChG,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,GAAG,cAAc,CAAC;IACnC,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,QAAQ,GAAG,QAAQ,EAAE,GAAG,IAAI,GAAG,SAAS,GAC/E,OAAO,CAAC,QAAQ,GAAG,QAAQ,EAAE,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;IACtD,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../src/types/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC5D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,QAAQ,CAAC;IACnB,UAAU,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,GAAG,YAAY,CAAC,CAAC,EAAE,CAAC;IAChG,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,GAAG,cAAc,CAAC;IACnC,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,QAAQ,GAAG,QAAQ,EAAE,GAAG,IAAI,GAAG,SAAS,GAC/E,OAAO,CAAC,QAAQ,GAAG,QAAQ,EAAE,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;IACtD,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,qGAAqG;AACrG,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAE/C,8EAA8E;AAC9E,eAAO,MAAM,sBAAsB,OAAO,CAAC"}
@@ -1 +1,4 @@
1
- export {};
1
+ /** Default row cap for search (page size and no-paginator LIMIT). Override via `maxItemsPerPage`. */
2
+ export const DEFAULT_MAX_ITEMS_PER_PAGE = 1000;
3
+ /** Default cap on bulk endpoint array length. Override via `maxBulkItems`. */
4
+ export const DEFAULT_MAX_BULK_ITEMS = 1000;
@@ -28,6 +28,13 @@ export interface SearchParams {
28
28
  */
29
29
  selectComputed?: string[];
30
30
  tenant?: TenantContext;
31
+ /**
32
+ * Row cap applied to the main query when no `paginator` is supplied, bounding an otherwise
33
+ * unbounded "fetch everything" search. Ignored when a paginator is present (the page size
34
+ * governs the LIMIT). Left undefined by programmatic callers (trusted); the HTTP search route
35
+ * sets it from `maxItemsPerPage`.
36
+ */
37
+ maxRows?: number;
31
38
  }
32
39
  export interface PaginationResult {
33
40
  total: number;
@@ -1 +1 @@
1
- {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/types/search.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AACvD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AACnF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEvD,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,WAAW,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,UAAU,CAAC,EAAE,eAAe,EAAE,CAAC;IAC/B,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IAC9C,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAC7C,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAC5C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,MAAM,CAAC,EAAE,aAAa,CAAC;CACxB;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACnD,SAAS,EAAE,SAAS,CAAC;CACtB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IAChC,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;IACzD,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;IACrD,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACpD,UAAU,CAAC,EAAE,gBAAgB,CAAC;CAC/B"}
1
+ {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/types/search.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AACvD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AACnF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEvD,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,WAAW,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,UAAU,CAAC,EAAE,eAAe,EAAE,CAAC;IAC/B,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IAC9C,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAC7C,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAC5C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACnD,SAAS,EAAE,SAAS,CAAC;CACtB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IAChC,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;IACzD,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;IACrD,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACpD,UAAU,CAAC,EAAE,gBAAgB,CAAC;CAC/B"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fastify-auto-sqlapi",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Fastify plugin for automatic CRUD API generation from PostgreSQL, MySQL and MariaDB tables",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -82,7 +82,7 @@
82
82
  "@fastify/postgres": ">=5.0.0",
83
83
  "@fastify/swagger": ">=9.0.0",
84
84
  "@fastify/swagger-ui": ">=5.0.0",
85
- "fastify": "5.8.5",
85
+ "fastify": ">=5.0.0",
86
86
  "mysql2": ">=3.0.0",
87
87
  "pg": ">=8.0.0"
88
88
  },