fastify-auto-sqlapi 0.1.6 → 0.1.8

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.
@@ -0,0 +1,319 @@
1
+ # Breaking Changes — Join API redesign
2
+
3
+ The join API was rewritten to be explicit, alias-based, and to add a real `LEFT JOIN` mode (`joinLeft`) for N:1 relationships. **No backward compatibility** — old names are rejected with 400. Both backend (table configuration) and frontend (request/response shapes) are affected. This document is the single source of truth for migrating either side.
4
+
5
+ ## Why this changed
6
+
7
+ - The old API conflated "filter by child", "fetch children", and "aggregate children" under generic names (`joinFilters`, `joins`, `joinGroups`).
8
+ - There was no way to embed a parent record (N:1) inline in the search results, nor to order/filter the main query by a parent field.
9
+ - The same table could not be joined twice with different semantic roles (e.g. `createdBy` and `updatedBy` both pointing to `user`) because lookup keys collided on `joinSchema.tableName`.
10
+
11
+ The new API addresses all three with a coherent `join*` prefix family and explicit `alias`.
12
+
13
+ ## Naming map
14
+
15
+ | Old (request body) | New (request body) | Direction | Cardinality | What it does |
16
+ |--------------------|--------------------|-----------|-------------|--------------|
17
+ | `joinFilters` | `joinMustExist` | child → main | 1:N | Filters main rows via `EXISTS` subquery |
18
+ | `joins` | `joinMultiple` | child → main | 1:N | Fetches child rows in a side query |
19
+ | `joinGroups` | `joinGroup` | child → main | 1:N | Aggregations on children (SUM/MIN/…) with optional GROUP BY |
20
+ | — | `joinLeft` | parent → main | N:1 | **NEW** real `LEFT JOIN` for embedding the parent |
21
+
22
+ | Old (response) | New (response) |
23
+ |----------------|----------------|
24
+ | `result.joins` | `result.joinMultiple` |
25
+ | `result.joinGroups` | `result.joinGroup` |
26
+ | — | `result.joinLeft` (NEW) |
27
+
28
+ | Old (`buildRelation` last arg) | New (`buildRelation` last arg) |
29
+ |---------------------------------|---------------------------------|
30
+ | `selection?: string` (positional) | `options?: { alias?: string; selection?: string; unique?: boolean }` (all optional; `alias` defaults to `joinSchema.tableName`) |
31
+
32
+ | Old (lookup key inside the body) | New (lookup key inside the body) |
33
+ |-----------------------------------|-----------------------------------|
34
+ | `joinSchema.tableName` | `alias` (declared in `buildRelation`) |
35
+
36
+ The `alias` is also the key used in:
37
+ - Response payloads (`result.joinMultiple.<alias>`, `result.joinLeft.<alias>`, `result.joinGroup.<alias>`)
38
+ - `secondaries` and `deletions` payloads in insert/update/bulk-upsert (`secondaries: { <alias>: [...] }`)
39
+ - Dotted notation in `orderBy` and `conditions`
40
+
41
+ ## buildRelation — new signature
42
+
43
+ ```typescript
44
+ // Options object is optional; every field has a sensible default.
45
+ buildRelation(mainSchema, mainField, joinSchema, joinField);
46
+
47
+ // Or with any subset of options:
48
+ buildRelation(mainSchema, mainField, joinSchema, joinField, {
49
+ alias: 'creator', // optional, default = joinSchema.tableName.
50
+ // Used as the key in request/response/secondaries payloads
51
+ // and in dotted-notation orderBy/conditions.
52
+ selection: '*', // optional, default '*'. Comma-separated list, e.g. 'id,name,email'.
53
+ unique: false, // optional, default false.
54
+ // true → relation is N:1 (each main row has at most one join row).
55
+ // Allowed in joinLeft. Forbidden in joinMultiple/joinMustExist/joinGroup.
56
+ // false → relation is 1:N. Allowed in the other three. Forbidden in joinLeft.
57
+ });
58
+ ```
59
+
60
+ You only need to declare an explicit `alias` when:
61
+ - you join the same table more than once (e.g. `createdBy` and `updatedBy` both pointing to `user`); or
62
+ - you want a name different from the join table's name in the API surface (e.g. `'orders'` instead of `'customer_order'`).
63
+
64
+ If two relations within the same `allowedReadJoins` (or `allowedWriteJoins`) end up resolving to the same alias — explicit or implicit — `defineTable` throws at startup with:
65
+
66
+ ```
67
+ defineTable: duplicate alias '<name>' in allowedReadJoins. When omitted,
68
+ alias defaults to joinSchema.tableName — declare an explicit alias to disambiguate.
69
+ ```
70
+
71
+ If a request hits an alias on the wrong family, the engine returns `400`:
72
+
73
+ ```
74
+ Join alias 'orders' is not declared with unique:true; use joinMultiple/joinMustExist/joinGroup instead
75
+ Join alias 'creator' is declared with unique:true; use joinLeft instead
76
+ Unknown join alias: <name>
77
+ ```
78
+
79
+ ## Request body — by family
80
+
81
+ ### `joinMustExist` (was `joinFilters`)
82
+
83
+ EXISTS-based filter: returns main rows that have at least one matching child.
84
+
85
+ ```json
86
+ {
87
+ "joinMustExist": {
88
+ "orders": {
89
+ "filters": { "status": "completed" },
90
+ "conditions": [{ "field": "total", "method": "isGreater", "params": [50] }]
91
+ }
92
+ }
93
+ }
94
+ ```
95
+
96
+ ### `joinMultiple` (was `joins`)
97
+
98
+ Fetches child rows in a side query; populates `result.joinMultiple.<alias>`.
99
+
100
+ ```json
101
+ {
102
+ "joinMultiple": {
103
+ "orders": {
104
+ "filters": { "status": "completed" },
105
+ "selection": "id,total,status"
106
+ }
107
+ }
108
+ }
109
+ ```
110
+
111
+ `selection` is optional and overrides the default declared in `buildRelation`.
112
+
113
+ ### `joinGroup` (was `joinGroups`)
114
+
115
+ Aggregations on the child table; populates `result.joinGroup.<alias>`.
116
+
117
+ ```json
118
+ {
119
+ "joinGroup": {
120
+ "orders": {
121
+ "aggregations": {
122
+ "by": "status",
123
+ "sum": ["total"],
124
+ "count": ["id"]
125
+ },
126
+ "filters": { "status": "completed" }
127
+ }
128
+ }
129
+ }
130
+ ```
131
+
132
+ ### `joinLeft` (NEW, requires `unique: true`)
133
+
134
+ Real `LEFT JOIN` for N:1 (parent) relationships. The parent rows are returned in `result.joinLeft.<alias>` as a deduplicated array (one entry per distinct parent PK).
135
+
136
+ ```json
137
+ {
138
+ "joinLeft": {
139
+ "creator": {},
140
+ "updater": { "filters": { "active": true }, "selection": "id,name" }
141
+ }
142
+ }
143
+ ```
144
+
145
+ The engine attaches a real `LEFT JOIN <parentTable> AS <alias>` to the main query **only when needed**:
146
+ - when the request includes `filters` or `conditions` on the parent (effectively turns into INNER JOIN behavior on those aliases — main rows whose parent doesn't match are excluded);
147
+ - when `orderBy` uses 2-parti dotted notation referring to a parent field (`alias.field`).
148
+
149
+ Otherwise only a side query `WHERE pk IN (distinct fk values)` is issued — no LEFT JOIN, no row duplication. Either way, the response shape is the same.
150
+
151
+ > **Limitation**: `extraFilters` declared via `extendedCondition` on the parent table are **not** applied inside `joinLeft.filters` (only schema fields). The other join families fully support extraFilters.
152
+
153
+ ## orderBy — dotted notations
154
+
155
+ | Form | Allowed alias source | What it does |
156
+ |------|----------------------|--------------|
157
+ | `<field>` | main schema | regular column ordering |
158
+ | `<alias>.<field>` | `joinLeft` (`unique: true` aliases) | orders main query by a parent field via LEFT JOIN |
159
+ | `<alias>.<fn>.<field>` | `joinGroup` (declared in the same body) | orders main query by an aggregation via correlated scalar subquery |
160
+
161
+ Ambiguity: 2-parti and 3-parti are disambiguated by counting dots. The 2-parti is rejected unless the alias is in `joinLeft` allowlist; 3-parti is rejected unless the alias is in the request `joinGroup` declaration.
162
+
163
+ ## Response shape
164
+
165
+ ```json
166
+ {
167
+ "table": "session",
168
+ "main": [...],
169
+ "joinLeft": { "creator": [...], "updater": [...] },
170
+ "joinMultiple": { "orders": [...] },
171
+ "joinGroup": { "orders": { "sum": { "total": 300 }, "count": { "id": 2 } } },
172
+ "pagination": { "total": 25, "pages": 3, "paginator": { "page": 1, "itemsPerPage": 20 } }
173
+ }
174
+ ```
175
+
176
+ All four `join*` keys appear only when requested. `pagination` only when `paginator` is provided.
177
+
178
+ Each value in `result.joinLeft.<alias>` is an array of parent rows (one entry per distinct FK in `main`). The client maps a main row to its parent by FK lookup (`main.userId === joinLeft.creator[i].id`).
179
+
180
+ ## Write side — secondaries / deletions
181
+
182
+ `secondaries` and `deletions` payloads in insert/update/bulk-upsert now use the **alias** as key (was `joinSchema.tableName`):
183
+
184
+ ```json
185
+ {
186
+ "main": { "name": "Mario" },
187
+ "secondaries": { "orders": [{ "total": 50 }] },
188
+ "deletions": { "orders": [{ "id": 10 }] }
189
+ }
190
+ ```
191
+
192
+ ## Migration recipes (mechanical)
193
+
194
+ ### 1) Find every `buildRelation` call
195
+
196
+ Old form:
197
+ ```
198
+ buildRelation(M, mF, J, jF)
199
+ buildRelation(M, mF, J, jF, '<sel>') // 5th positional arg = selection
200
+ ```
201
+
202
+ New form (alias is optional, defaults to `J.tableName`):
203
+ ```ts
204
+ // Simplest case — no rewrite needed beyond removing the old positional selection:
205
+ buildRelation(M, mF, J, jF)
206
+
207
+ // Or supply any options you actually need:
208
+ buildRelation(M, mF, J, jF, {
209
+ // alias: 'orders', // omit if joinSchema.tableName is fine as alias
210
+ // selection: 'id, total', // omit for '*'
211
+ // unique: true // ADD if jF is the PK of J (relation is N:1)
212
+ })
213
+ ```
214
+
215
+ **Heuristic for `unique`**: if `joinField` is the PK (or part of the composite PK) of `joinSchema`, the relation is N:1 and you almost certainly want `unique: true` so the alias is usable in `joinLeft`. Mark the call with a `// TODO verify unique:true` and review.
216
+
217
+ **When to add an explicit `alias`**: only if (a) you join the same table multiple times, or (b) you want a friendlier name than the SQL table name (e.g. `orders` vs `customer_order`).
218
+
219
+ ### 2) Rename request keys (regex-replace inside HTTP/JSON bodies and search params)
220
+
221
+ ```
222
+ joinFilters: → joinMustExist:
223
+ joins: → joinMultiple:
224
+ joinGroups: → joinGroup:
225
+ ```
226
+
227
+ ### 3) Rename response accessors
228
+
229
+ ```
230
+ result.joins → result.joinMultiple
231
+ result.joinGroups → result.joinGroup
232
+ body.joins → body.joinMultiple
233
+ body.joinGroups → body.joinGroup
234
+ ```
235
+
236
+ (Plus `result.joinLeft` is now available for the new family.)
237
+
238
+ ### 4) Rename secondaries keys
239
+
240
+ If you used the join table's name as the secondaries key, change it to the new alias:
241
+
242
+ ```diff
243
+ - "secondaries": { "customer_order": [...] }
244
+ + "secondaries": { "orders": [...] }
245
+ ```
246
+
247
+ ### 5) Inspect dotted orderBy/conditions
248
+
249
+ `<table>.<fn>.<field>` still works for joinGroup, but `<table>` must now be the alias declared in `buildRelation`. If the alias differs from the old `tableName`, update the string.
250
+
251
+ If you need to order by a parent field, **add a `joinLeft`-eligible relation** (with `unique: true`) and use 2-parti notation `alias.field` (NEW capability — not available before).
252
+
253
+ ## Validation summary (400 responses)
254
+
255
+ The engine rejects, with `statusCode: 400`, any of:
256
+
257
+ - `joinLeft` referencing an alias declared with `unique: false`
258
+ - `joinMultiple`/`joinMustExist`/`joinGroup` referencing an alias declared with `unique: true`
259
+ - request keys referencing an alias not declared in `allowedReadJoins`
260
+ - `orderBy` 2-parti `<alias>.<field>` whose alias is not in `joinLeft`-eligible (i.e. `unique:true`) declarations
261
+ - `orderBy` 3-parti `<alias>.<fn>.<field>` whose alias or `(fn, field)` pair is not declared in `joinGroup` for the same request
262
+ - `defineTable` with two `allowedReadJoins`/`allowedWriteJoins` entries resolving to the same alias (thrown at startup, not at request time)
263
+
264
+ ---
265
+
266
+ # Breaking Change — computed field placeholders use `?` markers
267
+
268
+ `ComputedFieldExpr.expr` must now mark each bound value with `?`. The engine assigns the
269
+ placeholder positions. Writing `$1` (or `db.ph(n)`) inside the expression no longer works and
270
+ is rejected with a descriptive error.
271
+
272
+ ## Why this changed
273
+
274
+ A computed field cannot know its own placeholder offset: the position depends on how many
275
+ values the rest of the query bound before it. The previous contract asked the *consumer* to
276
+ supply "stable indices", which is not knowable — so any computed that declared `values` and
277
+ was used together with another filter produced a query that referenced the wrong parameter:
278
+
279
+ ```
280
+ WHERE ("name" = $1) AND (CASE WHEN "role" = $1 THEN ... END = $3)
281
+ values: ['Mario', 'admin', 500]
282
+ ```
283
+
284
+ `$2` ('admin') was bound but never referenced, while the expression read `$1` ('Mario'). The
285
+ query did not fail — it silently returned the wrong rows.
286
+
287
+ ## Migration
288
+
289
+ ```typescript
290
+ // Before — placeholder guessed by the caller (silently misbound)
291
+ bonus: ({ db, qiCol }) => ({
292
+ expr: `CASE WHEN ${qiCol('role')} = ${db.ph(1)} THEN ${qiCol('salary')} ELSE 0 END`,
293
+ values: ['admin'],
294
+ type: Type.Number(),
295
+ }),
296
+
297
+ // After — `?` marker, position assigned by the engine
298
+ bonus: ({ qiCol }) => ({
299
+ expr: `CASE WHEN ${qiCol('role')} = ? THEN ${qiCol('salary')} ELSE 0 END`,
300
+ values: ['admin'],
301
+ type: Type.Number(),
302
+ }),
303
+ ```
304
+
305
+ Computed fields that declare **no** bound values (the majority: JSON extraction, concat,
306
+ `dateTrunc`, arithmetic) are unaffected and need no change. Their SQL is emitted verbatim, so
307
+ a literal `?` — the PostgreSQL jsonb operator — keeps working. In an expression that *does*
308
+ carry values, escape a literal question mark as `\?`.
309
+
310
+ A mismatch between the number of `?` markers and `values.length` now raises a descriptive
311
+ error instead of producing a wrong query.
312
+
313
+ ## What this unlocks
314
+
315
+ Bound values now work in every position that lands in the `WHERE` clause or in `ORDER BY`,
316
+ including `joinLeft.filters` / `joinLeft.conditions` and `orderBy`, which previously rejected
317
+ them with 400. They remain rejected in `selectComputed`, `computeMin/Max/Sum/Avg`,
318
+ `joinGroup.aggregations.by` and `defaultOrder`, where the expression precedes the `WHERE`
319
+ values in the parameter order.
package/CHANGELOG.md ADDED
@@ -0,0 +1,116 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. This project follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and adheres to
5
+ [Semantic Versioning](https://semver.org/) — with the caveat that while the version is
6
+ `0.x`, breaking changes may land in a minor release.
7
+
8
+ Migration instructions for breaking changes live in **[BREAKING_CHANGES.md](./BREAKING_CHANGES.md)**.
9
+
10
+ ## [0.1.7]
11
+
12
+ ### Fixed
13
+
14
+ - **Ambiguous column references in statements carrying a join.** A filtered `joinLeft` adds a
15
+ `LEFT JOIN` to the main query, and a tenant scope with `through` adds an `INNER JOIN`. Any
16
+ column name shared with the joined table (`id`, `name` — the common case) was ambiguous:
17
+ PostgreSQL rejected the statement, and a reference could otherwise resolve against the wrong
18
+ table. Every column reference is now table- or alias-qualified: filters, conditions, computed
19
+ expressions, `orderBy`, join selections, aggregations and tenant conditions.
20
+ - **Correlated subqueries broke on self-referencing relations.** `joinMustExist` emitted
21
+ `EXISTS (SELECT 1 FROM "t" WHERE "t"."fk" = "t"."id")` when a relation pointed back at its own
22
+ table; the inner name shadowed the outer one, so the correlation was lost and the filter
23
+ silently matched the wrong rows. The subquery source is now aliased.
24
+
25
+ ### Changed
26
+
27
+ - Placeholder offsets are owned by a single accumulator (`QueryParams`) instead of being
28
+ recomputed at each call site. No behaviour change; it removes the class of mistake where a
29
+ fragment's placeholders drift out of step with the values array.
30
+
31
+ ### Testing
32
+
33
+ - Unit tests now assert two invariants on **every** query the engine generates: placeholder
34
+ integrity (no placeholder referencing an unbound value, no bound value left unreferenced)
35
+ and column qualification in join-bearing statements. Both are enforced by the shared mock
36
+ driver, so they apply to the whole suite rather than to hand-written assertions.
37
+ - New integration suite executes ~45 request shapes — all four join families, computed fields
38
+ with and without bound values, every `orderBy` form, pagination, aggregations — against real
39
+ PostgreSQL and MySQL, so a syntax error fails a test instead of reaching a consumer.
40
+
41
+ ## [0.1.6]
42
+
43
+ ### Fixed
44
+
45
+ - **⚠️ Computed fields with bound values silently returned wrong rows.** A computed field
46
+ declaring `values` could not know its own placeholder offset, so its expression referenced
47
+ whichever parameter another filter happened to bind. The query did not fail — it filtered or
48
+ sorted on the wrong value. If you use `computedFields` with a non-empty `values` array,
49
+ **results produced by 0.1.5 and earlier may be incorrect.**
50
+
51
+ ### Breaking
52
+
53
+ - `ComputedFieldExpr.expr` now marks each bound value with `?`; the engine assigns placeholder
54
+ positions. Writing `$1` or `db.ph(n)` inside the expression is rejected with a descriptive
55
+ error. Computed fields that declare **no** bound values are unaffected — the majority case
56
+ (JSON extraction, `CONCAT`, `dateTrunc`, arithmetic) needs no change, and a literal `?` such
57
+ as the PostgreSQL jsonb operator keeps working.
58
+ See [BREAKING_CHANGES.md](./BREAKING_CHANGES.md#breaking-change--computed-field-placeholders-use--markers).
59
+
60
+ ### Added
61
+
62
+ - Bound values in computed fields now work in every position that lands in the `WHERE` clause
63
+ or in `ORDER BY`, including `joinLeft.filters`, `joinLeft.conditions` and `orderBy`, which
64
+ previously rejected them with 400. They remain rejected in `selectComputed`,
65
+ `computeMin/Max/Sum/Avg`, `joinGroup.aggregations.by` and `defaultOrder`, where the
66
+ expression precedes the `WHERE` values in the parameter order.
67
+ - A computed field whose `?` markers do not match `values.length` now fails with a clear error
68
+ instead of producing a query that binds values nothing references.
69
+
70
+ ## [0.1.5]
71
+
72
+ ### Added
73
+
74
+ - **`readExclude`** on `defineTable`: hide columns from every read while leaving writes
75
+ untouched — the case for a password hash or an access token, writable but never readable.
76
+ Excluded fields are not selected by search/get, are omitted from read response schemas and
77
+ from the table's default join selection, and cannot be referenced from `filters`,
78
+ `conditions`, `orderBy`, aggregations or an explicit join `selection` (400). Allowing a
79
+ hidden field to be filtered would leak its value by bisection. Primary keys cannot be
80
+ excluded.
81
+
82
+ ### Fixed
83
+
84
+ - **Granular route plugins did not work standalone.** Registering only `searchRoutes`/`getRoutes`
85
+ as documented produced a 500, because `fastify.sqlApi` was decorated by the main plugin alone.
86
+ Each route plugin now creates the decorator in its own scope when no ancestor provides it.
87
+ - **`defaultOrder` required raw column names** while the rest of the API is camelCase, so
88
+ `defaultOrder: 'squadIndex'` produced `column "squadindex" does not exist`. It is now mapped
89
+ through the schema like the request `orderBy`, and supports multiple fields and computed
90
+ fields. Raw SQL fragments still pass through unchanged, so existing configurations keep
91
+ working. The same mapping applies to the primary-key fallback, which had the same defect.
92
+ - **`sqlapi-generate-tables` with a subset of tables** generated a `dbTables.ts` importing every
93
+ schema, including tables whose `Table*.ts` was never created — the project did not compile
94
+ until the file was trimmed by hand. The index now references only the table files present on
95
+ disk, and the CLI reports which entries to add when `dbTables.ts` already exists.
96
+ - **Generated imports lacked the `.js` extension**, so the emitted files did not compile under
97
+ `moduleResolution: NodeNext` without editing them by hand.
98
+
99
+ ## [0.1.4]
100
+
101
+ ### Fixed
102
+
103
+ - Hardened tenant isolation, added request limits (`maxItemsPerPage`, `maxBulkItems`), and
104
+ fixed placeholder binding in search.
105
+
106
+ ## [0.1.3] and earlier
107
+
108
+ Released before this changelog was kept. See the git history for details: notable entries are
109
+ the join API redesign (documented in [BREAKING_CHANGES.md](./BREAKING_CHANGES.md)), composite
110
+ primary key support in updates, transactional insert/update with secondaries, the hook matrix,
111
+ and bulk operations.
112
+
113
+ [0.1.7]: https://github.com/LucaRainone/fastify-auto-sqlapi/releases/tag/v0.1.7
114
+ [0.1.6]: https://github.com/LucaRainone/fastify-auto-sqlapi/releases/tag/v0.1.6
115
+ [0.1.5]: https://github.com/LucaRainone/fastify-auto-sqlapi/releases/tag/v0.1.5
116
+ [0.1.4]: https://github.com/LucaRainone/fastify-auto-sqlapi/releases/tag/v0.1.4
package/README.md CHANGED
@@ -29,7 +29,9 @@ Point it at your database, and get a full REST API with search, advanced conditi
29
29
  - **Swagger UI** — optional, auto-configured from your schemas
30
30
  - **Composable** — register all routes or pick only what you need
31
31
 
32
- > ⚠️ **Migrating from a previous version?** The join API was redesigned. See **[BREAKING_CHANGES.md](./BREAKING_CHANGES.md)** for the full migration guide.
32
+ > ⚠️ **Upgrading?** See **[CHANGELOG.md](./CHANGELOG.md)** for what changed in each release, and
33
+ > **[BREAKING_CHANGES.md](./BREAKING_CHANGES.md)** for migration guides. If you use `computedFields`
34
+ > with bound values, read the 0.1.6 entry — earlier versions could return wrong rows.
33
35
 
34
36
  ## Quick Start
35
37
 
@@ -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;AAmkC3B,wBAAsB,YAAY,CAChC,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,YAAY,CAAC,CAkIvB"}
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
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,7 +44,9 @@ 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;
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}`;
45
50
  return { expr, computed: false };
46
51
  }
47
52
  const fn = tableConf?.computedFields?.[field];
@@ -57,16 +62,28 @@ function resolveFieldRef(field, schema, tableConf, db, alias) {
57
62
  * appends the tenant condition to `cb` (direct: `col IN ids`; indirect: condition on the
58
63
  * through table) and, for indirect scopes, pushes the INNER JOIN clause into `joins`.
59
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.
60
68
  */
61
- function appendJoinTenantScope(db, joinTableConf, tenant, joinTableName, cb, joins) {
69
+ function appendJoinTenantScope(db, joinTableConf, tenant, joinTableRef, cb, joins) {
62
70
  const scope = joinTableConf?.tenantScope;
63
71
  if (!tenant || !scope)
64
72
  return;
65
- cb.append(buildTenantCondition(db, scope, tenant.ids));
73
+ cb.append(buildTenantCondition(db, scope, tenant.ids, joinTableRef));
66
74
  if ('through' in scope) {
67
- joins?.push(buildTenantJoin(db, scope, joinTableName));
75
+ joins?.push(buildTenantJoin(db, scope, joinTableRef));
68
76
  }
69
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
+ }
70
87
  function findJoinByAlias(tableConf, alias) {
71
88
  return tableConf.allowedReadJoins?.find((j) => j.alias === alias);
72
89
  }
@@ -164,9 +181,10 @@ function buildSelectionColumns(selection, joinSchema, db, joinTableConf) {
164
181
  if (selection === '*') {
165
182
  return readableSelectColumns(joinTableConf, joinSchema, db) ?? '*';
166
183
  }
184
+ const table = db.qi(joinSchema.tableName);
167
185
  return selection
168
186
  .split(',')
169
- .map((c) => db.qi(validateSchemaField(c.trim(), joinSchema, joinTableConf)))
187
+ .map((c) => `${table}.${db.qi(validateSchemaField(c.trim(), joinSchema, joinTableConf))}`)
170
188
  .join(', ');
171
189
  }
172
190
  const AGG_FN = {
@@ -180,7 +198,12 @@ const AGG_FN = {
180
198
  function aggExpr(fn, qualifiedCol) {
181
199
  return fn.distinct ? `COUNT(DISTINCT ${qualifiedCol})` : `${fn.sql}(${qualifiedCol})`;
182
200
  }
183
- 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) {
184
207
  const aggFn = AGG_FN[fn];
185
208
  if (!aggFn)
186
209
  err400(`Invalid aggregation function: ${fn}`);
@@ -198,20 +221,24 @@ function buildAggOrderExpr(db, dbTables, tableConf, alias, fn, field, joinGroup,
198
221
  }
199
222
  const fieldCol = validateSchemaField(field, joinSchema);
200
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);
201
228
  let filterWhere = '';
202
229
  let filterVals = [];
203
230
  const joinTableConf = dbTables[joinSchema.tableName];
204
231
  if (groupReq.filters || groupReq.conditions?.length) {
205
- const cb = buildJoinRefCondition(joinTableConf, joinSchema, { filters: groupReq.filters, conditions: groupReq.conditions }, db);
206
- const built = cb.build(startIdx, db.ph);
207
- if (built) {
208
- filterWhere = ` AND ${built}`;
209
- 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];
210
237
  }
211
238
  }
212
- const qualifiedCol = `${refs.joinTable}.${db.qi(fieldCol)}`;
213
- const expr = `COALESCE((SELECT ${aggExpr(aggFn, qualifiedCol)} FROM ${refs.joinTable} WHERE ${refs.joinTable}.${refs.fkCol} = ${refs.mainTable}.${refs.mainCol}${filterWhere}), 0)`;
214
- 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);
215
242
  }
216
243
  /**
217
244
  * Pre-scan `orderBy` for the joinLeft aliases referenced in 2-parti notation (`<alias>.<field>`).
@@ -250,10 +277,11 @@ function validateOrderBy(orderBy, tableConf, db, dbTables, joinGroup, startIdx)
250
277
  err400('Cannot combine distinctResults with aggregation orderBy');
251
278
  }
252
279
  const [, alias, fn, field, dir] = dotted3;
253
- const { expr, values } = buildAggOrderExpr(db, dbTables, tableConf, alias, fn, field, joinGroup, currentIdx);
254
- outParts.push(`${expr} ${(dir || 'ASC').toUpperCase()}`);
255
- outValues.push(...values);
256
- 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;
257
285
  continue;
258
286
  }
259
287
  // 2-parti: <alias>.<field> [ASC|DESC] (joinLeft inline ordering)
@@ -368,7 +396,7 @@ function convertConfiguredOrder(order, tableConf, db) {
368
396
  const [, field, dir] = match;
369
397
  const suffix = dir ? ` ${dir.toUpperCase()}` : '';
370
398
  if (field in tableConf.Schema.fields) {
371
- return `${db.qi(tableConf.Schema.col(field))}${suffix}`;
399
+ return `${db.qi(tableConf.Schema.tableName)}.${db.qi(tableConf.Schema.col(field))}${suffix}`;
372
400
  }
373
401
  const computed = tableConf.computedFields?.[field];
374
402
  if (computed) {
@@ -463,7 +491,7 @@ async function executeJoinMultiple(db, dbTables, tableConf, mainResults, joinMul
463
491
  }
464
492
  const cb = buildJoinRefCondition(joinTableConf, joinSchema, ref || {}, db);
465
493
  const fkCol = joinSchema.col(joinField);
466
- cb.isIn(db.qi(fkCol), ids);
494
+ cb.isIn(`${db.qi(joinSchema.tableName)}.${db.qi(fkCol)}`, ids);
467
495
  const tenantJoins = [];
468
496
  appendJoinTenantScope(db, joinTableConf, tenant, joinSchema.tableName, cb, tenantJoins);
469
497
  const where = cb.build(1, db.ph);
@@ -497,7 +525,7 @@ async function executeJoinLeft(db, dbTables, tableConf, mainResults, joinLeft, t
497
525
  }
498
526
  const fkCol = joinSchema.col(joinField);
499
527
  const cb = new ConditionBuilder('AND', db.cbDialect);
500
- cb.isIn(db.qi(fkCol), ids);
528
+ cb.isIn(`${db.qi(joinSchema.tableName)}.${db.qi(fkCol)}`, ids);
501
529
  const tenantJoins = [];
502
530
  appendJoinTenantScope(db, joinTableConf, tenant, joinSchema.tableName, cb, tenantJoins);
503
531
  const where = cb.build(1, db.ph);
@@ -527,7 +555,7 @@ function buildByExpression(by, joinSchema, joinTableConf, db) {
527
555
  err400(`Invalid 'by' specification: expected a field or computed name`);
528
556
  }
529
557
  if (by in joinSchema.fields) {
530
- return db.qi(joinSchema.col(by));
558
+ return `${db.qi(joinSchema.tableName)}.${db.qi(joinSchema.col(by))}`;
531
559
  }
532
560
  const fn = joinTableConf?.computedFields?.[by];
533
561
  if (fn) {
@@ -561,7 +589,7 @@ async function executeJoinGroup(db, dbTables, tableConf, mainResults, joinGroup,
561
589
  const fn = AGG_FN[kind];
562
590
  for (const f of fields) {
563
591
  const col = validateSchemaField(f, joinSchema, joinTableConf);
564
- 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}"`);
565
593
  }
