fastify-auto-sqlapi 0.1.5 → 0.1.7

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
@@ -485,12 +485,22 @@ type ComputedFieldFn = (ctx: {
485
485
  db: QueryClient; // dialect-aware (qi, ph, dialectName, dateTrunc, ...)
486
486
  qiCol(field: string, opts?: { qualifier?: string }): string;
487
487
  }) => {
488
- expr: string; // SQL fragment
489
- 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
490
490
  type: TSchema; // REQUIRED — used by Swagger and body validation
491
491
  };
492
492
  ```
493
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
+
494
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.
495
505
 
496
506
  ### Validation & startup checks
@@ -505,8 +515,9 @@ For `joinLeft` specifically, the computed expr is automatically alias-prefixed b
505
515
 
506
516
  ### Limitations (first round, by design)
507
517
 
508
- - **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.
509
- - **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.
510
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.
511
522
  - **No chained computed** (a computed referencing another computed). Flat-only.
512
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.
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
@@ -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,CA2HhG;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,CA6HhG;AAID,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,MAAM,CAqBrE;AAID,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,MAAM,CA6FlE"}
@@ -151,7 +151,9 @@ export function generateSingleTableFile(schema, allSchemas) {
151
151
  lines.push(` // Schema,`);
152
152
  lines.push(` // extraFiltersValidation,`);
153
153
  lines.push(` // (condition, filters) => {`);
154
- lines.push(` // if (filters.q) condition.isILike(Schema.col('${schema.fields.includes('name') ? 'name' : schema.fields[0]}'), \`%\${filters.q}%\`);`);
154
+ lines.push(` // // Prefix columns with the table name: the query may carry joins, and a`);
155
+ lines.push(` // // bare column shared with a joined table would be ambiguous.`);
156
+ lines.push(` // if (filters.q) condition.isILike(\`\${Schema.tableName}.\${Schema.col('${schema.fields.includes('name') ? 'name' : schema.fields[0]}')}\`, \`%\${filters.q}%\`);`);
155
157
  lines.push(` // }`);
156
158
  lines.push(` // ),`);
157
159
  lines.push(` defaultOrder: '${pkFirst(pk)}',`);
