fastify-auto-sqlapi 0.2.0 → 0.2.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.
Files changed (40) hide show
  1. package/AGENTS_BACKEND.md +48 -3
  2. package/BREAKING_CHANGES.md +81 -0
  3. package/CHANGELOG.md +57 -0
  4. package/README.md +109 -2
  5. package/dist/index.d.ts +1 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/lib/db.d.ts +6 -1
  8. package/dist/lib/db.d.ts.map +1 -1
  9. package/dist/lib/db.js +8 -2
  10. package/dist/lib/engine/bulk/bulk-upsert.d.ts.map +1 -1
  11. package/dist/lib/engine/bulk/bulk-upsert.js +4 -3
  12. package/dist/lib/engine/rest/get.d.ts.map +1 -1
  13. package/dist/lib/engine/rest/get.js +5 -4
  14. package/dist/lib/engine/rest/update.js +1 -1
  15. package/dist/lib/engine/search/search.d.ts.map +1 -1
  16. package/dist/lib/engine/search/search.js +5 -4
  17. package/dist/lib/engine/write-helpers.d.ts.map +1 -1
  18. package/dist/lib/engine/write-helpers.js +2 -2
  19. package/dist/lib/errors.d.ts +24 -0
  20. package/dist/lib/errors.d.ts.map +1 -1
  21. package/dist/lib/errors.js +59 -0
  22. package/dist/lib/table-helpers.d.ts.map +1 -1
  23. package/dist/lib/table-helpers.js +48 -0
  24. package/dist/lib/tenant.d.ts +41 -6
  25. package/dist/lib/tenant.d.ts.map +1 -1
  26. package/dist/lib/tenant.js +169 -54
  27. package/dist/routes/auto/plugin.d.ts.map +1 -1
  28. package/dist/routes/auto/plugin.js +6 -0
  29. package/dist/routes/auto/route-helpers.d.ts.map +1 -1
  30. package/dist/routes/auto/route-helpers.js +12 -3
  31. package/dist/types/plugin.d.ts +19 -0
  32. package/dist/types/plugin.d.ts.map +1 -1
  33. package/dist/types/tenant.d.ts +26 -2
  34. package/dist/types/tenant.d.ts.map +1 -1
  35. package/docs/GLOSSARY.md +3 -1
  36. package/docs/adr/0006-raw-db-errors.md +2 -1
  37. package/docs/adr/0010-joins-do-not-run-route-guards.md +4 -1
  38. package/docs/adr/0013-sanitized-db-errors.md +114 -0
  39. package/docs/adr/README.md +1 -0
  40. package/package.json +1 -1
@@ -1,4 +1,5 @@
1
1
  import { ensureSqlApiDecorator } from '../../lib/sql-api-decorator.js';
2
+ import { sanitizeRouteError } from '../../lib/errors.js';
2
3
  import { isCompositePrimary } from '../../types.js';
3
4
  function mergeOnRequests(options, tableConf) {
4
5
  return [...(options.onRequests || []), ...(tableConf.onRequests || [])];
@@ -75,9 +76,17 @@ export async function registerForAllTables(fastify, options, spec) {
75
76
  schema: buildRouteSchema(tableName, tableConf, spec, schemas),
76
77
  onRequest: mergeOnRequests(options, tableConf),
77
78
  handler: async (request, reply) => {
78
- const result = await spec.handle(fastify, tableName, tableConf, request, reply);
79
- if (!reply.sent)
80
- reply.status(spec.successStatus).send(result);
79
+ try {
80
+ const result = await spec.handle(fastify, tableName, tableConf, request, reply);
81
+ if (!reply.sent)
82
+ reply.status(spec.successStatus).send(result);
83
+ }
84
+ catch (err) {
85
+ const safe = sanitizeRouteError(err, request.id, options.exposeDebugInfo);
86
+ if (safe !== err)
87
+ request.log.error({ err }, 'sqlapi: unhandled error');
88
+ throw safe;
89
+ }
81
90
  },
82
91
  });
83
92
  }