566
594
  };
567
595
  addAgg('distinctCount', aggregations.distinctCount);
@@ -577,7 +605,7 @@ async function executeJoinGroup(db, dbTables, tableConf, mainResults, joinGroup,
577
605
  const groupRef = { filters: groupFilters, conditions: groupConditions };
578
606
  const cb = buildJoinRefCondition(joinTableConf, joinSchema, groupRef, db);
579
607
  const fkCol = joinSchema.col(joinField);
580
- cb.isIn(db.qi(fkCol), ids);
608
+ cb.isIn(`${db.qi(joinSchema.tableName)}.${db.qi(fkCol)}`, ids);
581
609
  const tenantJoins = [];
582
610
  appendJoinTenantScope(db, joinTableConf, tenant, joinSchema.tableName, cb, tenantJoins);
583
611
  const where = cb.build(1, db.ph);
@@ -640,10 +668,13 @@ function dispatchConditionMethod(cb, method, colOrExpr, params) {
640
668
  cb[method](colOrExpr, true);
641
669
  }
642
670
  }
643
- function buildJoinRefCondition(joinTableConf, joinSchema, ref, db) {
671
+ function buildJoinRefCondition(joinTableConf, joinSchema, ref, db, qualifier) {
644
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;
645
676
  const cb = (ref.filters && joinTableConf)
646
- ? joinTableConf.filters(ref.filters, db.cbDialect)
677
+ ? joinTableConf.filters(ref.filters, db.cbDialect, colQualifier)
647
678
  : new ConditionBuilder('AND', db.cbDialect);
648
679
  // Equality filters targeting the join table's computed fields
649
680
  const computed = joinTableConf?.computedFields;
@@ -653,7 +684,7 @@ function buildJoinRefCondition(joinTableConf, joinSchema, ref, db) {
653
684
  continue;
654
685
  if (!computed[name])
655
686
  continue;
656
- const expr = evaluateComputedField(name, computed[name], joinTableConf.Schema, db, undefined, '', true);
687
+ const expr = evaluateComputedField(name, computed[name], joinTableConf.Schema, db, colQualifier, '', true);
657
688
  cb.isEqual(expr, value);
658
689
  }
659
690
  }
@@ -665,8 +696,8 @@ function buildJoinRefCondition(joinTableConf, joinSchema, ref, db) {
665
696
  // Computed fields resolve to an Expression carrying their own bound values, which
666
697
  // the ConditionBuilder places together with the compared value.
667
698
  const operand = computed?.[c.field]
668
- ? evaluateComputedField(c.field, computed[c.field], joinTableConf.Schema, db, undefined, '', true)
669
- : db.qi(validateSchemaField(c.field, joinSchema, joinTableConf));
699
+ ? evaluateComputedField(c.field, computed[c.field], joinTableConf.Schema, db, colQualifier, '', true)
700
+ : `${db.qi(colQualifier)}.${db.qi(validateSchemaField(c.field, joinSchema, joinTableConf))}`;
670
701
  dispatchConditionMethod(cb, c.method, operand, c.params ?? []);
671
702
  }
672
703
  }
