fastify-auto-sqlapi 0.3.0 → 0.4.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 +24 -111
- package/AGENTS_BACKEND.md +271 -1381
- package/AGENTS_FRONTEND.md +72 -47
- package/BREAKING_CHANGES.md +129 -0
- package/CHANGELOG.md +86 -1
- package/README.md +1 -1
- package/dist/bin/generate-schema.js +2 -2
- package/dist/lib/cli/mysql-introspect.d.ts.map +1 -1
- package/dist/lib/cli/mysql-introspect.js +24 -1
- package/dist/lib/cli/pg-introspect.d.ts.map +1 -1
- package/dist/lib/cli/pg-introspect.js +21 -2
- 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 +31 -3
- package/dist/lib/db.d.ts.map +1 -1
- package/dist/lib/db.js +187 -60
- package/dist/lib/dialect.d.ts +15 -10
- package/dist/lib/dialect.d.ts.map +1 -1
- package/dist/lib/dialect.js +12 -31
- package/dist/lib/engine/bulk/bulk-upsert.d.ts.map +1 -1
- package/dist/lib/engine/bulk/bulk-upsert.js +2 -1
- package/dist/lib/engine/rest/insert.d.ts.map +1 -1
- package/dist/lib/engine/rest/insert.js +2 -1
- package/dist/lib/engine/search/aggregations.d.ts +7 -0
- package/dist/lib/engine/search/aggregations.d.ts.map +1 -1
- package/dist/lib/engine/search/aggregations.js +8 -7
- package/dist/lib/engine/write-helpers.d.ts +13 -1
- package/dist/lib/engine/write-helpers.d.ts.map +1 -1
- package/dist/lib/engine/write-helpers.js +35 -0
- package/dist/lib/table-helpers.d.ts.map +1 -1
- package/dist/lib/table-helpers.js +41 -0
- package/dist/types/cli.d.ts +11 -0
- package/dist/types/cli.d.ts.map +1 -1
- package/dist/types/schema.d.ts +7 -0
- package/dist/types/schema.d.ts.map +1 -1
- package/docs/adr/0021-upsert-targets-a-verified-unique-key.md +153 -0
- package/docs/adr/README.md +1 -0
- package/package.json +1 -1
package/AGENTS_FRONTEND.md
CHANGED
|
@@ -1,111 +1,136 @@
|
|
|
1
1
|
# fastify-auto-sqlapi — client API (LLM reference)
|
|
2
2
|
|
|
3
|
-
This
|
|
4
|
-
vocabulary), fetch `GET {p}/agent/manifest.md` if available, or consult Swagger.
|
|
3
|
+
This is the request GRAMMAR. The vocabulary (this deployment's tables, fields, join aliases, computed fields, extra filters) comes from `GET {p}/agent/manifest.md` when enabled, or from Swagger.
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
- All request/response fields are camelCase schema field names; the server maps them to columns.
|
|
6
|
+
- `{p}` = deployment prefix (e.g. `/api`). `{table}` = the table's key in the manifest, which may differ from the DB table name (one table can be exposed under several keys, each with its own rules).
|
|
7
|
+
- Join keys are backend-declared ALIASES (default: the joined table name).
|
|
8
|
+
- Write responses return PKs only. Send only real schema fields in write bodies: unknown ones are dropped or rejected (400), depending on the server.
|
|
9
|
+
- Composite-PK tables have no get/delete/bulk-delete routes (404): use search / update.
|
|
7
10
|
|
|
8
11
|
## Endpoints
|
|
9
12
|
|
|
10
13
|
```
|
|
11
|
-
POST
|
|
12
|
-
GET
|
|
13
|
-
POST
|
|
14
|
-
PUT
|
|
15
|
-
DELETE {p}/rest/{table}/:id
|
|
16
|
-
PUT
|
|
17
|
-
POST
|
|
14
|
+
POST {p}/search/{table} body=search request → {table, main:[...], ...}
|
|
15
|
+
GET {p}/rest/{table}/:id → {main:{...}} | 404
|
|
16
|
+
POST {p}/rest/{table} body={main, secondaries?} → 201 {main:{id}, secondaries?}
|
|
17
|
+
PUT {p}/rest/{table} body={main(+PK), secondaries?, deletions?} → {main:{id}, secondaries?, deletions?}
|
|
18
|
+
DELETE {p}/rest/{table}/:id → {main:{id}} | 404
|
|
19
|
+
PUT {p}/bulk/{table} body=[{main, secondaries?, deletions?}, ...] → [{main:{id}}, ...]
|
|
20
|
+
POST {p}/bulk/{table}/delete body=[{id:1}, {id:2}] → [{main:{id:1}}, ...] // key = the table's PK field
|
|
18
21
|
```
|
|
19
22
|
|
|
20
|
-
##
|
|
23
|
+
## Search
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
Body (all optional; `{}` = first N rows). Everything below is combinable in one request:
|
|
25
|
+
Querystring, all optional: `orderBy`, `page`, `itemsPerPage`, `computeMin|Max|Sum|Avg=<field>`. Body, all optional and combinable; `{}` returns the first N rows.
|
|
24
26
|
|
|
25
27
|
```jsonc
|
|
26
28
|
// POST {p}/search/customer?orderBy=orders.sum.total DESC, creator.name ASC, id&page=1&itemsPerPage=20&computeSum=total
|
|
27
29
|
{
|
|
28
|
-
"filters": { "status": "active", "deletedById": null },
|
|
29
|
-
"conditions": [
|
|
30
|
+
"filters": { "status": "active", "deletedById": null }, // equality, AND; null → IS NULL; also computed fields and backend extra filters (see manifest). Unknown key → 400
|
|
31
|
+
"conditions": [ // AND; the same field may repeat
|
|
30
32
|
{ "field": "total", "method": "isBetween", "params": [100, 500] },
|
|
31
33
|
{ "field": "name", "method": "isILike", "params": ["%mario%"] },
|
|
32
34
|
{ "field": "status", "method": "isIn", "params": [["active", "pending"]] },
|
|
33
|
-
{ "field": "orders.count.id", "method": "isGreaterOrEqual", "params": [4] }
|
|
35
|
+
{ "field": "orders.count.id", "method": "isGreaterOrEqual", "params": [4] } // alias.fn.field, HAVING-style: needs the matching joinGroup below
|
|
34
36
|
],
|
|
35
|
-
"selectComputed": ["displayName"],
|
|
36
|
-
"joinMustExist": {
|
|
37
|
+
"selectComputed": ["displayName"], // computed fields are projected into main rows only when listed
|
|
38
|
+
"joinMustExist": { // EXISTS on children: decides WHICH main rows return; fetches nothing
|
|
37
39
|
"labels": { "filters": { "labelId": 1 }, "conditions": [] }
|
|
38
40
|
},
|
|
39
|
-
"joinMultiple": {
|
|
40
|
-
"orders": { "filters": { "status": "pending" }, "selection": "id,total,status" }
|
|
41
|
+
"joinMultiple": { // side query: child rows of the returned main rows
|
|
42
|
+
"orders": { "filters": { "status": "pending" }, "conditions": [], "selection": "id,total,status" }
|
|
41
43
|
},
|
|
42
|
-
"joinGroup": {
|
|
44
|
+
"joinGroup": { // aggregates child rows of the returned main rows
|
|
43
45
|
"orders": {
|
|
44
|
-
"aggregations": {
|
|
45
|
-
|
|
46
|
-
"sum": ["total"], "min": ["total"], "max": ["total"], "avg": ["total"],
|
|
47
|
-
"count": ["id"], "distinctCount": ["status"]
|
|
48
|
-
},
|
|
49
|
-
"filters": { "status": "active" } // narrows child rows before aggregating
|
|
46
|
+
"aggregations": { "by": "status", "sum": ["total"], "min": ["total"], "max": ["total"], "avg": ["total"], "count": ["id"], "distinctCount": ["status"] }, // by: optional GROUP BY (field or computed)
|
|
47
|
+
"filters": { "status": "active" }, "conditions": [] // narrow children before aggregating
|
|
50
48
|
}
|
|
51
49
|
},
|
|
52
|
-
"joinLeft": {
|
|
53
|
-
"creator": { "selection": "id,name" }
|
|
50
|
+
"joinLeft": { // N:1 parent (alias declared unique by the backend)
|
|
51
|
+
"creator": { "selection": "id,name", "filters": {}, "conditions": [] } // filters here EXCLUDE mains whose parent does not match; no extra filters here
|
|
54
52
|
}
|
|
55
53
|
}
|
|
56
54
|
```
|
|
57
55
|
|
|
58
|
-
Response
|
|
56
|
+
Response; each section is present only if requested:
|
|
59
57
|
|
|
60
58
|
```jsonc
|
|
61
59
|
{
|
|
62
60
|
"table": "customer",
|
|
63
61
|
"main": [{ "id": 1, "name": "Mario", "creatorId": 7, "displayName": "..." }],
|
|
64
|
-
"joinMultiple": { "orders": [{ "id": 10, "customerId": 1, "total": 50 }] },
|
|
62
|
+
"joinMultiple": { "orders": [{ "id": 10, "customerId": 1, "total": 50 }] }, // flat list for all mains: group client-side by FK
|
|
65
63
|
"joinGroup": { "orders": {
|
|
66
|
-
"sum": { "total": 150 },
|
|
67
|
-
"rows": [{ "by": "pending", "sum_total": 100 }]
|
|
64
|
+
"sum": { "total": 150 }, // no "by": ONE total over the children of ALL returned mains
|
|
65
|
+
"rows": [{ "by": "pending", "sum_total": 100 }] // with "by": one row per group; by = FK field gives per-main values
|
|
68
66
|
}},
|
|
69
|
-
"joinLeft": { "creator": [{ "id": 7, "name": "Alice" }] },
|
|
70
|
-
"pagination": {
|
|
67
|
+
"joinLeft": { "creator": [{ "id": 7, "name": "Alice" }] }, // deduplicated: match main.creatorId === creator.id
|
|
68
|
+
"pagination": { // only when page or itemsPerPage is sent
|
|
71
69
|
"total": 25, "pages": 3,
|
|
72
|
-
"computed": { "sum": { "total": 999 } },
|
|
70
|
+
"computed": { "sum": { "total": 999 } }, // computeMin/Max/Sum/Avg over all matching rows; require pagination
|
|
73
71
|
"paginator": { "page": 1, "itemsPerPage": 20 }
|
|
74
72
|
}
|
|
75
73
|
}
|
|
76
74
|
```
|
|
77
75
|
|
|
78
|
-
|
|
76
|
+
Pagination:
|
|
77
|
+
- Without `page`/`itemsPerPage`, the server caps rows at its maximum (default 1000) and returns no `pagination` block.
|
|
78
|
+
- With `page` only, `itemsPerPage` defaults to 500 (or the server maximum if lower).
|
|
79
|
+
- Both are integers ≥ 1. `itemsPerPage` above the server maximum → 400.
|
|
79
80
|
|
|
80
|
-
|
|
81
|
+
`orderBy` is a comma-separated list, each part one of:
|
|
82
|
+
- `field [ASC|DESC]`, including computed fields.
|
|
83
|
+
- `alias.parentField`: joinLeft alias; adds the LEFT JOIN.
|
|
84
|
+
- `alias.fn.field`, with fn one of `sum|min|max|avg|count|distinctCount`. The joinGroup with that fn and field must be in the body.
|
|
81
85
|
|
|
82
|
-
|
|
86
|
+
For `alias.fn.field`, in both `orderBy` and conditions, the joinGroup `by` must be absent or equal to the FK (else 400). In `orderBy` it is also a 400 on tables with distinct results.
|
|
87
|
+
|
|
88
|
+
A main without children (or none left after the joinGroup filters) has `count`, `distinctCount` and `sum` = 0, but `min`, `max` and `avg` = null. Null sorts where the database puts it (PostgreSQL: last in ASC, first in DESC; MySQL/MariaDB: first in ASC, last in DESC) and fails every comparison: `max < 5` excludes such mains; `isNull` selects them.
|
|
89
|
+
|
|
90
|
+
Condition methods and `params` arity:
|
|
91
|
+
- `[v]`: `isEqual|isNotEqual|isGreater|isGreaterOrEqual|isLess|isLessOrEqual|isNotGreater|isNotGreaterOrEqual|isNotLess|isNotLessOrEqual`
|
|
92
|
+
- `["%p%"]`: `isLike|isNotLike|isILike|isNotILike`
|
|
93
|
+
- `[from, to]`: `isBetween|isNotBetween`
|
|
94
|
+
- `[[v1, v2]]`: `isIn|isNotIn`
|
|
95
|
+
- `[]`: `isNull|isNotNull`
|
|
96
|
+
|
|
97
|
+
An unknown field or method, or `params` that is not an array or is too short → 400.
|
|
98
|
+
|
|
99
|
+
## Writes
|
|
83
100
|
|
|
84
101
|
```jsonc
|
|
85
|
-
// POST {p}/rest/customer
|
|
102
|
+
// POST {p}/rest/customer — insert; omit auto-generated PKs
|
|
86
103
|
{ "main": { "name": "Mario", "email": "m@t.it" },
|
|
87
|
-
"secondaries": { "orders": [{ "total": 50, "status": "pending" }] } }
|
|
104
|
+
"secondaries": { "orders": [{ "total": 50, "status": "pending" }] } } // child FK auto-filled from the new main PK
|
|
88
105
|
// → 201 { "main": { "id": 1 }, "secondaries": { "orders": [{ "id": 10 }] } }
|
|
89
106
|
|
|
90
|
-
// PUT {p}/rest/customer
|
|
107
|
+
// PUT {p}/rest/customer — update; main MUST include the PK; send only the fields to change; null sets NULL
|
|
91
108
|
{ "main": { "id": 1, "email": "new@t.it" },
|
|
92
|
-
"secondaries": { "orders": [{ "total": 75 }] },
|
|
93
|
-
"deletions":
|
|
109
|
+
"secondaries": { "orders": [{ "total": 75 }] }, // insert (or upsert) children, FK auto-filled
|
|
110
|
+
"deletions": { "orders": [{ "id": 10 }] } } // delete children by PK, only children of this main
|
|
94
111
|
// → 200 { "main": { "id": 1 }, "secondaries": { "orders": [{ "id": 20 }] }, "deletions": { "orders": [{ "id": 10 }] } }
|
|
95
112
|
|
|
96
|
-
// PUT {p}/bulk/customer
|
|
113
|
+
// PUT {p}/bulk/customer — bulk upsert; items need not share keys: an omitted field keeps the stored value (update) or takes the column default (insert)
|
|
97
114
|
[ { "main": { "name": "Mario" }, "secondaries": { "orders": [{ "total": 50 }] } },
|
|
98
115
|
{ "main": { "name": "Luigi" } } ]
|
|
99
116
|
// → 200 [ { "main": { "id": 1 }, "secondaries": {...} }, { "main": { "id": 2 } } ]
|
|
100
117
|
```
|
|
101
118
|
|
|
119
|
+
Upsert tables (see manifest):
|
|
120
|
+
- Every row, main or child, carries its conflict key: missing, null, or shared by two rows of one request → 400.
|
|
121
|
+
- When the key is the id, rows without an id are inserted.
|
|
122
|
+
- Bulk arrays are capped (default 1000): chunk client-side.
|
|
123
|
+
|
|
102
124
|
## Errors
|
|
103
125
|
|
|
104
|
-
400 (schema or business validation)
|
|
126
|
+
400 (schema or business validation): map `fields[].path` to form fields.
|
|
105
127
|
|
|
106
128
|
```json
|
|
107
129
|
{ "statusCode": 400, "error": "Bad Request", "message": "Validation failed",
|
|
108
130
|
"fields": [ { "path": "body.main.name", "code": "required", "message": "must have required property 'name'" } ] }
|
|
109
131
|
```
|
|
110
132
|
|
|
111
|
-
|
|
133
|
+
- 403: tenant violation.
|
|
134
|
+
- 404: record not found, or outside your tenant.
|
|
135
|
+
- 500: DB errors (e.g. constraint violations), unmapped, with a `requestId` to report.
|
|
136
|
+
- Per search, max 100 `conditions` per section (top level and each join) and max 20 `orderBy` parts (≤ 1024 chars); over either → 400. Collapse many equality conditions into one `isIn`.
|
package/BREAKING_CHANGES.md
CHANGED
|
@@ -744,3 +744,132 @@ export const dbTables: DbTables = { customer_order: TableCustomerOrder };
|
|
|
744
744
|
|
|
745
745
|
A client that read `table` from a search response to recover the physical table name should
|
|
746
746
|
use the key it called instead — that is now what `table` returns.
|
|
747
|
+
|
|
748
|
+
---
|
|
749
|
+
|
|
750
|
+
# Breaking Change — an upsert targets a unique key verified at startup
|
|
751
|
+
|
|
752
|
+
*Landed in **0.4.0**.* Decision and rationale: [ADR 0021](./docs/adr/0021-upsert-targets-a-verified-unique-key.md).
|
|
753
|
+
|
|
754
|
+
## Why this changed
|
|
755
|
+
|
|
756
|
+
`upsertMap` accepted any column list, and the dialects disagreed on what it meant:
|
|
757
|
+
|
|
758
|
+
- **No unique index on the declared columns**: PostgreSQL failed every write; MySQL never
|
|
759
|
+
conflicted and inserted duplicates, silently.
|
|
760
|
+
- **MySQL `ON DUPLICATE KEY UPDATE` fires on any unique index**, not on the declared key: a row
|
|
761
|
+
carrying another row's unique value (an `email`, an `id`) updated *that* row, silently, where
|
|
762
|
+
PostgreSQL refused the write.
|
|
763
|
+
- **A natural-key upsert on MySQL returned wrong PKs** for the rows it updated, and secondaries
|
|
764
|
+
were attached to them.
|
|
765
|
+
|
|
766
|
+
## What changed
|
|
767
|
+
|
|
768
|
+
- **The generated Schema carries `uniqueKeys`**, the table's unique keys other than the
|
|
769
|
+
primary key — emitted even when empty. Partial, expression and (MySQL) prefix indexes are not
|
|
770
|
+
keys an upsert can target and are left out.
|
|
771
|
+
- **`defineTable` refuses an `upsertMap` rule that is not exactly the Schema's `primaryKey` or
|
|
772
|
+
one of its `uniqueKeys`** (same fields, any order). This includes rules on write-join child
|
|
773
|
+
schemas — the child still needs no `defineTable` of its own. A rule on a view is refused.
|
|
774
|
+
- **Every upserted row must carry its key — `400` otherwise**: a natural key must be present
|
|
775
|
+
and not null; the primary key may be absent altogether (a plain insert with a generated key)
|
|
776
|
+
but not partial or null. Two rows of one request with the same key are a `400` too.
|
|
777
|
+
- **MySQL and MariaDB upsert by key** — probe, insert, update — instead of
|
|
778
|
+
`ON DUPLICATE KEY UPDATE`. A row whose other unique column belongs to another row is now a
|
|
779
|
+
database error (sanitized 500) on every dialect, never a write to that other row.
|
|
780
|
+
|
|
781
|
+
## Who is affected
|
|
782
|
+
|
|
783
|
+
- **Schemas generated before this release** used with a natural-key rule (`['email']`,
|
|
784
|
+
`['customerId', 'tag']` that is not the PK…): `defineTable` throws until they are
|
|
785
|
+
regenerated. Rules on the primary key keep working — `primaryKey` was already generated.
|
|
786
|
+
- **A rule on columns without a unique index**: it never worked on PostgreSQL, and on MySQL it
|
|
787
|
+
was inserting duplicates. `defineTable` now throws.
|
|
788
|
+
- **Clients sending upsert rows without their key**, or twice the same key in one request:
|
|
789
|
+
`400`.
|
|
790
|
+
- **MySQL/MariaDB**: an upsert costs two to three statements per chunk instead of one.
|
|
791
|
+
- **A driver configured without `CLIENT_FOUND_ROWS`** (MySQL/MariaDB): the upsert checks the
|
|
792
|
+
rows its UPDATE matched, and without that flag the driver reports changed rows instead —
|
|
793
|
+
a row re-sent unchanged would be reported as missing. `mysql2` sets the flag by default.
|
|
794
|
+
- **Code using the exported `SqlDialect` type or `getDialect()` for upserts**: `upsertSql()`
|
|
795
|
+
and `upsertUpdateSet()` are gone. They are replaced by `onConflict?: { sql(), updateSet() }`,
|
|
796
|
+
present on PostgreSQL only — its absence is what tells MySQL/MariaDB to upsert by key.
|
|
797
|
+
A custom `SqlDialect` implementation no longer compiles until it drops the two methods.
|
|
798
|
+
|
|
799
|
+
## Migration
|
|
800
|
+
|
|
801
|
+
1. Regenerate the schemas: `npx sqlapi-generate-schema`. Each Schema now ends with
|
|
802
|
+
`uniqueKeys: [...]`.
|
|
803
|
+
2. Start the application: a rule that is not a unique key fails with a message listing the
|
|
804
|
+
keys the table has, e.g.
|
|
805
|
+
`upsertMap rule on 'customer' (email) is not a unique key of the table. An upsert must target exactly one: (id) primary key (ADR 0021).`
|
|
806
|
+
Either add the unique index the rule assumed, or point the rule at an existing key.
|
|
807
|
+
3. Make sure every upsert row carries its key. For a write-join child keyed on
|
|
808
|
+
`(parentId, lang)`, the FK is filled from the parent — the client sends `lang`.
|
|
809
|
+
4. If you use `SqlDialect` directly: build an upsert through `QueryClient.insertOrUpdate` /
|
|
810
|
+
`bulkInsertOrUpdate`, which pick the right strategy per dialect, rather than from the
|
|
811
|
+
dialect's clause. The PostgreSQL clause, if you need it, is now
|
|
812
|
+
`getDialect('postgres').onConflict!.sql(keys, getDialect('postgres').onConflict!.updateSet(fields, keepAsIs))`
|
|
813
|
+
— `updateSet` returns `''` when nothing is left to update, as before. A custom dialect
|
|
814
|
+
implements `onConflict` only when its upsert syntax names its conflict target; otherwise it
|
|
815
|
+
leaves it out and gets the upsert by key.
|
|
816
|
+
|
|
817
|
+
```typescript
|
|
818
|
+
// before
|
|
819
|
+
const dialect = getDialect('postgres');
|
|
820
|
+
dialect.upsertSql(quotedKeys, dialect.upsertUpdateSet(fields, conflictKeys));
|
|
821
|
+
|
|
822
|
+
// after
|
|
823
|
+
const { onConflict } = getDialect('postgres');
|
|
824
|
+
onConflict!.sql(quotedKeys, onConflict!.updateSet(fields, conflictKeys));
|
|
825
|
+
```
|
|
826
|
+
|
|
827
|
+
---
|
|
828
|
+
|
|
829
|
+
# Breaking Change — `min`, `max` and `avg` of a main row without children are NULL
|
|
830
|
+
|
|
831
|
+
*Landed in **0.4.1**.*
|
|
832
|
+
|
|
833
|
+
## Why this changed
|
|
834
|
+
|
|
835
|
+
`orderBy` and HAVING-style conditions on `<alias>.<fn>.<field>` run a correlated subquery per
|
|
836
|
+
main row, and every aggregate was wrapped in `COALESCE(…, 0)`. Over no child rows, that 0 is
|
|
837
|
+
the right answer for `count`, `distinctCount` and `sum`, but `min`, `max` and `avg` have no
|
|
838
|
+
value at all:
|
|
839
|
+
|
|
840
|
+
- **PostgreSQL**: on a text, char, date or timestamp column the request failed with a 500,
|
|
841
|
+
`COALESCE types … and integer cannot be matched`.
|
|
842
|
+
- **MySQL/MariaDB**: no error, but those mains were compared as `0` — `max(orderDate) <
|
|
843
|
+
'2026-03-01'` matched every main without children.
|
|
844
|
+
- **Both, numeric columns**: a 0 that no child row holds decided the ordering and conditions.
|
|
845
|
+
|
|
846
|
+
## What changed
|
|
847
|
+
|
|
848
|
+
- `count`, `distinctCount` and `sum` over no rows are still 0.
|
|
849
|
+
- `min`, `max` and `avg` over no rows are NULL. In `orderBy` they sort where the database puts
|
|
850
|
+
NULL (PostgreSQL: last in ASC, first in DESC; MySQL/MariaDB: first in ASC, last in DESC). In
|
|
851
|
+
conditions they fail every comparison; `isNull` / `isNotNull` select them.
|
|
852
|
+
- "Without children" includes children all excluded by the joinGroup's own `filters` /
|
|
853
|
+
`conditions`.
|
|
854
|
+
- `result.joinGroup` is unchanged: it was never coalesced.
|
|
855
|
+
|
|
856
|
+
## Who is affected
|
|
857
|
+
|
|
858
|
+
Clients that order or filter by `min`, `max` or `avg` of a **numeric** column on tables where
|
|
859
|
+
some main rows have no children:
|
|
860
|
+
|
|
861
|
+
- `orderBy=orders.min.total ASC` on PostgreSQL: those mains move from first to last.
|
|
862
|
+
`… DESC`: from last to first. On MySQL/MariaDB the order is unchanged (NULL sorts as the
|
|
863
|
+
smallest value, where the 0 was for non-negative columns).
|
|
864
|
+
- `conditions: [{ field: 'orders.max.total', method: 'isLess', params: [5] }]`: those mains
|
|
865
|
+
no longer match. `isGreater` with a negative value, `isEqual` with 0 and similar change the
|
|
866
|
+
same way.
|
|
867
|
+
|
|
868
|
+
## Migration
|
|
869
|
+
|
|
870
|
+
- To drop mains without children from a result, add `{ field: 'orders.count.id', method:
|
|
871
|
+
'isGreater', params: [0] }` (or `isNotNull` on the aggregate).
|
|
872
|
+
- To list only them, use `isNull` on the aggregate, or `isEqual 0` on `count`.
|
|
873
|
+
- Conditions are ANDed, so "below 5 **or** no children" is no longer one request: run the
|
|
874
|
+
`isNull` search separately, or compare against a `count` instead where that expresses the
|
|
875
|
+
same thing.
|
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,89 @@ Migration instructions for breaking changes live in **[BREAKING_CHANGES.md](./BR
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
## [0.4.1]
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- **BREAKING: `min`, `max` and `avg` of a main row without children are NULL, not 0, in
|
|
17
|
+
`orderBy` and HAVING-style conditions.** The correlated subquery behind `alias.fn.field`
|
|
18
|
+
wrapped every aggregate in `COALESCE(…, 0)`. That 0 is right for `count`, `distinctCount` and
|
|
19
|
+
`sum`, which keep it; for `min`/`max`/`avg` it invented a value the children do not have. On
|
|
20
|
+
numeric columns, such mains now sort where the database puts NULL (PostgreSQL: last in ASC,
|
|
21
|
+
first in DESC; MySQL/MariaDB: first in ASC, last in DESC) instead of as 0, and a condition
|
|
22
|
+
like `max < 5` no longer matches them (`isNull` does). See
|
|
23
|
+
[BREAKING_CHANGES.md](./BREAKING_CHANGES.md#breaking-change--min-max-and-avg-of-a-main-row-without-children-are-null).
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- **PostgreSQL: `orderBy` or a condition on `min`/`max`/`avg` of a non-numeric column no longer
|
|
28
|
+
fails.** `orderBy=charge.max.monthYear` over a `character(7)` column (or any text, date or
|
|
29
|
+
timestamp one) was a 500, `COALESCE types character and integer cannot be matched`, whenever
|
|
30
|
+
the request reached the database. MySQL/MariaDB did not fail but compared those mains as `0`:
|
|
31
|
+
`max(orderDate) < '2026-03-01'` matched every main without children. Both now compare the
|
|
32
|
+
aggregate as NULL, as above.
|
|
33
|
+
|
|
34
|
+
## [0.4.0]
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- **BREAKING: an upsert targets a unique key verified at startup** ([ADR 0021](./docs/adr/0021-upsert-targets-a-verified-unique-key.md)).
|
|
39
|
+
`upsertMap` accepted any column list, and on MySQL `ON DUPLICATE KEY UPDATE` fired on *any*
|
|
40
|
+
unique index: a declared key without a unique index inserted duplicates, and a row carrying
|
|
41
|
+
another row's unique value updated that other row — both silently, where PostgreSQL failed.
|
|
42
|
+
Now:
|
|
43
|
+
- the generated Schema carries `uniqueKeys` (partial, expression and prefix indexes left out);
|
|
44
|
+
- `defineTable` refuses a rule that is not exactly the `primaryKey` or a `uniqueKeys` entry —
|
|
45
|
+
write-join children included, still without a `defineTable` of their own;
|
|
46
|
+
- an upserted row without its key, or two rows of one request with the same key, is a `400`
|
|
47
|
+
(a row naming no PK field at all is still a plain insert when the key is the PK);
|
|
48
|
+
- MySQL and MariaDB upsert by key — probe, insert, update: two to three statements per chunk
|
|
49
|
+
instead of one. PostgreSQL keeps `ON CONFLICT (key)`, unchanged;
|
|
50
|
+
- the exported `SqlDialect` type loses `upsertSql()` and `upsertUpdateSet()`, replaced by
|
|
51
|
+
`onConflict?: { sql(), updateSet() }`, present on PostgreSQL only.
|
|
52
|
+
Regenerate the schemas; see
|
|
53
|
+
[BREAKING_CHANGES.md](./BREAKING_CHANGES.md#breaking-change--an-upsert-targets-a-unique-key-verified-at-startup).
|
|
54
|
+
|
|
55
|
+
### Fixed
|
|
56
|
+
|
|
57
|
+
- **Bulk writes no longer lose or overwrite fields when items do not share the same keys.**
|
|
58
|
+
`bulkUpsert` (route and `sqlApi`), secondaries of every write, and
|
|
59
|
+
`QueryClient.bulkInsert` / `bulkInsertOrUpdate` took the column list of the whole batch from
|
|
60
|
+
the first record. A field present only on a later item was silently dropped; a field present
|
|
61
|
+
only on an earlier item was bound as NULL for the others — skipping the column default on
|
|
62
|
+
insert (or failing on a `NOT NULL DEFAULT` column) and wiping the stored value on upsert.
|
|
63
|
+
Each record now writes exactly its own fields: records are grouped by column set, one
|
|
64
|
+
statement per group, PKs still returned in input order. Batches whose items share the same
|
|
65
|
+
keys still run as a single statement.
|
|
66
|
+
- **An upsert item holding only its conflict keys returns its PK.** Such an item has nothing
|
|
67
|
+
to update, so an existing row is left untouched — but postgres `DO NOTHING` returns no row
|
|
68
|
+
for it and mysql's `insertId` says nothing about it: the single upsert returned `undefined`
|
|
69
|
+
and a bulk upsert failed with a 500 after writing. The PK now comes from the item when it
|
|
70
|
+
holds it, otherwise it is looked up by the conflict keys (one `SELECT` per chunk).
|
|
71
|
+
- **MySQL: an upsert by a natural key returns the PK of the row it updated.** With a conflict
|
|
72
|
+
key that is not the PK (the README's `email` sync), the PKs were synthesized as
|
|
73
|
+
`insertId + n`, which counts only inserted rows: re-sending a payload returned ids of other
|
|
74
|
+
rows, or of rows that do not exist, and secondaries were attached to them. The upsert by key
|
|
75
|
+
above takes them from its probe.
|
|
76
|
+
|
|
77
|
+
### Documentation
|
|
78
|
+
|
|
79
|
+
- **Agent files rewritten for LLM readers** — `AGENTS.md`, `AGENTS_BACKEND.md` and
|
|
80
|
+
`AGENTS_FRONTEND.md` went from 107 KB to 39 KB. Duplicated sections were merged, the
|
|
81
|
+
`defineTable` reference is now one annotated example, and markdown links and emoji were
|
|
82
|
+
removed. No API changed; several statements were wrong and are corrected:
|
|
83
|
+
- write bodies are closed, but under Fastify's default `removeAdditional: true` an unknown
|
|
84
|
+
property is stripped silently; it is a `400` only with `removeAdditional: false`. Unknown
|
|
85
|
+
`filters` keys, by contrast, are always a `400`;
|
|
86
|
+
- `ConditionBuilder.raw()` takes `?` markers, not `$1`;
|
|
87
|
+
- a computed field's `values` is required (the translated-jsonb example omitted it);
|
|
88
|
+
- a `joinGroup` without `by` returns one total over the children of all returned main rows,
|
|
89
|
+
not one per row; `by` set to the FK gives per-row values;
|
|
90
|
+
- search pagination is querystring-only (`page`, `itemsPerPage`), and `distinctResults`
|
|
91
|
+
rejects only aggregation `orderBy`, not aggregation conditions;
|
|
92
|
+
- the plugin options now list `agentManifest`, `sqlApi.search` lists `selectComputed` and
|
|
93
|
+
`maxRows`, and the exports list is complete.
|
|
94
|
+
|
|
12
95
|
## [0.3.0]
|
|
13
96
|
|
|
14
97
|
### Changed
|
|
@@ -685,7 +768,9 @@ the join API redesign (documented in [BREAKING_CHANGES.md](./BREAKING_CHANGES.md
|
|
|
685
768
|
primary key support in updates, transactional insert/update with secondaries, the hook matrix,
|
|
686
769
|
and bulk operations.
|
|
687
770
|
|
|
688
|
-
[Unreleased]: https://github.com/LucaRainone/fastify-auto-sqlapi/compare/v0.
|
|
771
|
+
[Unreleased]: https://github.com/LucaRainone/fastify-auto-sqlapi/compare/v0.4.1...HEAD
|
|
772
|
+
[0.4.1]: https://github.com/LucaRainone/fastify-auto-sqlapi/releases/tag/v0.4.1
|
|
773
|
+
[0.4.0]: https://github.com/LucaRainone/fastify-auto-sqlapi/releases/tag/v0.4.0
|
|
689
774
|
[0.3.0]: https://github.com/LucaRainone/fastify-auto-sqlapi/releases/tag/v0.3.0
|
|
690
775
|
[0.2.4]: https://github.com/LucaRainone/fastify-auto-sqlapi/releases/tag/v0.2.4
|
|
691
776
|
[0.2.3]: https://github.com/LucaRainone/fastify-auto-sqlapi/releases/tag/v0.2.3
|
package/README.md
CHANGED
|
@@ -52,7 +52,7 @@ Writes read the same way — *"sync these clients and their tags: update the one
|
|
|
52
52
|
// { "main": { "id": 2 } }]
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
Conflict keys are declared server-side (`upsertMap`: here customer on `email`, tag on `(customerId, tag)`), so re-sending the same payload updates rows instead of duplicating them — client-side sync without hand-written diffing.
|
|
55
|
+
Conflict keys are declared server-side (`upsertMap`: here customer on `email`, tag on `(customerId, tag)`) and checked at startup against the unique keys the database enforces, so re-sending the same payload updates rows instead of duplicating them and never touches a row through any other key — client-side sync without hand-written diffing.
|
|
56
56
|
|
|
57
57
|
And multi-tenancy is one plugin option (`getTenantId: (req) => req.user?.organizationId ?? null`), enforced on reads *and* writes with zero code in handlers. Rows of other tenants are simply invisible (`404`); here a caller in org B tries to re-point one of their own orders to a customer belonging to org A:
|
|
58
58
|
|
|
@@ -37,10 +37,10 @@ function fileStatus(filename, content) {
|
|
|
37
37
|
function writeSchemaFiles(tableMap, schemasDir) {
|
|
38
38
|
const report = { created: false, touched: false, untouched: 0, files: new Set() };
|
|
39
39
|
for (const schemaName of Object.keys(tableMap)) {
|
|
40
|
-
const { name: tableName, fields, colMap, primary, generated, isView } = tableMap[schemaName];
|
|
40
|
+
const { name: tableName, fields, colMap, primary, generated, isView, uniqueKeys } = tableMap[schemaName];
|
|
41
41
|
const filename = path.join(schemasDir, `${schemaName}.ts`);
|
|
42
42
|
report.files.add(`${schemaName}.ts`);
|
|
43
|
-
const content = generateSchemaFile(schemaName, tableName, fields, colMap, primary, generated, isView);
|
|
43
|
+
const content = generateSchemaFile(schemaName, tableName, fields, colMap, primary, generated, isView, uniqueKeys);
|
|
44
44
|
const status = fileStatus(filename, content);
|
|
45
45
|
if (status === 'created')
|
|
46
46
|
report.created = true;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mysql-introspect.d.ts","sourceRoot":"","sources":["../../../src/lib/cli/mysql-introspect.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEjD,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;
|
|
1
|
+
{"version":3,"file":"mysql-introspect.d.ts","sourceRoot":"","sources":["../../../src/lib/cli/mysql-introspect.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEjD,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAyBD,oFAAoF;AACpF,UAAU,cAAc;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qBAAqB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,qBAAqB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,wBAAgB,0BAA0B,IAAI,qBAAqB,CAmBlE;AAyED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,cAAc,GAAG,UAAU,CAejE;AAED,wBAAsB,qBAAqB,CACzC,gBAAgB,EAAE,qBAAqB,EACvC,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,UAAU,EAAE,CAAC,CA+CvB"}
|
|
@@ -117,7 +117,30 @@ export async function introspectMysqlTables(connectionConfig, schema) {
|
|
|
117
117
|
ON t.table_schema = c.table_schema AND t.table_name = c.table_name
|
|
118
118
|
WHERE c.table_schema = ?
|
|
119
119
|
ORDER BY c.table_name, c.ordinal_position`, [schema]);
|
|
120
|
-
|
|
120
|
+
// Unique indexes (constraints included) other than the primary key. An index is left out
|
|
121
|
+
// whole when any part is an expression (no COLUMN_NAME) or a prefix (SUB_PART): the first
|
|
122
|
+
// cannot be named by a column list, the second makes only the leading characters unique —
|
|
123
|
+
// neither is a key an upsert can target (ADR 0021).
|
|
124
|
+
const [uniqueRows] = await connection.query(`SELECT s.table_name AS table_name, s.column_name AS column_name, s.index_name AS index_name
|
|
125
|
+
FROM information_schema.statistics s
|
|
126
|
+
WHERE s.table_schema = ? AND s.non_unique = 0 AND s.index_name <> 'PRIMARY'
|
|
127
|
+
AND NOT EXISTS (
|
|
128
|
+
SELECT 1 FROM information_schema.statistics x
|
|
129
|
+
WHERE x.table_schema = s.table_schema AND x.table_name = s.table_name
|
|
130
|
+
AND x.index_name = s.index_name
|
|
131
|
+
AND (x.column_name IS NULL OR x.sub_part IS NOT NULL)
|
|
132
|
+
)
|
|
133
|
+
ORDER BY s.table_name, s.index_name, s.seq_in_index`, [schema]);
|
|
134
|
+
const uniqueKeys = new Map();
|
|
135
|
+
for (const u of uniqueRows) {
|
|
136
|
+
const key = `${u.table_name}.${u.column_name}`;
|
|
137
|
+
uniqueKeys.set(key, [...(uniqueKeys.get(key) ?? []), u.index_name]);
|
|
138
|
+
}
|
|
139
|
+
return rows.map((row) => {
|
|
140
|
+
const column = mapMysqlColumnRow(row);
|
|
141
|
+
const indexes = uniqueKeys.get(`${column.table_name}.${column.column_name}`);
|
|
142
|
+
return indexes ? { ...column, unique_keys: indexes } : column;
|
|
143
|
+
});
|
|
121
144
|
}
|
|
122
145
|
finally {
|
|
123
146
|
await connection.end();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pg-introspect.d.ts","sourceRoot":"","sources":["../../../src/lib/cli/pg-introspect.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAmBjD,wBAAgB,qBAAqB,IAAI,MAAM,CAY9C;
|
|
1
|
+
{"version":3,"file":"pg-introspect.d.ts","sourceRoot":"","sources":["../../../src/lib/cli/pg-introspect.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAmBjD,wBAAgB,qBAAqB,IAAI,MAAM,CAY9C;AAgBD,wBAAsB,gBAAgB,CACpC,gBAAgB,EAAE,MAAM,EACxB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,UAAU,EAAE,CAAC,CAsFvB"}
|
|
@@ -26,7 +26,8 @@ export async function introspectTables(connectionString, schema) {
|
|
|
26
26
|
c.is_identity::text, c.is_generated::text,
|
|
27
27
|
(pk.column_name IS NOT NULL) AS is_primary,
|
|
28
28
|
(t.table_type = 'VIEW') AS is_view,
|
|
29
|
-
c.ordinal_position::int AS ordinal_position
|
|
29
|
+
c.ordinal_position::int AS ordinal_position,
|
|
30
|
+
uk.unique_keys
|
|
30
31
|
FROM information_schema.columns c
|
|
31
32
|
JOIN information_schema.tables t
|
|
32
33
|
ON t.table_schema = c.table_schema AND t.table_name = c.table_name
|
|
@@ -38,12 +39,28 @@ export async function introspectTables(connectionString, schema) {
|
|
|
38
39
|
AND tc.table_schema = kcu.table_schema
|
|
39
40
|
WHERE tc.constraint_type = 'PRIMARY KEY' AND tc.table_schema = $1
|
|
40
41
|
) pk ON pk.table_name = c.table_name AND pk.column_name = c.column_name
|
|
42
|
+
-- Unique indexes (constraints are backed by one) other than the primary key, over
|
|
43
|
+
-- plain columns only: a partial index (indpred) or one on an expression (indexprs)
|
|
44
|
+
-- cannot be named by a column list, so it is no upsert key (ADR 0021).
|
|
45
|
+
LEFT JOIN (
|
|
46
|
+
SELECT tbl.relname::text AS table_name, att.attname::text AS column_name,
|
|
47
|
+
array_agg(idx.relname::text ORDER BY idx.relname) AS unique_keys
|
|
48
|
+
FROM pg_index i
|
|
49
|
+
JOIN pg_class idx ON idx.oid = i.indexrelid
|
|
50
|
+
JOIN pg_class tbl ON tbl.oid = i.indrelid
|
|
51
|
+
JOIN pg_namespace ns ON ns.oid = tbl.relnamespace
|
|
52
|
+
JOIN pg_attribute att ON att.attrelid = tbl.oid AND att.attnum = ANY(i.indkey)
|
|
53
|
+
WHERE i.indisunique AND NOT i.indisprimary
|
|
54
|
+
AND i.indpred IS NULL AND i.indexprs IS NULL
|
|
55
|
+
AND ns.nspname = $1
|
|
56
|
+
GROUP BY tbl.relname, att.attname
|
|
57
|
+
) uk ON uk.table_name = c.table_name AND uk.column_name = c.column_name
|
|
41
58
|
WHERE c.table_schema = $1
|
|
42
59
|
UNION ALL
|
|
43
60
|
SELECT mc.relname::text, ma.attname::text, mt.typname::text,
|
|
44
61
|
NULL::text, CASE WHEN ma.attnotnull THEN 'NO' ELSE 'YES' END,
|
|
45
62
|
'NO'::text, 'NEVER'::text,
|
|
46
|
-
false, true, ma.attnum::int
|
|
63
|
+
false, true, ma.attnum::int, NULL::text[]
|
|
47
64
|
FROM pg_class mc
|
|
48
65
|
JOIN pg_namespace mn ON mn.oid = mc.relnamespace
|
|
49
66
|
JOIN pg_attribute ma
|
|
@@ -55,6 +72,7 @@ export async function introspectTables(connectionString, schema) {
|
|
|
55
72
|
// The query's own columns are spelled out rather than spread: `is_identity` and
|
|
56
73
|
// `is_generated` arrive as the standard's string enums and `ordinal_position` only exists
|
|
57
74
|
// to order the union, so none of the three belongs in the `ColumnInfo` handed back.
|
|
75
|
+
// `unique_keys` is NULL for a column in no unique index.
|
|
58
76
|
return result.rows.map((row) => ({
|
|
59
77
|
table_name: row.table_name,
|
|
60
78
|
column_name: row.column_name,
|
|
@@ -67,6 +85,7 @@ export async function introspectTables(connectionString, schema) {
|
|
|
67
85
|
// mandatory field and the table cannot be inserted into at all.
|
|
68
86
|
...(row.is_identity === 'YES' ? { is_auto_increment: true } : {}),
|
|
69
87
|
...(row.is_generated === 'ALWAYS' ? { is_generated: true } : {}),
|
|
88
|
+
...(row.unique_keys?.length ? { unique_keys: row.unique_keys } : {}),
|
|
70
89
|
}));
|
|
71
90
|
}
|
|
72
91
|
finally {
|
|
@@ -30,5 +30,5 @@ export declare function applyExcludeTables(tableMap: TableMap, excludeTables?: s
|
|
|
30
30
|
* Returns the excluded columns as `table.column`, in map order.
|
|
31
31
|
*/
|
|
32
32
|
export declare function applyExcludeColumns(tableMap: TableMap, excludeColumns?: string[]): string[];
|
|
33
|
-
export declare function generateSchemaFile(schemaName: string, tableName: string, fields: Record<string, string>, colMap: Record<string, string>, primary?: string[], generated?: string[], isView?: boolean): string;
|
|
33
|
+
export declare function generateSchemaFile(schemaName: string, tableName: string, fields: Record<string, string>, colMap: Record<string, string>, primary?: string[], generated?: string[], isView?: boolean, uniqueKeys?: string[][]): string;
|
|
34
34
|
//# sourceMappingURL=schema-codegen.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema-codegen.d.ts","sourceRoot":"","sources":["../../../src/lib/cli/schema-codegen.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE3D;;GAEG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,UAAU,EAAE;IACV,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB,GACA,MAAM,CA6DR;
|
|
1
|
+
{"version":3,"file":"schema-codegen.d.ts","sourceRoot":"","sources":["../../../src/lib/cli/schema-codegen.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE3D;;GAEG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,UAAU,EAAE;IACV,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB,GACA,MAAM,CA6DR;AAcD,wBAAgB,aAAa,CAAC,IAAI,EAAE,UAAU,EAAE,GAAG,QAAQ,CAwD1D;AAaD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,QAAQ,EAAE,aAAa,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAgBzF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,cAAc,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAmD3F;AAED,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,OAAO,CAAC,EAAE,MAAM,EAAE,EAClB,SAAS,CAAC,EAAE,MAAM,EAAE,EACpB,MAAM,CAAC,EAAE,OAAO,EAChB,UAAU,CAAC,EAAE,MAAM,EAAE,EAAE,GACtB,MAAM,CAuDR"}
|