fastify-auto-sqlapi 0.0.4 → 0.1.1
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.md +15 -2
- package/AGENTS_BACKEND.md +160 -32
- package/AGENTS_FRONTEND.md +220 -49
- package/README.md +147 -27
- package/dist/bin/generate-schema.js +12 -29
- package/dist/bin/generate-tables.js +10 -33
- package/dist/bin/utils.d.ts +29 -1
- package/dist/bin/utils.d.ts.map +1 -1
- package/dist/bin/utils.js +71 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/lib/cli/load-dependency.d.ts +8 -0
- package/dist/lib/cli/load-dependency.d.ts.map +1 -0
- package/dist/lib/cli/load-dependency.js +21 -0
- package/dist/lib/cli/mysql-introspect.d.ts.map +1 -1
- package/dist/lib/cli/mysql-introspect.js +2 -11
- package/dist/lib/cli/pg-introspect.d.ts.map +1 -1
- package/dist/lib/cli/pg-introspect.js +2 -17
- package/dist/lib/cli/schema-codegen.d.ts +1 -1
- package/dist/lib/cli/schema-codegen.d.ts.map +1 -1
- package/dist/lib/cli/schema-codegen.js +11 -4
- package/dist/lib/cli/tables-codegen.d.ts.map +1 -1
- package/dist/lib/cli/tables-codegen.js +4 -3
- package/dist/lib/db.d.ts +1 -0
- package/dist/lib/db.d.ts.map +1 -1
- package/dist/lib/db.js +33 -49
- package/dist/lib/dialect.d.ts +17 -0
- package/dist/lib/dialect.d.ts.map +1 -1
- package/dist/lib/dialect.js +32 -0
- package/dist/lib/engine/bulk/bulk-upsert.d.ts.map +1 -1
- package/dist/lib/engine/bulk/bulk-upsert.js +26 -50
- package/dist/lib/engine/rest/delete.d.ts.map +1 -1
- package/dist/lib/engine/rest/delete.js +5 -10
- package/dist/lib/engine/rest/get.d.ts.map +1 -1
- package/dist/lib/engine/rest/get.js +4 -6
- package/dist/lib/engine/rest/insert.d.ts.map +1 -1
- package/dist/lib/engine/rest/insert.js +20 -39
- package/dist/lib/engine/rest/update.d.ts.map +1 -1
- package/dist/lib/engine/rest/update.js +35 -67
- package/dist/lib/engine/search/search.d.ts.map +1 -1
- package/dist/lib/engine/search/search.js +650 -130
- package/dist/lib/engine/write-helpers.d.ts +39 -2
- package/dist/lib/engine/write-helpers.d.ts.map +1 -1
- package/dist/lib/engine/write-helpers.js +58 -15
- package/dist/lib/errors.d.ts +11 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +12 -0
- package/dist/lib/naming.d.ts +11 -2
- package/dist/lib/naming.d.ts.map +1 -1
- package/dist/lib/naming.js +33 -5
- package/dist/lib/schema/bulk-upsert.d.ts.map +1 -1
- package/dist/lib/schema/bulk-upsert.js +9 -25
- package/dist/lib/schema/helpers.d.ts +28 -1
- package/dist/lib/schema/helpers.d.ts.map +1 -1
- package/dist/lib/schema/helpers.js +38 -0
- package/dist/lib/schema/insert.d.ts.map +1 -1
- package/dist/lib/schema/insert.js +9 -19
- package/dist/lib/schema/search.d.ts.map +1 -1
- package/dist/lib/schema/search.js +147 -53
- package/dist/lib/schema/update.d.ts.map +1 -1
- package/dist/lib/schema/update.js +9 -25
- package/dist/lib/sql-api.d.ts +6 -6
- package/dist/lib/sql-api.d.ts.map +1 -1
- package/dist/lib/sql-api.js +8 -8
- package/dist/lib/table-helpers.d.ts +6 -1
- package/dist/lib/table-helpers.d.ts.map +1 -1
- package/dist/lib/table-helpers.js +51 -3
- package/dist/lib/tenant.d.ts +23 -0
- package/dist/lib/tenant.d.ts.map +1 -1
- package/dist/lib/tenant.js +57 -9
- package/dist/routes/auto/bulk-delete.routes.d.ts.map +1 -1
- package/dist/routes/auto/bulk-delete.routes.js +17 -23
- package/dist/routes/auto/bulk-upsert.routes.d.ts.map +1 -1
- package/dist/routes/auto/bulk-upsert.routes.js +16 -22
- package/dist/routes/auto/delete.routes.d.ts.map +1 -1
- package/dist/routes/auto/delete.routes.js +18 -24
- package/dist/routes/auto/get.routes.d.ts.map +1 -1
- package/dist/routes/auto/get.routes.js +16 -23
- package/dist/routes/auto/insert.routes.d.ts.map +1 -1
- package/dist/routes/auto/insert.routes.js +19 -26
- package/dist/routes/auto/route-helpers.d.ts +27 -2
- package/dist/routes/auto/route-helpers.d.ts.map +1 -1
- package/dist/routes/auto/route-helpers.js +39 -1
- package/dist/routes/auto/search.routes.d.ts.map +1 -1
- package/dist/routes/auto/search.routes.js +47 -47
- package/dist/routes/auto/update.routes.d.ts.map +1 -1
- package/dist/routes/auto/update.routes.js +20 -27
- package/dist/types/cli.d.ts +22 -0
- package/dist/types/cli.d.ts.map +1 -0
- package/dist/types/cli.js +1 -0
- package/dist/types/computed.d.ts +29 -0
- package/dist/types/computed.d.ts.map +1 -0
- package/dist/types/computed.js +1 -0
- package/dist/types/conditions.d.ts +16 -0
- package/dist/types/conditions.d.ts.map +1 -0
- package/dist/types/conditions.js +1 -0
- package/dist/types/db.d.ts +22 -0
- package/dist/types/db.d.ts.map +1 -0
- package/dist/types/db.js +1 -0
- package/dist/types/dml.d.ts +64 -0
- package/dist/types/dml.d.ts.map +1 -0
- package/dist/types/dml.js +1 -0
- package/dist/types/join.d.ts +44 -0
- package/dist/types/join.d.ts.map +1 -0
- package/dist/types/join.js +1 -0
- package/dist/types/plugin.d.ts +20 -0
- package/dist/types/plugin.d.ts.map +1 -0
- package/dist/types/plugin.js +1 -0
- package/dist/types/schema.d.ts +10 -0
- package/dist/types/schema.d.ts.map +1 -0
- package/dist/types/schema.js +1 -0
- package/dist/types/search.d.ts +45 -0
- package/dist/types/search.d.ts.map +1 -0
- package/dist/types/search.js +1 -0
- package/dist/types/table.d.ts +52 -0
- package/dist/types/table.d.ts.map +1 -0
- package/dist/types/table.js +7 -0
- package/dist/types/tenant.d.ts +19 -0
- package/dist/types/tenant.d.ts.map +1 -0
- package/dist/types/tenant.js +1 -0
- package/dist/types/validation.d.ts +21 -0
- package/dist/types/validation.d.ts.map +1 -0
- package/dist/types/validation.js +1 -0
- package/dist/types.d.ts +12 -258
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +12 -7
- package/package.json +8 -5
package/AGENTS_FRONTEND.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
How to call the auto-generated CRUD endpoints.
|
|
4
4
|
|
|
5
|
+
> ⚠️ **Migrating from a previous version?** The join API was redesigned (no backward compat). See **[BREAKING_CHANGES.md](./BREAKING_CHANGES.md)** for the full migration guide — request/response key renames, new `joinLeft` family, dotted notation rules, secondaries key renames. Common to backend and frontend.
|
|
6
|
+
|
|
5
7
|
## Generated Endpoints
|
|
6
8
|
|
|
7
9
|
The plugin defines routes with these base paths: `/search/`, `/rest/`, `/bulk/`. The prefix from `register()` is prepended by Fastify. For a table `customer` with `prefix: '/auto'`:
|
|
@@ -32,7 +34,7 @@ Without prefix, routes are at root: `/search/customer`, `/rest/customer/:id`, et
|
|
|
32
34
|
&computeMin=id&computeMax=id&computeSum=total&computeAvg=total
|
|
33
35
|
```
|
|
34
36
|
|
|
35
|
-
- `orderBy` — SQL ORDER BY clause (default: `defaultOrder` from table config)
|
|
37
|
+
- `orderBy` — SQL ORDER BY clause (default: `defaultOrder` from table config). Supports dotted notations for joins, see below.
|
|
36
38
|
- `page` + `itemsPerPage` — pagination (when either is present, response includes `pagination` object; `page` defaults to 1, `itemsPerPage` defaults to 500)
|
|
37
39
|
- `computeMin`, `computeMax`, `computeSum`, `computeAvg` — aggregate a column, returned in `pagination.computed`
|
|
38
40
|
|
|
@@ -44,14 +46,14 @@ Without prefix, routes are at root: `/search/customer`, `/rest/customer/:id`, et
|
|
|
44
46
|
"conditions": [
|
|
45
47
|
{ "field": "createdAt", "method": "isGreater", "params": ["2024-01-01"] }
|
|
46
48
|
],
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
+
"joinMustExist": {
|
|
50
|
+
"labels": { "filters": { "labelId": 1 } }
|
|
49
51
|
},
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
+
"joinMultiple": {
|
|
53
|
+
"orders": { "filters": { "status": "pending" } }
|
|
52
54
|
},
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
+
"joinGroup": {
|
|
56
|
+
"orders": {
|
|
55
57
|
"aggregations": {
|
|
56
58
|
"by": "status",
|
|
57
59
|
"sum": ["total"],
|
|
@@ -61,10 +63,15 @@ Without prefix, routes are at root: `/search/customer`, `/rest/customer/:id`, et
|
|
|
61
63
|
},
|
|
62
64
|
"filters": { "status": "active" }
|
|
63
65
|
}
|
|
66
|
+
},
|
|
67
|
+
"joinLeft": {
|
|
68
|
+
"creator": { "selection": "id,name,email" }
|
|
64
69
|
}
|
|
65
70
|
}
|
|
66
71
|
```
|
|
67
72
|
|
|
73
|
+
The keys inside `joinMustExist`, `joinMultiple`, `joinGroup`, `joinLeft` are **aliases** declared by the backend in `buildRelation`. By default an alias equals the joined table's name; backends may override it (e.g. `'orders'` instead of `'customer_order'`) or declare multiple aliases for the same table (e.g. `'creator'` and `'updater'` both pointing to `user`). Check the Swagger description of each `/search/{table}` endpoint to see the available aliases.
|
|
74
|
+
|
|
68
75
|
### Filters
|
|
69
76
|
|
|
70
77
|
Pass any schema field as a key in `filters`. The plugin auto-applies `WHERE col = value` for each. Extra filters (defined via `extraFiltersValidation` in the table config) are also accepted but handled by custom logic.
|
|
@@ -103,19 +110,53 @@ Pass any schema field as a key in `filters`. The plugin auto-applies `WHERE col
|
|
|
103
110
|
Also available: `isNotGreater`, `isNotGreaterOrEqual`, `isNotLess`, `isNotLessOrEqual`.
|
|
104
111
|
|
|
105
112
|
- **Multiple conditions on the same field** — it's an array, so `total > 100 AND total < 500` is natural
|
|
106
|
-
- **Combinable** with `filters` (equality), `
|
|
113
|
+
- **Combinable** with `filters` (equality), `joinMustExist` (EXISTS), pagination, etc.
|
|
107
114
|
- **Field validation** — field names are validated against the table schema (400 if unknown)
|
|
108
115
|
- **Method validation** — only whitelisted methods are allowed (`raw`, `append` etc. are blocked)
|
|
109
116
|
|
|
110
|
-
|
|
117
|
+
#### Conditions on joinGroup aggregations (HAVING-style)
|
|
111
118
|
|
|
112
|
-
|
|
119
|
+
You can filter main rows based on the value of a `joinGroup` aggregation using 3-parti dot notation: `<alias>.<fn>.<field>`.
|
|
120
|
+
|
|
121
|
+
Example: "users with at least 4 sessions":
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{
|
|
125
|
+
"conditions": [
|
|
126
|
+
{ "field": "sessions.count.id", "method": "isGreaterOrEqual", "params": [4] }
|
|
127
|
+
],
|
|
128
|
+
"joinGroup": {
|
|
129
|
+
"sessions": { "aggregations": { "count": ["id"] } }
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Generated SQL:
|
|
135
|
+
```sql
|
|
136
|
+
SELECT * FROM "user"
|
|
137
|
+
WHERE COALESCE((
|
|
138
|
+
SELECT COUNT("session"."id")
|
|
139
|
+
FROM "session"
|
|
140
|
+
WHERE "session"."user_id" = "user"."id"
|
|
141
|
+
), 0) >= $1
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**Rules:**
|
|
145
|
+
- The joinGroup must be declared in `joinGroup.<alias>.aggregations.<fn>` with the referenced field. The engine refuses undeclared references with 400.
|
|
146
|
+
- The joinGroup's own `filters` are applied inside the correlated subquery (consistent with the breakdown).
|
|
147
|
+
- All ConditionBuilder methods work: `isEqual`, `isGreater`, `isLess`, `isBetween`, `isIn`, `isNull`, `isNotNull`, etc.
|
|
148
|
+
- Can be combined with plain-field conditions and with aggregation `orderBy` in the same request.
|
|
149
|
+
- Respects `aggregations.by` rules: allowed only when `by` equals the correlation FK, otherwise 400.
|
|
150
|
+
|
|
151
|
+
### `joinMustExist` (filter main by related table — EXISTS)
|
|
152
|
+
|
|
153
|
+
`joinMustExist` restricts **main table results** based on conditions on a related child table. Aliases must come from `allowedReadJoins` declarations with `unique: false`. Uses `EXISTS` subquery — no duplicate rows, works correctly with pagination.
|
|
113
154
|
|
|
114
155
|
```json
|
|
115
156
|
{
|
|
116
157
|
"filters": { "name": "Mario" },
|
|
117
|
-
"
|
|
118
|
-
"
|
|
158
|
+
"joinMustExist": {
|
|
159
|
+
"labels": { "filters": { "labelId": 1 } }
|
|
119
160
|
}
|
|
120
161
|
}
|
|
121
162
|
```
|
|
@@ -128,29 +169,44 @@ WHERE "name" = $1
|
|
|
128
169
|
WHERE "customer_id" = "customer"."id" AND "label_id" = $2)
|
|
129
170
|
```
|
|
130
171
|
|
|
131
|
-
`
|
|
172
|
+
`joinMustExist` accepts `{ filters, conditions }` (both optional). Supports schema fields + extra filters defined via `extendedCondition` on the related table. Combinable with `filters`, `joinMultiple`, `joinGroup`, `joinLeft`, and pagination.
|
|
132
173
|
|
|
133
|
-
**
|
|
174
|
+
**Difference from `joinMultiple`**: `joinMustExist` filters which main records are returned. `joinMultiple` fetches related child data for the returned main records. They can be used together.
|
|
134
175
|
|
|
135
|
-
###
|
|
176
|
+
### `joinMultiple` (fetch related child rows)
|
|
136
177
|
|
|
137
|
-
Request related table data via `
|
|
178
|
+
Request related child table data via `joinMultiple`. Aliases must come from `allowedReadJoins` declarations with `unique: false`. Each entry accepts `{ filters?, conditions?, selection? }`.
|
|
138
179
|
|
|
139
|
-
|
|
180
|
+
```json
|
|
181
|
+
{
|
|
182
|
+
"joinMultiple": {
|
|
183
|
+
"orders": {
|
|
184
|
+
"filters": { "status": "pending" },
|
|
185
|
+
"selection": "id,total,status"
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
`selection` is optional and overrides the default declared in `buildRelation` (which itself defaults to `'*'`).
|
|
192
|
+
|
|
193
|
+
The fetch executes as a separate side query: `SELECT {selection} FROM {childTable} WHERE {fk} IN ({main PKs})`. Returned in `result.joinMultiple.<alias>` as an array.
|
|
140
194
|
|
|
141
|
-
###
|
|
195
|
+
### `joinGroup` (aggregations on related child rows)
|
|
142
196
|
|
|
143
|
-
`
|
|
197
|
+
`joinGroup` computes aggregated values on a related child table:
|
|
144
198
|
|
|
145
199
|
```json
|
|
146
200
|
{
|
|
147
|
-
"
|
|
148
|
-
"
|
|
201
|
+
"joinGroup": {
|
|
202
|
+
"orders": {
|
|
149
203
|
"aggregations": {
|
|
150
204
|
"by": "status",
|
|
151
205
|
"sum": ["total"],
|
|
152
206
|
"min": ["total"],
|
|
153
207
|
"max": ["total"],
|
|
208
|
+
"avg": ["total"],
|
|
209
|
+
"count": ["id"],
|
|
154
210
|
"distinctCount": ["status"]
|
|
155
211
|
},
|
|
156
212
|
"filters": { "status": "active" }
|
|
@@ -159,23 +215,132 @@ The join executes as a separate query: `SELECT ... FROM {joinTable} WHERE {fk} I
|
|
|
159
215
|
}
|
|
160
216
|
```
|
|
161
217
|
|
|
162
|
-
- `by` — GROUP BY field
|
|
163
|
-
- `sum`, `min`, `max` — aggregate functions on specified columns
|
|
218
|
+
- `by` — GROUP BY name (optional). Either a schema field on the join table or a **computed field** declared on it (e.g. for date bucketing, the backend declares a computed using `db.dateTrunc(unit, qiCol('orderDate'))` and you reference it by name).
|
|
219
|
+
- `sum`, `min`, `max`, `avg` — aggregate functions on specified columns
|
|
220
|
+
- `count` — COUNT(col)
|
|
164
221
|
- `distinctCount` — COUNT(DISTINCT col)
|
|
165
222
|
- `filters` — optional, narrow rows before aggregation
|
|
166
223
|
|
|
167
|
-
|
|
224
|
+
Example — monthly revenue per customer (the backend declared a `monthBucket` computed):
|
|
168
225
|
|
|
169
|
-
|
|
226
|
+
```json
|
|
227
|
+
{
|
|
228
|
+
"joinGroup": {
|
|
229
|
+
"orders": {
|
|
230
|
+
"aggregations": {
|
|
231
|
+
"by": "monthBucket",
|
|
232
|
+
"sum": ["total"]
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
```
|
|
170
238
|
|
|
239
|
+
Response:
|
|
240
|
+
```json
|
|
241
|
+
{
|
|
242
|
+
"joinGroup": {
|
|
243
|
+
"orders": {
|
|
244
|
+
"rows": [
|
|
245
|
+
{ "by": "2026-01-01", "sum_total": 150 },
|
|
246
|
+
{ "by": "2026-02-01", "sum_total": 200 }
|
|
247
|
+
]
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
171
251
|
```
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
252
|
+
|
|
253
|
+
Returned as `result.joinGroup.<alias>` — keyed by the function name when no `by`, or with a `rows` array when `by` is set.
|
|
254
|
+
|
|
255
|
+
Aliases must come from `allowedReadJoins` declarations with `unique: false`.
|
|
256
|
+
|
|
257
|
+
### `joinLeft` (embed N:1 parent inline)
|
|
258
|
+
|
|
259
|
+
`joinLeft` embeds a parent record (N:1 relation) into the result. Aliases must come from `allowedReadJoins` declarations with `unique: true`. Each entry accepts `{ filters?, conditions?, selection? }`.
|
|
260
|
+
|
|
261
|
+
```json
|
|
262
|
+
{
|
|
263
|
+
"joinLeft": {
|
|
264
|
+
"creator": {},
|
|
265
|
+
"updater": { "filters": { "active": true }, "selection": "id,name" }
|
|
266
|
+
}
|
|
267
|
+
}
|
|
175
268
|
```
|
|
176
269
|
|
|
270
|
+
The engine attaches a real `LEFT JOIN <parentTable> AS <alias>` to the main query **only when needed**:
|
|
271
|
+
- 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);
|
|
272
|
+
- when `orderBy` uses 2-parti dotted notation referring to a parent field (`<alias>.<field>`).
|
|
273
|
+
|
|
274
|
+
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.
|
|
275
|
+
|
|
276
|
+
The parent rows are returned in `result.joinLeft.<alias>` as a deduplicated array. The client maps a main row to its parent by FK lookup:
|
|
277
|
+
|
|
278
|
+
```js
|
|
279
|
+
const parent = result.joinLeft.creator.find((u) => u.id === mainRow.userId);
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
> **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.
|
|
283
|
+
|
|
284
|
+
### Ordering
|
|
285
|
+
|
|
286
|
+
Use the `orderBy` query parameter. Three forms are supported:
|
|
287
|
+
|
|
288
|
+
| Form | Source of `<alias>` | Example |
|
|
289
|
+
|------|---------------------|---------|
|
|
290
|
+
| `<field> [ASC\|DESC]` | main schema | `?orderBy=name ASC` |
|
|
291
|
+
| `<alias>.<field> [ASC\|DESC]` | `joinLeft`-eligible (`unique: true`) | `?orderBy=creator.name ASC` |
|
|
292
|
+
| `<alias>.<fn>.<field> [ASC\|DESC]` | `joinGroup` declared in same body | `?orderBy=orders.sum.total DESC` |
|
|
293
|
+
|
|
294
|
+
Multiple parts may be comma-separated: `?orderBy=creator.name ASC, id DESC`.
|
|
295
|
+
|
|
177
296
|
If not specified, the table's `defaultOrder` is used.
|
|
178
297
|
|
|
298
|
+
#### Ordering by joinGroup aggregations (3-parti)
|
|
299
|
+
|
|
300
|
+
Supported functions: `sum`, `min`, `max`, `avg`, `count`, `distinctCount`.
|
|
301
|
+
|
|
302
|
+
Example: get users ordered by total session duration DESC.
|
|
303
|
+
|
|
304
|
+
```
|
|
305
|
+
POST /auto/search/user?orderBy=sessions.sum.duration%20DESC
|
|
306
|
+
```
|
|
307
|
+
```json
|
|
308
|
+
{
|
|
309
|
+
"joinGroup": {
|
|
310
|
+
"sessions": {
|
|
311
|
+
"aggregations": { "sum": ["duration"] }
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Generated SQL (simplified):
|
|
318
|
+
```sql
|
|
319
|
+
SELECT * FROM "user"
|
|
320
|
+
ORDER BY (
|
|
321
|
+
SELECT SUM("session"."duration")
|
|
322
|
+
FROM "session"
|
|
323
|
+
WHERE "session"."user_id" = "user"."id"
|
|
324
|
+
) DESC
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
**Rules:**
|
|
328
|
+
- The joinGroup **must be declared in the request body** (`joinGroup.<alias>.aggregations.<fn>` must include the field). The engine refuses undeclared references with 400.
|
|
329
|
+
- If the joinGroup declares `filters`, those filters are applied inside the correlated subquery — consistent with the breakdown.
|
|
330
|
+
- **`aggregations.by`**: allowed only when `by` is the same field used for the join correlation. Using `by` on any other column is rejected with 400.
|
|
331
|
+
- **Not allowed** when the main table has `distinctResults: true`. 400 error.
|
|
332
|
+
- Can be combined with plain-field ordering and with 2-parti `joinLeft` ordering: `?orderBy=sessions.sum.duration DESC, creator.name ASC, name`.
|
|
333
|
+
- The joinGroup breakdown is still returned in the response (`result.joinGroup.<alias>`) — you get both ordered main results and the aggregated summary.
|
|
334
|
+
- **No-data rows**: rows with no matching joined records are coalesced to `0` via `COALESCE(..., 0)`. On DESC they appear last, on ASC they appear first.
|
|
335
|
+
|
|
336
|
+
#### Ordering by parent fields (2-parti)
|
|
337
|
+
|
|
338
|
+
```
|
|
339
|
+
?orderBy=creator.name ASC
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
The alias must be declared with `unique: true` (so it's eligible for `joinLeft`). The engine adds a real `LEFT JOIN` on the main query to make the column orderable. Main row count is preserved (no duplication, since N:1).
|
|
343
|
+
|
|
179
344
|
### Pagination
|
|
180
345
|
|
|
181
346
|
Add `page` or `itemsPerPage` to the query string to enable pagination:
|
|
@@ -201,11 +366,14 @@ These compute aggregates on the main table's result set (respecting filters). Re
|
|
|
201
366
|
{
|
|
202
367
|
"table": "customer",
|
|
203
368
|
"main": [{ "id": 1, "name": "Mario", "email": "m@t.it" }],
|
|
204
|
-
"
|
|
205
|
-
"
|
|
369
|
+
"joinLeft": {
|
|
370
|
+
"creator": [{ "id": 7, "name": "Alice", "email": "a@x.it" }]
|
|
371
|
+
},
|
|
372
|
+
"joinMultiple": {
|
|
373
|
+
"orders": [{ "id": 10, "customerId": 1, "total": 50 }]
|
|
206
374
|
},
|
|
207
|
-
"
|
|
208
|
-
"
|
|
375
|
+
"joinGroup": {
|
|
376
|
+
"orders": {
|
|
209
377
|
"sum": { "total": 150 },
|
|
210
378
|
"rows": [{ "by": "pending", "sum_total": 100 }]
|
|
211
379
|
}
|
|
@@ -219,7 +387,7 @@ These compute aggregates on the main table's result set (respecting filters). Re
|
|
|
219
387
|
}
|
|
220
388
|
```
|
|
221
389
|
|
|
222
|
-
`
|
|
390
|
+
`joinLeft`, `joinMultiple`, `joinGroup`, `pagination` appear ONLY if requested. A simple search returns just `{ table, main }`.
|
|
223
391
|
|
|
224
392
|
---
|
|
225
393
|
|
|
@@ -245,7 +413,7 @@ Returns 404 if not found.
|
|
|
245
413
|
{
|
|
246
414
|
"main": { "name": "Mario", "email": "m@t.it" },
|
|
247
415
|
"secondaries": {
|
|
248
|
-
"
|
|
416
|
+
"orders": [
|
|
249
417
|
{ "total": 50, "status": "pending" }
|
|
250
418
|
]
|
|
251
419
|
}
|
|
@@ -253,7 +421,7 @@ Returns 404 if not found.
|
|
|
253
421
|
```
|
|
254
422
|
|
|
255
423
|
- `main` — the record to insert. Omit auto-increment PKs (they are in `excludeFromCreation`).
|
|
256
|
-
- `secondaries` — optional. Related records to insert.
|
|
424
|
+
- `secondaries` — optional. Related records to insert. Keys are **aliases** from `allowedWriteJoins`. FK fields (e.g. `customerId` in orders) are auto-filled from the inserted main record's PK.
|
|
257
425
|
|
|
258
426
|
**Response (201):** PK-only for main and secondaries.
|
|
259
427
|
|
|
@@ -261,7 +429,7 @@ Returns 404 if not found.
|
|
|
261
429
|
{
|
|
262
430
|
"main": { "id": 1 },
|
|
263
431
|
"secondaries": {
|
|
264
|
-
"
|
|
432
|
+
"orders": [{ "id": 10 }]
|
|
265
433
|
}
|
|
266
434
|
}
|
|
267
435
|
```
|
|
@@ -276,25 +444,25 @@ Returns 404 if not found.
|
|
|
276
444
|
{
|
|
277
445
|
"main": { "id": 1, "name": "Mario Updated", "email": "new@t.it" },
|
|
278
446
|
"secondaries": {
|
|
279
|
-
"
|
|
447
|
+
"orders": [{ "total": 75, "status": "shipped" }]
|
|
280
448
|
},
|
|
281
449
|
"deletions": {
|
|
282
|
-
"
|
|
450
|
+
"orders": [{ "id": 10 }]
|
|
283
451
|
}
|
|
284
452
|
}
|
|
285
453
|
```
|
|
286
454
|
|
|
287
455
|
- `main` — MUST include the PK. Only changed fields need to be sent (partial update).
|
|
288
|
-
- `secondaries` — optional. New related records to insert (FK auto-filled).
|
|
289
|
-
- `deletions` — optional. Related records to delete (by PK).
|
|
456
|
+
- `secondaries` — optional. New related records to insert (FK auto-filled). Keys are **aliases**.
|
|
457
|
+
- `deletions` — optional. Related records to delete (by PK). Keys are **aliases**.
|
|
290
458
|
|
|
291
459
|
**Response (200):** PK-only for main, secondaries, and deletions.
|
|
292
460
|
|
|
293
461
|
```json
|
|
294
462
|
{
|
|
295
463
|
"main": { "id": 1 },
|
|
296
|
-
"secondaries": { "
|
|
297
|
-
"deletions":
|
|
464
|
+
"secondaries": { "orders": [{ "id": 20 }] },
|
|
465
|
+
"deletions": { "orders": [{ "id": 10 }] }
|
|
298
466
|
}
|
|
299
467
|
```
|
|
300
468
|
|
|
@@ -324,8 +492,8 @@ Insert or update multiple records in a single request.
|
|
|
324
492
|
[
|
|
325
493
|
{
|
|
326
494
|
"main": { "name": "Mario", "email": "m@t.it" },
|
|
327
|
-
"secondaries": { "
|
|
328
|
-
"deletions": { "
|
|
495
|
+
"secondaries": { "orders": [{ "total": 50 }] },
|
|
496
|
+
"deletions": { "orders": [{ "id": 99 }] }
|
|
329
497
|
},
|
|
330
498
|
{
|
|
331
499
|
"main": { "name": "Luigi", "email": "l@t.it" }
|
|
@@ -333,7 +501,7 @@ Insert or update multiple records in a single request.
|
|
|
333
501
|
]
|
|
334
502
|
```
|
|
335
503
|
|
|
336
|
-
All main records are inserted/upserted in a single SQL query. Secondaries and deletions are processed per-item.
|
|
504
|
+
All main records are inserted/upserted in a single SQL query. Secondaries and deletions are processed per-item. `secondaries`/`deletions` keys are **aliases**.
|
|
337
505
|
|
|
338
506
|
**Response (200):** Array of `{ main (PK-only), secondaries? (PK-only), deletions? }`.
|
|
339
507
|
|
|
@@ -375,13 +543,13 @@ Both return the same response format:
|
|
|
375
543
|
"message": "Validation failed",
|
|
376
544
|
"fields": [
|
|
377
545
|
{ "path": "body.main.name", "code": "required", "message": "must have required property 'name'" },
|
|
378
|
-
{ "path": "
|
|
546
|
+
{ "path": "periods[1].startDate", "code": "overlap", "message": "overlaps with another period" }
|
|
379
547
|
]
|
|
380
548
|
}
|
|
381
549
|
```
|
|
382
550
|
|
|
383
551
|
Each entry in `fields`:
|
|
384
|
-
- `path` — the field path (e.g. `body.main.name` for schema errors, `name` or `
|
|
552
|
+
- `path` — the field path (e.g. `body.main.name` for schema errors, `name` or `periods[1].startDate` for custom validation)
|
|
385
553
|
- `code` — machine-readable error code (e.g. `required`, `type`, `overlap`, `unique`)
|
|
386
554
|
- `message` — human-readable description
|
|
387
555
|
|
|
@@ -391,7 +559,7 @@ Each entry in `fields`:
|
|
|
391
559
|
const response = await fetch('/api/rest/session', {
|
|
392
560
|
method: 'POST',
|
|
393
561
|
headers: { 'Content-Type': 'application/json' },
|
|
394
|
-
body: JSON.stringify({ main: { name: '' }, secondaries: {
|
|
562
|
+
body: JSON.stringify({ main: { name: '' }, secondaries: { periods } }),
|
|
395
563
|
});
|
|
396
564
|
|
|
397
565
|
if (!response.ok) {
|
|
@@ -411,4 +579,7 @@ if (!response.ok) {
|
|
|
411
579
|
|
|
412
580
|
- **PK-only responses**: Insert, update, delete, bulk operations return only the primary key fields, not the full record. This is by design for performance and consistency.
|
|
413
581
|
- **camelCase fields**: All request and response fields use camelCase (e.g. `customerId`, not `customer_id`). The plugin converts automatically.
|
|
414
|
-
- **
|
|
582
|
+
- **Aliases everywhere**: in request bodies, response payloads, `secondaries`/`deletions`, and dotted notation, the keys are aliases declared by the backend in `buildRelation`. Default = the joined table's name; can be overridden (e.g. `orders` for `customer_order`).
|
|
583
|
+
- **Computed fields**: the backend may declare virtual fields via `computedFields` on a table. They appear in Swagger as additional filter/orderBy names, behave exactly like schema fields (filters, conditions, orderBy, computeMin/Max/Sum/Avg), and are projected into the main response only when explicitly listed in `selectComputed: ['<name>']` in the body.
|
|
584
|
+
- **`joinMultiple` / `joinMustExist` / `joinGroup`** are 1:N (child→main) and use side queries / EXISTS / correlated subqueries — no row duplication of main.
|
|
585
|
+
- **`joinLeft`** is N:1 (parent→main) and adds a real `LEFT JOIN` on demand (only when filtering/ordering by parent).
|