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
@@ -0,0 +1,86 @@
1
+ # 0015. A column the database computes is not writable — `writeExclude` (narrows 0004)
2
+
3
+ - **Status**: accepted
4
+ - **Date**: 2026-08-28
5
+
6
+ ## Context
7
+
8
+ [ADR 0004](./0004-updates-always-open.md) rejected `excludeFromUpdate`: a static per-field
9
+ write block, proposed to protect `isAdmin`, `createdAt` and ownership columns. It lost on one
10
+ argument — those cases are **session-dependent**, a static list cannot express them, and
11
+ offering one would give a false sense of security to consumers who used it for exactly that.
12
+ That reasoning stands unchanged.
13
+
14
+ It does not, however, cover a case the plugin met later: a column the database itself refuses
15
+ to be told about.
16
+
17
+ ```sql
18
+ subtotal INTEGER GENERATED ALWAYS AS (qty * price) STORED
19
+ ```
20
+
21
+ Both engines reject the whole statement — not the field — when a write so much as names such a
22
+ column: `428C9 cannot insert a non-DEFAULT value into column "subtotal"` on PostgreSQL,
23
+ `ER_NON_DEFAULT_VALUE_FOR_GENERATED_COLUMN` on MySQL. Yet nothing in the generated schema said
24
+ so, so the column was offered as an ordinary writable field, and a client that sent it back
25
+ what a read had just returned got a **500** — a raw driver error, per
26
+ [ADR 0006](./0006-raw-db-errors.md) — for a payload nobody could have made valid.
27
+
28
+ The same blind spot hid `GENERATED ... AS IDENTITY`, which has no `column_default` to reveal
29
+ that the database fills it in: the generated field came out mandatory and the table could not
30
+ be inserted into at all.
31
+
32
+ ## Decision
33
+
34
+ Fields the database computes are excluded from every write path. The CLI records them in the
35
+ generated Schema's `generatedFields`, and the runtime removes them from the insert, update and
36
+ bulk-upsert bodies (main and secondaries) and again inside the engines, which `sqlApi.*`
37
+ reaches without those schemas.
38
+
39
+ `ITable.writeExclude` exposes the same mechanism for the columns introspection cannot know
40
+ about — one a trigger owns, one a migration is about to drop.
41
+
42
+ This narrows 0004 rather than reversing it, and the boundary is what the exclusion is *about*:
43
+
44
+ - **a fact about the column** — the database rejects the value whoever assigned it, so there is
45
+ no configuration in which offering the field helps anyone. This ADR.
46
+ - **a rule about the caller** — who may change what, when. Still product logic, still
47
+ `beforeUpdate` / `validate` / a dedicated endpoint. Still ADR 0004.
48
+
49
+ Three consequences follow from that boundary and are enforced:
50
+
51
+ - `writeExclude` runs **after** the write hooks — the opposite of `excludeFromCreation`, which
52
+ is sanitized before them so a hook can assign the field ([ADR 0005](./0005-insert-pipeline-sanitize-before-hooks.md)).
53
+ A hook cannot put back a value the database will refuse.
54
+ - `defineTable` rejects a field that is also in `readExclude`: neither readable nor writable is
55
+ what removing it from the Schema means.
56
+ - `defineTable` rejects the primary key, which the update body needs to identify the row.
57
+
58
+ ## Alternatives considered
59
+
60
+ - **`Schema.generatedFields` only, no public key** — rejected: it covers the introspected case
61
+ and nothing else, and a consumer whose schema is hand-written or whose column is owned by a
62
+ trigger would be back to a driver error with no way out.
63
+ - **Let the write through and map the driver error to a 400** — rejected: it contradicts
64
+ [ADR 0006](./0006-raw-db-errors.md) (no SQLSTATE→HTTP mapping), it costs a round trip to the
65
+ database to learn what the schema already knew, and the field would still be advertised as
66
+ writable in Swagger and in the agent manifest.
67
+ - **Trim the column out of the generated Schema** — rejected: that removes it from reads too,
68
+ and a computed column is precisely the one a client wants to read.
69
+ - **Reopen 0004 and add a general `excludeFromUpdate`** — rejected: the false-sense-of-security
70
+ argument is untouched by anything here. The documentation for `writeExclude` says what it is
71
+ not for, and the key deliberately has no per-caller or per-role dimension that would invite
72
+ the misuse 0004 warned about.
73
+
74
+ ## Consequences
75
+
76
+ - Under Fastify's default `removeAdditional: true`, a computed column in a write body is
77
+ **stripped silently** rather than rejected; the row is written with the value the database
78
+ computed. With `removeAdditional: false` the closed body schema answers 400. Either way the
79
+ driver error is gone — which was the point.
80
+ - A consumer upgrading gets the fix from **regenerating the Schema files alone**: the exclusion
81
+ travels in `generatedFields`, not in the `Table*.ts` the generator never overwrites.
82
+ - The generated `Table*.ts` names the computed columns in a comment and leaves `writeExclude`
83
+ commented out, and `sqlapi-generate-schema` prints them per table — a generated source file
84
+ is not always read, its command's output is.
85
+ - Reads are untouched: a computed column stays projected, filterable, orderable and
86
+ aggregatable, because it is a real column with a real value.
@@ -0,0 +1,92 @@
1
+ # 0016. A view is a table config — the generator refuses to invent its primary key
2
+
3
+ - **Status**: accepted
4
+ - **Date**: 2026-08-28
5
+
6
+ ## Context
7
+
8
+ Views were never designed for, and they worked anyway: both introspections read
9
+ `information_schema.columns`, which lists view columns exactly like table columns, so
10
+ `sqlapi-generate-schema` produced a Schema and searching the result worked with no special
11
+ support. Three things underneath that were wrong.
12
+
13
+ **The primary key was invented in silence.** A view carries no `PRIMARY KEY` constraint, so
14
+ `is_primary` is false on every column and the generated Schema had no `primaryKey`. The table
15
+ generator then fell through to its heuristic for hand-written schemas: `id` if present,
16
+ otherwise **the first integer column**, otherwise the first column of any kind. On
17
+ `SELECT status, count(*) AS order_count ... GROUP BY status` that hands back `order_count` — a
18
+ count — as the key, and `get/:id`, `update` and `delete` then address rows by it. Nothing said
19
+ so, in the file or on the console.
20
+
21
+ **Nothing validated `primary` at all.** `defineTable` checked `tenantScope`, `readExclude`,
22
+ `computedFields` and alias uniqueness, but a `primary` naming a field the schema does not have
23
+ passed straight through and became a raw SQL error on the first request — a 500, per
24
+ [ADR 0006](./0006-raw-db-errors.md).
25
+
26
+ **PostgreSQL materialized views were invisible.** They are not in `information_schema` at all;
27
+ they live only in `pg_catalog`. They were skipped without a word — and a materialized view is
28
+ precisely the kind of expensive read one wants to expose.
29
+
30
+ ## Decision
31
+
32
+ A view **is** a table config. No `ViewName` file, no `View*` type, no `readOnly` flag: the
33
+ plugin has one concept for "a relation with columns you can query" and a view is that one. A
34
+ second name would fork the `dbTables` keys, the generator, the docs and the manifest to express
35
+ nothing the existing vocabulary cannot.
36
+
37
+ What changes is what may be **assumed**, and all of it lives in the generator and in startup
38
+ validation, not in the runtime semantics:
39
+
40
+ - Introspection reports `is_view` on both engines, and PostgreSQL materialized views are read
41
+ from `pg_catalog` and reported the same way. The generated Schema carries `isView: true`.
42
+ - For a view the generator **never guesses a key from a column type**. It accepts `id` when the
43
+ view has one — a strong enough signal — and says in the file that the key was *inferred, not
44
+ read from the database*. With no `id`, it writes a `TODO_pick_a_unique_column` placeholder
45
+ rather than a wrong answer.
46
+ - A view's generated config starts read-only: `operations: ['search', 'get']`, or
47
+ `['search']` when the key is unresolved. `defaultOrder` names a real column, so search never
48
+ falls back to the placeholder, and the config **works as generated** — the TODO is a note for
49
+ whoever wants to widen it, not a defect.
50
+ - `defineTable` now validates `primary` against the schema fields. The exception is exact and
51
+ narrow: a table exposing **only** `search` and naming its own `defaultOrder` never reads
52
+ `primary`, so it is allowed. Anything else fails at startup.
53
+ - `sqlapi-generate-schema` prints the views it found and both assumptions it had to make. A
54
+ generated source file is not always read; the output of the command that produced it is.
55
+
56
+ Writes through a view are **not** blocked at runtime. A view simple enough for the engine to
57
+ make updatable accepts INSERT and UPDATE on both PostgreSQL and MySQL, and using one as a
58
+ projection or permission layer is a legitimate pattern the plugin has no business refusing. The
59
+ read-only start is the *generated template* — a file the developer then owns — which is a
60
+ different thing from the runtime default that [ADR 0002](./0002-open-by-default.md) settles.
61
+ That default is untouched: a hand-written view config with no `operations` still exposes
62
+ everything.
63
+
64
+ ## Alternatives considered
65
+
66
+ - **`ViewName` files / a distinct type** — rejected: it forks a concept for no expressive gain
67
+ and breaks the one-name-per-concept rule the glossary exists to enforce.
68
+ - **A `readOnly: true` flag on `defineTable`** — rejected: a second spelling of
69
+ `operations: ['search', 'get']`.
70
+ - **Blocking writes to views at runtime** — rejected: simple views are genuinely updatable on
71
+ both engines (verified), and the plugin cannot tell which ones from the schema alone. It
72
+ would remove a working feature to prevent an error the database already reports.
73
+ - **Keep guessing the key, just add a comment** — rejected: the wrong-key failure is silent and
74
+ addresses *rows*, which is the worst kind. A placeholder that refuses to start is the loud
75
+ version of the same information.
76
+ - **Skip views entirely, or leave materialized views unsupported and document it** — rejected:
77
+ they already worked for reads, which is the case that matters, and silence about a relation
78
+ the database has is the least defensible of the options.
79
+
80
+ ## Consequences
81
+
82
+ - A view without an `id` column needs one manual edit before it can serve anything but
83
+ `search`. That is the point: the edit is a decision about uniqueness that only the author of
84
+ the view can make.
85
+ - `defineTable` now throws at startup on a `primary` that names no schema field. A config that
86
+ relied on that tolerance — a primary key deliberately kept out of the Schema — has to name a
87
+ schema field or restrict itself to `operations: ['search']`.
88
+ - Materialized views appear in generation runs where they did not before, so a project that
89
+ regenerates with `--all` will find new `Table*.ts` files for them.
90
+ - The `is_view` flag is generator-facing. Nothing in the request path branches on it: a view
91
+ searches, filters, orders, paginates, joins and (when the database allows) writes through the
92
+ same code as a base table.
@@ -0,0 +1,85 @@
1
+ # 0017. The database schema is the API contract — by default, and only by default
2
+
3
+ - **Status**: accepted
4
+ - **Date**: 2026-08-28
5
+
6
+ ## Context
7
+
8
+ The first objection this plugin meets is always the same: *it couples the API to the database.*
9
+ Change a column and you change the API, and through it every client.
10
+
11
+ The objection is real and worth answering precisely, because the imprecise answer ("it's fine
12
+ for internal tools") concedes more than it has to and defends less than it should.
13
+
14
+ ## Decision
15
+
16
+ The database schema is the API contract **by default**. That is the mechanism, not a shortcut,
17
+ and it rests on three things.
18
+
19
+ **The coupling is exposed, not created.** A hand-written CRUD layer over the same tables is
20
+ coupled to exactly the same schema — it just retypes it. Rename a column there and you edit the
21
+ model, the DTO, the mapper, the validator, the Swagger annotation and the client, in six places
22
+ instead of one. What that layer genuinely buys is different: an indirection that lets the
23
+ contract stay still while the schema moves. That is a real thing to want, and it is paid for
24
+ continuously and cashed in rarely. This plugin makes the opposite bet, and states it.
25
+
26
+ **It is checked, not implied.** The Schema files are generated from `information_schema`, so a
27
+ schema change that breaks the API breaks it when you regenerate and compile — for TypeScript
28
+ consumers, at build time. A hand-written mapper that nobody remembered to update fails in
29
+ production instead. (Untyped clients — a browser fetch — still find out at runtime; the
30
+ generated Swagger and `GET {prefix}/agent/manifest.md` are what they check against.)
31
+
32
+ **It is a default, and every part of it is overridable.** The exposed shape is not forced to be
33
+ the stored shape:
34
+
35
+ | To decouple | Use |
36
+ |---|---|
37
+ | a column exists but must never be read | `readExclude` |
38
+ | a column exists but must never be written | `writeExclude` ([ADR 0015](./0015-non-writable-columns.md)) |
39
+ | a column must not exist for the API at all | trim it out of the Schema |
40
+ | the API type must be stricter than the column | `schemaOverrides` |
41
+ | the API value is derived, not stored | `computedFields` |
42
+ | the stored representation is not the API one | `afterRead` (decrypt, unpack, rescale) |
43
+ | a table must not have all seven routes | `operations` |
44
+ | a relation must be named for the API, not the DB | the relation `alias`, plus its `fields` allowlist |
45
+ | a filter has no column behind it | `extraFilters` + `extendedCondition` |
46
+ | different callers need different surfaces | register the plugin more than once, under different prefixes |
47
+
48
+ And when none of that fits, the operation is not a CRUD operation: write the route by hand and
49
+ call `app.sqlApi.*` inside it. That is not the fallback path — it is the design. The custom
50
+ route keeps filters, joins, tenant scoping, hooks and validation; what it does not keep is the
51
+ assumption that the endpoint looks like a table.
52
+
53
+ **Where the objection is right.** A public or third-party API — consumers you cannot redeploy
54
+ alongside your schema, a contract with a version number and a deprecation policy — is a surface
55
+ whose stability has to outlive refactors. There an indirection that survives schema movement
56
+ earns its cost, and generating that surface from the schema is the wrong default. Do not
57
+ auto-expose it: keep those tables off the auto routes with `operations`, or behind their own
58
+ hand-written endpoints, and let the plugin serve the surface it is for.
59
+
60
+ That surface is the internal one: back-office and admin tools where superadmin, admin and
61
+ tenant-admin roles need full control over the domain, the schema *is* the domain model, and the
62
+ people who change a column are the people who change the screen that shows it. There, the
63
+ translation layer has no reader — it exists only to be kept in sync with the thing it
64
+ translates.
65
+
66
+ ## Alternatives considered
67
+
68
+ - **A mapping/DTO layer inside the plugin** — rejected: that is the hand-written layer again,
69
+ re-expressed as configuration instead of code, with the same maintenance cost and less
70
+ freedom. The per-concern levers above cover what most projects actually need to hide or
71
+ reshape, and a custom route covers the rest without inventing a second schema language.
72
+ - **Frozen schema snapshots, versioned independently of the database** — rejected: a second
73
+ source of truth to keep in sync, which is precisely what [ADR 0001](./0001-no-orm-raw-sql.md)
74
+ refused when it refused an ORM's models and migrations.
75
+ - **Saying nothing and letting readers work it out** — rejected: it is the first question every
76
+ reviewer asks, and an unanswered one reads as an unconsidered one.
77
+
78
+ ## Consequences
79
+
80
+ - Renaming a column changes the API field. Accepted: it is one regeneration, and typed
81
+ consumers fail to compile rather than failing quietly.
82
+ - The plugin is not the right tool for a versioned public API, and says so in the README rather
83
+ than leaving it to be discovered.
84
+ - Every "the API should not look like the table" request has a specific answer in the table
85
+ above, or is a custom route. Neither is a gap in the plugin.
@@ -0,0 +1,68 @@
1
+ # 0018. The HTTP framework is reachable through one seam — `ApiRequest`
2
+
3
+ - **Status**: accepted
4
+ - **Date**: 2026-08-29
5
+
6
+ ## Context
7
+
8
+ The question was what it would take to serve the same engine over something other than Fastify.
9
+ Measuring first turned up a codebase that was already almost free of one:
10
+
11
+ - of 74 files under `src/`, 26 named a Fastify type and **not one imported a value from it**;
12
+ - **nothing under `src/lib/` ever read a property off the request** — no `body`, no `params`, no
13
+ `log`. It arrives from the route, is forwarded to `getTenantId`, `validate` and the hooks, and
14
+ is otherwise untouched;
15
+ - `httpError` produces a plain `Error` carrying `statusCode`. Fastify happens to read that; the
16
+ plugin does not produce it *for* Fastify.
17
+
18
+ So the engine, the schema builders and the write pipeline depend on a web framework for exactly
19
+ one thing: the **name of a type they never dereference**. That dependency was spread across
20
+ thirteen files, where every new hook signature quietly widened it, and nothing prevented an
21
+ engine from starting to read `request.headers` tomorrow.
22
+
23
+ ## Decision
24
+
25
+ The request type is declared once, in `src/types/request.ts`, and everything in the core refers
26
+ to that name:
27
+
28
+ ```ts
29
+ export type ApiRequest = FastifyRequest;
30
+ ```
31
+
32
+ `.dependency-cruiser.mjs` enforces it: `http-framework-behind-one-seam` fails the build on a
33
+ `fastify` import — **including a type-only one** — anywhere under `src/lib/`, `src/types/` or
34
+ `src/bin/` other than that file. The two Fastify bridge helpers that lived in `lib/`
35
+ (`setup-swagger`, `sql-api-decorator`) moved to `src/routes/`, which is where the adapter is, so
36
+ the rule needs no exceptions.
37
+
38
+ A plain alias, not a generic parameter on `ITable`. `ITable` appears as a bare type in over a
39
+ hundred places; threading `<Req>` through all of them — and through `DbTables`, and every
40
+ `*Params` — changes the public type surface today to buy a property that only pays off once the
41
+ core ships as a package of its own. The alias makes the seam **one file**, which is the whole
42
+ requirement, and leaves the generic available later without having spent anything on it now.
43
+
44
+ This decision is about where the dependency lives, not about supporting a second framework. It
45
+ records what the measurement found, and stops it from silently degrading.
46
+
47
+ ## Alternatives considered
48
+
49
+ - **`ITable<F, Req = FastifyRequest>`** — rejected for now, as above: the churn is large, it
50
+ changes the public types, and mixed `DbTables` maps get awkward. It is the right shape for a
51
+ three-package split and can be done then, from this seam, in one file's worth of decisions.
52
+ - **Leave `FastifyRequest` inline and write the constraint in the docs** — rejected: a property
53
+ that thirteen files must remember is a property that lasts until the next hook is added.
54
+ Gates, not documentation (ADR 0012).
55
+ - **Make the core read nothing by typing the request as `unknown`** — rejected: it would break
56
+ every consumer's typed hook to express something the consumer does not need to know.
57
+
58
+ ## Consequences
59
+
60
+ - Consumers see no change: `ApiRequest` *is* `FastifyRequest`, and it is exported from the
61
+ package for anyone declaring a hook outside a `defineTable` call.
62
+ - The core cannot regress. An engine that starts naming a Fastify type fails `npm run verify`.
63
+ - A second HTTP adapter is now a question about `src/routes/` (582 lines) plus what Fastify does
64
+ for free — request validation against the generated schemas, and response serialization, which
65
+ is part of the contract rather than plumbing — and not a question about the engine at all.
66
+ - Fixing the rule so it could fire revealed that `no-static-optional-peer` had never been able
67
+ to: `includeOnly: '^src/'` dropped every npm edge from the graph. It is live now, qualified to
68
+ what its name says (static, value-level imports of optional peers).
@@ -28,6 +28,11 @@ not by editing history.
28
28
  | [0011](./0011-join-fields-allowlist.md) | Per-relation field restriction is an allowlist, not a blocklist |
29
29
  | [0012](./0012-anti-duplication-tooling.md) | Repository rules are enforced by executable gates, not by documentation |
30
30
  | [0013](./0013-sanitized-db-errors.md) | Database error messages are sanitized on the wire by default (narrows 0006) |
31
+ | [0014](./0014-what-follows-a-write-join.md) | A secondary runs the child table's transform, not its verdicts |
32
+ | [0015](./0015-non-writable-columns.md) | A column the database computes is not writable — `writeExclude` (narrows 0004) |
33
+ | [0016](./0016-a-view-is-a-table-config.md) | A view is a table config — the generator refuses to invent its primary key |
34
+ | [0017](./0017-the-schema-is-the-contract.md) | The database schema is the API contract — by default, and only by default |
35
+ | [0018](./0018-one-seam-to-the-http-framework.md) | The HTTP framework is reachable through one seam — `ApiRequest` |
31
36
 
32
37
  ## When to write one
33
38
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fastify-auto-sqlapi",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Fastify plugin for automatic CRUD API generation from PostgreSQL, MySQL and MariaDB tables",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1 +0,0 @@
1
- {"version":3,"file":"setup-swagger.d.ts","sourceRoot":"","sources":["../../src/lib/setup-swagger.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,KAAK,EAAE,mBAAmB,EAAkB,MAAM,aAAa,CAAC;AAEvE,wBAAsB,YAAY,CAChC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,IAAI,CAAC,mBAAmB,EAAE,SAAS,CAAC,GAC5C,OAAO,CAAC,IAAI,CAAC,CA6Bf"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"sql-api-decorator.d.ts","sourceRoot":"","sources":["../../src/lib/sql-api-decorator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAI/C,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,cAAc,CAAC;AACzD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAEvD,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAU,eAAe;QACvB,MAAM,EAAE,MAAM,CAAC;KAChB;CACF;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,IAAI,CA2BN"}
File without changes
File without changes