@@ -35,6 +35,25 @@ export interface SqlApiPluginOptions {
35
35
  * routes run behind the same global `onRequests` hooks as the data routes.
36
36
  */
37
37
  agentManifest?: boolean;
38
+ /**
39
+ * Attach a `debugInfo` payload — the driver message, its `code`/`constraint`/`detail`,
40
+ * the stack — to the `500` an error the plugin did not raise itself produces (a
41
+ * constraint violation, a hook throwing without a `statusCode`).
42
+ *
43
+ * **Development only**: it hands whoever triggers an error a description of the schema,
44
+ * and the auto routes are open by default (ADR 0002). Off unless set. It is additive —
45
+ * `statusCode`, `error`, `message` and `requestId` keep the shape they have in production,
46
+ * so client-side error handling does not fork between environments.
47
+ *
48
+ * Without it the response carries no database detail, and the original error is still
49
+ * reachable: logged through `request.log.error` under the same `reqId` the body reports as
50
+ * `requestId`, and attached as the `cause` of the error a consumer `setErrorHandler`
51
+ * receives — which is where policy such as unique-violation → `409` belongs (ADR 0006).
52
+ *
53
+ * Nothing derives this: not `debug` (whoever registers the plugin decides that one, and
54
+ * may wire it to an environment variable), not `NODE_ENV`. See ADR 0013.
55
+ */
56
+ exposeDebugInfo?: boolean;
38
57
  }
39
58
  /** Default row cap for search (page size and no-paginator LIMIT). Override via `maxItemsPerPage`. */
40
59
  export declare const DEFAULT_MAX_ITEMS_PER_PAGE = 1000;
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../src/types/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC5D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,QAAQ,CAAC;IACnB,UAAU,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,GAAG,YAAY,CAAC,CAAC,EAAE,CAAC;IAChG,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,GAAG,cAAc,CAAC;IACnC,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,QAAQ,GAAG,QAAQ,EAAE,GAAG,IAAI,GAAG,SAAS,GAC/E,OAAO,CAAC,QAAQ,GAAG,QAAQ,EAAE,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;IACtD,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,qGAAqG;AACrG,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAE/C,8EAA8E;AAC9E,eAAO,MAAM,sBAAsB,OAAO,CAAC"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../src/types/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC5D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,QAAQ,CAAC;IACnB,UAAU,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,GAAG,YAAY,CAAC,CAAC,EAAE,CAAC;IAChG,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,GAAG,cAAc,CAAC;IACnC,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,QAAQ,GAAG,QAAQ,EAAE,GAAG,IAAI,GAAG,SAAS,GAC/E,OAAO,CAAC,QAAQ,GAAG,QAAQ,EAAE,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;IACtD,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;;;;;;;;;;;;;;;OAiBG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,qGAAqG;AACrG,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAE/C,8EAA8E;AAC9E,eAAO,MAAM,sBAAsB,OAAO,CAAC"}
@@ -11,9 +11,33 @@ export interface TenantScopeIndirect {
11
11
  foreignField: string;
12
12
  };
13
13
  }
14
- export type TenantScope = TenantScopeDirect | TenantScopeIndirect;
14
+ /**
15
+ * A row owned by several parties, visible to any of them: a message (`sender_id` /
16
+ * `recipient_id`), a transfer (`from_account_id` / `to_account_id`), a shift swap. Reads test
17
+ * every listed column with an OR, so a row matches when *any* of them holds a tenant id.
18
+ *
19
+ * Writes cannot auto-inject a tenant the way a single-column scope does — the plugin has no way
20
+ * to know which party the caller is meant to be — so the payload must anchor the row itself:
21
+ * see `assertTenantAnchor`. Columns are DB column names, like `TenantScopeDirect.column`.
22
+ *
23
+ * Not combinable with `column`/`through`: an entry reached through a parent FK would need its
24
+ * own join per entry, and is rejected at `defineTable` time rather than half-supported.
25
+ */
26
+ export interface TenantScopeAnyOf {
27
+ anyOf: string[];
28
+ }
29
+ export type TenantScope = TenantScopeDirect | TenantScopeIndirect | TenantScopeAnyOf;
30
+ /**
31
+ * Who the caller is, for this request — plus how the table currently being addressed is scoped.
32
+ *
33
+ * `ids` is request-level: it is resolved once from `getTenantId` and stays valid for every table
34
+ * the request touches. `scope` is table-level and therefore **optional**: the addressed table may
35
+ * declare no scope of its own while a table it reaches through a relation does. Absent `scope`
36
+ * means "no filtering on this table", never "no tenant" — `tenantForTable` re-targets the same
37
+ * ids onto a related table's own scope.
38
+ */
15
39
  export interface TenantContext {
16
40
  ids: TenantId[];
17
- scope: TenantScope;
41
+ scope?: TenantScope;
18
42
  }
