fastify-auto-sqlapi 0.2.1 → 0.2.2

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.
Files changed (106) hide show
  1. package/AGENTS.md +50 -0
  2. package/AGENTS_BACKEND.md +264 -6
  3. package/AGENTS_FRONTEND.md +1 -1
  4. package/BREAKING_CHANGES.md +51 -0
  5. package/CHANGELOG.md +113 -0
  6. package/README.md +254 -983
  7. package/dist/bin/generate-schema.js +49 -2
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +1 -1
  11. package/dist/lib/agent/manifest.d.ts +2 -0
  12. package/dist/lib/agent/manifest.d.ts.map +1 -1
  13. package/dist/lib/agent/manifest.js +4 -0
  14. package/dist/lib/cli/mysql-introspect.d.ts.map +1 -1
  15. package/dist/lib/cli/mysql-introspect.js +15 -5
  16. package/dist/lib/cli/pg-introspect.d.ts.map +1 -1
  17. package/dist/lib/cli/pg-introspect.js +53 -14
  18. package/dist/lib/cli/schema-codegen.d.ts +2 -1
  19. package/dist/lib/cli/schema-codegen.d.ts.map +1 -1
  20. package/dist/lib/cli/schema-codegen.js +17 -4
  21. package/dist/lib/cli/tables-codegen.d.ts +4 -0
  22. package/dist/lib/cli/tables-codegen.d.ts.map +1 -1
  23. package/dist/lib/cli/tables-codegen.js +125 -27
  24. package/dist/lib/engine/bulk/bulk-upsert.js +2 -2
  25. package/dist/lib/engine/read-hooks.d.ts +19 -0
  26. package/dist/lib/engine/read-hooks.d.ts.map +1 -0
  27. package/dist/lib/engine/read-hooks.js +20 -0
  28. package/dist/lib/engine/rest/get.d.ts.map +1 -1
  29. package/dist/lib/engine/rest/get.js +5 -2
  30. package/dist/lib/engine/rest/insert.js +1 -1
  31. package/dist/lib/engine/rest/update.d.ts.map +1 -1
  32. package/dist/lib/engine/rest/update.js +14 -10
  33. package/dist/lib/engine/search/join-families.d.ts +11 -2
  34. package/dist/lib/engine/search/join-families.d.ts.map +1 -1
  35. package/dist/lib/engine/search/join-families.js +11 -13
  36. package/dist/lib/engine/search/search.d.ts.map +1 -1
  37. package/dist/lib/engine/search/search.js +9 -3
  38. package/dist/lib/engine/validate.d.ts +3 -3
  39. package/dist/lib/engine/validate.d.ts.map +1 -1
  40. package/dist/lib/engine/write-helpers.d.ts +7 -3
  41. package/dist/lib/engine/write-helpers.d.ts.map +1 -1
  42. package/dist/lib/engine/write-helpers.js +27 -28
  43. package/dist/lib/schema/bulk-upsert.d.ts.map +1 -1
  44. package/dist/lib/schema/bulk-upsert.js +2 -1
  45. package/dist/lib/schema/get.d.ts +14 -0
  46. package/dist/lib/schema/get.d.ts.map +1 -0
  47. package/dist/lib/schema/get.js +18 -0
  48. package/dist/lib/schema/helpers.d.ts +21 -3
  49. package/dist/lib/schema/helpers.d.ts.map +1 -1
  50. package/dist/lib/schema/helpers.js +37 -5
  51. package/dist/lib/schema/insert.d.ts.map +1 -1
  52. package/dist/lib/schema/insert.js +7 -3
  53. package/dist/lib/schema/search.d.ts.map +1 -1
  54. package/dist/lib/schema/search.js +3 -11
  55. package/dist/lib/schema/update.d.ts.map +1 -1
  56. package/dist/lib/schema/update.js +7 -3
  57. package/dist/lib/sql-api.d.ts +9 -9
  58. package/dist/lib/sql-api.d.ts.map +1 -1
  59. package/dist/lib/sql-api.js +2 -1
  60. package/dist/lib/table-helpers.d.ts.map +1 -1
  61. package/dist/lib/table-helpers.js +123 -2
  62. package/dist/lib/tenant.d.ts +2 -2
  63. package/dist/lib/tenant.d.ts.map +1 -1
  64. package/dist/lib/write-access.d.ts +36 -0
  65. package/dist/lib/write-access.d.ts.map +1 -0
  66. package/dist/lib/write-access.js +58 -0
  67. package/dist/routes/auto/get.routes.d.ts.map +1 -1
  68. package/dist/routes/auto/get.routes.js +3 -2
  69. package/dist/routes/auto/plugin.js +2 -2
  70. package/dist/routes/auto/route-helpers.js +1 -1
  71. package/dist/routes/setup-swagger.d.ts.map +1 -0
  72. package/dist/{lib → routes}/sql-api-decorator.d.ts +1 -1
  73. package/dist/routes/sql-api-decorator.d.ts.map +1 -0
  74. package/dist/{lib → routes}/sql-api-decorator.js +4 -4
  75. package/dist/types/cli.d.ts +27 -1
  76. package/dist/types/cli.d.ts.map +1 -1
  77. package/dist/types/dml.d.ts +6 -4
  78. package/dist/types/dml.d.ts.map +1 -1
  79. package/dist/types/plugin.d.ts +3 -3
  80. package/dist/types/plugin.d.ts.map +1 -1
  81. package/dist/types/request.d.ts +24 -0
  82. package/dist/types/request.d.ts.map +1 -0
  83. package/dist/types/request.js +1 -0
  84. package/dist/types/schema.d.ts +15 -0
  85. package/dist/types/schema.d.ts.map +1 -1
  86. package/dist/types/search.d.ts +3 -0
  87. package/dist/types/search.d.ts.map +1 -1
  88. package/dist/types/table.d.ts +84 -11
  89. package/dist/types/table.d.ts.map +1 -1
  90. package/dist/types/validation.d.ts +3 -3
  91. package/dist/types/validation.d.ts.map +1 -1
  92. package/dist/types.d.ts +1 -0
  93. package/dist/types.d.ts.map +1 -1
  94. package/dist/types.js +1 -0
  95. package/docs/adr/0004-updates-always-open.md +2 -1
  96. package/docs/adr/0014-what-follows-a-write-join.md +84 -0
  97. package/docs/adr/0015-non-writable-columns.md +86 -0
  98. package/docs/adr/0016-a-view-is-a-table-config.md +92 -0
  99. package/docs/adr/0017-the-schema-is-the-contract.md +85 -0
  100. package/docs/adr/0018-one-seam-to-the-http-framework.md +68 -0
  101. package/docs/adr/README.md +5 -0
  102. package/package.json +1 -1
  103. package/dist/lib/setup-swagger.d.ts.map +0 -1
  104. package/dist/lib/sql-api-decorator.d.ts.map +0 -1
  105. /package/dist/{lib → routes}/setup-swagger.d.ts +0 -0
  106. /package/dist/{lib → routes}/setup-swagger.js +0 -0
