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 +15 -4
- package/README.md +10 -0
- package/dist/lib/cli/tables-codegen.d.ts.map +1 -1
- package/dist/lib/cli/tables-codegen.js +3 -1
- package/dist/lib/engine/query-params.d.ts +40 -0
- package/dist/lib/engine/query-params.d.ts.map +1 -0
- package/dist/lib/engine/query-params.js +48 -0
- package/dist/lib/engine/rest/get.d.ts.map +1 -1
- package/dist/lib/engine/rest/get.js +4 -2
- package/dist/lib/engine/search/search.d.ts.map +1 -1
- package/dist/lib/engine/search/search.js +191 -228
- package/dist/lib/table-helpers.d.ts.map +1 -1
- package/dist/lib/table-helpers.js +7 -3
- package/dist/lib/tenant.d.ts +1 -1
- package/dist/lib/tenant.d.ts.map +1 -1
- package/dist/lib/tenant.js +0 -0
- package/dist/types/computed.d.ts +13 -3
- package/dist/types/computed.d.ts.map +1 -1
- package/dist/types/table.d.ts +6 -1
- package/dist/types/table.d.ts.map +1 -1
- package/package.json +2 -2
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[]; //
|
|
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**
|
|
509
|
-
-
|
|
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,
|
|
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(` //
|
|
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,
|
|
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
|
-
|
|
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":"
|
|
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
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
|
102
|
-
*
|
|
103
|
-
*
|
|
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
|
-
|
|
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
|
|
176
|
-
if (
|
|
177
|
-
filterWhere = ` AND ${
|
|
178
|
-
filterVals =
|
|
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 = `${
|
|
182
|
-
const expr = `COALESCE((SELECT ${aggExpr(aggFn, qualifiedCol)} FROM ${refs.joinTable} WHERE ${
|
|
183
|
-
return
|
|
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
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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:
|
|
283
|
-
|
|
284
|
-
|
|
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
|
|
300
|
-
dispatchConditionMethod(cb, c.method,
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
433
|
-
|
|
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)
|
|
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`).
|
|
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)
|
|
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
|
-
|
|
552
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
655
|
-
|
|
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 (!
|
|
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
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
708
|
-
*
|
|
709
|
-
*
|
|
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
|
|
714
|
-
const out = [];
|
|
731
|
+
function applyComputedFilters(condition, filters, tableConf, db) {
|
|
715
732
|
const computed = tableConf.computedFields;
|
|
716
733
|
if (!computed)
|
|
717
|
-
return
|
|
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
|
|
724
|
-
|
|
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(
|
|
750
|
-
let where =
|
|
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
|
-
|
|
764
|
-
|
|
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
|
-
|
|
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
|
|
764
|
+
return where;
|
|
773
765
|
}
|
|
774
766
|
// ─── joinMustExist (EXISTS subquery filter on main) ─────────
|
|
775
|
-
function buildJoinMustExistClauses(db, dbTables, tableConf, joinMustExist,
|
|
776
|
-
let where =
|
|
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
|
-
|
|
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,
|
|
786
|
-
const
|
|
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
|
-
? `${
|
|
804
|
-
: `${
|
|
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
|
|
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
|
|
831
|
-
//
|
|
832
|
-
|
|
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
|
-
|
|
813
|
+
applyComputedFilters(condition, filters, tableConf, db);
|
|
835
814
|
}
|
|
836
|
-
let computedConditionClauses = [];
|
|
837
815
|
if (conditions?.length) {
|
|
838
|
-
|
|
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
|
-
|
|
849
|
-
|
|
850
|
-
//
|
|
851
|
-
//
|
|
852
|
-
|
|
853
|
-
|
|
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
|
-
|
|
834
|
+
where += buildJoinMustExistClauses(bound, db, dbTables, tableConf, joinMustExist, tenant);
|
|
861
835
|
}
|
|
862
836
|
// Aggregation conditions (HAVING-style)
|
|
863
|
-
if (conditions?.length) {
|
|
864
|
-
|
|
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
|
-
//
|
|
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,
|
|
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
|
-
//
|
|
896
|
-
//
|
|
897
|
-
|
|
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
|
-
|
|
902
|
-
|
|
903
|
-
|
|
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,
|
|
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)
|
|
21
|
+
condition.isEqual(`${table}.${qi(Schema.col(field), dialect)}`, filterValues[field]);
|
|
18
22
|
}
|
|
19
23
|
}
|
|
20
24
|
// extraFilters are handled exclusively by extendedCondition
|
package/dist/lib/tenant.d.ts
CHANGED
|
@@ -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>;
|
package/dist/lib/tenant.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tenant.d.ts","sourceRoot":"","sources":["../../src/lib/tenant.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,EAAE,gBAAgB,EAAE,KAAK,cAAc,EAAE,KAAK,yBAAyB,EAAE,MAAM,wBAAwB,CAAC;AAE/G,OAAO,KAAK,EACV,MAAM,EACN,mBAAmB,EACnB,QAAQ,EACR,WAAW,EACX,mBAAmB,EACnB,aAAa,EACd,MAAM,aAAa,CAAC;AAMrB,wBAAsB,aAAa,CACjC,OAAO,EAAE,mBAAmB,EAC5B,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CASpC;AAED,wBAAgB,oBAAoB,CAClC,EAAE,EAAE,WAAW,EACf,KAAK,EAAE,WAAW,EAClB,SAAS,EAAE,QAAQ,EAAE,
|
|
1
|
+
{"version":3,"file":"tenant.d.ts","sourceRoot":"","sources":["../../src/lib/tenant.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/tenant.js
CHANGED
|
Binary file
|
package/dist/types/computed.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
20
|
-
*
|
|
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
|
|
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"}
|
package/dist/types/table.d.ts
CHANGED
|
@@ -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":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,yBAAyB,EAAE,WAAW,IAAI,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAChI,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC5D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEpE,4FAA4F;AAC5F,MAAM,MAAM,cAAc,GACtB,QAAQ,GACR,KAAK,GACL,QAAQ,GACR,QAAQ,GACR,QAAQ,GACR,YAAY,GACZ,YAAY,CAAC;AAEjB,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,yBAAyB,CAAC,CAAC;AACrE,MAAM,MAAM,mBAAmB,GAAG,CAAC,SAAS,EAAE,gBAAgB,EAAE,OAAO,EAAE,YAAY,KAAK,IAAI,CAAC;AAC/F
|
|
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.
|
|
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
|
|
67
|
+
"node-condition-builder": "^1.2.0"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
70
|
"@fastify/postgres": "^6.0.2",
|