fastify-auto-sqlapi 0.0.5 → 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 +144 -20
- package/AGENTS_FRONTEND.md +161 -67
- package/README.md +47 -26
- package/dist/bin/generate-schema.js +9 -26
- package/dist/bin/generate-tables.js +7 -30
- package/dist/bin/utils.d.ts +28 -0
- package/dist/bin/utils.d.ts.map +1 -1
- package/dist/bin/utils.js +69 -0
- 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 -18
- package/dist/lib/cli/pg-introspect.d.ts.map +1 -1
- package/dist/lib/cli/pg-introspect.js +2 -17
- package/dist/lib/db.d.ts +1 -0
- package/dist/lib/db.d.ts.map +1 -1
- package/dist/lib/db.js +33 -55
- 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 +15 -43
- 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 +3 -5
- package/dist/lib/engine/rest/insert.d.ts.map +1 -1
- package/dist/lib/engine/rest/insert.js +11 -31
- package/dist/lib/engine/rest/update.d.ts.map +1 -1
- package/dist/lib/engine/rest/update.js +13 -46
- package/dist/lib/engine/search/search.d.ts.map +1 -1
- package/dist/lib/engine/search/search.js +553 -229
- 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 +55 -12
- 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/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 +138 -50
- 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 +42 -2
- 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 -277
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +12 -7
- package/package.json +3 -3
package/AGENTS.md
CHANGED
|
@@ -8,9 +8,11 @@ The plugin generates REST endpoints (search, get, insert, update, delete, bulk u
|
|
|
8
8
|
|
|
9
9
|
## Documentation
|
|
10
10
|
|
|
11
|
-
- **[
|
|
11
|
+
- **[BREAKING_CHANGES.md](./BREAKING_CHANGES.md)** — **READ FIRST when migrating.** Maps the old join API (`joinFilters`, `joins`, `joinGroups`) to the new alias-based one (`joinMustExist`, `joinMultiple`, `joinGroup`, `joinLeft`). Covers `buildRelation` new options-object signature (alias defaults to `joinSchema.tableName`, plus `selection` and `unique`), request/response key renames, secondaries key renames, dotted notation rules, and validation/400 errors. Common to backend and frontend.
|
|
12
12
|
|
|
13
|
-
- **[
|
|
13
|
+
- **[AGENTS_BACKEND.md](./AGENTS_BACKEND.md)** — Setup workflow, CLI, schema/table generation, `defineTable()` complete reference, `buildRelation` (alias + unique), `extraFilters` + `extendedCondition`, **SqlApi** (programmatic high-level API for custom routes), ConditionBuilder API, QueryClient API, Swagger, multi-tenant configuration, **validation** (`validate` + `validateBulk`), hooks, key conventions, common backend patterns, FAQ/gotchas, dialect differences.
|
|
14
|
+
|
|
15
|
+
- **[AGENTS_FRONTEND.md](./AGENTS_FRONTEND.md)** — Generated endpoints overview, search (POST with body filters, conditions/advanced filters using ConditionBuilder methods, query params for ordering/pagination/aggregation), `joinMustExist` (EXISTS-based filtering by related tables), `joinMultiple` (fetch child rows), `joinGroup` (aggregations), `joinLeft` (embed N:1 parent inline), GET, INSERT (with secondaries), UPDATE (with secondaries + deletions), DELETE, bulk upsert, bulk delete, response shapes (PK-only), **validation errors** (structured field-level 400 responses), pagination, computed values.
|
|
14
16
|
|
|
15
17
|
## Quick Reference
|
|
16
18
|
|
|
@@ -54,3 +56,14 @@ export const TableCustomer = defineTable({
|
|
|
54
56
|
...exportTableInfo(Schema),
|
|
55
57
|
});
|
|
56
58
|
```
|
|
59
|
+
|
|
60
|
+
### Joins at a glance
|
|
61
|
+
|
|
62
|
+
| Family | Direction | Cardinality | Output |
|
|
63
|
+
|--------|-----------|-------------|--------|
|
|
64
|
+
| `joinMustExist` | child → main | 1:N | filters main via EXISTS |
|
|
65
|
+
| `joinMultiple` | child → main | 1:N | side query, child rows in `result.joinMultiple.<alias>` |
|
|
66
|
+
| `joinGroup` | child → main | 1:N | aggregations in `result.joinGroup.<alias>` |
|
|
67
|
+
| `joinLeft` | parent → main | N:1 | real LEFT JOIN (on demand), parent rows in `result.joinLeft.<alias>` |
|
|
68
|
+
|
|
69
|
+
Declare relations with `buildRelation(M, mF, J, jF, options?)`. Options are all optional: `alias` defaults to `joinSchema.tableName` (override only when joining the same table twice or to use a friendlier name), `selection` defaults to `'*'`, `unique` defaults to `false`. Set `unique: true` for N:1 (parent) relations to enable `joinLeft`. See [BREAKING_CHANGES.md](./BREAKING_CHANGES.md) for the full migration guide.
|
package/AGENTS_BACKEND.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
How to set up schemas, tables, and configure the plugin.
|
|
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, `buildRelation` signature, and validation rules. Common to backend and frontend.
|
|
6
|
+
|
|
5
7
|
## Setup Workflow
|
|
6
8
|
|
|
7
9
|
### 1. Install
|
|
@@ -234,13 +236,20 @@ export const TableCustomer = defineTable({
|
|
|
234
236
|
excludeFromCreation: ['id'], // Fields omitted from INSERT — MUST include auto-increment PKs
|
|
235
237
|
distinctResults: true, // Use SELECT DISTINCT
|
|
236
238
|
|
|
237
|
-
// JOINS — relations to other tables
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
239
|
+
// JOINS — relations to other tables. Alias defaults to joinSchema.tableName —
|
|
240
|
+
// declare it explicitly only when joining the same table twice or when you want
|
|
241
|
+
// a friendlier name. Set `unique: true` for N:1 (parent) relations to enable `joinLeft`.
|
|
242
|
+
allowedReadJoins: [ // Available for search: joinMustExist / joinMultiple / joinGroup / joinLeft
|
|
243
|
+
buildRelation(SchemaCustomer, 'id', SchemaOrder, 'customerId'), // alias = 'order' (default)
|
|
244
|
+
buildRelation(SchemaCustomer, 'id', SchemaAddress, 'customerId', { selection: 'id, city, zip' }), // alias = 'address'
|
|
245
|
+
// N:1 example (enables joinLeft):
|
|
246
|
+
// buildRelation(SchemaSession, 'userId', SchemaUser, 'id', { unique: true }), // alias = 'user'
|
|
247
|
+
// Alias multipli sulla stessa tabella (richiede alias espliciti):
|
|
248
|
+
// buildRelation(SchemaSession, 'createdBy', SchemaUser, 'id', { alias: 'creator', unique: true }),
|
|
249
|
+
// buildRelation(SchemaSession, 'updatedBy', SchemaUser, 'id', { alias: 'updater', unique: true }),
|
|
241
250
|
],
|
|
242
|
-
allowedWriteJoins: [ // Available for insert/update secondaries
|
|
243
|
-
buildRelation(SchemaCustomer, 'id', SchemaOrder, 'customerId'),
|
|
251
|
+
allowedWriteJoins: [ // Available for insert/update secondaries (alias is the request body key)
|
|
252
|
+
buildRelation(SchemaCustomer, 'id', SchemaOrder, 'customerId'), // alias = 'order'
|
|
244
253
|
],
|
|
245
254
|
|
|
246
255
|
// UPSERT — ON CONFLICT resolution
|
|
@@ -271,8 +280,9 @@ export const TableCustomer = defineTable({
|
|
|
271
280
|
if (main.name === '') {
|
|
272
281
|
errors.push(['name', 'required', 'cannot be empty']);
|
|
273
282
|
}
|
|
274
|
-
// Cross-entity: validate secondaries (e.g. date overlap in periods)
|
|
275
|
-
|
|
283
|
+
// Cross-entity: validate secondaries (e.g. date overlap in periods).
|
|
284
|
+
// Note: secondaries are keyed by alias (declared in allowedWriteJoins), not by tableName.
|
|
285
|
+
if (secondaries?.orders) {
|
|
276
286
|
// ... check overlaps, business rules across related records
|
|
277
287
|
}
|
|
278
288
|
return errors;
|
|
@@ -308,14 +318,42 @@ export const TableCustomer = defineTable({
|
|
|
308
318
|
### buildRelation signature
|
|
309
319
|
|
|
310
320
|
```typescript
|
|
311
|
-
buildRelation(mainSchema, mainField, joinSchema, joinField,
|
|
321
|
+
buildRelation(mainSchema, mainField, joinSchema, joinField, options?)
|
|
322
|
+
|
|
323
|
+
// where options is fully optional:
|
|
324
|
+
// {
|
|
325
|
+
// alias?: string; // default = joinSchema.tableName. The key used in request/response
|
|
326
|
+
// // payloads, secondaries/deletions, and dotted-notation orderBy/conditions.
|
|
327
|
+
// selection?: string; // default '*'. Comma-separated columns: 'id, name, total'.
|
|
328
|
+
// unique?: boolean; // default false.
|
|
329
|
+
// // false → 1:N (child→main). Allowed in joinMustExist/joinMultiple/joinGroup.
|
|
330
|
+
// // true → N:1 (parent→main). Allowed in joinLeft only.
|
|
331
|
+
// }
|
|
312
332
|
```
|
|
313
333
|
|
|
314
334
|
- `mainField`: field(s) in main table (string or string[])
|
|
315
|
-
- `joinField`: field in join table that
|
|
316
|
-
- `
|
|
335
|
+
- `joinField`: field in join table that joins to `mainField`
|
|
336
|
+
- For 1:N (child→main): `joinField` is the FK on the child table → `mainField` is the PK on main
|
|
337
|
+
- For N:1 (parent→main): `joinField` is the PK on the parent table → `mainField` is the FK on main
|
|
338
|
+
|
|
339
|
+
**When to declare an explicit `alias`**: only when (a) you join the same table multiple times in the same `allowedReadJoins`/`allowedWriteJoins`, or (b) you want a friendlier name in the public API surface than the SQL table name (e.g. `'orders'` instead of `'customer_order'`). Otherwise omit it — the default is `joinSchema.tableName`.
|
|
317
340
|
|
|
318
|
-
|
|
341
|
+
**Choosing `unique`**: if `joinField` is the PK (or part of the composite PK) of `joinSchema`, the relation is N:1 — you almost certainly want `unique: true` so the alias is usable in `joinLeft`.
|
|
342
|
+
|
|
343
|
+
**Aliasing the same table twice**: declare two `buildRelation` entries with different `alias`. Example: a `session` table referencing `user` for both `createdBy` and `updatedBy`:
|
|
344
|
+
|
|
345
|
+
```typescript
|
|
346
|
+
allowedReadJoins: [
|
|
347
|
+
buildRelation(SchemaSession, 'createdBy', SchemaUser, 'id', { alias: 'creator', unique: true }),
|
|
348
|
+
buildRelation(SchemaSession, 'updatedBy', SchemaUser, 'id', { alias: 'updater', unique: true }),
|
|
349
|
+
]
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
If two entries in the same `allowedReadJoins`/`allowedWriteJoins` resolve to the same alias (explicit or implicit), `defineTable` throws at startup. So you cannot accidentally have two relations sharing an alias — the error tells you to disambiguate explicitly.
|
|
353
|
+
|
|
354
|
+
The 1:N joins (`joinMustExist` / `joinMultiple` / `joinGroup`) work as: `SELECT {selection} FROM {childTable} WHERE {childFK} IN ({mainPK values})`. The N:1 join (`joinLeft`) adds `LEFT JOIN {parentTable} AS {alias} ON {alias}.{parentPK} = {main}.{mainFK}` to the main query, but only when the request actually needs it (filters/orderBy on parent fields). Otherwise a side query `SELECT FROM {parentTable} WHERE {parentPK} IN (distinct main FK values)` is used.
|
|
355
|
+
|
|
356
|
+
**`joinLeft` 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.
|
|
319
357
|
|
|
320
358
|
### extraFilters + extendedCondition
|
|
321
359
|
|
|
@@ -351,6 +389,89 @@ export const TableCustomer = defineTable({
|
|
|
351
389
|
|
|
352
390
|
---
|
|
353
391
|
|
|
392
|
+
## Computed Fields (extension system)
|
|
393
|
+
|
|
394
|
+
`computedFields` lets you declare **virtual fields** as SQL expressions on a per-table basis. Each computed becomes usable like a regular schema field across the search API: `filters` (equality), `conditions` (operators), `orderBy` (1-parte), `computeMin/Max/Sum/Avg`, and (opt-in) in `selectComputed` for the main response. Same machinery serves JSON column extraction, derived strings, dialect-aware date/calendar bucketing — without growing the library case-by-case.
|
|
395
|
+
|
|
396
|
+
```typescript
|
|
397
|
+
import { defineTable, exportTableInfo, Type } from 'fastify-auto-sqlapi';
|
|
398
|
+
import { SchemaCustomer as Schema } from '../schemas/SchemaCustomer';
|
|
399
|
+
|
|
400
|
+
export const TableCustomer = defineTable({
|
|
401
|
+
primary: 'id',
|
|
402
|
+
...exportTableInfo(Schema),
|
|
403
|
+
computedFields: {
|
|
404
|
+
// JSON path extraction — dialect-aware (Postgres -> arrow, MySQL -> JSON_EXTRACT).
|
|
405
|
+
statusFromMeta: ({ db, qiCol }) => ({
|
|
406
|
+
expr: db.dialectName === 'postgres'
|
|
407
|
+
? `${qiCol('metadata')}->>'status'`
|
|
408
|
+
: `JSON_UNQUOTE(JSON_EXTRACT(${qiCol('metadata')}, '$.status'))`,
|
|
409
|
+
values: [],
|
|
410
|
+
type: Type.String(),
|
|
411
|
+
}),
|
|
412
|
+
// Derived string column — dialect-aware concat.
|
|
413
|
+
fullName: ({ db, qiCol }) => ({
|
|
414
|
+
expr: db.dialectName === 'postgres'
|
|
415
|
+
? `${qiCol('firstName')} || ' ' || ${qiCol('lastName')}`
|
|
416
|
+
: `CONCAT(${qiCol('firstName')}, ' ', ${qiCol('lastName')})`,
|
|
417
|
+
values: [],
|
|
418
|
+
type: Type.String(),
|
|
419
|
+
}),
|
|
420
|
+
},
|
|
421
|
+
});
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
Client side, the computed name behaves like any other field:
|
|
425
|
+
|
|
426
|
+
```json
|
|
427
|
+
{
|
|
428
|
+
"filters": { "statusFromMeta": "active" },
|
|
429
|
+
"conditions": [
|
|
430
|
+
{ "field": "fullName", "method": "isLike", "params": ["%Mario%"] }
|
|
431
|
+
],
|
|
432
|
+
"orderBy": "fullName ASC",
|
|
433
|
+
"selectComputed": ["fullName", "statusFromMeta"]
|
|
434
|
+
}
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
The values returned by `selectComputed` appear as extra fields on each `main[i]` row.
|
|
438
|
+
|
|
439
|
+
### `ComputedFieldFn` signature
|
|
440
|
+
|
|
441
|
+
```typescript
|
|
442
|
+
type ComputedFieldFn = (ctx: {
|
|
443
|
+
db: QueryClient; // dialect-aware (qi, ph, dialectName, dateTrunc, ...)
|
|
444
|
+
qiCol(field: string, opts?: { qualifier?: string }): string;
|
|
445
|
+
}) => {
|
|
446
|
+
expr: string; // SQL fragment
|
|
447
|
+
values: unknown[]; // bound values (see limitations below)
|
|
448
|
+
type: TSchema; // REQUIRED — used by Swagger and body validation
|
|
449
|
+
};
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
`qiCol(field)` returns a properly-quoted column reference, optionally prefixed by an alias qualifier — the engine passes the alias automatically when the computed is invoked inside a `joinLeft` LEFT JOIN, so the same function works both inline on the main query and as a parent-side column.
|
|
453
|
+
|
|
454
|
+
### Validation & startup checks
|
|
455
|
+
|
|
456
|
+
`defineTable` throws synchronously if a computed name collides with a schema field or an `extraFilters` key on the same table. The error message tells you which name to change.
|
|
457
|
+
|
|
458
|
+
### Side queries (`joinMustExist`, `joinMultiple`, `joinGroup`, `joinLeft`)
|
|
459
|
+
|
|
460
|
+
Computed fields are **per-table**: a side query operating on a join target reads `joinTableConf.computedFields`, not the main table's. So if you need `statusFromMeta` on `joinLeft.user.filters`, declare it on `TableUser`, not on `TableSession`.
|
|
461
|
+
|
|
462
|
+
For `joinLeft` specifically, the computed expr is automatically alias-prefixed by `qiCol` (the engine forwards the alias). For the other three families the side query is a separate SELECT, so plain column references suffice.
|
|
463
|
+
|
|
464
|
+
### Limitations (first round, by design)
|
|
465
|
+
|
|
466
|
+
- **Bound `values` in computed expressions** are supported only on `filters`/`conditions` (main and `joinMustExist`/`joinMultiple`/`joinGroup`). They are rejected with 400 on `selectComputed`, `computeMin/Max/Sum/Avg`, `orderBy`, and `joinLeft.filters`/`conditions` — those paths require placeholder coordination not done in the first round. Most use cases (JSON extraction, concat, dateTrunc, simple ops) need no placeholders.
|
|
467
|
+
- **Computed CAN be used in `joinGroup.aggregations.by`** (just pass the computed name as a string). Bound `values` are not supported in this position (rejected with 400). The existing FK-correlation rule for `orderBy <alias>.<fn>.<field>` still applies: 3-parti aggregation orderBy on a `by` that isn't the correlation FK is rejected — by definition a computed-by produces multiple groups per main row, so this is never valid.
|
|
468
|
+
- **Computed cannot be used as aggregation function** (`sum`/`min`/`max`/...). The values inside `aggregations.sum: ['<name>']` must be schema field names. To aggregate on a derived expression, declare the derivation as a computed and pass the computed name as the field, BUT only via `computeMin/Max/Sum/Avg` (top-level main aggregates), not the joinGroup ones.
|
|
469
|
+
- **No chained computed** (a computed referencing another computed). Flat-only.
|
|
470
|
+
- **Read-only**. Computed fields are not usable in insert/update bodies — the consumer's `expr` is for SELECT/WHERE/ORDER BY, never for writes.
|
|
471
|
+
- **Same expression evaluated multiple times** when used in WHERE + ORDER BY + SELECT. Cheap exprs are fine; for expensive ones, query planner CSE often helps.
|
|
472
|
+
|
|
473
|
+
---
|
|
474
|
+
|
|
354
475
|
## ConditionBuilder API
|
|
355
476
|
|
|
356
477
|
Used in `extendedCondition` callbacks, hooks, and exposed via the `conditions` field in the search API (see [AGENTS_FRONTEND.md](./AGENTS_FRONTEND.md#conditions-advanced-filters)).
|
|
@@ -401,7 +522,7 @@ app.get('/billing/:customerId', async (request) => {
|
|
|
401
522
|
const customerId = Number(request.params.customerId);
|
|
402
523
|
return app.sqlApi.search('subscription', {
|
|
403
524
|
filters: { customerId, status: 'active' },
|
|
404
|
-
|
|
525
|
+
joinMustExist: { orders: { filters: { status: 'pending' } } },
|
|
405
526
|
paginator: { page: 1, itemsPerPage: 50 },
|
|
406
527
|
}, request);
|
|
407
528
|
});
|
|
@@ -431,10 +552,13 @@ const sqlApi = createSqlApi(db, dbTables, { dialect: 'mysql' });
|
|
|
431
552
|
|
|
432
553
|
```typescript
|
|
433
554
|
// Search — full filter, join, pagination, aggregation support
|
|
434
|
-
// orderBy supports
|
|
435
|
-
//
|
|
555
|
+
// orderBy supports two dotted notations:
|
|
556
|
+
// - 3-parti `<alias>.<fn>.<field>` for joinGroup aggregations (must be declared in the request body)
|
|
557
|
+
// - 2-parti `<alias>.<field>` for joinLeft parent fields
|
|
558
|
+
// See AGENTS_FRONTEND.md and BREAKING_CHANGES.md for the full reference.
|
|
436
559
|
sqlApi.search(tableName, {
|
|
437
|
-
filters?,
|
|
560
|
+
filters?, conditions?,
|
|
561
|
+
joinMustExist?, joinMultiple?, joinGroup?, joinLeft?,
|
|
438
562
|
orderBy?, paginator?, computeMin?, computeMax?, computeSum?, computeAvg?,
|
|
439
563
|
}, request?): Promise<SearchResult>
|
|
440
564
|
|
|
@@ -640,7 +764,7 @@ const TableSession = defineTable({
|
|
|
640
764
|
primary: 'id',
|
|
641
765
|
...exportTableInfo(SchemaSession),
|
|
642
766
|
allowedWriteJoins: [
|
|
643
|
-
buildRelation(SchemaSession, 'id', SchemaPeriod, 'sessionId'),
|
|
767
|
+
buildRelation(SchemaSession, 'id', SchemaPeriod, 'sessionId', { alias: 'periods' }),
|
|
644
768
|
],
|
|
645
769
|
validate: async (db, req, main, secondaries) => {
|
|
646
770
|
// ValidationError is a tuple: [field, code] or [field, code, message]
|
|
@@ -660,13 +784,13 @@ const TableSession = defineTable({
|
|
|
660
784
|
}
|
|
661
785
|
}
|
|
662
786
|
|
|
663
|
-
// Cross-entity: validate periods don't overlap
|
|
664
|
-
const periods = secondaries?.
|
|
787
|
+
// Cross-entity: validate periods don't overlap (secondaries keyed by alias)
|
|
788
|
+
const periods = secondaries?.periods;
|
|
665
789
|
if (periods?.length) {
|
|
666
790
|
for (let i = 0; i < periods.length; i++) {
|
|
667
791
|
for (let j = i + 1; j < periods.length; j++) {
|
|
668
792
|
if (periods[i].startDate < periods[j].endDate && periods[j].startDate < periods[i].endDate) {
|
|
669
|
-
errors.push([`
|
|
793
|
+
errors.push([`periods[${j}].startDate`, 'overlap', 'overlaps with another period']);
|
|
670
794
|
}
|
|
671
795
|
}
|
|
672
796
|
}
|