fastify-auto-sqlapi 0.1.4 → 0.1.6

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.
package/AGENTS_BACKEND.md CHANGED
@@ -184,6 +184,8 @@ await app.register(async (instance) => {
184
184
 
185
185
  **Note**: After registering the plugin, `app.sqlApi` is available for custom routes — see [SqlApi](#sqlapi--programmatic-high-level-api).
186
186
 
187
+ **Note**: Granular route plugins create their own `sqlApi` decorator automatically, so they work standalone (no main plugin required). The decorator is scoped to each route plugin: if you also need `app.sqlApi` in your own routes, register the main plugin.
188
+
187
189
  ---
188
190
 
189
191
  ## defineTable() — Complete Reference
@@ -234,8 +236,12 @@ export const TableCustomer = defineTable({
234
236
  ...exportTableInfo(Schema), // Schema + auto-filter builder
235
237
 
236
238
  // OPTIONAL
237
- defaultOrder: 'name', // ORDER BY default (supports multi: 'name ASC, id DESC')
239
+ defaultOrder: 'name', // ORDER BY default. camelCase fields are mapped to DB
240
+ // columns (e.g. 'squadIndex' -> "squad_index"); supports
241
+ // multi ('name ASC, id DESC'), computed fields, and raw
242
+ // SQL fragments (unknown tokens pass through unchanged)
238
243
  excludeFromCreation: ['id'], // Fields omitted from INSERT — MUST include auto-increment PKs
244
+ readExclude: ['passwordHash'], // Fields hidden from ALL reads (writes unaffected)
239
245
  distinctResults: true, // Use SELECT DISTINCT
240
246
 
241
247
  // JOINS — relations to other tables. Alias defaults to joinSchema.tableName —
@@ -479,12 +485,22 @@ type ComputedFieldFn = (ctx: {
479
485
  db: QueryClient; // dialect-aware (qi, ph, dialectName, dateTrunc, ...)
480
486
  qiCol(field: string, opts?: { qualifier?: string }): string;
481
487
  }) => {
482
- expr: string; // SQL fragment
483
- values: unknown[]; // bound values (see limitations below)
488
+ expr: string; // SQL fragment; mark each bound value with `?`
489
+ values: unknown[]; // one entry per `?` marker in expr
484
490
  type: TSchema; // REQUIRED — used by Swagger and body validation
485
491
  };
486
492
  ```
487
493
 
494
+ **Bound values use `?` markers.** The engine assigns the placeholder positions — never write `$1` or `db.ph(n)` yourself. Use `\?` for a literal question mark (PostgreSQL jsonb operator). A computed whose marker count does not match `values.length` is rejected with a descriptive error, because it would bind values the query never references:
495
+
496
+ ```typescript
497
+ bonus: ({ qiCol }) => ({
498
+ expr: `CASE WHEN ${qiCol('role')} = ? THEN ${qiCol('salary')} ELSE 0 END`,
499
+ values: ['admin'],
500
+ type: Type.Number(),
501
+ }),
502
+ ```
503
+
488
504
  `qiCol(field)` returns a properly-quoted column reference, optionally prefixed by an alias qualifier — the engine passes the alias automatically when the computed is invoked inside a `joinLeft` LEFT JOIN, so the same function works both inline on the main query and as a parent-side column.
489
505
 
490
506
  ### Validation & startup checks
@@ -499,8 +515,9 @@ For `joinLeft` specifically, the computed expr is automatically alias-prefixed b
499
515
 
500
516
  ### Limitations (first round, by design)
501
517
 
502
- - **Bound `values` in computed expressions** are supported only on `filters`/`conditions` (main and `joinMustExist`/`joinMultiple`/`joinGroup`). They are rejected with 400 on `selectComputed`, `computeMin/Max/Sum/Avg`, `orderBy`, and `joinLeft.filters`/`conditions` — those paths require placeholder coordination not done in the first round. Most use cases (JSON extraction, concat, dateTrunc, simple ops) need no placeholders.
503
- - **Computed CAN be used in `joinGroup.aggregations.by`** (just pass the computed name as a string). Bound `values` are not supported in this position (rejected with 400). The existing FK-correlation rule for `orderBy <alias>.<fn>.<field>` still applies: 3-parti aggregation orderBy on a `by` that isn't the correlation FK is rejected — by definition a computed-by produces multiple groups per main row, so this is never valid.
518
+ - **Bound `values` in computed expressions** work anywhere the expression lands in the `WHERE` clause or in `ORDER BY`: `filters` and `conditions` (main, `joinMustExist`, `joinMultiple`, `joinGroup`, `joinLeft`) and `orderBy`. The ConditionBuilder assigns the placeholder positions there, so no coordination is needed from the caller.
519
+ - They are still rejected with 400 on `selectComputed`, `computeMin/Max/Sum/Avg`, `joinGroup.aggregations.by` and `defaultOrder`. Those expressions are emitted in the `SELECT` / `GROUP BY` list, which precedes the `WHERE` values in the parameter order — there is no correct position for them there. Most use cases (JSON extraction, concat, dateTrunc, simple ops) need no bound values at all.
520
+ - **Computed CAN be used in `joinGroup.aggregations.by`** (just pass the computed name as a string). Bound `values` are not supported in this position (rejected with 400 — it is emitted in the `SELECT`/`GROUP BY` list). The existing FK-correlation rule for `orderBy <alias>.<fn>.<field>` still applies: 3-parti aggregation orderBy on a `by` that isn't the correlation FK is rejected — by definition a computed-by produces multiple groups per main row, so this is never valid.
504
521
  - **Computed cannot be used as aggregation function** (`sum`/`min`/`max`/...). The values inside `aggregations.sum: ['<name>']` must be schema field names. To aggregate on a derived expression, declare the derivation as a computed and pass the computed name as the field, BUT only via `computeMin/Max/Sum/Avg` (top-level main aggregates), not the joinGroup ones.
505
522
  - **No chained computed** (a computed referencing another computed). Flat-only.
506
523
  - **Read-only**. Computed fields are not usable in insert/update bodies — the consumer's `expr` is for SELECT/WHERE/ORDER BY, never for writes.
@@ -757,6 +774,7 @@ Tables without `tenantScope` are unaffected — no filtering regardless of `getT
757
774
  - **camelCase in API, any case in DB**: all request/response fields are camelCase. The plugin converts via `col()` and `colMap`. For snake_case DB columns (default), conversion is automatic. For camelCase DB columns (e.g. betterauth), the CLI generates a `colMap` that preserves the original column names — no conversion needed. Manual schemas without `colMap` fall back to `toUnderscore()`.
758
775
  - **All fields Optional in response**: `RETURNING *` may return any subset. Response schemas use `Type.Partial`.
759
776
  - **`excludeFromCreation`**: **IMPORTANT** — auto-increment PKs (e.g. `id` serial/auto_increment) MUST be listed here, otherwise INSERT will try to send them and fail. The CLI auto-detects this and adds it by default. Also useful for `createdAt`/`updatedAt` columns managed by DB defaults or hooks.
777
+ - **`readExclude`**: fields hidden from every read — not projected by search/get, omitted from read response schemas and from this table's default (`*`) join selection when it is a join target. Referencing one from `filters`, `conditions`, `orderBy`, `joinGroup` aggregations or an explicit join `selection` returns 400: hiding a field from output while allowing it to be filtered would leak its value by bisection. **Writes are unaffected** — insert/update/bulk still accept the field, so a column can be writable but never readable (password hash, access token). Primary keys cannot be excluded. Complementary to trimming the Schema, which removes the column from reads *and* writes.
760
778
  - **`upsertMap`**: when present for a schema, INSERT becomes upsert. PostgreSQL: `ON CONFLICT (...) DO UPDATE`. MySQL/MariaDB: `ON DUPLICATE KEY UPDATE`. Applies to both main and secondary tables.
761
779
  - **`schemaOverrides`**: override auto-generated schema fields with stricter TypeBox types (e.g. `{ email: Type.String({ format: 'email' }) }`). Overrides are merged into the body schema for insert, update, and bulk-upsert. The original Schema file is never modified. In updates, overridden fields are still wrapped in Optional (validates only when present). Overrides appear in Swagger.
762
780
  - **Validation receives camelCase**: `validate` receives the original camelCase record (as sent by the client) and secondaries. Field names match the schema definition, with full TypeScript inference (`main.startDate`, not `main.start_date`). It returns `ValidationError[]` — tuples of `[field, code]` or `[field, code, message]`. If any errors are returned, the request is rejected with 400 before hooks or SQL execute.
package/README.md CHANGED
@@ -4,6 +4,16 @@ Automatic CRUD API generation from PostgreSQL, MySQL, and MariaDB tables for [Fa
4
4
 
5
5
  Point it at your database, and get a full REST API with search, advanced conditions, pagination, joins, aggregations, bulk operations, multi-tenant isolation, validation, and hooks.
6
6
 
7
+ ## What LLMs Are Saying
8
+
9
+ > API: 5 new read-only tables — and this is where your plugin really shined.
10
+
11
+ > Yes, it genuinely saved me a lot of work, and the numbers make that obvious: I deleted about 350 lines of hand-written API repository and route code, replacing them with roughly 150 lines of declarative configuration. And it wasn't a one-for-one trade. In return, I gained features I simply didn't have before: complete Swagger documentation, the conditions DSL (previously I only had sorting and fixed filters), pagination with total counts, structured 400 errors, and consistent camelCase conversion.
12
+
13
+ > The moment I thought, "Okay, this is well designed," was when I used computedFields: the case of "attributes stored inside a jsonb column that still need to be sortable and filterable" is exactly the kind of problem that usually forces you to step outside the framework, yet it fit in cleanly. It worked end-to-end with the role-based CASE expression, remained sortable, could be projected with selectComputed, and required zero SQL in the routes.
14
+
15
+ > The AGENTS files deserve a mention: I configured everything without ever opening the source code—CLI schema, defineTable, buildRelation, computed, extraFilters, and the frontend request/response shapes. As "documentation for agents," they genuinely work, and that's more than I can say for most libraries.
16
+
7
17
  ## Features
8
18
 
9
19
  - **Zero boilerplate** — define your tables, get 7 endpoints each
@@ -143,8 +153,9 @@ const TableCustomer = defineTable({
143
153
  ...exportTableInfo(SchemaCustomer),
144
154
 
145
155
  // Ordering & filtering
146
- defaultOrder: 'name', // default ORDER BY
156
+ defaultOrder: 'name', // default ORDER BY (camelCase fields map to DB columns)
147
157
  excludeFromCreation: ['id'], // omit from INSERT (e.g. auto-increment)
158
+ readExclude: ['passwordHash'], // hide from all reads (writes unaffected)
148
159
  distinctResults: true, // SELECT DISTINCT
149
160
 
150
161
  // Relations — alias defaults to joinSchema.tableName. Override with `{ alias: '...' }`
@@ -320,6 +331,21 @@ only columns present in the generated Schema (as narrowed by `schemaOverrides` /
320
331
  column — the Schema is the write whitelist. Trim the generated Schema (or use `excludeFromCreation`)
321
332
  to keep sensitive columns out of it.
322
333
 
334
+ ### Read visibility
335
+
336
+ `readExclude` hides columns from every read while leaving writes untouched — the case for a
337
+ password hash or an access token: writable, never readable.
338
+
339
+ ```typescript
340
+ readExclude: ['accessToken'],
341
+ ```
342
+
343
+ Excluded fields are not selected by search/get, are omitted from read response schemas and from
344
+ the table's default join selection, and cannot be referenced from `filters`, `conditions`,
345
+ `orderBy`, aggregations or an explicit join `selection` (`400`). That last part is the point:
346
+ letting a hidden field be filtered would leak its value by bisection. Primary keys cannot be
347
+ excluded.
348
+
323
349
  ## API Reference
324
350
 
325
351
  ### POST /search/{table}
@@ -20,7 +20,7 @@ function printHelp() {
20
20
  console.log(' sqlapi-generate-tables --all');
21
21
  console.log('');
22
22
  display('Table*.ts files are only created if they do not already exist.', CONSOLE_COLORS.magenta);
23
- display('dbTables.ts is always regenerated to include all schemas.', CONSOLE_COLORS.magenta);
23
+ display('dbTables.ts is created if missing, indexing the Table*.ts files present on disk.', CONSOLE_COLORS.magenta);
24
24
  }
25
25
  await runCli('fastify-auto-sqlapi: generating tables template', async () => {
26
26
  const cliArgs = parseArgs({
@@ -84,9 +84,11 @@ await runCli('fastify-auto-sqlapi: generating tables template', async () => {
84
84
  }
85
85
  }
86
86
  }
87
+ const tableVarOf = (schema) => 'Table' + schema.schemaName.replace(/^Schema/, '');
87
88
  // Generate individual Table*.ts files (skip if already exists)
89
+ const createdTableVars = [];
88
90
  for (const schema of targetSchemas) {
89
- const tableVarName = 'Table' + schema.schemaName.replace(/^Schema/, '');
91
+ const tableVarName = tableVarOf(schema);
90
92
  const tableFile = path.join(tablesDir, `${tableVarName}.ts`);
91
93
  if (fs.existsSync(tableFile)) {
92
94
  displayAsTableRow(`${tableVarName}.ts`, 'skipped (already exists)', 60, CONSOLE_COLORS.gray);
@@ -94,17 +96,29 @@ await runCli('fastify-auto-sqlapi: generating tables template', async () => {
94
96
  else {
95
97
  const content = generateSingleTableFile(schema, allSchemas);
96
98
  fs.writeFileSync(tableFile, content);
99
+ createdTableVars.push(tableVarName);
97
100
  displayAsTableRow(`${tableVarName}.ts`, 'created', 60, CONSOLE_COLORS.green);
98
101
  }
99
102
  }
100
- // Generate dbTables.ts only if it does not exist
103
+ // Generate dbTables.ts only if it does not exist. The index must reference only
104
+ // Table*.ts files actually present on disk, otherwise a subset run would emit
105
+ // imports for files that were never generated and the project would not compile.
101
106
  const dbTablesFile = path.join(tablesDir, 'dbTables.ts');
102
107
  if (fs.existsSync(dbTablesFile)) {
103
108
  displayAsTableRow('dbTables.ts', 'skipped (already exists)', 60, CONSOLE_COLORS.gray);
109
+ const indexContent = fs.readFileSync(dbTablesFile, 'utf-8');
110
+ const missing = createdTableVars.filter((v) => !indexContent.includes(v));
111
+ if (missing.length > 0) {
112
+ display(`Remember to add the new tables to dbTables.ts: ${missing.join(', ')}`, CONSOLE_COLORS.yellow);
113
+ }
104
114
  }
105
115
  else {
106
- const dbTablesContent = generateDbTablesIndex(allSchemas);
107
- fs.writeFileSync(dbTablesFile, dbTablesContent);
116
+ const existingTableVars = new Set(fs
117
+ .readdirSync(tablesDir)
118
+ .filter((f) => f.startsWith('Table') && f.endsWith('.ts'))
119
+ .map((f) => f.slice(0, -'.ts'.length)));
120
+ const indexSchemas = allSchemas.filter((s) => existingTableVars.has(tableVarOf(s)));
121
+ fs.writeFileSync(dbTablesFile, generateDbTablesIndex(indexSchemas));
108
122
  displayAsTableRow('dbTables.ts', 'created', 60, CONSOLE_COLORS.green);
109
123
  }
110
124
  console.log('');
@@ -1 +1 @@
1
- {"version":3,"file":"tables-codegen.d.ts","sourceRoot":"","sources":["../../../src/lib/cli/tables-codegen.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,0FAA0F;IAC1F,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;CACpB;AAID,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAuCpE;AAuCD,wBAAgB,eAAe,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,gBAAgB,EAAE,CAwB3E;AAID,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,GAAG,MAAM,CAwHhG;AAID,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,MAAM,CAqBrE;AAID,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,MAAM,CA6FlE"}
1
+ {"version":3,"file":"tables-codegen.d.ts","sourceRoot":"","sources":["../../../src/lib/cli/tables-codegen.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,0FAA0F;IAC1F,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;CACpB;AAID,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAuCpE;AAuCD,wBAAgB,eAAe,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,gBAAgB,EAAE,CAwB3E;AAID,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,GAAG,MAAM,CA2HhG;AAID,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,MAAM,CAqBrE;AAID,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,MAAM,CA6FlE"}
@@ -130,10 +130,10 @@ export function generateSingleTableFile(schema, allSchemas) {
130
130
  // Imports
131
131
  lines.push(`import {defineTable, exportTableInfo, Type} from 'fastify-auto-sqlapi';`);
132
132
  lines.push(`import type {ValidationError} from 'fastify-auto-sqlapi';`);
133
- lines.push(`import {${schema.schemaName} as Schema} from '../schemas/${schema.schemaName}';`);
133
+ lines.push(`import {${schema.schemaName} as Schema} from '../schemas/${schema.schemaName}.js';`);
134
134
  // Commented imports for related schemas
135
135
  for (const relSchema of relatedSchemas) {
136
- lines.push(`// import {${relSchema}} from '../schemas/${relSchema}';`);
136
+ lines.push(`// import {${relSchema}} from '../schemas/${relSchema}.js';`);
137
137
  }
138
138
  lines.push(``);
139
139
  // Extra filters placeholder
@@ -161,6 +161,8 @@ export function generateSingleTableFile(schema, allSchemas) {
161
161
  else {
162
162
  lines.push(` // excludeFromCreation: [],`);
163
163
  }
164
+ // Hide columns from every read (writes still accept them, e.g. a password hash)
165
+ lines.push(` // readExclude: [],`);
164
166
  if (parentRels.length > 0) {
165
167
  lines.push(` // allowedReadJoins: [`);
166
168
  for (const rel of parentRels) {
@@ -213,7 +215,7 @@ export function generateDbTablesIndex(schemas) {
213
215
  lines.push(``);
214
216
  for (const schema of schemas) {
215
217
  const tableVarName = 'Table' + schema.schemaName.replace(/^Schema/, '');
216
- lines.push(`import {${tableVarName}} from './${tableVarName}';`);
218
+ lines.push(`import {${tableVarName}} from './${tableVarName}.js';`);
217
219
  }
218
220
  lines.push(``);
219
221
  lines.push(`export const dbTables: DbTables = {`);
@@ -245,7 +247,7 @@ export function generateTablesFile(schemas) {
245
247
  lines.push(``);
246
248
  // Schema imports
247
249
  for (const schema of schemas) {
248
- lines.push(`import {${schema.schemaName}} from './${schema.schemaName}';`);
250
+ lines.push(`import {${schema.schemaName}} from './${schema.schemaName}.js';`);
249
251
  }
250
252
  // Table definitions
251
253
  for (const schema of schemas) {
@@ -1 +1 @@
1
- {"version":3,"file":"get.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/get.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAuB,MAAM,mBAAmB,CAAC;AAEnF,wBAAsB,SAAS,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC,CA6BrE"}
1
+ {"version":3,"file":"get.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/get.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAuB,MAAM,mBAAmB,CAAC;AAEnF,wBAAsB,SAAS,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC,CA8BrE"}
@@ -2,6 +2,7 @@ import { ConditionBuilder } from 'node-condition-builder';
2
2
  import { camelcaseObject } from '../../naming.js';
3
3
  import { buildTenantCondition, buildTenantJoin } from '../../tenant.js';
4
4
  import { httpError } from '../../errors.js';
5
+ import { readableSelectColumns } from '../../read-access.js';
5
6
  import { primaryAsString } from '../../../types.js';
6
7
  export async function getEngine(params) {
7
8
  const { db, tableConf, id, tenant } = params;
@@ -19,6 +20,7 @@ export async function getEngine(params) {
19
20
  const values = cb.getValues();
20
21
  const rows = await db.select({
21
22
  tableName: tableConf.Schema.tableName,
23
+ columns: readableSelectColumns(tableConf, tableConf.Schema, db),
22
24
  where,
23
25
  values,
24
26
  limit: '1',
@@ -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;AA4jC3B,wBAAsB,YAAY,CAChC,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,YAAY,CAAC,CA2IvB"}
1
+ {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/search/search.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACV,QAAQ,EACR,YAAY,EACZ,YAAY,EAcb,MAAM,mBAAmB,CAAC;AAmkC3B,wBAAsB,YAAY,CAChC,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,YAAY,CAAC,CAkIvB"}
@@ -1,14 +1,16 @@
1
- import { ConditionBuilder } from 'node-condition-builder';
1
+ import { ConditionBuilder, Expression } from 'node-condition-builder';
2
2
  import { camelcaseObject } from '../../naming.js';
3
3
  import { buildTenantCondition, buildTenantJoin } from '../../tenant.js';
4
+ import { assertReadable, assertFiltersReadable, readableSelectColumns } from '../../read-access.js';
4
5
  import { primaryAsString } from '../../../types.js';
5
6
  // ─── Schema field validation ────────────────────────────────
6
- function validateSchemaField(field, schema) {
7
+ function validateSchemaField(field, schema, tableConf) {
7
8
  if (!(field in schema.fields)) {
8
9
  const err = new Error(`Unknown field: ${field}`);
9
10
  err.statusCode = 400;
10
11
  throw err;
11
12
  }
13
+ assertReadable(tableConf, field);
12
14
  return schema.col(field);
13
15
  }
14
16
  function err400(msg) {
@@ -37,15 +39,15 @@ function buildComputedContext(db, schema, alias) {
37
39
  }
38
40
  function resolveFieldRef(field, schema, tableConf, db, alias) {
39
41
  if (field in schema.fields) {
42
+ assertReadable(tableConf, field);
40
43
  const col = db.qi(schema.col(field));
41
44
  const expr = alias ? `${db.qi(alias)}.${col}` : col;
42
- return { expr, values: [], computed: false };
45
+ return { expr, computed: false };
43
46
  }
44
47
  const fn = tableConf?.computedFields?.[field];
45
48
  if (fn) {
46
49
  const ctx = buildComputedContext(db, schema, alias);
47
- const out = fn(ctx);
48
- return { expr: out.expr, values: out.values ?? [], computed: true };
50
+ return { expr: computedExpression(field, fn(ctx)), computed: true };
49
51
  }
50
52
  err400(`Unknown field: ${field}`);
51
53
  }
@@ -95,16 +97,48 @@ function extractJoinRefs(db, tableConf, joinDef) {
95
97
  };
96
98
  }
97
99
  /**
98
- * Evaluate a computed field. When `allowBoundValues=false`, throws 400 if the computed binds
99
- * any parameters — used in positions where parameter binding cannot be coordinated (orderBy,
100
- * compute*, joinLeft.filters, etc).
100
+ * Evaluate a computed field into an Expression carrying its own bound values.
101
+ *
102
+ * Inside a WHERE clause the ConditionBuilder assigns the placeholder indexes, so bound
103
+ * values are always safe there. `allowBoundValues=false` is for the positions that sit
104
+ * *before* the WHERE values in the parameter order (SELECT projections, aggregations),
105
+ * where they cannot be placed correctly; those still reject with 400.
101
106
  */
102
- function evaluateComputedField(fn, schema, db, alias, message, allowBoundValues = false) {
107
+ function evaluateComputedField(name, fn, schema, db, alias, message, allowBoundValues = false) {
103
108
  const ev = fn(buildComputedContext(db, schema, alias));
104
109
  if (!allowBoundValues && (ev.values?.length ?? 0) > 0) {
105
110
  err400(message);
106
111
  }
107
- return ev;
112
+ return computedExpression(name, ev);
113
+ }
114
+ /**
115
+ * Build the Expression for a computed field, checking that it marks each bound value
116
+ * with a `?`.
117
+ *
118
+ * A mismatch is always a table-configuration bug and would otherwise produce a query that
119
+ * binds values nobody references — silently returning wrong rows. Expressions without
120
+ * bound values are exempt: they are emitted verbatim, so a `?` there is a literal operator
121
+ * (PostgreSQL jsonb) rather than a marker.
122
+ */
123
+ function computedExpression(name, ev) {
124
+ const values = ev.values ?? [];
125
+ if (values.length > 0) {
126
+ const markers = (ev.expr.match(/\\\?|\?/g) ?? []).filter((m) => m === '?').length;
127
+ if (markers !== values.length) {
128
+ throw new Error(`Computed field '${name}' declares ${values.length} bound value(s) but its expression ` +
129
+ `contains ${markers} '?' marker(s). Mark each bound value with '?' (use '\\?' for a ` +
130
+ `literal question mark) so the engine can assign placeholder positions.`);
131
+ }
132
+ }
133
+ return new Expression(ev.expr, values);
134
+ }
135
+ /** Bound values carried by a resolved field reference. */
136
+ function refValues(expr) {
137
+ return expr instanceof Expression ? [...expr.values] : [];
138
+ }
139
+ /** Render a resolved field reference to SQL, binding any values from `startIndex`. */
140
+ function renderRef(expr, startIndex, db) {
141
+ return expr instanceof Expression ? expr.render(startIndex, db.ph) : expr;
108
142
  }
109
143
  /** Map rows from snake_case columns to camelCase fields, preserving any computed-field columns
110
144
  * that are projected by their declared name (skipped by camelcaseObject). */
@@ -121,11 +155,19 @@ function mapRowsToCamelCase(rows, schema, computedFieldNames) {
121
155
  return camel;
122
156
  });
123
157
  }
124
- /** Build the SELECT columns list for a join fetch: '*' or comma-separated quoted columns. */
125
- function buildSelectionColumns(selection, joinSchema, db) {
126
- if (selection === '*')
127
- return '*';
128
- return selection.split(',').map((c) => db.qi(joinSchema.col(c.trim()))).join(', ');
158
+ /**
159
+ * Build the SELECT columns list for a join fetch: '*' or comma-separated quoted columns.
160
+ * A default '*' selection narrows to the join table's readable columns when it declares
161
+ * `readExclude`; an explicit selection naming an excluded field is rejected with 400.
162
+ */
163
+ function buildSelectionColumns(selection, joinSchema, db, joinTableConf) {
164
+ if (selection === '*') {
165
+ return readableSelectColumns(joinTableConf, joinSchema, db) ?? '*';
166
+ }
167
+ return selection
168
+ .split(',')
169
+ .map((c) => db.qi(validateSchemaField(c.trim(), joinSchema, joinTableConf)))
170
+ .join(', ');
129
171
  }
130
172
  const AGG_FN = {
131
173
  sum: { sql: 'SUM', distinct: false },
@@ -219,7 +261,7 @@ function validateOrderBy(orderBy, tableConf, db, dbTables, joinGroup, startIdx)
219
261
  if (dotted2) {
220
262
  const [, alias, field, dir] = dotted2;
221
263
  const joinDef = requireJoin(tableConf, alias, true);
222
- const col = validateSchemaField(field, joinDef.joinSchema);
264
+ const col = validateSchemaField(field, joinDef.joinSchema, dbTables[joinDef.joinSchema.tableName]);
223
265
  // Reference the LEFT JOIN'd table via its alias (SQL identifier).
224
266
  outParts.push(`${db.qi(alias)}.${db.qi(col)} ${(dir || 'ASC').toUpperCase()}`);
225
267
  leftJoinAliases.add(alias);
@@ -232,9 +274,12 @@ function validateOrderBy(orderBy, tableConf, db, dbTables, joinGroup, startIdx)
232
274
  }
233
275
  const [, field, dir] = plain;
234
276
  const ref = resolveFieldRef(field, tableConf.Schema, tableConf, db);
235
- outParts.push(`${ref.expr} ${(dir || 'ASC').toUpperCase()}`);
236
- outValues.push(...ref.values);
237
- currentIdx += ref.values.length;
277
+ // A computed field binds its own values here: ORDER BY is emitted after the WHERE, so
278
+ // its placeholders continue from `currentIdx`.
279
+ const refVals = refValues(ref.expr);
280
+ outParts.push(`${renderRef(ref.expr, currentIdx, db)} ${(dir || 'ASC').toUpperCase()}`);
281
+ outValues.push(...refVals);
282
+ currentIdx += refVals.length;
238
283
  }
239
284
  return { sql: outParts.join(', '), values: outValues, leftJoinAliases };
240
285
  }
@@ -259,6 +304,7 @@ function buildLeftJoinClauses(db, dbTables, tableConf, aliasesNeedingJoin, joinL
259
304
  const cb = new ConditionBuilder('AND', db.cbDialect);
260
305
  const joinTableConf = dbTables[joinSchema.tableName];
261
306
  const computed = joinTableConf?.computedFields;
307
+ assertFiltersReadable(ref.filters, joinTableConf);
262
308
  if (ref.filters) {
263
309
  for (const [field, value] of Object.entries(ref.filters)) {
264
310
  if (value === null || value === undefined)
@@ -267,9 +313,10 @@ function buildLeftJoinClauses(db, dbTables, tableConf, aliasesNeedingJoin, joinL
267
313
  cb.isEqual(`${aliasIdent}.${db.qi(joinSchema.col(field))}`, value);
268
314
  }
269
315
  else if (computed?.[field]) {
270
- // Computed field on the parent table: build expr with alias-qualified columns.
271
- const ev = evaluateComputedField(computed[field], joinSchema, db, alias, `Computed field '${field}' with bound values is not supported in joinLeft.filters`);
272
- cb.isEqual(ev.expr, value);
316
+ // Computed field on the parent table: expr with alias-qualified columns. Bound
317
+ // values are placed by this ConditionBuilder, whose SQL lands in the WHERE.
318
+ const expr = evaluateComputedField(field, computed[field], joinSchema, db, alias, '', true);
319
+ cb.isEqual(expr, value);
273
320
  }
274
321
  }
275
322
  }
@@ -279,12 +326,13 @@ function buildLeftJoinClauses(db, dbTables, tableConf, aliasesNeedingJoin, joinL
279
326
  err400(`Invalid condition method: ${c.method}`);
280
327
  }
281
328
  if (c.field in joinSchema.fields) {
329
+ assertReadable(dbTables[joinSchema.tableName], c.field);
282
330
  const col = `${aliasIdent}.${db.qi(joinSchema.col(c.field))}`;
283
331
  dispatchConditionMethod(cb, c.method, col, c.params ?? []);
284
332
  }
285
333
  else if (computed?.[c.field]) {
286
- const ev = evaluateComputedField(computed[c.field], joinSchema, db, alias, `Computed field '${c.field}' with bound values is not supported in joinLeft.conditions`);
287
- dispatchConditionMethod(cb, c.method, ev.expr, c.params ?? []);
334
+ const expr = evaluateComputedField(c.field, computed[c.field], joinSchema, db, alias, '', true);
335
+ dispatchConditionMethod(cb, c.method, expr, c.params ?? []);
288
336
  }
289
337
  else {
290
338
  err400(`Unknown field: ${c.field}`);
@@ -303,25 +351,58 @@ function buildLeftJoinClauses(db, dbTables, tableConf, aliasesNeedingJoin, joinL
303
351
  return { joinClauses, whereExtras, values };
304
352
  }
305
353
  // ─── Main query execution ───────────────────────────────────
354
+ /**
355
+ * Converts a configured order (`defaultOrder` or the primary-key fallback) to SQL.
356
+ * Unlike the request `orderBy` (strictly validated by `validateOrderBy`), this is
357
+ * lenient for backward compatibility: tokens matching a camelCase schema field are
358
+ * mapped to their quoted DB column, computed fields (without bound values) expand
359
+ * to their expression, and anything else passes through unchanged as raw SQL.
360
+ */
361
+ function convertConfiguredOrder(order, tableConf, db) {
362
+ return order
363
+ .split(',')
364
+ .map((part) => {
365
+ const trimmed = part.trim();
366
+ const match = trimmed.match(/^(\w+)(?:\s+(ASC|DESC))?$/i);
367
+ if (match) {
368
+ const [, field, dir] = match;
369
+ const suffix = dir ? ` ${dir.toUpperCase()}` : '';
370
+ if (field in tableConf.Schema.fields) {
371
+ return `${db.qi(tableConf.Schema.col(field))}${suffix}`;
372
+ }
373
+ const computed = tableConf.computedFields?.[field];
374
+ if (computed) {
375
+ const ev = evaluateComputedField(field, computed, tableConf.Schema, db, undefined, `Computed field '${field}' with bound values cannot be used in defaultOrder`);
376
+ return `${ev.value}${suffix}`;
377
+ }
378
+ }
379
+ return trimmed;
380
+ })
381
+ .join(', ');
382
+ }
306
383
  async function executeMainQuery(db, tableConf, where, values, orderBy, paginator, extraJoins = [], selectComputed, maxRows) {
307
384
  const tableName = tableConf.Schema.tableName;
308
- const order = orderBy || tableConf.defaultOrder || primaryAsString(tableConf.primary);
385
+ // Request orderBy arrives already validated and mapped by validateOrderBy;
386
+ // configured fallbacks (defaultOrder / primary key) are converted leniently here.
387
+ const order = orderBy || convertConfiguredOrder(tableConf.defaultOrder || primaryAsString(tableConf.primary), tableConf, db);
309
388
  // With a paginator, the page size governs the LIMIT. Without one, apply `maxRows` (if set) so an
310
389
  // unbounded search cannot dump the whole table; a plain integer is a safe, non-injectable LIMIT.
311
390
  const limit = paginator
312
391
  ? `${paginator.itemsPerPage} OFFSET ${(paginator.page - 1) * paginator.itemsPerPage}`
313
392
  : (maxRows != null ? String(maxRows) : null);
393
+ // Base projection: '*' unless the table hides fields via readExclude.
394
+ const readableColumns = readableSelectColumns(tableConf, tableConf.Schema, db);
314
395
  // Optional computed projections — bound values, if any, are NOT yet supported
315
396
  // here (would require placeholder-aware composition with WHERE values).
316
- let columns;
397
+ let columns = readableColumns;
317
398
  if (selectComputed?.length) {
318
- const projections = ['*'];
399
+ const projections = [readableColumns ?? '*'];
319
400
  for (const name of selectComputed) {
320
401
  const fn = tableConf.computedFields?.[name];
321
402
  if (!fn)
322
403
  err400(`Unknown computed field in selectComputed: '${name}'`);
323
- const out = evaluateComputedField(fn, tableConf.Schema, db, undefined, `Computed field '${name}' with bound values cannot be used in selectComputed`);
324
- projections.push(`${out.expr} AS ${db.qi(name)}`);
404
+ const out = evaluateComputedField(name, fn, tableConf.Schema, db, undefined, `Computed field '${name}' with bound values cannot be used in selectComputed`);
405
+ projections.push(`${out.value} AS ${db.qi(name)}`);
325
406
  }
326
407
  columns = projections.join(', ');
327
408
  }
@@ -352,10 +433,12 @@ async function buildPagination(db, tableConf, where, values, paginator, extraJoi
352
433
  for (const { key, field, fn } of computations) {
353
434
  if (field) {
354
435
  const ref = resolveFieldRef(field, tableConf.Schema, tableConf, db);
355
- if (ref.values.length > 0) {
436
+ // The aggregate sits in the SELECT list, before the WHERE values: a computed that
437
+ // binds parameters cannot be placed here.
438
+ if (refValues(ref.expr).length > 0) {
356
439
  err400(`Computed field '${field}' with bound values cannot be used in compute${key.charAt(0).toUpperCase() + key.slice(1)}`);
357
440
  }
358
- const result = await db.query(`SELECT ${fn}(${ref.expr}) as value FROM ${db.qi(tableName)}${joinClause} WHERE ${where}`, values);
441
+ const result = await db.query(`SELECT ${fn}(${renderRef(ref.expr, 1, db)}) as value FROM ${db.qi(tableName)}${joinClause} WHERE ${where}`, values);
359
442
  computed[key] = { [field]: result.rows[0].value };
360
443
  }
361
444
  }
@@ -383,15 +466,10 @@ async function executeJoinMultiple(db, dbTables, tableConf, mainResults, joinMul
383
466
  cb.isIn(db.qi(fkCol), ids);
384
467
  const tenantJoins = [];
385
468
  appendJoinTenantScope(db, joinTableConf, tenant, joinSchema.tableName, cb, tenantJoins);
386
- let where = cb.build(1, db.ph);
387
- let values = cb.getValues();
388
- // Side-channel: computed-field clauses on the join table
389
- const computedClauses = collectJoinRefComputedClauses(joinTableConf, ref || {}, db);
390
- if (computedClauses.length > 0) {
391
- ({ where, values } = appendComputedConditions(where, values, computedClauses, db));
392
- }
469
+ const where = cb.build(1, db.ph);
470
+ const values = cb.getValues();
393
471
  const selection = ref?.selection ?? defaultSelection;
394
- const columns = buildSelectionColumns(selection, joinSchema, db);
472
+ const columns = buildSelectionColumns(selection, joinSchema, db, joinTableConf);
395
473
  const rows = await db.select({
396
474
  tableName: joinSchema.tableName,
397
475
  columns,
@@ -425,7 +503,7 @@ async function executeJoinLeft(db, dbTables, tableConf, mainResults, joinLeft, t
425
503
  const where = cb.build(1, db.ph);
426
504
  const values = cb.getValues();
427
505
  const selection = ref?.selection ?? defaultSelection;
428
- const columns = buildSelectionColumns(selection, joinSchema, db);
506
+ const columns = buildSelectionColumns(selection, joinSchema, db, joinTableConf);
429
507
  const rows = await db.select({
430
508
  tableName: joinSchema.tableName,
431
509
  columns,
@@ -453,7 +531,7 @@ function buildByExpression(by, joinSchema, joinTableConf, db) {
453
531
  }
454
532
  const fn = joinTableConf?.computedFields?.[by];
455
533
  if (fn) {
456
- return evaluateComputedField(fn, joinSchema, db, undefined, `Computed field '${by}' with bound values cannot be used in aggregations.by`).expr;
534
+ return evaluateComputedField(by, fn, joinSchema, db, undefined, `Computed field '${by}' with bound values cannot be used in aggregations.by`).value;
457
535
  }
458
536
  err400(`Unknown field: ${by}`);
459
537
  }
@@ -482,7 +560,7 @@ async function executeJoinGroup(db, dbTables, tableConf, mainResults, joinGroup,
482
560
  return;
483
561
  const fn = AGG_FN[kind];
484
562
  for (const f of fields) {
485
- const col = validateSchemaField(f, joinSchema);
563
+ const col = validateSchemaField(f, joinSchema, joinTableConf);
486
564
  selectParts.push(`${aggExpr(fn, db.qi(col))} as "${kind}_${f}"`);
487
565
  }
488
566
  };
@@ -502,13 +580,8 @@ async function executeJoinGroup(db, dbTables, tableConf, mainResults, joinGroup,
502
580
  cb.isIn(db.qi(fkCol), ids);
503
581
  const tenantJoins = [];
504
582
  appendJoinTenantScope(db, joinTableConf, tenant, joinSchema.tableName, cb, tenantJoins);
505
- let where = cb.build(1, db.ph);
506
- let values = cb.getValues();
507
- // Side-channel: computed-field clauses on the join table
508
- const computedClauses = collectJoinRefComputedClauses(joinTableConf, groupRef, db);
509
- if (computedClauses.length > 0) {
510
- ({ where, values } = appendComputedConditions(where, values, computedClauses, db));
511
- }
583
+ const where = cb.build(1, db.ph);
584
+ const values = cb.getValues();
512
585
  const groupBy = groupByParts.length > 0 ? `GROUP BY ${groupByParts.join(', ')}` : '';
513
586
  const fromJoins = tenantJoins.length > 0 ? ` ${tenantJoins.join(' ')}` : '';
514
587
  const sql = `SELECT ${selectParts.join(', ')} FROM ${db.qi(joinSchema.tableName)}${fromJoins} WHERE ${where} ${groupBy}`;
@@ -568,63 +641,36 @@ function dispatchConditionMethod(cb, method, colOrExpr, params) {
568
641
  }
569
642
  }
570
643
  function buildJoinRefCondition(joinTableConf, joinSchema, ref, db) {
644
+ assertFiltersReadable(ref.filters, joinTableConf);
571
645
  const cb = (ref.filters && joinTableConf)
572
646
  ? joinTableConf.filters(ref.filters, db.cbDialect)
573
647
  : new ConditionBuilder('AND', db.cbDialect);
574
- if (ref.conditions?.length) {
575
- for (const c of ref.conditions) {
576
- if (!ALLOWED_SET.has(c.method)) {
577
- err400(`Invalid condition method: ${c.method}`);
578
- }
579
- // Skip computed-field conditions here — they are processed via side-channel
580
- // by `collectJoinRefComputedClauses` since ConditionBuilder cannot bind values
581
- // for an LHS expression.
582
- if (joinTableConf?.computedFields?.[c.field])
583
- continue;
584
- const col = db.qi(validateSchemaField(c.field, joinSchema));
585
- dispatchConditionMethod(cb, c.method, col, c.params ?? []);
586
- }
587
- }
588
- return cb;
589
- }
590
- /**
591
- * Side-channel: gather equality + operator clauses targeting computed fields
592
- * declared on the join table's `computedFields`. To be appended to a WHERE
593
- * after the main `cb.build()` with correct placeholder offsets.
594
- */
595
- function collectJoinRefComputedClauses(joinTableConf, ref, db) {
596
- const out = [];
648
+ // Equality filters targeting the join table's computed fields
597
649
  const computed = joinTableConf?.computedFields;
598
- if (!computed)
599
- return out;
600
- // Equality filters on computed names
601
- if (ref.filters) {
650
+ if (computed && ref.filters) {
602
651
  for (const [name, value] of Object.entries(ref.filters)) {
603
652
  if (value === null || value === undefined)
604
653
  continue;
605
654
  if (!computed[name])
606
655
  continue;
607
- const ev = evaluateComputedField(computed[name], joinTableConf.Schema, db, undefined, '', true);
608
- out.push({ method: 'isEqual', expr: ev.expr, values: ev.values ?? [], params: [value] });
656
+ const expr = evaluateComputedField(name, computed[name], joinTableConf.Schema, db, undefined, '', true);
657
+ cb.isEqual(expr, value);
609
658
  }
610
659
  }
611
- // Operator conditions on computed names
612
660
  if (ref.conditions?.length) {
613
661
  for (const c of ref.conditions) {
614
- if (!computed[c.field])
615
- continue;
616
- if (!ALLOWED_SET.has(c.method))
662
+ if (!ALLOWED_SET.has(c.method)) {
617
663
  err400(`Invalid condition method: ${c.method}`);
618
- const ev = evaluateComputedField(computed[c.field], joinTableConf.Schema, db, undefined, '', true);
619
- out.push({
620
- method: c.method,
621
- expr: ev.expr,
622
- values: ev.values ?? [],
623
- params: c.params ?? [],
624
- });
664
+ }
665
+ // Computed fields resolve to an Expression carrying their own bound values, which
666
+ // the ConditionBuilder places together with the compared value.
667
+ const operand = computed?.[c.field]
668
+ ? evaluateComputedField(c.field, computed[c.field], joinTableConf.Schema, db, undefined, '', true)
669
+ : db.qi(validateSchemaField(c.field, joinSchema, joinTableConf));
670
+ dispatchConditionMethod(cb, c.method, operand, c.params ?? []);
625
671
  }
626
672
  }
627
- return out;
673
+ return cb;
628
674
  }
629
675
  /**
630
676
  * Apply non-dotted conditions to the main `condition` ConditionBuilder when
@@ -633,7 +679,6 @@ function collectJoinRefComputedClauses(joinTableConf, ref, db) {
633
679
  * offsets), since ConditionBuilder cannot bind values for an LHS expression.
634
680
  */
635
681
  function applyConditions(condition, conditions, schema, tableConf, db) {
636
- const computedClauses = [];
637
682
  for (const c of conditions) {
638
683
  // Skip dot-notation fields — those become aggregation conditions processed later
639
684
  if (c.field.includes('.'))
@@ -641,63 +686,29 @@ function applyConditions(condition, conditions, schema, tableConf, db) {
641
686
  if (!ALLOWED_SET.has(c.method)) {
642
687
  err400(`Invalid condition method: ${c.method}`);
643
688
  }
689
+ // A computed field resolves to an Expression carrying its own bound values; the
690
+ // ConditionBuilder places them, so no placeholder offset is computed here.
644
691
  const ref = resolveFieldRef(c.field, schema, tableConf, db);
645
- if (!ref.computed) {
646
- dispatchConditionMethod(condition, c.method, ref.expr, c.params);
647
- }
648
- else {
649
- computedClauses.push({
650
- method: c.method,
651
- expr: ref.expr,
652
- values: ref.values,
653
- params: c.params ?? [],
654
- });
655
- }
692
+ dispatchConditionMethod(condition, c.method, ref.expr, c.params);
656
693
  }
657
- return computedClauses;
658
694
  }
659
695
  /**
660
- * Equality filters targeting computed fields: when `filters.<computedName>` is
661
- * set on the request body and the computed exists on `tableConf.computedFields`,
662
- * generate a side-channel clause `<expr> = ?` with the user value as parameter.
663
- * This complements the auto-generated equality on schema fields done by
664
- * `tableConf.filters()`.
696
+ * Equality filters targeting computed fields: `filters.<computedName>` becomes
697
+ * `<expr> = <value>` on the same ConditionBuilder used for schema fields, which
698
+ * `tableConf.filters()` has already populated.
665
699
  */
666
- function collectComputedFilterClauses(filters, tableConf, db) {
667
- const out = [];
700
+ function applyComputedFilters(condition, filters, tableConf, db) {
668
701
  const computed = tableConf.computedFields;
669
702
  if (!computed)
670
- return out;
703
+ return;
671
704
  for (const [name, value] of Object.entries(filters)) {
672
705
  if (value === null || value === undefined)
673
706
  continue;
674
707
  if (!computed[name])
675
708
  continue;
676
- const ref = evaluateComputedField(computed[name], tableConf.Schema, db, undefined, '', true);
677
- out.push({ method: 'isEqual', expr: ref.expr, values: ref.values ?? [], params: [value] });
678
- }
679
- return out;
680
- }
681
- /**
682
- * Append computed-field condition clauses (side-channel from applyConditions)
683
- * to the current WHERE, coordinating placeholder indices with the running
684
- * `values` array.
685
- */
686
- function appendComputedConditions(currentWhere, currentValues, clauses, db) {
687
- let where = currentWhere;
688
- let values = [...currentValues];
689
- for (const c of clauses) {
690
- // Append the computed's own bound values first; the expr placeholders
691
- // (if any) reference them via stable indices set by the consumer.
692
- values = [...values, ...c.values];
693
- const tmpCb = new ConditionBuilder('AND', db.cbDialect);
694
- dispatchConditionMethod(tmpCb, c.method, c.expr, c.params);
695
- const startIdx = values.length + 1;
696
- const clauseSql = tmpCb.build(startIdx, db.ph);
697
- where += ` AND ${clauseSql}`;
698
- values.push(...tmpCb.getValues());
709
+ const expr = evaluateComputedField(name, computed[name], tableConf.Schema, db, undefined, '', true);
710
+ condition.isEqual(expr, value);
699
711
  }
700
- return { where, values };
701
712
  }
702
713
  function appendAggConditions(currentWhere, currentValues, conditions, db, dbTables, tableConf, joinGroup) {
703
714
  let where = currentWhere;
@@ -736,20 +747,11 @@ function buildJoinMustExistClauses(db, dbTables, tableConf, joinMustExist, curre
736
747
  const filterCondition = buildJoinRefCondition(joinTableConf, joinSchema, ref, db);
737
748
  const tenantJoins = [];
738
749
  appendJoinTenantScope(db, joinTableConf, tenant, joinSchema.tableName, filterCondition, tenantJoins);
750
+ // The sub-condition (computed fields included) is built by the ConditionBuilder, which
751
+ // numbers its placeholders from where the outer values end.
739
752
  const startIdx = values.length + 1;
740
- let filterWhere = filterCondition.build(startIdx, db.ph);
741
- let filterVals = [...filterCondition.getValues()];
742
- // Side-channel: computed-field clauses
743
- const computedClauses = collectJoinRefComputedClauses(joinTableConf, ref, db);
744
- if (computedClauses.length > 0) {
745
- let cwhere = filterWhere;
746
- let cvalues = filterVals;
747
- ({ where: cwhere, values: cvalues } = appendComputedConditions(cwhere, [...values, ...cvalues], computedClauses, db));
748
- // appendComputedConditions starts with currentValues; we need just the
749
- // delta belonging to this EXISTS subquery to update filterVals.
750
- filterVals = cvalues.slice(values.length);
751
- filterWhere = cwhere;
752
- }
753
+ const filterWhere = filterCondition.build(startIdx, db.ph);
754
+ const filterVals = [...filterCondition.getValues()];
753
755
  // Qualify the FK with the join table: the EXISTS FROM may include the tenant
754
756
  // through-join, so a bare column could become ambiguous.
755
757
  const innerWhere = filterWhere
@@ -778,16 +780,16 @@ export async function searchEngine(dbTables, params) {
778
780
  for (const a of Object.keys(joinLeft))
779
781
  requireJoin(tableConf, a, true);
780
782
  // Build main condition
783
+ assertFiltersReadable(filters, tableConf);
781
784
  const condition = tableConf.filters(filters || {}, db.cbDialect);
782
- // Filters that target computed fields (declared on tableConf.computedFields)
783
- // are applied via the side-channel pattern further down.
784
- let computedFilterClauses = [];
785
+ // Filters and conditions targeting computed fields go on the same ConditionBuilder:
786
+ // a computed resolves to an Expression carrying its own values, and the builder is
787
+ // what assigns every placeholder index.
785
788
  if (filters && tableConf.computedFields) {
786
- computedFilterClauses = collectComputedFilterClauses(filters, tableConf, db);
789
+ applyComputedFilters(condition, filters, tableConf, db);
787
790
  }
788
- let computedConditionClauses = [];
789
791
  if (conditions?.length) {
790
- computedConditionClauses = applyConditions(condition, conditions, tableConf.Schema, tableConf, db);
792
+ applyConditions(condition, conditions, tableConf.Schema, tableConf, db);
791
793
  }
792
794
  // Tenant
793
795
  const tenantJoins = [];
@@ -799,14 +801,6 @@ export async function searchEngine(dbTables, params) {
799
801
  }
800
802
  let where = condition.build(1, db.ph);
801
803
  let values = [...condition.getValues()];
802
- // Append computed filter (equality) and condition (operator) clauses with
803
- // correct placeholder offsets.
804
- if (computedFilterClauses.length > 0) {
805
- ({ where, values } = appendComputedConditions(where, values, computedFilterClauses, db));
806
- }
807
- if (computedConditionClauses.length > 0) {
808
- ({ where, values } = appendComputedConditions(where, values, computedConditionClauses, db));
809
- }
810
804
  // joinMustExist (EXISTS)
811
805
  if (joinMustExist && Object.keys(joinMustExist).length > 0) {
812
806
  ({ where, values } = buildJoinMustExistClauses(db, dbTables, tableConf, joinMustExist, where, values, tenant));
@@ -0,0 +1,31 @@
1
+ import type { QueryClient } from './db.js';
2
+ import type { ITable, SchemaDefinition } from '../types.js';
3
+ /**
4
+ * Read visibility (`ITable.readExclude`).
5
+ *
6
+ * Excluded fields are never projected by any read query and cannot be referenced
7
+ * from filters, conditions, orderBy, aggregations or join selections: allowing a
8
+ * field to be filtered while hiding it from the output would still leak its value
9
+ * by bisection. Write paths (insert/update/upsert) are deliberately unaffected —
10
+ * a field can be writable but never readable (e.g. a password hash).
11
+ */
12
+ export declare function isReadExcluded(tableConf: ITable | undefined, field: string): boolean;
13
+ /** Throws 400 when `field` is read-excluded on `tableConf`. */
14
+ export declare function assertReadable(tableConf: ITable | undefined, field: string): void;
15
+ /**
16
+ * Throws 400 when a filter map targets a read-excluded field. Keys that are not
17
+ * excluded pass through untouched (extraFilters and computed names included).
18
+ */
19
+ export declare function assertFiltersReadable(filters: Record<string, unknown> | undefined, tableConf: ITable | undefined): void;
20
+ /** Schema field names that may be read, in schema declaration order. */
21
+ export declare function readableFieldNames(tableConf: ITable | undefined, schema: SchemaDefinition): string[];
22
+ /**
23
+ * SELECT column list for a table with read exclusions, or `undefined` when the
24
+ * table has none (callers keep their existing `*` projection).
25
+ *
26
+ * Columns are qualified with the table name: read queries may carry a tenant
27
+ * `through` INNER JOIN, where a bare column name shared with the through table
28
+ * would be ambiguous.
29
+ */
30
+ export declare function readableSelectColumns(tableConf: ITable | undefined, schema: SchemaDefinition, db: QueryClient): string | undefined;
31
+ //# sourceMappingURL=read-access.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read-access.d.ts","sourceRoot":"","sources":["../../src/lib/read-access.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAG5D;;;;;;;;GAQG;AAEH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAEpF;AAED,+DAA+D;AAC/D,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAIjF;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAC5C,SAAS,EAAE,MAAM,GAAG,SAAS,GAC5B,IAAI,CAKN;AAED,wEAAwE;AACxE,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,MAAM,EAAE,gBAAgB,GACvB,MAAM,EAAE,CAIV;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,MAAM,EAAE,gBAAgB,EACxB,EAAE,EAAE,WAAW,GACd,MAAM,GAAG,SAAS,CAMpB"}
@@ -0,0 +1,53 @@
1
+ import { httpError } from './errors.js';
2
+ /**
3
+ * Read visibility (`ITable.readExclude`).
4
+ *
5
+ * Excluded fields are never projected by any read query and cannot be referenced
6
+ * from filters, conditions, orderBy, aggregations or join selections: allowing a
7
+ * field to be filtered while hiding it from the output would still leak its value
8
+ * by bisection. Write paths (insert/update/upsert) are deliberately unaffected —
9
+ * a field can be writable but never readable (e.g. a password hash).
10
+ */
11
+ export function isReadExcluded(tableConf, field) {
12
+ return tableConf?.readExclude?.includes(field) ?? false;
13
+ }
14
+ /** Throws 400 when `field` is read-excluded on `tableConf`. */
15
+ export function assertReadable(tableConf, field) {
16
+ if (isReadExcluded(tableConf, field)) {
17
+ throw httpError(400, `Field is not readable: ${field}`);
18
+ }
19
+ }
20
+ /**
21
+ * Throws 400 when a filter map targets a read-excluded field. Keys that are not
22
+ * excluded pass through untouched (extraFilters and computed names included).
23
+ */
24
+ export function assertFiltersReadable(filters, tableConf) {
25
+ if (!filters || !tableConf?.readExclude?.length)
26
+ return;
27
+ for (const key of Object.keys(filters)) {
28
+ assertReadable(tableConf, key);
29
+ }
30
+ }
31
+ /** Schema field names that may be read, in schema declaration order. */
32
+ export function readableFieldNames(tableConf, schema) {
33
+ const fields = Object.keys(schema.fields);
34
+ if (!tableConf?.readExclude?.length)
35
+ return fields;
36
+ return fields.filter((f) => !isReadExcluded(tableConf, f));
37
+ }
38
+ /**
39
+ * SELECT column list for a table with read exclusions, or `undefined` when the
40
+ * table has none (callers keep their existing `*` projection).
41
+ *
42
+ * Columns are qualified with the table name: read queries may carry a tenant
43
+ * `through` INNER JOIN, where a bare column name shared with the through table
44
+ * would be ambiguous.
45
+ */
46
+ export function readableSelectColumns(tableConf, schema, db) {
47
+ if (!tableConf?.readExclude?.length)
48
+ return undefined;
49
+ const table = db.qi(schema.tableName);
50
+ return readableFieldNames(tableConf, schema)
51
+ .map((f) => `${table}.${db.qi(schema.col(f))}`)
52
+ .join(', ');
53
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/search.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,OAAO,EAAgB,MAAM,mBAAmB,CAAC;AAErE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAY/C,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAuHlF;AAED,eAAO,MAAM,sBAAsB;;;;;;;;EASjC,CAAC;AAEH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAmElF"}
1
+ {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/search.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,OAAO,EAAgB,MAAM,mBAAmB,CAAC;AAGrE,OAAO,KAAK,EAAE,QAAQ,EAA4B,MAAM,gBAAgB,CAAC;AAyBzE,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAuHlF;AAED,eAAO,MAAM,sBAAsB;;;;;;;;EASjC,CAAC;AAEH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAsElF"}
@@ -1,5 +1,16 @@
1
1
  import { Type } from '@sinclair/typebox';
2
2
  import { ALLOWED_METHODS } from '../condition-methods.js';
3
+ import { readableFieldNames } from '../read-access.js';
4
+ /** Schema fields minus the ones hidden by `readExclude`. */
5
+ function readableFields(schema, tableConf) {
6
+ if (!tableConf?.readExclude?.length)
7
+ return { ...schema.fields };
8
+ const out = {};
9
+ for (const field of readableFieldNames(tableConf, schema)) {
10
+ out[field] = schema.fields[field];
11
+ }
12
+ return out;
13
+ }
3
14
  const JoinGroupResultItem = Type.Object({
4
15
  sum: Type.Optional(Type.Record(Type.String(), Type.Any())),
5
16
  min: Type.Optional(Type.Record(Type.String(), Type.Any())),
@@ -38,7 +49,7 @@ export function SearchTableBodyPost(dbTables, tableName) {
38
49
  }
39
50
  }
40
51
  const filterFields = {
41
- ...schema.fields,
52
+ ...readableFields(schema, tableConf),
42
53
  ...tableConf.extraFilters,
43
54
  ...computedTypes,
44
55
  };
@@ -71,7 +82,7 @@ export function SearchTableBodyPost(dbTables, tableName) {
71
82
  }
72
83
  }
73
84
  const joinFilterFields = joinTableConf
74
- ? { ...joinSchema.fields, ...joinTableConf.extraFilters, ...joinComputedTypes }
85
+ ? { ...readableFields(joinSchema, joinTableConf), ...joinTableConf.extraFilters, ...joinComputedTypes }
75
86
  : { ...joinSchema.fields };
76
87
  const joinRefShape = {
77
88
  filters: Type.Optional(Type.Partial(Type.Object(joinFilterFields))),
@@ -150,16 +161,17 @@ export function SearchTableResponse(dbTables, tableName) {
150
161
  }
151
162
  }
152
163
  }
164
+ const mainReadable = readableFields(tableConf.Schema, tableConf);
153
165
  const mainItem = Object.keys(computedTypes).length > 0
154
- ? Type.Partial(Type.Object({ ...tableConf.Schema.fields, ...computedTypes }))
155
- : Type.Partial(Type.Object(tableConf.Schema.fields));
166
+ ? Type.Partial(Type.Object({ ...mainReadable, ...computedTypes }))
167
+ : Type.Partial(Type.Object(mainReadable));
156
168
  const joinMultipleProps = {};
157
169
  const joinLeftProps = {};
158
170
  const joinGroupProps = {};
159
171
  if (tableConf.allowedReadJoins) {
160
172
  for (const joinDef of tableConf.allowedReadJoins) {
161
173
  const { joinSchema, alias, unique } = joinDef;
162
- const itemArray = Type.Array(Type.Partial(Type.Object(joinSchema.fields)));
174
+ const itemArray = Type.Array(Type.Partial(Type.Object(readableFields(joinSchema, dbTables[joinSchema.tableName]))));
163
175
  if (unique) {
164
176
  joinLeftProps[alias] = itemArray;
165
177
  }
@@ -0,0 +1,18 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import { type SqlApi } from './sql-api.js';
3
+ import type { SqlApiPluginOptions } from '../types.js';
4
+ declare module 'fastify' {
5
+ interface FastifyInstance {
6
+ sqlApi: SqlApi;
7
+ }
8
+ }
9
+ /**
10
+ * Decorates `fastify.sqlApi` (lazy getter) on the given instance unless the
11
+ * decorator is already present in scope. The main plugin calls this at the
12
+ * fp level (parent scope); granular route plugins call it through
13
+ * `registerForAllTables` so they also work standalone, each within its own
14
+ * encapsulation context. The underlying pool (`fastify.pg` / `fastify.mysql`)
15
+ * is shared either way.
16
+ */
17
+ export declare function ensureSqlApiDecorator(fastify: FastifyInstance, options: SqlApiPluginOptions): void;
18
+ //# sourceMappingURL=sql-api-decorator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sql-api-decorator.d.ts","sourceRoot":"","sources":["../../src/lib/sql-api-decorator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAI/C,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,cAAc,CAAC;AACzD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAEvD,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAU,eAAe;QACvB,MAAM,EAAE,MAAM,CAAC;KAChB;CACF;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,IAAI,CA2BN"}
@@ -0,0 +1,40 @@
1
+ import { createQueryClient } from './db.js';
2
+ import { pgQueryable } from './adapters/pg-adapter.js';
3
+ import { mysqlQueryable } from './adapters/mysql-adapter.js';
4
+ import { createSqlApi } from './sql-api.js';
5
+ /**
6
+ * Decorates `fastify.sqlApi` (lazy getter) on the given instance unless the
7
+ * decorator is already present in scope. The main plugin calls this at the
8
+ * fp level (parent scope); granular route plugins call it through
9
+ * `registerForAllTables` so they also work standalone, each within its own
10
+ * encapsulation context. The underlying pool (`fastify.pg` / `fastify.mysql`)
11
+ * is shared either way.
12
+ */
13
+ export function ensureSqlApiDecorator(fastify, options) {
14
+ if (fastify.hasDecorator('sqlApi'))
15
+ return;
16
+ // Lazy-init QueryClient (internal closure — not decorated on fastify)
17
+ let cachedDb;
18
+ function getDb() {
19
+ if (!cachedDb) {
20
+ const pool = (options.dialect === 'mysql' || options.dialect === 'mariadb')
21
+ ? mysqlQueryable(fastify.mysql)
22
+ : pgQueryable(fastify.pg);
23
+ cachedDb = createQueryClient(pool, options.dialect);
24
+ if (options.debug)
25
+ cachedDb.setDebug(true);
26
+ }
27
+ return cachedDb;
28
+ }
29
+ let cachedSqlApi;
30
+ fastify.decorate('sqlApi', {
31
+ getter() {
32
+ if (!cachedSqlApi) {
33
+ cachedSqlApi = createSqlApi(getDb(), options.DbTables, {
34
+ getTenantId: options.getTenantId,
35
+ });
36
+ }
37
+ return cachedSqlApi;
38
+ },
39
+ });
40
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"table-helpers.d.ts","sourceRoot":"","sources":["../../src/lib/table-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,KAAK,yBAAyB,EAAiC,MAAM,wBAAwB,CAAC;AACzH,OAAO,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,KAAK,EACV,gBAAgB,EAChB,cAAc,EACd,aAAa,EACb,MAAM,EACP,MAAM,aAAa,CAAC;AAarB,KAAK,UAAU,CAAC,EAAE,IAAI,EAAE,SAAS,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,EAAE,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAExH,wBAAgB,eAAe,CAC7B,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,EAAE,SAAS,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,EAErE,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC3B,YAAY,GAAE,EAAa,EAC3B,iBAAiB,CAAC,EAAE,CAClB,SAAS,EAAE,gBAAgB,EAC3B,OAAO,EAAE;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,MAAM,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,yBAAyB;CAAE,KAC3E,IAAI,GACR;IAAE,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,CAAC;IAAC,OAAO,EAAE,aAAa,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CA0BhG;AAED,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC3D,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,GAChB,MAAM,CAAC,CAAC,CAAC,CAKX;AA2CD,MAAM,WAAW,oBAAoB;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,wBAAgB,aAAa,CAC3B,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAEjC,UAAU,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC/B,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC,EAAE,EAClD,UAAU,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC/B,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC,EAC3B,OAAO,CAAC,EAAE,oBAAoB,GAC7B,cAAc,CAShB;AAED,wBAAgB,eAAe,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/D,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC3B,OAAO,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC,EAAE,GAC5B,CAAC,gBAAgB,EAAE,MAAM,EAAE,CAAC,CAE9B;AAED,wBAAgB,gBAAgB,CAC9B,GAAG,KAAK,EAAE,CAAC,gBAAgB,EAAE,MAAM,EAAE,CAAC,EAAE,GACvC,GAAG,CAAC,gBAAgB,EAAE,MAAM,EAAE,CAAC,CAEjC"}
1
+ {"version":3,"file":"table-helpers.d.ts","sourceRoot":"","sources":["../../src/lib/table-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,KAAK,yBAAyB,EAAiC,MAAM,wBAAwB,CAAC;AACzH,OAAO,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,KAAK,EACV,gBAAgB,EAChB,cAAc,EACd,aAAa,EACb,MAAM,EACP,MAAM,aAAa,CAAC;AAarB,KAAK,UAAU,CAAC,EAAE,IAAI,EAAE,SAAS,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,EAAE,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAExH,wBAAgB,eAAe,CAC7B,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,EAAE,SAAS,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,EAErE,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC3B,YAAY,GAAE,EAAa,EAC3B,iBAAiB,CAAC,EAAE,CAClB,SAAS,EAAE,gBAAgB,EAC3B,OAAO,EAAE;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,MAAM,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,yBAAyB;CAAE,KAC3E,IAAI,GACR;IAAE,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,CAAC;IAAC,OAAO,EAAE,aAAa,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CA0BhG;AAED,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC3D,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,GAChB,MAAM,CAAC,CAAC,CAAC,CAMX;AAkED,MAAM,WAAW,oBAAoB;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,wBAAgB,aAAa,CAC3B,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAEjC,UAAU,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC/B,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC,EAAE,EAClD,UAAU,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC/B,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC,EAC3B,OAAO,CAAC,EAAE,oBAAoB,GAC7B,cAAc,CAShB;AAED,wBAAgB,eAAe,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/D,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC3B,OAAO,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC,EAAE,GAC5B,CAAC,gBAAgB,EAAE,MAAM,EAAE,CAAC,CAE9B;AAED,wBAAgB,gBAAgB,CAC9B,GAAG,KAAK,EAAE,CAAC,gBAAgB,EAAE,MAAM,EAAE,CAAC,EAAE,GACvC,GAAG,CAAC,gBAAgB,EAAE,MAAM,EAAE,CAAC,CAEjC"}
@@ -33,8 +33,26 @@ export function defineTable(config) {
33
33
  validateAliasUniqueness(config.allowedReadJoins, 'allowedReadJoins');
34
34
  validateAliasUniqueness(config.allowedWriteJoins, 'allowedWriteJoins');
35
35
  validateComputedFields(config);
36
+ validateReadExclude(config);
36
37
  return config;
37
38
  }
39
+ function validateReadExclude(config) {
40
+ const excluded = config.readExclude;
41
+ if (!excluded?.length)
42
+ return;
43
+ const schemaFields = Object.keys(config.Schema.fields);
44
+ const pkFields = Array.isArray(config.primary) ? config.primary : [config.primary];
45
+ for (const field of excluded) {
46
+ if (!schemaFields.includes(field)) {
47
+ throw new Error(`defineTable: readExclude field '${field}' is not a schema field on ` +
48
+ `table '${config.Schema.tableName}'.`);
49
+ }
50
+ if (pkFields.includes(field)) {
51
+ throw new Error(`defineTable: readExclude cannot hide the primary key field '${field}' on ` +
52
+ `table '${config.Schema.tableName}' — reads and joins rely on it.`);
53
+ }
54
+ }
55
+ }
38
56
  function validateComputedFields(config) {
39
57
  const computed = config.computedFields;
40
58
  if (!computed)
@@ -1,11 +1,5 @@
1
1
  import type { FastifyInstance } from 'fastify';
2
- import { type SqlApi } from '../../lib/sql-api.js';
3
2
  import type { SqlApiPluginOptions } from '../../types.js';
4
- declare module 'fastify' {
5
- interface FastifyInstance {
6
- sqlApi: SqlApi;
7
- }
8
- }
9
3
  declare const _default: (fastify: FastifyInstance, options: SqlApiPluginOptions) => Promise<void>;
10
4
  export default _default;
11
5
  //# sourceMappingURL=plugin.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAgB,MAAM,SAAS,CAAC;AAO7D,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,sBAAsB,CAAC;AASjE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAU,eAAe;QACvB,MAAM,EAAE,MAAM,CAAC;KAChB;CACF;kCAuDU,eAAe,WACf,mBAAmB,KAC3B,OAAO,CAAC,IAAI,CAAC;AAHhB,wBAiFoC"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAgB,MAAM,SAAS,CAAC;AAa7D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;kCAuD/C,eAAe,WACf,mBAAmB,KAC3B,OAAO,CAAC,IAAI,CAAC;AAHhB,wBA0DoC"}
@@ -1,10 +1,7 @@
1
1
  import fp from 'fastify-plugin';
2
2
  import { ConditionBuilder } from 'node-condition-builder';
3
3
  import { getDialect } from '../../lib/dialect.js';
4
- import { createQueryClient } from '../../lib/db.js';
5
- import { pgQueryable } from '../../lib/adapters/pg-adapter.js';
6
- import { mysqlQueryable } from '../../lib/adapters/mysql-adapter.js';
7
- import { createSqlApi } from '../../lib/sql-api.js';
4
+ import { ensureSqlApiDecorator } from '../../lib/sql-api-decorator.js';
8
5
  import { setupSwagger } from '../../lib/setup-swagger.js';
9
6
  import searchRoutes from './search.routes.js';
10
7
  import getRoutes from './get.routes.js';
@@ -56,31 +53,8 @@ export default fp(async function fastifyAutoSqlApi(fastify, options) {
56
53
  // Set ConditionBuilder dialect globally
57
54
  const dialect = getDialect(options.dialect || 'postgres');
58
55
  ConditionBuilder.DIALECT = dialect.cbDialect;
59
- // Lazy-init QueryClient (internal closure — not decorated on fastify)
60
- let cachedDb;
61
- function getDb() {
62
- if (!cachedDb) {
63
- const pool = (options.dialect === 'mysql' || options.dialect === 'mariadb')
64
- ? mysqlQueryable(fastify.mysql)
65
- : pgQueryable(fastify.pg);
66
- cachedDb = createQueryClient(pool, options.dialect);
67
- if (options.debug)
68
- cachedDb.setDebug(true);
69
- }
70
- return cachedDb;
71
- }
72
56
  // SqlApi: exposed to parent scope via fp
73
- let cachedSqlApi;
74
- fastify.decorate('sqlApi', {
75
- getter() {
76
- if (!cachedSqlApi) {
77
- cachedSqlApi = createSqlApi(getDb(), options.DbTables, {
78
- getTenantId: options.getTenantId,
79
- });
80
- }
81
- return cachedSqlApi;
82
- },
83
- });
57
+ ensureSqlApiDecorator(fastify, options);
84
58
  // Routes in a child scope — prefix applies here, not at fp level
85
59
  const { prefix, ...routeOptions } = options;
86
60
  await fastify.register(async (instance) => {
@@ -1 +1 @@
1
- {"version":3,"file":"route-helpers.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/route-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,KAAK,EAAE,MAAM,EAAE,mBAAmB,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAE5F,KAAK,WAAW,GAAG,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,GAAG,YAAY,CAAC,CAAC;AAElG,wBAAgB,eAAe,CAAC,OAAO,EAAE,mBAAmB,EAAE,SAAS,EAAE,MAAM,GAAG,WAAW,EAAE,CAE9F;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAQlG;AAED,UAAU,WAAW;IAEnB,IAAI,CAAC,EAAE,GAAG,CAAC;IAEX,MAAM,CAAC,EAAE,GAAG,CAAC;IAEb,WAAW,CAAC,EAAE,GAAG,CAAC;IAElB,QAAQ,CAAC,EAAE,GAAG,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,iGAAiG;IACjG,SAAS,EAAE,cAAc,CAAC;IAC1B,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,CAAC;IAC1C,gFAAgF;IAChF,GAAG,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC;IACnC,aAAa,EAAE,GAAG,GAAG,GAAG,CAAC;IACzB,wIAAwI;IACxI,OAAO,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,WAAW,CAAC;IACnF,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,KAAK,MAAM,CAAC;IACpF;2EACuE;IACvE,MAAM,EAAE,CACN,OAAO,EAAE,eAAe,EACxB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,cAAc,EACvB,KAAK,EAAE,YAAY,KAChB,OAAO,CAAC,OAAO,CAAC,CAAC;CACvB;AAED;;;;GAIG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,EAC5B,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC,IAAI,CAAC,CAiCf"}
1
+ {"version":3,"file":"route-helpers.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/route-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE7E,OAAO,KAAK,EAAE,MAAM,EAAE,mBAAmB,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAE5F,KAAK,WAAW,GAAG,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,GAAG,YAAY,CAAC,CAAC;AAElG,wBAAgB,eAAe,CAAC,OAAO,EAAE,mBAAmB,EAAE,SAAS,EAAE,MAAM,GAAG,WAAW,EAAE,CAE9F;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAQlG;AAED,UAAU,WAAW;IAEnB,IAAI,CAAC,EAAE,GAAG,CAAC;IAEX,MAAM,CAAC,EAAE,GAAG,CAAC;IAEb,WAAW,CAAC,EAAE,GAAG,CAAC;IAElB,QAAQ,CAAC,EAAE,GAAG,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,iGAAiG;IACjG,SAAS,EAAE,cAAc,CAAC;IAC1B,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,CAAC;IAC1C,gFAAgF;IAChF,GAAG,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC;IACnC,aAAa,EAAE,GAAG,GAAG,GAAG,CAAC;IACzB,wIAAwI;IACxI,OAAO,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,WAAW,CAAC;IACnF,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,KAAK,MAAM,CAAC;IACpF;2EACuE;IACvE,MAAM,EAAE,CACN,OAAO,EAAE,eAAe,EACxB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,cAAc,EACvB,KAAK,EAAE,YAAY,KAChB,OAAO,CAAC,OAAO,CAAC,CAAC;CACvB;AAED;;;;GAIG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,EAC5B,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC,IAAI,CAAC,CAqCf"}
@@ -1,3 +1,4 @@
1
+ import { ensureSqlApiDecorator } from '../../lib/sql-api-decorator.js';
1
2
  export function mergeOnRequests(options, tableConf) {
2
3
  return [...(options.onRequests || []), ...(tableConf.onRequests || [])];
3
4
  }
@@ -16,6 +17,9 @@ export function buildWriteDescription(action, tableName, tableConf) {
16
17
  * default reply when handler returns a value.
17
18
  */
18
19
  export async function registerForAllTables(fastify, options, spec) {
20
+ // Granular composition support: when a route plugin is registered without the
21
+ // main plugin, no ancestor has decorated `sqlApi` — create it in this scope.
22
+ ensureSqlApiDecorator(fastify, options);
19
23
  const { DbTables } = options;
20
24
  for (const [tableName, tableConf] of Object.entries(DbTables)) {
21
25
  // operations acts as a whitelist; when omitted every operation is exposed.
@@ -15,10 +15,20 @@ export interface ComputedFieldContext {
15
15
  }
16
16
  /**
17
17
  * Result returned by a ComputedFieldFn.
18
- * - `expr`: SQL fragment usable as a scalar expression.
19
- * - `values`: bound parameter values (always parameterized — never interpolate
20
- * user-derived data into `expr`).
18
+ * - `expr`: SQL fragment usable as a scalar expression. Mark each bound value with `?`
19
+ * (use `\?` for a literal question mark, e.g. the PostgreSQL jsonb operator). The engine
20
+ * assigns the placeholder positions — never write `$1` / `db.ph(n)` yourself, and never
21
+ * interpolate user-derived data into the SQL.
22
+ * - `values`: bound parameter values, one per `?` marker in `expr`. A mismatch between
23
+ * markers and values is rejected: it would bind values the query never references.
21
24
  * - `type`: TypeBox schema for Swagger filters and main response (REQUIRED).
25
+ *
26
+ * Example:
27
+ * ({ qiCol }) => ({
28
+ * expr: `CASE WHEN ${qiCol('role')} = ? THEN ${qiCol('salary')} ELSE 0 END`,
29
+ * values: ['admin'],
30
+ * type: Type.Number(),
31
+ * })
22
32
  */
23
33
  export interface ComputedFieldExpr {
24
34
  expr: string;
@@ -1 +1 @@
1
- {"version":3,"file":"computed.d.ts","sourceRoot":"","sources":["../../src/types/computed.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD;;;;;;GAMG;AACH,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,WAAW,CAAC;IAChB,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,MAAM,CAAC;CAC7D;AAED;;;;;;GAMG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,EAAE,CAAC;IAClB,IAAI,EAAE,OAAO,CAAC;CACf;AAED,MAAM,MAAM,eAAe,GAAG,CAAC,GAAG,EAAE,oBAAoB,KAAK,iBAAiB,CAAC"}
1
+ {"version":3,"file":"computed.d.ts","sourceRoot":"","sources":["../../src/types/computed.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD;;;;;;GAMG;AACH,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,WAAW,CAAC;IAChB,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,MAAM,CAAC;CAC7D;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,EAAE,CAAC;IAClB,IAAI,EAAE,OAAO,CAAC;CACf;AAED,MAAM,MAAM,eAAe,GAAG,CAAC,GAAG,EAAE,oBAAoB,KAAK,iBAAiB,CAAC"}
@@ -102,6 +102,19 @@ export interface ITable<F extends Record<string, TSchema> = Record<string, TSche
102
102
  * affected.
103
103
  */
104
104
  operations?: TableOperation[];
105
+ /**
106
+ * Fields hidden from every read: they are not projected by search/get, not included
107
+ * in the read response schemas, and omitted from this table's default join selection
108
+ * when it is the target of a join.
109
+ *
110
+ * Referencing an excluded field from filters, conditions, orderBy, aggregations or an
111
+ * explicit join selection is rejected with 400 — hiding a field from the output while
112
+ * letting it be filtered would still leak its value by bisection.
113
+ *
114
+ * Write paths are NOT affected: a field can be writable but never readable (e.g. a
115
+ * password hash). Primary-key fields cannot be excluded.
116
+ */
117
+ readExclude?: (string & keyof F)[];
105
118
  defaultOrder?: string;
106
119
  excludeFromCreation?: (string & keyof F)[];
107
120
  distinctResults?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"table.d.ts","sourceRoot":"","sources":["../../src/types/table.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"table.d.ts","sourceRoot":"","sources":["../../src/types/table.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fastify-auto-sqlapi",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
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",
@@ -64,7 +64,7 @@
64
64
  "homepage": "https://github.com/LucaRainone/fastify-auto-sqlapi#readme",
65
65
  "dependencies": {
66
66
  "@sinclair/typebox": "^0.34.14",
67
- "node-condition-builder": "^1.0.6"
67
+ "node-condition-builder": "^1.2.0"
68
68
  },
69
69
  "devDependencies": {
70
70
  "@fastify/postgres": "^6.0.2",