fastify-auto-sqlapi 0.1.7 → 0.1.9
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/BREAKING_CHANGES.md +319 -0
- package/CHANGELOG.md +116 -0
- package/README.md +3 -1
- package/package.json +4 -2
|
@@ -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
|
-
> ⚠️ **
|
|
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
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fastify-auto-sqlapi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
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",
|