@@ -710,9 +741,8 @@ function applyComputedFilters(condition, filters, tableConf, db) {
710
741
  condition.isEqual(expr, value);
711
742
  }
712
743
  }
713
- function appendAggConditions(currentWhere, currentValues, conditions, db, dbTables, tableConf, joinGroup) {
714
- let where = currentWhere;
715
- let values = [...currentValues];
744
+ function appendAggConditions(params, conditions, db, dbTables, tableConf, joinGroup) {
745
+ let where = '';
716
746
  for (const c of conditions) {
717
747
  if (!c.field.includes('.'))
718
748
  continue;
@@ -724,44 +754,38 @@ function appendAggConditions(currentWhere, currentValues, conditions, db, dbTabl
724
754
  err400(`Invalid dotted field in condition: ${c.field} (expected <alias>.<fn>.<field>)`);
725
755
  }
726
756
  const [alias, fn, field] = parts;
727
- const { expr, values: exprValues } = buildAggOrderExpr(db, dbTables, tableConf, alias, fn, field, joinGroup, values.length + 1);
728
- 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);
729
760
  const tmpCb = new ConditionBuilder('AND', db.cbDialect);
730
761
  dispatchConditionMethod(tmpCb, c.method, expr, c.params);
731
- const startIdx = values.length + 1;
732
- const clause = tmpCb.build(startIdx, db.ph);
733
- where += ` AND ${clause}`;
734
- values.push(...tmpCb.getValues());
762
+ where += ` AND ${params.emitCondition(tmpCb, db)}`;
735
763
  }
736
- return { where, values };
764
+ return where;
737
765
  }