19
43
  //# sourceMappingURL=tenant.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tenant.d.ts","sourceRoot":"","sources":["../../src/types/tenant.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAC;AAEvC,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE;QACP,MAAM,EAAE,gBAAgB,CAAC;QACzB,UAAU,EAAE,MAAM,CAAC;QACnB,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC;CACH;AAED,MAAM,MAAM,WAAW,GAAG,iBAAiB,GAAG,mBAAmB,CAAC;AAElE,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,QAAQ,EAAE,CAAC;IAChB,KAAK,EAAE,WAAW,CAAC;CACpB"}
1
+ {"version":3,"file":"tenant.d.ts","sourceRoot":"","sources":["../../src/types/tenant.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAC;AAEvC,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE;QACP,MAAM,EAAE,gBAAgB,CAAC;QACzB,UAAU,EAAE,MAAM,CAAC;QACnB,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC;CACH;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,MAAM,WAAW,GAAG,iBAAiB,GAAG,mBAAmB,GAAG,gBAAgB,CAAC;AAErF;;;;;;;;GAQG;AACH,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,QAAQ,EAAE,CAAC;IAChB,KAAK,CAAC,EAAE,WAAW,CAAC;CACrB"}
package/docs/GLOSSARY.md CHANGED
@@ -19,7 +19,9 @@ existing concept makes search fail and the concept get built twice.
19
19
  | Extra filter | Non-column search filter declared per table. (not: custom filter) |
20
20
  | Extended condition | Consumer-supplied SQL fragment merged into a search WHERE. |
21
21
  | Dotted notation | `orderBy`/condition key counted in parts: 1-part `field`, 2-part `alias.field`, 3-part `alias.fn.field`. (not: parti) |
22
- | Tenant scope | Row-level isolation: `direct` (own column) or `indirect` (via FK). |
22
+ | Tenant scope | Row-level isolation: `direct` (own column), `indirect` (via FK) or `anyOf`. |
23
+ | `anyOf` scope | Tenant scope over several owner columns: the row is visible to any of them. |
24
+ | Anchor | The owner column an `anyOf` write names to tie a new row to the caller. |
23
25
  | Engine | Dialect-agnostic function performing one operation. (not: service, handler) |
24
26
  | Route | Fastify plugin exposing an engine over HTTP. (not: controller) |
25
27
  | Dialect | Per-DB SQL differences (`postgres` \| `mysql`). |
@@ -1,6 +1,7 @@
1
1
  # 0006. Raw DB errors surface as 500 — no SQLSTATE→HTTP mapping
2
2
 
3
- - **Status**: accepted
3
+ - **Status**: accepted on the mapping; message exposure narrowed by
4
+ [0013](./0013-sanitized-db-errors.md)
4
5
  - **Date**: 2026-07-23 (confirmed by the maintainer in June 2026)
5
6
 
6
7
  ## Context
@@ -97,4 +97,7 @@ programmatic `sqlApi.search()` path.
97
97
  narrow. Not decided here.
98
98
  - `tenantScope` remains the one request-derived protection that crosses joins, and is the
99
99
  precedent for the shape any future one should take: table configuration plus a plugin-level
100
- resolver, not a route hook.
100
+ resolver, not a route hook. It crosses joins **unconditionally** only since the tenant stopped
101
+ being resolved from the addressed table alone; before that, a host declaring no scope of its
102
+ own carried none into its join targets, which made the row above true only when the host
103
+ happened to be scoped too (see BREAKING_CHANGES.md).
@@ -0,0 +1,114 @@
1
+ # 0013. Database error messages are sanitized on the wire by default
2
+
3
+ - **Status**: accepted — narrows [0006](./0006-raw-db-errors.md)
4
+ - **Date**: 2026-08-05
5
+
6
+ ## Context
7
+
8
+ [ADR 0006](./0006-raw-db-errors.md) rejected two distinct proposals in one decision: mapping
9
+ SQLSTATE codes to HTTP semantics, and sanitizing driver messages. The first rejection holds
10
+ — the mapping is dialect-specific and encodes product decisions the plugin should not make.
11
+ The second rested on "hiding details is an environment policy, and Fastify already gives
12
+ consumers the hook for it", and three facts undermine it:
13
+
14
+ - The plugin installs its own `setErrorHandler` in the routes' child scope
15
+ (`src/routes/auto/plugin.ts`), and it sends `error.message` verbatim for any status code.
16
+ A consumer handler registered on the parent scope never sees these errors, so the escape
17
+ route 0006 points at does not exist for the main-plugin path.
18
+ - The posture is inconsistent with the rest of the codebase. `maxItemsPerPage`,
19
+ `maxBulkItems`, prototype-key filtering and the join field allowlist are all guards the
20
+ plugin applies by default where the cost is zero. A constraint name describing the schema
21
+ to whoever triggered a duplicate is the same class of free hardening.
22
+ - [ADR 0002](./0002-open-by-default.md) amplifies the exposure: the scenario the plugin
23
+ optimizes for — internal scaffolding wired up in five minutes — is exactly the one where
24
+ nobody wrote an error handler.
25
+
26
+ The failure mode is silent. A consumer who never thinks about it ships the leak and gets no
27
+ signal.
28
+
29
+ ## Decision
30
+
31
+ Errors escaping an auto-generated route handler are replaced with a bare
32
+ `500 Internal Server Error` carrying no database detail. Errors the plugin raises itself are
33
+ deliberate `4xx` with messages written for clients (`httpError`, `validationErrors`) and pass
34
+ through untouched — the rule is `statusCode` in `[400, 500)`, nothing else.
35
+
36
+ No status code is remapped: a `500` stays a `500`. ADR 0006 stands on that point.
37
+
38
+ Nothing is lost. The original error is logged through `request.log.error` and attached as the
39
+ `cause` of the error the consumer's `setErrorHandler` receives, so unique-violation → `409`
40
+ remains implementable, and remains the consumer's decision. The sanitized body carries
41
+ `requestId` — the same value Fastify binds as `reqId` on the request logger — so the client
42
+ can name the log line holding the real error instead of describing a `500` that happened
43
+ "around then".
44
+
45
+ Developing against the API is a real need the log does not serve — a client building on it,
46
+ an agent especially, may have no way to read the server's log, and a round trip per failure
47
+ is expensive. `exposeDebugInfo: true` answers it by **adding** a `debugInfo` payload (driver
48
+ message, `code`, `constraint`/`detail`, stack) beside the fields above, which keep the shape
49
+ they have in production. Client-side error handling therefore does not fork between
50
+ environments — the difference is extra data, not different data.
51
+
52
+ `exposeDebugInfo` derives from nothing. Not `debug`, not `NODE_ENV`, not the log level. The
53
+ deciding argument is that **`debug` belongs to the consumer**: whoever registers the plugin
54
+ decides how it is wired — a constant in one deployment, an environment variable flipped
55
+ during an incident in another — so the library cannot assume its lifecycle, and an option
56
+ whose value the library does not control must not decide what leaves the server. Beyond that,
57
+ `debug` writes to the log, an access-controlled internal channel; the response body is
58
+ external, and under ADR 0002 often unauthenticated. And the posture wanted during a
59
+ production incident — log everything, tell clients nothing — stops being expressible the
60
+ moment one switch drives both.
61
+
62
+ The guard sits in the handler built by `registerForAllTables`
63
+ (`src/routes/auto/route-helpers.ts`) — the single choke point every auto route passes
64
+ through, including route plugins registered standalone without the main plugin. The
65
+ `debugInfo` payload is copied from the driver error by allowlist, never spread: an error
66
+ object that starts carrying the connection config must not reach a client through it.
67
+
68
+ ## Alternatives considered
69
+
70
+ - **`fastify.setErrorHandler` in the plugin** — rejected: the plugin is registered through
71
+ `fp()`, so a handler installed at that level would hijack error rendering for the
72
+ consumer's entire application, not just the auto routes.
73
+ - **Sanitizing inside the existing child-scope handler in `plugin.ts`** — rejected: route
74
+ plugins registered standalone (`searchRoutes` alone, per AGENTS_BACKEND.md "Register only
75
+ specific routes") never reach that handler, and would keep leaking.
76
+ - **Leaving it to the consumer, as 0006 decided** — rejected for the three reasons in
77
+ Context; the deciding one is that the plugin's own child-scope handler makes the
78
+ prescribed consumer remedy unreachable.
79
+ - **Deriving the behaviour from `debug`** — rejected, for the reasons in Decision: the
80
+ consumer owns that option and may wire it to an environment variable, it currently governs
81
+ an internal channel only, and coupling makes "verbose log, silent response" unexpressible.
82
+ An override (`debug: true` + an explicit opt-out) would keep the combination reachable but
83
+ leaves the default pointing the dangerous way, which is the failure mode this ADR exists to
84
+ close.
85
+ - **Keying it on `NODE_ENV`** — rejected. `NODE_ENV !== 'production'` is fail-open: the
86
+ variable is unset often enough in real deployments (a bare `node dist/server.js`, many
87
+ container images, systemd units) that the leak would come back precisely where nothing was
88
+ configured. `NODE_ENV === 'development'` would at least fail closed, but a security-relevant
89
+ response decided by the shell instead of by the registration call is action at a distance.
90
+ - **Replacing `message` with the driver text instead of adding `debugInfo`** — rejected: it
91
+ makes one field mean different things in different environments, so client error handling
92
+ has to be written twice. The additive payload costs nothing and carries more, structured.
93
+ - **Echoing only the SQLSTATE code** (`23505`, no names) — rejected: a third middle mode is
94
+ harder to reason about than a boolean, and reading meaning into SQLSTATE is the direction
95
+ 0006 closed.
96
+ - **Sending the detail only to localhost callers** — rejected: it makes the response depend
97
+ on network topology, which reverse proxies routinely flatten.
98
+
99
+ ## Consequences
100
+
101
+ - Constraint violations reach clients as `{"statusCode":500,"error":"Internal Server
102
+ Error","message":"Internal Server Error","requestId":"…"}`. Diagnosing one requires the
103
+ server log — which is where the error was already going, under that same id.
104
+ - Behavioural breaking change for consumers who parsed driver messages out of `500` bodies.
105
+ They read `debugInfo` instead, and only where the deployment opted in.
106
+ - Development needs one more line (`exposeDebugInfo: true`) than it did. That is the price of
107
+ the option meaning exactly one thing: what a client sees is readable from the registration
108
+ call alone, without knowing how `debug` or the environment happen to be wired.
109
+ - A deployment that leaves `exposeDebugInfo` on by mistake is back to disclosing schema
110
+ detail. The name is the mitigation: `expose` is the word that makes a reader stop, which
111
+ `debug` is not.
112
+ - Errors thrown by consumer `onRequests` hooks are outside the handler and unaffected: that
113
+ is consumer code running before the plugin's SQL surface, and its messages are the
114
+ consumer's to write.
@@ -27,6 +27,7 @@ not by editing history.
27
27
  | [0010](./0010-joins-do-not-run-route-guards.md) | A declared join is a read grant — joins do not run the target table's route guards |
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
+ | [0013](./0013-sanitized-db-errors.md) | Database error messages are sanitized on the wire by default (narrows 0006) |
30
31
 
31
32
  ## When to write one
32
33
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fastify-auto-sqlapi",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
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",