package/CHANGELOG.md CHANGED
@@ -11,6 +11,69 @@ Migration instructions for breaking changes live in **[BREAKING_CHANGES.md](./BR
11
11
 
12
12
  ### Added
13
13
 
14
+ - **`ApiRequest` — one seam between the core and the HTTP framework.** The engines, the schema
15
+ builders and the write pipeline never read a property off the request: it arrives from the
16
+ route, is forwarded to `getTenantId`, `validate` and the hooks, and is otherwise untouched. That
17
+ dependency was spread over thirteen files as a `FastifyRequest` type import; it is now declared
18
+ once in `src/types/request.ts` and exported from the package. `ApiRequest` *is* `FastifyRequest`,
19
+ so nothing changes for consumers — hooks keep their exact types. A new dependency-cruiser rule
20
+ fails the build on a `fastify` import (type-only included) anywhere under `src/lib/`,
21
+ `src/types/` or `src/bin/` but that file, and the two Fastify bridge helpers that lived in
22
+ `lib/` (`setupSwagger`, the `sqlApi` decorator) moved to `src/routes/` so the rule needs no
23
+ exceptions. See [ADR 0018](./docs/adr/0018-one-seam-to-the-http-framework.md).
24
+
25
+ - **Views are supported explicitly, and PostgreSQL materialized views at all.** A view was
26
+ already usable — both introspections read `information_schema.columns`, which lists view
27
+ columns like any other — but the generator treated it as a base table and **invented a
28
+ primary key from the column types**: on an aggregating view that takes a count as the key,
29
+ and `get/:id`, `update` and `delete` then address rows by it, silently. Introspection now
30
+ reports `is_view` on both engines, the generated Schema carries `isView`, and the table
31
+ generator refuses to guess: it uses `id` when the view has one and says the key was *inferred,
32
+ not read from the database*, and otherwise writes a `TODO_pick_a_unique_column` placeholder
33
+ with `operations: ['search']` and a real `defaultOrder` — a config that works as generated,
34
+ since search never reads the primary key. Writes start off in the template but are **not**
35
+ blocked at runtime: a view the engine can make updatable is a legitimate projection layer.
36
+ PostgreSQL **materialized views** are now introspected from `pg_catalog`; they are absent from
37
+ `information_schema` and were being skipped without a word. `sqlapi-generate-schema` prints
38
+ the views it found and the assumptions it had to make. See
39
+ [ADR 0016](./docs/adr/0016-a-view-is-a-table-config.md).
40
+
41
+ - **Columns the database computes are no longer offered as writable.** A
42
+ `GENERATED ALWAYS AS (<expr>)` column — PostgreSQL STORED, MySQL VIRTUAL or STORED — is
43
+ rejected by both engines when a write so much as names it, and the whole statement fails.
44
+ The plugin used to advertise those columns as ordinary writable fields, so a client sending
45
+ back what a read had just returned got a `500` for a payload nobody could have made valid.
46
+ `sqlapi-generate-schema` now detects them and records them in the generated Schema's
47
+ `generatedFields`; the runtime drops them from the insert, update and bulk-upsert bodies
48
+ (main and secondaries) and again inside the engines, which `sqlApi.*` reaches without those
49
+ schemas. **Regenerating the Schema files is enough** — the exclusion does not live in the
50
+ `Table*.ts` the generator never overwrites. The CLI also prints the columns it found, per
51
+ table, since a generated source file is not always read. See
52
+ [ADR 0015](./docs/adr/0015-non-writable-columns.md).
53
+ - **`writeExclude` on `defineTable`** — the write-side counterpart of `readExclude`, for the
54
+ non-writable columns introspection cannot know about (one a trigger owns, one a migration is
55
+ about to drop). Reads are untouched: the field stays projected, filterable and orderable. It
56
+ runs **after** the write hooks, unlike `excludeFromCreation` which is sanitized before them
57
+ (ADR 0005): a hook cannot put back a value the database will refuse. `defineTable` rejects an
58
+ unknown field, the primary key, and a field that is also `readExclude`d. It is static and
59
+ applies to everyone — a rule that depends on *who* is asking is still product logic (ADR 0004).
60
+ - **The agent manifest marks non-writable fields** `readOnly`, alongside the existing
61
+ `writeOnly` for `readExclude`.
62
+
63
+ - **`afterRead` — the read-side hook.** The counterpart of `beforeInsert`/`beforeUpdate`, for
64
+ turning a stored representation back into the API one: decrypting a column, unpacking a blob,
65
+ rescaling a unit. Called **once per result set** with camelCase rows mutated **in place** and
66
+ awaited, so a hook that calls a remote KMS gets one invocation per page instead of one per row.
67
+ Declared on the table that **owns** the column and it follows a join: the hook runs wherever
68
+ that table's rows surface — `get`, `search`, and a `joinMultiple`/`joinLeft` declared on
69
+ another table, with `ctx.source` and `ctx.alias` saying which — so a relation elsewhere needs
70
+ no extra wiring, the way `readExclude` and `tenantScope` already work (ADR 0010). It does not
71
+ run on `joinGroup` (aggregates, not table rows) nor on an empty result, and rows cannot be
72
+ added or removed: the pagination `COUNT` has already run. On the main table it runs *after* the
73
+ join side queries, which correlate on the values the database returned, so a hook rewriting the
74
+ column a relation joins on cannot break the correlation. `SearchParams`/`GetParams` now carry
75
+ the optional `request`, which `sqlApi.search()`/`sqlApi.get()` forward when given one.
76
+
14
77
  - **`tenantScope` over several owner columns — `{ anyOf: [...] }`.** A row that belongs to two
15
78
  parties and is visible to either — a message (`sender_id` / `recipient_id`), a transfer
16
79
  (`from_account_id` / `to_account_id`), a shift swap — could not be expressed before, leaving
@@ -83,8 +146,58 @@ Migration instructions for breaking changes live in **[BREAKING_CHANGES.md](./BR
83
146
  `LEFT JOIN` alias), so the filter applied nothing. They are now rejected with a `400` naming
84
147
  the reason and pointing at `joinMustExist` on the same relation.
85
148
 
149
+ - **The update body requires the primary key**, which identifies the row it updates
150
+ (`PUT /rest/:table` carries it in `main`). The generated PK field is `Optional` — it is
151
+ absent on insert — and that modifier reached the update body unchanged, so a request omitting
152
+ `main.id` built its `WHERE` on `undefined` instead of being rejected. The code comment said
153
+ "PK required" all along.
154
+ - **A secondary now runs the child table's `beforeInsert`.** `processSecondaries` already applied
155
+ the child's `excludeFromCreation` and its `tenantScope` but skipped its `beforeInsert`, so a
156
+ table whose hook *transformed* a value — encrypting a column, normalising a unit — stored the
157
+ raw client value whenever its rows arrived as another table's children, and the correct one
158
+ through its own route. Nothing failed at write time; the row read back as garbage later. The
159
+ hook runs after the exclusion strip and before the engine's FK auto-fill, which stays
160
+ authoritative, and receives the transaction connection. The child's `validate` and
161
+ `afterInsert` deliberately still do not run — see
162
+ [ADR 0014](./docs/adr/0014-what-follows-a-write-join.md). **Breaking** for a `beforeInsert`
163
+ written assuming it only ever ran on its own table's route.
164
+ - **The get response schema honours `readExclude`**, like the search response already did; it
165
+ previously advertised fields the engine never selects. It now lives in `lib/schema/get.ts`
166
+ (`GetTableResponse`) instead of inline in the route, which is how it had drifted.
167
+ `schemaOverrides` stays where it was — write bodies only: an override rules what the API
168
+ accepts from now on, while the rows already stored predate it, so narrowing the response
169
+ would promise something no one can retroactively make true.
170
+
86
171
  ### Fixed
87
172
 
173
+ - **A `tenantScope` naming a schema field instead of a column now fails at startup.**
174
+ `tenantScope` names real DB columns — the scope is applied to the SQL, after the payload has
175
+ been converted, and `through.foreignField` belongs to a parent table that may have no config of
176
+ its own. Nothing checked it, so writing `organizationId` where the column is `organization_id`
177
+ passed `defineTable` and surfaced as `column "organizationId" does not exist` on every request
178
+ to the table: an opaque `500`, failing closed but saying nothing. All four positions are now
179
+ checked (`column`, each `anyOf` entry, `through.localField`, and `through.foreignField` against
180
+ the parent schema). The check is narrow by design — it fires only when the name *is* a schema
181
+ field whose column differs, so a column absent from the schema raises nothing and a camelCase
182
+ database, where `col()` is the identity, raises nothing either.
183
+ - **The `no-static-optional-peer` gate had never been able to fire.** `includeOnly: '^src/'` in
184
+ `.dependency-cruiser.mjs` dropped every npm edge from the dependency graph, so the rule meant to
185
+ stop a static import of `pg` / `mysql2` / `@fastify/*` — which breaks installs that use the other
186
+ dialect — matched nothing, ever. The graph now keeps npm dependencies as leaves, and the rule is
187
+ qualified to what its name says: static, value-level imports only, since a type-only import is
188
+ erased at compile time and a dynamic one is what the rule asks for. Both gates were verified by
189
+ introducing a violation and watching them fail.
190
+ - **`defineTable` never validated `primary`.** A primary key naming a field the schema does not
191
+ have passed startup untouched and became a raw SQL error on the first request — a `500`. It
192
+ now throws at `defineTable` time, naming the field and the operations that need it. The one
193
+ exception is exact: a table exposing only `search` and declaring its own `defaultOrder` never
194
+ reads `primary`, which is the shape generated for a view whose key could not be inferred.
195
+ - **PostgreSQL identity columns made a table impossible to insert into.**
196
+ `GENERATED ... AS IDENTITY` reports no `column_default`, which is what the generator read to
197
+ decide that the database fills a column in — so the primary key came out **mandatory** in the
198
+ write body while PostgreSQL rejected any explicit value for it (`Column "id" is an identity
199
+ column defined as GENERATED ALWAYS`). Identity columns are now flagged like MySQL's
200
+ `AUTO_INCREMENT`: the field is Optional and the CLI lists it under `excludeFromCreation`.
88
201
  - **Join validation no longer depends on the main result set.** `joinMultiple` / `joinLeft` /
89
202
  `joinGroup` skip their side query entirely when the main query matched no rows, so everything
90
203
  validated inside it was skipped with it — the same request answered 400 or 200 depending on