@@ -0,0 +1,40 @@
1
+ import type { ConditionBuilder } from 'node-condition-builder';
2
+ import type { QueryClient } from '../db.js';
3
+ /**
4
+ * Owns the bound values of a statement while it is being assembled.
5
+ *
6
+ * A statement is built from several fragments (WHERE condition, EXISTS subqueries,
7
+ * aggregation clauses, LEFT JOIN filters, ORDER BY), each of which must number its
8
+ * placeholders from wherever the preceding fragments left off. Computing that offset at the
9
+ * call site is how placeholders drift out of step with values — a query that binds a value
10
+ * nobody references does not fail, it silently reads the wrong parameter.
11
+ *
12
+ * Here the offset is derived in exactly one place. `emit` hands a producer the index its
13
+ * first placeholder must take and absorbs the values it returned in the same step, so SQL
14
+ * and values cannot be appended independently.
15
+ *
16
+ * Fragments must be emitted in the order their placeholders appear in the final SQL: MySQL
17
+ * binds `?` positionally, so a fragment numbered out of textual order would break there while
18
+ * working on PostgreSQL.
19
+ */
20
+ export declare class QueryParams {
21
+ private values;
22
+ /** Values bound so far. */
23
+ get length(): number;
24
+ /**
25
+ * Render a fragment at the current offset and record the values it bound.
26
+ * Returns whatever the producer returned, so callers can read its SQL and any extras.
27
+ */
28
+ emit<T extends {
29
+ values: unknown[];
30
+ }>(produce: (startIndex: number) => T): T;
31
+ /** Render a ConditionBuilder at the current offset and return its SQL. */
32
+ emitCondition(condition: ConditionBuilder, db: QueryClient): string;
33
+ /**
34
+ * Copy of the values bound so far. Take a snapshot before emitting a fragment that only
35
+ * the main query carries (ORDER BY), so the COUNT/aggregate queries can bind the subset
36
+ * they actually reference.
37
+ */
38
+ snapshot(): unknown[];
39
+ }
40
+ //# sourceMappingURL=query-params.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query-params.d.ts","sourceRoot":"","sources":["../../../src/lib/engine/query-params.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAE5C;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,MAAM,CAAiB;IAE/B,2BAA2B;IAC3B,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED;;;OAGG;IACH,IAAI,CAAC,CAAC,SAAS;QAAE,MAAM,EAAE,OAAO,EAAE,CAAA;KAAE,EAAE,OAAO,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,CAAC,GAAG,CAAC;IAM5E,0EAA0E;IAC1E,aAAa,CAAC,SAAS,EAAE,gBAAgB,EAAE,EAAE,EAAE,WAAW,GAAG,MAAM;IAOnE;;;;OAIG;IACH,QAAQ,IAAI,OAAO,EAAE;CAGtB"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Owns the bound values of a statement while it is being assembled.
3
+ *
4
+ * A statement is built from several fragments (WHERE condition, EXISTS subqueries,
5
+ * aggregation clauses, LEFT JOIN filters, ORDER BY), each of which must number its
6
+ * placeholders from wherever the preceding fragments left off. Computing that offset at the
7
+ * call site is how placeholders drift out of step with values — a query that binds a value
8
+ * nobody references does not fail, it silently reads the wrong parameter.
9
+ *
10
+ * Here the offset is derived in exactly one place. `emit` hands a producer the index its
11
+ * first placeholder must take and absorbs the values it returned in the same step, so SQL
12
+ * and values cannot be appended independently.
13
+ *
14
+ * Fragments must be emitted in the order their placeholders appear in the final SQL: MySQL
15
+ * binds `?` positionally, so a fragment numbered out of textual order would break there while
16
+ * working on PostgreSQL.
17
+ */
18
+ export class QueryParams {
19
+ values = [];
20
+ /** Values bound so far. */
21
+ get length() {
22
+ return this.values.length;
23
+ }
24
+ /**
25
+ * Render a fragment at the current offset and record the values it bound.
26
+ * Returns whatever the producer returned, so callers can read its SQL and any extras.
27
+ */
28
+ emit(produce) {
29
+ const out = produce(this.values.length + 1);
30
+ this.values.push(...out.values);
31
+ return out;
32
+ }
33
+ /** Render a ConditionBuilder at the current offset and return its SQL. */
34
+ emitCondition(condition, db) {
35
+ return this.emit((startIndex) => ({
36
+ sql: condition.build(startIndex, db.ph),
37
+ values: condition.getValues(),
38
+ })).sql;
39
+ }
40
+ /**
41
+ * Copy of the values bound so far. Take a snapshot before emitting a fragment that only
42
+ * the main query carries (ORDER BY), so the COUNT/aggregate queries can bind the subset
43
+ * they actually reference.
44
+ */
45
+ snapshot() {
46
+ return [...this.values];
47
+ }
48
+ }
@@ -1 +1 @@
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"}
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,CAgCrE"}
@@ -8,10 +8,12 @@ export async function getEngine(params) {
8
8
  const { db, tableConf, id, tenant } = params;
9
9
  const pkCol = tableConf.Schema.col(primaryAsString(tableConf.primary));
10
10
  const cb = new ConditionBuilder('AND', db.cbDialect);
11
- cb.isEqual(db.qi(pkCol), id);
11
+ // Table-qualified: the tenant scope may add a through-join, and a bare PK column
12
+ // shared with the through table would be ambiguous.
13
+ cb.isEqual(`${db.qi(tableConf.Schema.tableName)}.${db.qi(pkCol)}`, id);
12
14
  const joins = [];
13
15
  if (tenant) {
14
- cb.append(buildTenantCondition(db, tenant.scope, tenant.ids));
16
+ cb.append(buildTenantCondition(db, tenant.scope, tenant.ids, tableConf.Schema.tableName));
15
17
  if ('through' in tenant.scope) {
16
18
  joins.push(buildTenantJoin(db, tenant.scope, tableConf.Schema.tableName));
17
19
  }
@@ -1 +1 @@
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;AA+nC3B,wBAAsB,YAAY,CAChC,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,YAAY,CAAC,CA4IvB"}
1
+ {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/search/search.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EACV,QAAQ,EACR,YAAY,EACZ,YAAY,EAcb,MAAM,mBAAmB,CAAC;AAwlC3B,wBAAsB,YAAY,CAChC,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,YAAY,CAAC,CA4HvB"}
@@ -1,5 +1,6 @@
1
- import { ConditionBuilder } from 'node-condition-builder';
1
+ import { ConditionBuilder, Expression } from 'node-condition-builder';
2
2
  import { camelcaseObject } from '../../naming.js';
3
+ import { QueryParams } from '../query-params.js';
3
4
  import { buildTenantCondition, buildTenantJoin } from '../../tenant.js';
4
5
  import { assertReadable, assertFiltersReadable, readableSelectColumns } from '../../read-access.js';
5
6
  import { primaryAsString } from '../../../types.js';
@@ -32,7 +33,9 @@ function buildComputedContext(db, schema, alias) {
32
33
  err400(`Unknown field referenced by computed expression: ${field}`);
33
34
  }
34
35
  const col = db.qi(schema.col(field));
35
- const qualifier = opts?.qualifier ?? alias;
36
+ // Default to the owning table: the statement may carry joins, and a bare
37
+ // column shared with a joined table would be ambiguous.
38
+ const qualifier = opts?.qualifier ?? alias ?? schema.tableName;
36
39
  return qualifier ? `${db.qi(qualifier)}.${col}` : col;
37
40
  },
38
41
  };
@@ -41,14 +44,15 @@ function resolveFieldRef(field, schema, tableConf, db, alias) {
41
44
  if (field in schema.fields) {
42
45
  assertReadable(tableConf, field);
43
46
  const col = db.qi(schema.col(field));
44
- const expr = alias ? `${db.qi(alias)}.${col}` : col;
45
- return { expr, values: [], computed: false };
47
+ // Qualified with the alias or the owning table — never bare, so the reference
48
+ // stays unambiguous when the statement carries joins.
49
+ const expr = `${db.qi(alias ?? schema.tableName)}.${col}`;
50
+ return { expr, computed: false };
46
51
  }
47
52
  const fn = tableConf?.computedFields?.[field];
48
53
  if (fn) {
49
54
  const ctx = buildComputedContext(db, schema, alias);
50
- const out = fn(ctx);
51
- return { expr: out.expr, values: out.values ?? [], computed: true };
55
+ return { expr: computedExpression(field, fn(ctx)), computed: true };
52
56
  }
53
57
  err400(`Unknown field: ${field}`);
54
58
  }
@@ -58,16 +62,28 @@ function resolveFieldRef(field, schema, tableConf, db, alias) {
58
62
  * appends the tenant condition to `cb` (direct: `col IN ids`; indirect: condition on the
59
63
  * through table) and, for indirect scopes, pushes the INNER JOIN clause into `joins`.
60
64
  * No-op without a tenant context (admin) or when the join table is not tenant-scoped.
65
+ *
66
+ * `joinTableRef` is the raw name the join table goes by in the statement: its table name
67
+ * in plain side-queries, or the subquery alias inside an aliased, correlated EXISTS.
61
68
  */
62
- function appendJoinTenantScope(db, joinTableConf, tenant, joinTableName, cb, joins) {
69
+ function appendJoinTenantScope(db, joinTableConf, tenant, joinTableRef, cb, joins) {
63
70
  const scope = joinTableConf?.tenantScope;
64
71
  if (!tenant || !scope)
65
72
  return;
66
- cb.append(buildTenantCondition(db, scope, tenant.ids));
73
+ cb.append(buildTenantCondition(db, scope, tenant.ids, joinTableRef));
67
74
  if ('through' in scope) {
68
- joins?.push(buildTenantJoin(db, scope, joinTableName));
75
+ joins?.push(buildTenantJoin(db, scope, joinTableRef));
69
76
  }
70
77
  }
78
+ /**
79
+ * Alias for the FROM of a correlated subquery (EXISTS, aggregate). The subquery must not
80
+ * say `FROM <table>` bare: on a self-referencing relation the inner table would shadow the
81
+ * outer one and the correlation would silently compare each row with itself. The request
82
+ * alias is used, suffixed when it would itself collide with the outer table's name.
83
+ */
84
+ function subqueryAlias(alias, tableConf) {
85
+ return alias === tableConf.Schema.tableName ? `${alias}_sub` : alias;
86
+ }
71
87
  function findJoinByAlias(tableConf, alias) {
72
88
  return tableConf.allowedReadJoins?.find((j) => j.alias === alias);
73
89
  }
@@ -98,16 +114,48 @@ function extractJoinRefs(db, tableConf, joinDef) {
98
114
  };
99
115
  }
100
116
  /**
101
- * Evaluate a computed field. When `allowBoundValues=false`, throws 400 if the computed binds
102
- * any parameters — used in positions where parameter binding cannot be coordinated (orderBy,
103
- * compute*, joinLeft.filters, etc).
117
+ * Evaluate a computed field into an Expression carrying its own bound values.
118
+ *
119
+ * Inside a WHERE clause the ConditionBuilder assigns the placeholder indexes, so bound
120
+ * values are always safe there. `allowBoundValues=false` is for the positions that sit
121
+ * *before* the WHERE values in the parameter order (SELECT projections, aggregations),
122
+ * where they cannot be placed correctly; those still reject with 400.
104
123
  */
105
- function evaluateComputedField(fn, schema, db, alias, message, allowBoundValues = false) {
124
+ function evaluateComputedField(name, fn, schema, db, alias, message, allowBoundValues = false) {
106
125
  const ev = fn(buildComputedContext(db, schema, alias));
107
126
  if (!allowBoundValues && (ev.values?.length ?? 0) > 0) {
108
127
  err400(message);
109
128
  }
110
- return ev;
129
+ return computedExpression(name, ev);
130
+ }
131
+ /**
132
+ * Build the Expression for a computed field, checking that it marks each bound value
133
+ * with a `?`.
134
+ *
135
+ * A mismatch is always a table-configuration bug and would otherwise produce a query that
136
+ * binds values nobody references — silently returning wrong rows. Expressions without
137
+ * bound values are exempt: they are emitted verbatim, so a `?` there is a literal operator
138
+ * (PostgreSQL jsonb) rather than a marker.
139
+ */
140
+ function computedExpression(name, ev) {
141
+ const values = ev.values ?? [];
142
+ if (values.length > 0) {
143
+ const markers = (ev.expr.match(/\\\?|\?/g) ?? []).filter((m) => m === '?').length;
144
+ if (markers !== values.length) {
145
+ throw new Error(`Computed field '${name}' declares ${values.length} bound value(s) but its expression ` +
146
+ `contains ${markers} '?' marker(s). Mark each bound value with '?' (use '\\?' for a ` +
147
+ `literal question mark) so the engine can assign placeholder positions.`);
148
+ }
149
+ }
150
+ return new Expression(ev.expr, values);
151
+ }
152
+ /** Bound values carried by a resolved field reference. */
153
+ function refValues(expr) {
154
+ return expr instanceof Expression ? [...expr.values] : [];
155
+ }
156
+ /** Render a resolved field reference to SQL, binding any values from `startIndex`. */
157
+ function renderRef(expr, startIndex, db) {
158
+ return expr instanceof Expression ? expr.render(startIndex, db.ph) : expr;
111
159
  }
112
160
  /** Map rows from snake_case columns to camelCase fields, preserving any computed-field columns
113
161
  * that are projected by their declared name (skipped by camelcaseObject). */
@@ -133,9 +181,10 @@ function buildSelectionColumns(selection, joinSchema, db, joinTableConf) {
133
181
  if (selection === '*') {
134
182
  return readableSelectColumns(joinTableConf, joinSchema, db) ?? '*';
135
183
  }
184
+ const table = db.qi(joinSchema.tableName);
136
185
  return selection
137
186
  .split(',')
138
- .map((c) => db.qi(validateSchemaField(c.trim(), joinSchema, joinTableConf)))
187
+ .map((c) => `${table}.${db.qi(validateSchemaField(c.trim(), joinSchema, joinTableConf))}`)
139
188
  .join(', ');
140
189
  }
141
190
  const AGG_FN = {
@@ -149,7 +198,12 @@ const AGG_FN = {
149
198
  function aggExpr(fn, qualifiedCol) {
150
199
  return fn.distinct ? `COUNT(DISTINCT ${qualifiedCol})` : `${fn.sql}(${qualifiedCol})`;
151
200
  }
152
- function buildAggOrderExpr(db, dbTables, tableConf, alias, fn, field, joinGroup, startIdx) {
201
+ /**
202
+ * Correlated aggregate subquery for `<alias>.<fn>.<field>`, as an Expression carrying the
203
+ * values of its optional filter. Markers are `?`, so whoever embeds it — a ConditionBuilder
204
+ * or an ORDER BY list — decides the placeholder positions.
205
+ */
206
+ function buildAggOrderExpr(db, dbTables, tableConf, alias, fn, field, joinGroup) {
153
207
  const aggFn = AGG_FN[fn];
154
208
  if (!aggFn)
155
209
  err400(`Invalid aggregation function: ${fn}`);
@@ -167,20 +221,24 @@ function buildAggOrderExpr(db, dbTables, tableConf, alias, fn, field, joinGroup,
167
221
  }
168
222
  const fieldCol = validateSchemaField(field, joinSchema);
169
223
  const refs = extractJoinRefs(db, tableConf, joinDef);
224
+ // The subquery FROM is aliased so the correlation to the outer table survives a
225
+ // self-referencing relation; everything inside references the alias.
226
+ const subAlias = subqueryAlias(alias, tableConf);
227
+ const subRef = db.qi(subAlias);
170
228
  let filterWhere = '';
171
229
  let filterVals = [];
172
230
  const joinTableConf = dbTables[joinSchema.tableName];
173
231
  if (groupReq.filters || groupReq.conditions?.length) {
174
- const cb = buildJoinRefCondition(joinTableConf, joinSchema, { filters: groupReq.filters, conditions: groupReq.conditions }, db);
175
- const built = cb.build(startIdx, db.ph);
176
- if (built) {
177
- filterWhere = ` AND ${built}`;
178
- filterVals = cb.getValues();
232
+ const cb = buildJoinRefCondition(joinTableConf, joinSchema, { filters: groupReq.filters, conditions: groupReq.conditions }, db, subAlias);
233
+ const fragment = cb.toExpression();
234
+ if (fragment.value) {
235
+ filterWhere = ` AND ${fragment.value}`;
236
+ filterVals = [...fragment.values];
179
237
  }
180
238
  }
181
- const qualifiedCol = `${refs.joinTable}.${db.qi(fieldCol)}`;
182
- const expr = `COALESCE((SELECT ${aggExpr(aggFn, qualifiedCol)} FROM ${refs.joinTable} WHERE ${refs.joinTable}.${refs.fkCol} = ${refs.mainTable}.${refs.mainCol}${filterWhere}), 0)`;
183
- return { expr, values: filterVals };
239
+ const qualifiedCol = `${subRef}.${db.qi(fieldCol)}`;
240
+ const expr = `COALESCE((SELECT ${aggExpr(aggFn, qualifiedCol)} FROM ${refs.joinTable} AS ${subRef} WHERE ${subRef}.${refs.fkCol} = ${refs.mainTable}.${refs.mainCol}${filterWhere}), 0)`;
241
+ return new Expression(expr, filterVals);
184
242
  }
185
243
  /**
186
244
  * Pre-scan `orderBy` for the joinLeft aliases referenced in 2-parti notation (`<alias>.<field>`).
@@ -219,10 +277,11 @@ function validateOrderBy(orderBy, tableConf, db, dbTables, joinGroup, startIdx)
219
277
  err400('Cannot combine distinctResults with aggregation orderBy');
220
278
  }
221
279
  const [, alias, fn, field, dir] = dotted3;
222
- const { expr, values } = buildAggOrderExpr(db, dbTables, tableConf, alias, fn, field, joinGroup, currentIdx);
223
- outParts.push(`${expr} ${(dir || 'ASC').toUpperCase()}`);
224
- outValues.push(...values);
225
- currentIdx += values.length;
280
+ const expr = buildAggOrderExpr(db, dbTables, tableConf, alias, fn, field, joinGroup);
281
+ const aggVals = [...expr.values];
282
+ outParts.push(`${renderRef(expr, currentIdx, db)} ${(dir || 'ASC').toUpperCase()}`);
283
+ outValues.push(...aggVals);
284
+ currentIdx += aggVals.length;
226
285
  continue;
227
286
  }
228
287
  // 2-parti: <alias>.<field> [ASC|DESC] (joinLeft inline ordering)
@@ -243,9 +302,12 @@ function validateOrderBy(orderBy, tableConf, db, dbTables, joinGroup, startIdx)
243
302
  }
244
303
  const [, field, dir] = plain;
245
304
  const ref = resolveFieldRef(field, tableConf.Schema, tableConf, db);
246
- outParts.push(`${ref.expr} ${(dir || 'ASC').toUpperCase()}`);
247
- outValues.push(...ref.values);
248
- currentIdx += ref.values.length;
305
+ // A computed field binds its own values here: ORDER BY is emitted after the WHERE, so
306
+ // its placeholders continue from `currentIdx`.
307
+ const refVals = refValues(ref.expr);
308
+ outParts.push(`${renderRef(ref.expr, currentIdx, db)} ${(dir || 'ASC').toUpperCase()}`);
309
+ outValues.push(...refVals);
310
+ currentIdx += refVals.length;
249
311
  }
250
312
  return { sql: outParts.join(', '), values: outValues, leftJoinAliases };
251
313
  }
@@ -279,9 +341,10 @@ function buildLeftJoinClauses(db, dbTables, tableConf, aliasesNeedingJoin, joinL
279
341
  cb.isEqual(`${aliasIdent}.${db.qi(joinSchema.col(field))}`, value);
280
342
  }
281
343
  else if (computed?.[field]) {
282
- // Computed field on the parent table: build expr with alias-qualified columns.
283
- const ev = evaluateComputedField(computed[field], joinSchema, db, alias, `Computed field '${field}' with bound values is not supported in joinLeft.filters`);
284
- cb.isEqual(ev.expr, value);
344
+ // Computed field on the parent table: expr with alias-qualified columns. Bound
345
+ // values are placed by this ConditionBuilder, whose SQL lands in the WHERE.
346
+ const expr = evaluateComputedField(field, computed[field], joinSchema, db, alias, '', true);
347
+ cb.isEqual(expr, value);
285
348
  }
286
349
  }
287
350
  }
@@ -296,8 +359,8 @@ function buildLeftJoinClauses(db, dbTables, tableConf, aliasesNeedingJoin, joinL
296
359
  dispatchConditionMethod(cb, c.method, col, c.params ?? []);
297
360
  }
298
361
  else if (computed?.[c.field]) {
299
- const ev = evaluateComputedField(computed[c.field], joinSchema, db, alias, `Computed field '${c.field}' with bound values is not supported in joinLeft.conditions`);
300
- dispatchConditionMethod(cb, c.method, ev.expr, c.params ?? []);
362
+ const expr = evaluateComputedField(c.field, computed[c.field], joinSchema, db, alias, '', true);
363
+ dispatchConditionMethod(cb, c.method, expr, c.params ?? []);
301
364
  }
302
365
  else {
303
366
  err400(`Unknown field: ${c.field}`);
@@ -333,12 +396,12 @@ function convertConfiguredOrder(order, tableConf, db) {
333
396
  const [, field, dir] = match;
334
397
  const suffix = dir ? ` ${dir.toUpperCase()}` : '';
335
398
  if (field in tableConf.Schema.fields) {
336
- return `${db.qi(tableConf.Schema.col(field))}${suffix}`;
399
+ return `${db.qi(tableConf.Schema.tableName)}.${db.qi(tableConf.Schema.col(field))}${suffix}`;
337
400
  }
338
401
  const computed = tableConf.computedFields?.[field];
339
402
  if (computed) {
340
- const ev = evaluateComputedField(computed, tableConf.Schema, db, undefined, `Computed field '${field}' with bound values cannot be used in defaultOrder`);
341
- return `${ev.expr}${suffix}`;
403
+ const ev = evaluateComputedField(field, computed, tableConf.Schema, db, undefined, `Computed field '${field}' with bound values cannot be used in defaultOrder`);
404
+ return `${ev.value}${suffix}`;
342
405
  }
343
406
  }
344
407
  return trimmed;
@@ -366,8 +429,8 @@ async function executeMainQuery(db, tableConf, where, values, orderBy, paginator
366
429
  const fn = tableConf.computedFields?.[name];
367
430
  if (!fn)
368
431
  err400(`Unknown computed field in selectComputed: '${name}'`);
369
- const out = evaluateComputedField(fn, tableConf.Schema, db, undefined, `Computed field '${name}' with bound values cannot be used in selectComputed`);
370
- projections.push(`${out.expr} AS ${db.qi(name)}`);
432
+ const out = evaluateComputedField(name, fn, tableConf.Schema, db, undefined, `Computed field '${name}' with bound values cannot be used in selectComputed`);
433
+ projections.push(`${out.value} AS ${db.qi(name)}`);
371
434
  }
372
435
  columns = projections.join(', ');
373
436
  }
@@ -398,10 +461,12 @@ async function buildPagination(db, tableConf, where, values, paginator, extraJoi
398
461
  for (const { key, field, fn } of computations) {
399
462
  if (field) {
400
463
  const ref = resolveFieldRef(field, tableConf.Schema, tableConf, db);
401
- if (ref.values.length > 0) {
464
+ // The aggregate sits in the SELECT list, before the WHERE values: a computed that
465
+ // binds parameters cannot be placed here.
466
+ if (refValues(ref.expr).length > 0) {
402
467
  err400(`Computed field '${field}' with bound values cannot be used in compute${key.charAt(0).toUpperCase() + key.slice(1)}`);
403
468
  }
404
- const result = await db.query(`SELECT ${fn}(${ref.expr}) as value FROM ${db.qi(tableName)}${joinClause} WHERE ${where}`, values);
469
+ const result = await db.query(`SELECT ${fn}(${renderRef(ref.expr, 1, db)}) as value FROM ${db.qi(tableName)}${joinClause} WHERE ${where}`, values);
405
470
  computed[key] = { [field]: result.rows[0].value };
406
471
  }
407
472
  }
@@ -426,16 +491,11 @@ async function executeJoinMultiple(db, dbTables, tableConf, mainResults, joinMul
426
491
  }
427
492
  const cb = buildJoinRefCondition(joinTableConf, joinSchema, ref || {}, db);
428
493
  const fkCol = joinSchema.col(joinField);
429
- cb.isIn(db.qi(fkCol), ids);
494
+ cb.isIn(`${db.qi(joinSchema.tableName)}.${db.qi(fkCol)}`, ids);
430
495
  const tenantJoins = [];
431
496
  appendJoinTenantScope(db, joinTableConf, tenant, joinSchema.tableName, cb, tenantJoins);
432
- let where = cb.build(1, db.ph);
433
- let values = cb.getValues();
434
- // Side-channel: computed-field clauses on the join table
435
- const computedClauses = collectJoinRefComputedClauses(joinTableConf, ref || {}, db);
436
- if (computedClauses.length > 0) {
437
- ({ where, values } = appendComputedConditions(where, values, computedClauses, db));
438
- }
497
+ const where = cb.build(1, db.ph);
498
+ const values = cb.getValues();
439
499
  const selection = ref?.selection ?? defaultSelection;
440
500
  const columns = buildSelectionColumns(selection, joinSchema, db, joinTableConf);
441
501
  const rows = await db.select({
@@ -465,7 +525,7 @@ async function executeJoinLeft(db, dbTables, tableConf, mainResults, joinLeft, t
465
525
  }
466
526
  const fkCol = joinSchema.col(joinField);
467
527
  const cb = new ConditionBuilder('AND', db.cbDialect);
468
- cb.isIn(db.qi(fkCol), ids);
528
+ cb.isIn(`${db.qi(joinSchema.tableName)}.${db.qi(fkCol)}`, ids);
469
529
  const tenantJoins = [];
470
530
  appendJoinTenantScope(db, joinTableConf, tenant, joinSchema.tableName, cb, tenantJoins);
471
531
  const where = cb.build(1, db.ph);
@@ -495,11 +555,11 @@ function buildByExpression(by, joinSchema, joinTableConf, db) {
495
555
  err400(`Invalid 'by' specification: expected a field or computed name`);
496
556
  }
497
557
  if (by in joinSchema.fields) {
498
- return db.qi(joinSchema.col(by));
558
+ return `${db.qi(joinSchema.tableName)}.${db.qi(joinSchema.col(by))}`;
499
559
  }
500
560
  const fn = joinTableConf?.computedFields?.[by];
501
561
  if (fn) {
502
- return evaluateComputedField(fn, joinSchema, db, undefined, `Computed field '${by}' with bound values cannot be used in aggregations.by`).expr;
562
+ return evaluateComputedField(by, fn, joinSchema, db, undefined, `Computed field '${by}' with bound values cannot be used in aggregations.by`).value;
503
563
  }
504
564
  err400(`Unknown field: ${by}`);
505
565
  }
@@ -529,7 +589,7 @@ async function executeJoinGroup(db, dbTables, tableConf, mainResults, joinGroup,
529
589
  const fn = AGG_FN[kind];
530
590
  for (const f of fields) {
531
591
  const col = validateSchemaField(f, joinSchema, joinTableConf);
532
- selectParts.push(`${aggExpr(fn, db.qi(col))} as "${kind}_${f}"`);
592
+ selectParts.push(`${aggExpr(fn, `${db.qi(joinSchema.tableName)}.${db.qi(col)}`)} as "${kind}_${f}"`);
533
593
  }
534
594
  };
535
595
  addAgg('distinctCount', aggregations.distinctCount);
@@ -545,16 +605,11 @@ async function executeJoinGroup(db, dbTables, tableConf, mainResults, joinGroup,
545
605
  const groupRef = { filters: groupFilters, conditions: groupConditions };
546
606
  const cb = buildJoinRefCondition(joinTableConf, joinSchema, groupRef, db);
547
607
  const fkCol = joinSchema.col(joinField);
548
- cb.isIn(db.qi(fkCol), ids);
608
+ cb.isIn(`${db.qi(joinSchema.tableName)}.${db.qi(fkCol)}`, ids);
549
609
  const tenantJoins = [];
550
610
  appendJoinTenantScope(db, joinTableConf, tenant, joinSchema.tableName, cb, tenantJoins);
551
- let where = cb.build(1, db.ph);
552
- let values = cb.getValues();
553
- // Side-channel: computed-field clauses on the join table
554
- const computedClauses = collectJoinRefComputedClauses(joinTableConf, groupRef, db);
555
- if (computedClauses.length > 0) {
556
- ({ where, values } = appendComputedConditions(where, values, computedClauses, db));
557
- }
611
+ const where = cb.build(1, db.ph);
612
+ const values = cb.getValues();
558
613
  const groupBy = groupByParts.length > 0 ? `GROUP BY ${groupByParts.join(', ')}` : '';
559
614
  const fromJoins = tenantJoins.length > 0 ? ` ${tenantJoins.join(' ')}` : '';
560
615
  const sql = `SELECT ${selectParts.join(', ')} FROM ${db.qi(joinSchema.tableName)}${fromJoins} WHERE ${where} ${groupBy}`;
@@ -613,65 +668,40 @@ function dispatchConditionMethod(cb, method, colOrExpr, params) {
613
668
  cb[method](colOrExpr, true);
614
669
  }
615
670
  }
616
- function buildJoinRefCondition(joinTableConf, joinSchema, ref, db) {
671
+ function buildJoinRefCondition(joinTableConf, joinSchema, ref, db, qualifier) {
617
672
  assertFiltersReadable(ref.filters, joinTableConf);
673
+ // Bare table name for plain side-queries; the subquery alias when the condition lands
674
+ // inside an aliased, correlated FROM (there the table name would resolve to the outer query).
675
+ const colQualifier = qualifier ?? joinSchema.tableName;
618
676
  const cb = (ref.filters && joinTableConf)
619
- ? joinTableConf.filters(ref.filters, db.cbDialect)
677
+ ? joinTableConf.filters(ref.filters, db.cbDialect, colQualifier)
620
678
  : new ConditionBuilder('AND', db.cbDialect);
621
- if (ref.conditions?.length) {
622
- for (const c of ref.conditions) {
623
- if (!ALLOWED_SET.has(c.method)) {
624
- err400(`Invalid condition method: ${c.method}`);
625
- }
626
- // Skip computed-field conditions here — they are processed via side-channel
627
- // by `collectJoinRefComputedClauses` since ConditionBuilder cannot bind values
628
- // for an LHS expression.
629
- if (joinTableConf?.computedFields?.[c.field])
630
- continue;
631
- const col = db.qi(validateSchemaField(c.field, joinSchema, joinTableConf));
632
- dispatchConditionMethod(cb, c.method, col, c.params ?? []);
633
- }
634
- }
635
- return cb;
636
- }
637
- /**
638
- * Side-channel: gather equality + operator clauses targeting computed fields
639
- * declared on the join table's `computedFields`. To be appended to a WHERE
640
- * after the main `cb.build()` with correct placeholder offsets.
641
- */
642
- function collectJoinRefComputedClauses(joinTableConf, ref, db) {
643
- const out = [];
679
+ // Equality filters targeting the join table's computed fields
644
680
  const computed = joinTableConf?.computedFields;
645
- if (!computed)
646
- return out;
647
- // Equality filters on computed names
648
- if (ref.filters) {
681
+ if (computed && ref.filters) {
649
682
  for (const [name, value] of Object.entries(ref.filters)) {
650
683
  if (value === null || value === undefined)
651
684
  continue;
652
685
  if (!computed[name])
653
686
  continue;
654
- const ev = evaluateComputedField(computed[name], joinTableConf.Schema, db, undefined, '', true);
655
- out.push({ method: 'isEqual', expr: ev.expr, values: ev.values ?? [], params: [value] });
687
+ const expr = evaluateComputedField(name, computed[name], joinTableConf.Schema, db, colQualifier, '', true);
688
+ cb.isEqual(expr, value);
656
689
  }
657
690
  }
658
- // Operator conditions on computed names
659
691
  if (ref.conditions?.length) {
660
692
  for (const c of ref.conditions) {
661
- if (!computed[c.field])
662
- continue;
663
- if (!ALLOWED_SET.has(c.method))
693
+ if (!ALLOWED_SET.has(c.method)) {
664
694
  err400(`Invalid condition method: ${c.method}`);
665
- const ev = evaluateComputedField(computed[c.field], joinTableConf.Schema, db, undefined, '', true);
666
- out.push({
667
- method: c.method,
668
- expr: ev.expr,
669
- values: ev.values ?? [],
670
- params: c.params ?? [],
671
- });
695
+ }
696
+ // Computed fields resolve to an Expression carrying their own bound values, which
697
+ // the ConditionBuilder places together with the compared value.
698
+ const operand = computed?.[c.field]
699
+ ? evaluateComputedField(c.field, computed[c.field], joinTableConf.Schema, db, colQualifier, '', true)
700
+ : `${db.qi(colQualifier)}.${db.qi(validateSchemaField(c.field, joinSchema, joinTableConf))}`;
701
+ dispatchConditionMethod(cb, c.method, operand, c.params ?? []);
672
702
  }
673
703
  }
674
- return out;
704
+ return cb;
675
705
  }
676
706
  /**
677
707
  * Apply non-dotted conditions to the main `condition` ConditionBuilder when
@@ -680,7 +710,6 @@ function collectJoinRefComputedClauses(joinTableConf, ref, db) {
680
710
  * offsets), since ConditionBuilder cannot bind values for an LHS expression.
681
711
  */
682
712
  function applyConditions(condition, conditions, schema, tableConf, db) {
683
- const computedClauses = [];
684
713
  for (const c of conditions) {
685
714
  // Skip dot-notation fields — those become aggregation conditions processed later
686
715
  if (c.field.includes('.'))
@@ -688,67 +717,32 @@ function applyConditions(condition, conditions, schema, tableConf, db) {
688
717
  if (!ALLOWED_SET.has(c.method)) {
689
718
  err400(`Invalid condition method: ${c.method}`);
690
719
  }
720
+ // A computed field resolves to an Expression carrying its own bound values; the
721
+ // ConditionBuilder places them, so no placeholder offset is computed here.
691
722
  const ref = resolveFieldRef(c.field, schema, tableConf, db);
692
- if (!ref.computed) {
693
- dispatchConditionMethod(condition, c.method, ref.expr, c.params);
694
- }
695
- else {
696
- computedClauses.push({
697
- method: c.method,
698
- expr: ref.expr,
699
- values: ref.values,
700
- params: c.params ?? [],
701
- });
702
- }
723
+ dispatchConditionMethod(condition, c.method, ref.expr, c.params);
703
724
  }
704
- return computedClauses;
705
725
  }
706
726
  /**
707
- * Equality filters targeting computed fields: when `filters.<computedName>` is
708
- * set on the request body and the computed exists on `tableConf.computedFields`,
709
- * generate a side-channel clause `<expr> = ?` with the user value as parameter.
710
- * This complements the auto-generated equality on schema fields done by
711
- * `tableConf.filters()`.
727
+ * Equality filters targeting computed fields: `filters.<computedName>` becomes
728
+ * `<expr> = <value>` on the same ConditionBuilder used for schema fields, which
729
+ * `tableConf.filters()` has already populated.
712
730
  */
713
- function collectComputedFilterClauses(filters, tableConf, db) {
714
- const out = [];
731
+ function applyComputedFilters(condition, filters, tableConf, db) {
715
732
  const computed = tableConf.computedFields;
716
733
  if (!computed)
717
- return out;
734
+ return;
718
735
  for (const [name, value] of Object.entries(filters)) {
719
736
  if (value === null || value === undefined)
720
737
  continue;
721
738
  if (!computed[name])
722
739
  continue;
723
- const ref = evaluateComputedField(computed[name], tableConf.Schema, db, undefined, '', true);
724
- out.push({ method: 'isEqual', expr: ref.expr, values: ref.values ?? [], params: [value] });
725
- }
726
- return out;
727
- }
728
- /**
729
- * Append computed-field condition clauses (side-channel from applyConditions)
730
- * to the current WHERE, coordinating placeholder indices with the running
731
- * `values` array.
732
- */
733
- function appendComputedConditions(currentWhere, currentValues, clauses, db) {
734
- let where = currentWhere;
735
- let values = [...currentValues];
736
- for (const c of clauses) {
737
- // Append the computed's own bound values first; the expr placeholders
738
- // (if any) reference them via stable indices set by the consumer.
739
- values = [...values, ...c.values];
740
- const tmpCb = new ConditionBuilder('AND', db.cbDialect);
741
- dispatchConditionMethod(tmpCb, c.method, c.expr, c.params);
742
- const startIdx = values.length + 1;
743
- const clauseSql = tmpCb.build(startIdx, db.ph);
744
- where += ` AND ${clauseSql}`;
745
- values.push(...tmpCb.getValues());
740
+ const expr = evaluateComputedField(name, computed[name], tableConf.Schema, db, undefined, '', true);
741
+ condition.isEqual(expr, value);
746
742
  }
747
- return { where, values };
748
743
  }
749
- function appendAggConditions(currentWhere, currentValues, conditions, db, dbTables, tableConf, joinGroup) {
750
- let where = currentWhere;
751
- let values = [...currentValues];
744
+ function appendAggConditions(params, conditions, db, dbTables, tableConf, joinGroup) {
745
+ let where = '';
752
746
  for (const c of conditions) {
753
747
  if (!c.field.includes('.'))
754
748
  continue;
@@ -760,53 +754,38 @@ function appendAggConditions(currentWhere, currentValues, conditions, db, dbTabl
760
754
  err400(`Invalid dotted field in condition: ${c.field} (expected <alias>.<fn>.<field>)`);
761
755
  }
762
756
  const [alias, fn, field] = parts;
763
- const { expr, values: exprValues } = buildAggOrderExpr(db, dbTables, tableConf, alias, fn, field, joinGroup, values.length + 1);
764
- values = [...values, ...exprValues];
757
+ // The aggregate carries the values of its own filter; passing it as the left-hand side
758
+ // lets the ConditionBuilder place those and the compared value in one step.
759
+ const expr = buildAggOrderExpr(db, dbTables, tableConf, alias, fn, field, joinGroup);
765
760
  const tmpCb = new ConditionBuilder('AND', db.cbDialect);
766
761
  dispatchConditionMethod(tmpCb, c.method, expr, c.params);
767
- const startIdx = values.length + 1;
768
- const clause = tmpCb.build(startIdx, db.ph);
769
- where += ` AND ${clause}`;
770
- values.push(...tmpCb.getValues());
762
+ where += ` AND ${params.emitCondition(tmpCb, db)}`;
771
763
  }
772
- return { where, values };
764
+ return where;
773
765
  }
774
766
  // ─── joinMustExist (EXISTS subquery filter on main) ─────────
775
- function buildJoinMustExistClauses(db, dbTables, tableConf, joinMustExist, currentWhere, currentValues, tenant) {
776
- let where = currentWhere;
777
- const values = [...currentValues];
767
+ function buildJoinMustExistClauses(params, db, dbTables, tableConf, joinMustExist, tenant) {
768
+ let where = '';
778
769
  for (const [alias, ref] of Object.entries(joinMustExist)) {
779
770
  const joinDef = requireJoin(tableConf, alias, false);
780
771
  const { joinSchema } = joinDef;
781
772
  const joinTableConf = dbTables[joinSchema.tableName];
782
773
  const refs = extractJoinRefs(db, tableConf, joinDef);
783
- const filterCondition = buildJoinRefCondition(joinTableConf, joinSchema, ref, db);
774
+ // The EXISTS FROM is aliased so the correlation to the outer table survives a
775
+ // self-referencing relation; everything inside references the alias.
776
+ const subAlias = subqueryAlias(alias, tableConf);
777
+ const subRef = db.qi(subAlias);
778
+ const filterCondition = buildJoinRefCondition(joinTableConf, joinSchema, ref, db, subAlias);
784
779
  const tenantJoins = [];
785
- appendJoinTenantScope(db, joinTableConf, tenant, joinSchema.tableName, filterCondition, tenantJoins);
786
- const startIdx = values.length + 1;
787
- let filterWhere = filterCondition.build(startIdx, db.ph);
788
- let filterVals = [...filterCondition.getValues()];
789
- // Side-channel: computed-field clauses
790
- const computedClauses = collectJoinRefComputedClauses(joinTableConf, ref, db);
791
- if (computedClauses.length > 0) {
792
- let cwhere = filterWhere;
793
- let cvalues = filterVals;
794
- ({ where: cwhere, values: cvalues } = appendComputedConditions(cwhere, [...values, ...cvalues], computedClauses, db));
795
- // appendComputedConditions starts with currentValues; we need just the
796
- // delta belonging to this EXISTS subquery to update filterVals.
797
- filterVals = cvalues.slice(values.length);
798
- filterWhere = cwhere;
799
- }
800
- // Qualify the FK with the join table: the EXISTS FROM may include the tenant
801
- // through-join, so a bare column could become ambiguous.
780
+ appendJoinTenantScope(db, joinTableConf, tenant, subAlias, filterCondition, tenantJoins);
781
+ const filterWhere = params.emitCondition(filterCondition, db);
802
782
  const innerWhere = filterWhere
803
- ? `${refs.joinTable}.${refs.fkCol} = ${refs.mainTable}.${refs.mainCol} AND ${filterWhere}`
804
- : `${refs.joinTable}.${refs.fkCol} = ${refs.mainTable}.${refs.mainCol}`;
783
+ ? `${subRef}.${refs.fkCol} = ${refs.mainTable}.${refs.mainCol} AND ${filterWhere}`
784
+ : `${subRef}.${refs.fkCol} = ${refs.mainTable}.${refs.mainCol}`;
805
785
  const existsJoins = tenantJoins.length > 0 ? ` ${tenantJoins.join(' ')}` : '';
806
- where += ` AND EXISTS (SELECT 1 FROM ${refs.joinTable}${existsJoins} WHERE ${innerWhere})`;
807
- values.push(...filterVals);
786
+ where += ` AND EXISTS (SELECT 1 FROM ${refs.joinTable} AS ${subRef}${existsJoins} WHERE ${innerWhere})`;
808
787
  }
809
- return { where, values };
788
+ return where;
810
789
  }
811
790
  // ─── searchEngine entrypoint ────────────────────────────────
812
791
  export async function searchEngine(dbTables, params) {
@@ -827,44 +806,36 @@ export async function searchEngine(dbTables, params) {
827
806
  // Build main condition
828
807
  assertFiltersReadable(filters, tableConf);
829
808
  const condition = tableConf.filters(filters || {}, db.cbDialect);
830
- // Filters that target computed fields (declared on tableConf.computedFields)
831
- // are applied via the side-channel pattern further down.
832
- let computedFilterClauses = [];
809
+ // Filters and conditions targeting computed fields go on the same ConditionBuilder:
810
+ // a computed resolves to an Expression carrying its own values, and the builder is
811
+ // what assigns every placeholder index.
833
812
  if (filters && tableConf.computedFields) {
834
- computedFilterClauses = collectComputedFilterClauses(filters, tableConf, db);
813
+ applyComputedFilters(condition, filters, tableConf, db);
835
814
  }
836
- let computedConditionClauses = [];
837
815
  if (conditions?.length) {
838
- computedConditionClauses = applyConditions(condition, conditions, tableConf.Schema, tableConf, db);
816
+ applyConditions(condition, conditions, tableConf.Schema, tableConf, db);
839
817
  }
840
818
  // Tenant
841
819
  const tenantJoins = [];
842
820
  if (tenant) {
843
- condition.append(buildTenantCondition(db, tenant.scope, tenant.ids));
821
+ condition.append(buildTenantCondition(db, tenant.scope, tenant.ids, tableConf.Schema.tableName));
844
822
  if ('through' in tenant.scope) {
845
823
  tenantJoins.push(buildTenantJoin(db, tenant.scope, tableConf.Schema.tableName));
846
824
  }
847
825
  }
848
- let where = condition.build(1, db.ph);
849
- let values = [...condition.getValues()];
850
- // Append computed filter (equality) and condition (operator) clauses with
851
- // correct placeholder offsets.
852
- if (computedFilterClauses.length > 0) {
853
- ({ where, values } = appendComputedConditions(where, values, computedFilterClauses, db));
854
- }
855
- if (computedConditionClauses.length > 0) {
856
- ({ where, values } = appendComputedConditions(where, values, computedConditionClauses, db));
857
- }
826
+ // From here the statement is assembled fragment by fragment. `bound` owns the values and
827
+ // hands each fragment the placeholder index it must start from, so no offset is computed
828
+ // at any call site. Fragments must be emitted in the order their placeholders appear in
829
+ // the final SQL — MySQL binds `?` positionally.
830
+ const bound = new QueryParams();
831
+ let where = bound.emitCondition(condition, db);
858
832
  // joinMustExist (EXISTS)
859
833
  if (joinMustExist && Object.keys(joinMustExist).length > 0) {
860
- ({ where, values } = buildJoinMustExistClauses(db, dbTables, tableConf, joinMustExist, where, values, tenant));
834
+ where += buildJoinMustExistClauses(bound, db, dbTables, tableConf, joinMustExist, tenant);
861
835
  }
862
836
  // Aggregation conditions (HAVING-style)
863
- if (conditions?.length) {
864
- const hasAggConditions = conditions.some((c) => c.field.includes('.'));
865
- if (hasAggConditions) {
866
- ({ where, values } = appendAggConditions(where, values, conditions, db, dbTables, tableConf, joinGroup));
867
- }
837
+ if (conditions?.length && conditions.some((c) => c.field.includes('.'))) {
838
+ where += appendAggConditions(bound, conditions, db, dbTables, tableConf, joinGroup);
868
839
  }
869
840
  // Determine which joinLeft aliases need a LEFT JOIN on the main query:
870
841
  // - any alias used in 2-parti orderBy
@@ -880,32 +851,24 @@ export async function searchEngine(dbTables, params) {
880
851
  aliasesNeedingLeftJoin.add(alias);
881
852
  }
882
853
  }
883
- // Build LEFT JOIN clauses + extra WHERE for filtered parents. Their placeholders start right
884
- // after the WHERE values; the same clause SQL is reused by the pagination COUNT/compute queries,
885
- // whose value array is [...WHERE, ...leftJoin] — so this base MUST stay `values.length + 1`.
854
+ // LEFT JOIN clauses + extra WHERE for filtered parents.
886
855
  const extraJoinClauses = [...tenantJoins];
887
- let leftJoinValues = [];
888
856
  if (aliasesNeedingLeftJoin.size > 0) {
889
- const lj = buildLeftJoinClauses(db, dbTables, tableConf, aliasesNeedingLeftJoin, joinLeft, values.length + 1);
857
+ const lj = bound.emit((startIndex) => buildLeftJoinClauses(db, dbTables, tableConf, aliasesNeedingLeftJoin, joinLeft, startIndex));
890
858
  extraJoinClauses.push(...lj.joinClauses);
891
- leftJoinValues = lj.values;
892
859
  for (const w of lj.whereExtras)
893
860
  where += ` AND ${w}`;
894
861
  }
895
- // orderBy is parsed AFTER the LEFT JOIN values so aggregation-orderBy placeholders start past
896
- // them (main query binds [...WHERE, ...leftJoin, ...orderBy]). Parsing it earlier with base
897
- // `values.length + 1` misbinds when BOTH an aggregation orderBy and a filtered joinLeft exist.
862
+ // The pagination COUNT and the compute* queries reuse the WHERE and its joins but drop the
863
+ // ORDER BY, so they bind everything up to this point and nothing after it.
864
+ const whereAndJoinValues = bound.snapshot();
865
+ // ORDER BY is emitted last, so an aggregation-orderBy numbers its placeholders past the
866
+ // WHERE and LEFT JOIN values.
898
867
  let safeOrderBy;
899
- let orderByValues = [];
900
868
  if (orderBy) {
901
- const obResult = validateOrderBy(orderBy, tableConf, db, dbTables, joinGroup, values.length + leftJoinValues.length + 1);
902
- safeOrderBy = obResult.sql;
903
- orderByValues = obResult.values;
904
- }
905
- // Bind WHERE values + LEFT JOIN values + orderBy aggregation values for main query.
906
- // Pagination COUNT/compute include WHERE + LEFT JOIN values (no orderBy).
907
- const whereAndJoinValues = [...values, ...leftJoinValues];
908
- const mainValues = [...whereAndJoinValues, ...orderByValues];
869
+ safeOrderBy = bound.emit((startIndex) => validateOrderBy(orderBy, tableConf, db, dbTables, joinGroup, startIndex)).sql;
870
+ }
871
+ const mainValues = bound.snapshot();
909
872
  const main = await executeMainQuery(db, tableConf, where, mainValues, safeOrderBy, paginator, extraJoinClauses, params.selectComputed, params.maxRows);
910
873
  let pagination;
911
874
  if (paginator) {
@@ -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,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"}
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,CA8BhG;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"}
@@ -9,12 +9,16 @@ function qi(field, dialect) {
9
9
  return getDialect(dialect ?? ConditionBuilder.DIALECT).qi(field);
10
10
  }
11
11
  export function exportTableInfo(Schema, extraFilters = {}, extendedCondition) {
12
- const filters = (filterValues, dialect) => {
12
+ const filters = (filterValues, dialect, qualifier) => {
13
13
  const condition = new ConditionBuilder('AND', dialect);
14
- // Only auto-match real schema fields (DB columns)
14
+ // Only auto-match real schema fields (DB columns). Columns are table-qualified:
15
+ // the statement may carry joins (LEFT JOIN parents, tenant through-joins), and a
16
+ // bare column shared with a joined table would be ambiguous. A caller-provided
17
+ // qualifier (a subquery alias) takes precedence over the table name.
18
+ const table = qi(qualifier ?? Schema.tableName, dialect);
15
19
  for (const field of Object.keys(Schema.fields)) {
16
20
  if (field in filterValues && filterValues[field] !== null && filterValues[field] !== undefined) {
17
- condition.isEqual(qi(Schema.col(field), dialect), filterValues[field]);
21
+ condition.isEqual(`${table}.${qi(Schema.col(field), dialect)}`, filterValues[field]);
18
22
  }
19
23
  }
20
24
  // extraFilters are handled exclusively by extendedCondition
@@ -3,7 +3,7 @@ import type { QueryClient } from './db.js';
3
3
  import { ConditionBuilder, type ConditionValue, type ConditionValueOrUndefined } from 'node-condition-builder';
4
4
  import type { ITable, SqlApiPluginOptions, TenantId, TenantScope, TenantScopeIndirect, TenantContext } from '../types.js';
5
5
  export declare function resolveTenant(options: SqlApiPluginOptions, tableConf: ITable, request: FastifyRequest): Promise<TenantContext | undefined>;
6
- export declare function buildTenantCondition(db: QueryClient, scope: TenantScope, tenantIds: TenantId[]): ConditionBuilder;
6
+ export declare function buildTenantCondition(db: QueryClient, scope: TenantScope, tenantIds: TenantId[], ownTable: string): ConditionBuilder;
7
7
  export declare function buildTenantJoin(db: QueryClient, scope: TenantScopeIndirect, mainTableName: string): string;
8
8
  export declare function injectTenantValue(record: Record<string, unknown>, scope: TenantScope, tenantIds: TenantId[]): void;
9
9
  export declare function validateTenantFK(db: QueryClient, scope: TenantScopeIndirect, tenantIds: TenantId[], fkValues: unknown[]): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"tenant.d.ts","sourceRoot":"","sources":["../../src/lib/tenant.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tenant.d.ts","sourceRoot":"","sources":["../../src/lib/tenant.ts"],"names":[],"mappings":"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"}
Binary file
@@ -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"}
@@ -16,8 +16,13 @@ export type ExtendedConditionFn = (condition: ConditionBuilder, filters: FilterR
16
16
  * `dialect` selects identifier quoting and builder behavior per call (the engines pass
17
17
  * the dialect of the QueryClient that runs the query); when omitted, the global
18
18
  * `ConditionBuilder.DIALECT` is used.
19
+ *
20
+ * `qualifier` is the raw (unquoted) table name or alias to prefix columns with; it
21
+ * defaults to the table's own name. The engines pass an alias when the condition lands
22
+ * inside a correlated subquery whose FROM is aliased — there the bare table name would
23
+ * resolve against the OUTER query.
19
24
  */
20
- export type TableFilterFn = (filters: FilterRecord, dialect?: CbDialect) => ConditionBuilder;
25
+ export type TableFilterFn = (filters: FilterRecord, dialect?: CbDialect, qualifier?: string) => ConditionBuilder;
21
26
  export interface ITable<F extends Record<string, TSchema> = Record<string, TSchema>> {
22
27
  primary: (string & keyof F) | (string & keyof F)[];
23
28
  Schema: SchemaDefinition<F>;
@@ -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.5",
3
+ "version": "0.1.7",
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",