738
766
  // ─── joinMustExist (EXISTS subquery filter on main) ─────────
739
- function buildJoinMustExistClauses(db, dbTables, tableConf, joinMustExist, currentWhere, currentValues, tenant) {
740
- let where = currentWhere;
741
- const values = [...currentValues];
767
+ function buildJoinMustExistClauses(params, db, dbTables, tableConf, joinMustExist, tenant) {
768
+ let where = '';
742
769
  for (const [alias, ref] of Object.entries(joinMustExist)) {
743
770
  const joinDef = requireJoin(tableConf, alias, false);
744
771
  const { joinSchema } = joinDef;
745
772
  const joinTableConf = dbTables[joinSchema.tableName];
746
773
  const refs = extractJoinRefs(db, tableConf, joinDef);
747
- 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);
748
779
  const tenantJoins = [];
749
- appendJoinTenantScope(db, joinTableConf, tenant, joinSchema.tableName, filterCondition, tenantJoins);
750
- // The sub-condition (computed fields included) is built by the ConditionBuilder, which
751
- // numbers its placeholders from where the outer values end.
752
- const startIdx = values.length + 1;
753
- const filterWhere = filterCondition.build(startIdx, db.ph);
754
- const filterVals = [...filterCondition.getValues()];
755
- // Qualify the FK with the join table: the EXISTS FROM may include the tenant
756
- // through-join, so a bare column could become ambiguous.
780
+ appendJoinTenantScope(db, joinTableConf, tenant, subAlias, filterCondition, tenantJoins);
781
+ const filterWhere = params.emitCondition(filterCondition, db);
757
782
  const innerWhere = filterWhere
758
- ? `${refs.joinTable}.${refs.fkCol} = ${refs.mainTable}.${refs.mainCol} AND ${filterWhere}`
759
- : `${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}`;
760
785
  const existsJoins = tenantJoins.length > 0 ? ` ${tenantJoins.join(' ')}` : '';
761
- where += ` AND EXISTS (SELECT 1 FROM ${refs.joinTable}${existsJoins} WHERE ${innerWhere})`;
762
- values.push(...filterVals);
786
+ where += ` AND EXISTS (SELECT 1 FROM ${refs.joinTable} AS ${subRef}${existsJoins} WHERE ${innerWhere})`;
763
787
  }
764
- return { where, values };
788
+ return where;
765
789
  }
766
790
  // ─── searchEngine entrypoint ────────────────────────────────
767
791
  export async function searchEngine(dbTables, params) {
@@ -794,23 +818,24 @@ export async function searchEngine(dbTables, params) {
794
818
  // Tenant
795
819
  const tenantJoins = [];
796
820
  if (tenant) {
797
- condition.append(buildTenantCondition(db, tenant.scope, tenant.ids));
821
+ condition.append(buildTenantCondition(db, tenant.scope, tenant.ids, tableConf.Schema.tableName));
798
822
  if ('through' in tenant.scope) {
799
823
  tenantJoins.push(buildTenantJoin(db, tenant.scope, tableConf.Schema.tableName));
800
824
  }
801
825
  }
802
- let where = condition.build(1, db.ph);
803
- let values = [...condition.getValues()];
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);
804
832
  // joinMustExist (EXISTS)
805
833
  if (joinMustExist && Object.keys(joinMustExist).length > 0) {
806
- ({ where, values } = buildJoinMustExistClauses(db, dbTables, tableConf, joinMustExist, where, values, tenant));
834
+ where += buildJoinMustExistClauses(bound, db, dbTables, tableConf, joinMustExist, tenant);
807
835
  }
808
836
  // Aggregation conditions (HAVING-style)
809
- if (conditions?.length) {
810
- const hasAggConditions = conditions.some((c) => c.field.includes('.'));
811
- if (hasAggConditions) {
812
- ({ where, values } = appendAggConditions(where, values, conditions, db, dbTables, tableConf, joinGroup));
813
- }
837
+ if (conditions?.length && conditions.some((c) => c.field.includes('.'))) {
838
+ where += appendAggConditions(bound, conditions, db, dbTables, tableConf, joinGroup);
814
839
  }
815
840
  // Determine which joinLeft aliases need a LEFT JOIN on the main query:
816
841
  // - any alias used in 2-parti orderBy
@@ -826,32 +851,24 @@ export async function searchEngine(dbTables, params) {
826
851
  aliasesNeedingLeftJoin.add(alias);
827
852
  }
828
853
  }
829
- // Build LEFT JOIN clauses + extra WHERE for filtered parents. Their placeholders start right
830
- // after the WHERE values; the same clause SQL is reused by the pagination COUNT/compute queries,
831
- // whose value array is [...WHERE, ...leftJoin] — so this base MUST stay `values.length + 1`.
854
+ // LEFT JOIN clauses + extra WHERE for filtered parents.
832
855
  const extraJoinClauses = [...tenantJoins];
833
- let leftJoinValues = [];
834
856
  if (aliasesNeedingLeftJoin.size > 0) {
835
- const lj = buildLeftJoinClauses(db, dbTables, tableConf, aliasesNeedingLeftJoin, joinLeft, values.length + 1);
857
+ const lj = bound.emit((startIndex) => buildLeftJoinClauses(db, dbTables, tableConf, aliasesNeedingLeftJoin, joinLeft, startIndex));
836
858
  extraJoinClauses.push(...lj.joinClauses);
837
- leftJoinValues = lj.values;
838
859
  for (const w of lj.whereExtras)
839
860
  where += ` AND ${w}`;
840
861
  }
841
- // orderBy is parsed AFTER the LEFT JOIN values so aggregation-orderBy placeholders start past
842
- // them (main query binds [...WHERE, ...leftJoin, ...orderBy]). Parsing it earlier with base
843
- // `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.
844
867
  let safeOrderBy;
845
- let orderByValues = [];
846
868
  if (orderBy) {
847
- const obResult = validateOrderBy(orderBy, tableConf, db, dbTables, joinGroup, values.length + leftJoinValues.length + 1);
848
- safeOrderBy = obResult.sql;
849
- orderByValues = obResult.values;
850
- }
851
- // Bind WHERE values + LEFT JOIN values + orderBy aggregation values for main query.
852
- // Pagination COUNT/compute include WHERE + LEFT JOIN values (no orderBy).
853
- const whereAndJoinValues = [...values, ...leftJoinValues];
854
- const mainValues = [...whereAndJoinValues, ...orderByValues];
869
+ safeOrderBy = bound.emit((startIndex) => validateOrderBy(orderBy, tableConf, db, dbTables, joinGroup, startIndex)).sql;
870
+ }
871
+ const mainValues = bound.snapshot();
855
872
  const main = await executeMainQuery(db, tableConf, where, mainValues, safeOrderBy, paginator, extraJoinClauses, params.selectComputed, params.maxRows);
856
873
  let pagination;
857
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
@@ -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.6",
3
+ "version": "0.1.8",
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",
@@ -20,7 +20,9 @@
20
20
  "dist",
21
21
  "AGENTS.md",
22
22
  "AGENTS_BACKEND.md",
23
- "AGENTS_FRONTEND.md"
23
+ "AGENTS_FRONTEND.md",
24
+ "CHANGELOG.md",
25
+ "BREAKING_CHANGES.md"
24
26
  ],
25
27
  "scripts": {
26
28
  "clean": "rm -rf dist",