fastify-auto-sqlapi 0.1.9 → 0.1.11

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 (63) hide show
  1. package/AGENTS.md +3 -1
  2. package/AGENTS_BACKEND.md +102 -3
  3. package/AGENTS_FRONTEND.md +69 -545
  4. package/BREAKING_CHANGES.md +63 -0
  5. package/CHANGELOG.md +100 -0
  6. package/README.md +190 -25
  7. package/dist/index.d.ts +5 -1
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +6 -1
  10. package/dist/lib/agent/manifest.d.ts +53 -0
  11. package/dist/lib/agent/manifest.d.ts.map +1 -0
  12. package/dist/lib/agent/manifest.js +153 -0
  13. package/dist/lib/agent/tool-schemas.d.ts +36 -0
  14. package/dist/lib/agent/tool-schemas.d.ts.map +1 -0
  15. package/dist/lib/agent/tool-schemas.js +47 -0
  16. package/dist/lib/cli/schema-codegen.d.ts.map +1 -1
  17. package/dist/lib/cli/schema-codegen.js +9 -1
  18. package/dist/lib/engine/bulk/bulk-delete.d.ts.map +1 -1
  19. package/dist/lib/engine/bulk/bulk-delete.js +5 -1
  20. package/dist/lib/engine/rest/delete.d.ts.map +1 -1
  21. package/dist/lib/engine/rest/delete.js +4 -1
  22. package/dist/lib/engine/rest/get.d.ts.map +1 -1
  23. package/dist/lib/engine/rest/get.js +4 -1
  24. package/dist/lib/engine/write-helpers.d.ts +18 -4
  25. package/dist/lib/engine/write-helpers.d.ts.map +1 -1
  26. package/dist/lib/engine/write-helpers.js +31 -10
  27. package/dist/lib/nullable.d.ts +17 -0
  28. package/dist/lib/nullable.d.ts.map +1 -0
  29. package/dist/lib/nullable.js +24 -0
  30. package/dist/lib/schema/search.d.ts +11 -9
  31. package/dist/lib/schema/search.d.ts.map +1 -1
  32. package/dist/lib/schema/search.js +27 -10
  33. package/dist/lib/table-helpers.d.ts.map +1 -1
  34. package/dist/lib/table-helpers.js +12 -2
  35. package/dist/routes/auto/agent-manifest.routes.d.ts +12 -0
  36. package/dist/routes/auto/agent-manifest.routes.d.ts.map +1 -0
  37. package/dist/routes/auto/agent-manifest.routes.js +30 -0
  38. package/dist/routes/auto/bulk-delete.routes.d.ts.map +1 -1
  39. package/dist/routes/auto/bulk-delete.routes.js +1 -0
  40. package/dist/routes/auto/delete.routes.d.ts.map +1 -1
  41. package/dist/routes/auto/delete.routes.js +1 -0
  42. package/dist/routes/auto/get.routes.d.ts.map +1 -1
  43. package/dist/routes/auto/get.routes.js +1 -0
  44. package/dist/routes/auto/plugin.d.ts.map +1 -1
  45. package/dist/routes/auto/plugin.js +4 -0
  46. package/dist/routes/auto/route-helpers.d.ts +7 -0
  47. package/dist/routes/auto/route-helpers.d.ts.map +1 -1
  48. package/dist/routes/auto/route-helpers.js +11 -0
  49. package/dist/routes/auto/search.routes.js +3 -3
  50. package/dist/types/plugin.d.ts +7 -0
  51. package/dist/types/plugin.d.ts.map +1 -1
  52. package/dist/types/table.d.ts +6 -0
  53. package/dist/types/table.d.ts.map +1 -1
  54. package/dist/types/table.js +8 -0
  55. package/docs/adr/0001-no-orm-raw-sql.md +38 -0
  56. package/docs/adr/0002-open-by-default.md +37 -0
  57. package/docs/adr/0003-bulk-not-transactional.md +35 -0
  58. package/docs/adr/0004-updates-always-open.md +46 -0
  59. package/docs/adr/0005-insert-pipeline-sanitize-before-hooks.md +51 -0
  60. package/docs/adr/0006-raw-db-errors.md +34 -0
  61. package/docs/adr/0007-explicit-table-generation.md +39 -0
  62. package/docs/adr/README.md +50 -0
  63. package/package.json +2 -1
@@ -0,0 +1,12 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import type { SqlApiPluginOptions } from '../../types.js';
3
+ /**
4
+ * GET /agent/manifest → JSON description of every exposed table
5
+ * GET /agent/manifest.md → compact markdown for an LLM system prompt
6
+ *
7
+ * Opt-in via the `agentManifest` plugin option (like `swagger`). Runs behind the same
8
+ * global `onRequests` hooks as the data routes. The manifest is built once at startup —
9
+ * `DbTables` is static configuration.
10
+ */
11
+ export default function agentManifestRoutes(fastify: FastifyInstance, options: SqlApiPluginOptions): Promise<void>;
12
+ //# sourceMappingURL=agent-manifest.routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-manifest.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/agent-manifest.routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D;;;;;;;GAOG;AACH,wBAA8B,mBAAmB,CAC/C,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAsBf"}
@@ -0,0 +1,30 @@
1
+ import { buildAgentManifest, renderAgentManifestMd } from '../../lib/agent/manifest.js';
2
+ /**
3
+ * GET /agent/manifest → JSON description of every exposed table
4
+ * GET /agent/manifest.md → compact markdown for an LLM system prompt
5
+ *
6
+ * Opt-in via the `agentManifest` plugin option (like `swagger`). Runs behind the same
7
+ * global `onRequests` hooks as the data routes. The manifest is built once at startup —
8
+ * `DbTables` is static configuration.
9
+ */
10
+ export default async function agentManifestRoutes(fastify, options) {
11
+ const manifest = buildAgentManifest(options.DbTables);
12
+ const markdown = renderAgentManifestMd(manifest);
13
+ const onRequest = options.onRequests?.length ? options.onRequests : undefined;
14
+ fastify.get('/agent/manifest', {
15
+ ...(onRequest ? { onRequest } : {}),
16
+ schema: {
17
+ tags: ['SqlAPI-agent'],
18
+ summary: 'Agent manifest (JSON)',
19
+ description: 'Machine-readable description of every exposed table: fields, operations, join aliases, computed fields. Pair with AGENTS_FRONTEND.md.',
20
+ },
21
+ }, async () => manifest);
22
+ fastify.get('/agent/manifest.md', {
23
+ ...(onRequest ? { onRequest } : {}),
24
+ schema: {
25
+ tags: ['SqlAPI-agent'],
26
+ summary: 'Agent manifest (markdown)',
27
+ description: 'Compact markdown rendering of the manifest, designed for an LLM system prompt.',
28
+ },
29
+ }, async (_req, reply) => reply.type('text/markdown; charset=utf-8').send(markdown));
30
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"bulk-delete.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/bulk-delete.routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAK/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,wBAA8B,gBAAgB,CAC5C,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAkBf"}
1
+ {"version":3,"file":"bulk-delete.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/bulk-delete.routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAK/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,wBAA8B,gBAAgB,CAC5C,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAmBf"}
@@ -5,6 +5,7 @@ import { DEFAULT_MAX_BULK_ITEMS } from '../../types.js';
5
5
  export default async function bulkDeleteRoutes(fastify, options) {
6
6
  await registerForAllTables(fastify, options, {
7
7
  operation: 'bulkDelete',
8
+ singlePkOnly: true,
8
9
  method: 'POST',
9
10
  url: (tc) => `/bulk/${tc.Schema.tableName}/delete`,
10
11
  successStatus: 200,
@@ -1 +1 @@
1
- {"version":3,"file":"delete.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/delete.routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAI/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,wBAA8B,YAAY,CACxC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAoBf"}
1
+ {"version":3,"file":"delete.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/delete.routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAI/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,wBAA8B,YAAY,CACxC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAqBf"}
@@ -4,6 +4,7 @@ import { registerForAllTables } from './route-helpers.js';
4
4
  export default async function deleteRoutes(fastify, options) {
5
5
  await registerForAllTables(fastify, options, {
6
6
  operation: 'delete',
7
+ singlePkOnly: true,
7
8
  method: 'DELETE',
8
9
  url: (tc) => `/rest/${tc.Schema.tableName}/:id`,
9
10
  successStatus: 200,
@@ -1 +1 @@
1
- {"version":3,"file":"get.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/get.routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAG/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,wBAA8B,SAAS,CACrC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAiBf"}
1
+ {"version":3,"file":"get.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/get.routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAG/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,wBAA8B,SAAS,CACrC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAkBf"}
@@ -3,6 +3,7 @@ import { registerForAllTables } from './route-helpers.js';
3
3
  export default async function getRoutes(fastify, options) {
4
4
  await registerForAllTables(fastify, options, {
5
5
  operation: 'get',
6
+ singlePkOnly: true,
6
7
  method: 'GET',
7
8
  url: (tc) => `/rest/${tc.Schema.tableName}/:id`,
8
9
  successStatus: 200,
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAgB,MAAM,SAAS,CAAC;AAa7D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;kCAuD/C,eAAe,WACf,mBAAmB,KAC3B,OAAO,CAAC,IAAI,CAAC;AAHhB,wBA0DoC"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAgB,MAAM,SAAS,CAAC;AAc7D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;kCAuD/C,eAAe,WACf,mBAAmB,KAC3B,OAAO,CAAC,IAAI,CAAC;AAHhB,wBA6DoC"}
@@ -10,6 +10,7 @@ import updateRoutes from './update.routes.js';
10
10
  import deleteRoutes from './delete.routes.js';
11
11
  import bulkUpsertRoutes from './bulk-upsert.routes.js';
12
12
  import bulkDeleteRoutes from './bulk-delete.routes.js';
13
+ import agentManifestRoutes from './agent-manifest.routes.js';
13
14
  // Schema combinators for which Ajv reports one error per failed branch plus a
14
15
  // single combinator-level summary error, all targeting the same path.
15
16
  const COMBINATOR_KEYWORDS = new Set(['anyOf', 'oneOf']);
@@ -97,5 +98,8 @@ export default fp(async function fastifyAutoSqlApi(fastify, options) {
97
98
  await instance.register(deleteRoutes, routeOptions);
98
99
  await instance.register(bulkUpsertRoutes, routeOptions);
99
100
  await instance.register(bulkDeleteRoutes, routeOptions);
101
+ if (options.agentManifest) {
102
+ await instance.register(agentManifestRoutes, routeOptions);
103
+ }
100
104
  }, { prefix });
101
105
  }, { name: 'fastify-auto-sqlapi' });
@@ -12,6 +12,13 @@ interface RouteSchema {
12
12
  export interface AutoRouteSpec {
13
13
  /** Operation key matched against `ITable.operations` to decide whether to register the route. */
14
14
  operation: TableOperation;
15
+ /**
16
+ * Set when the route addresses records by a single PK value (get, delete, bulkDelete).
17
+ * Tables with a composite primary key skip these routes: matching on the first PK column
18
+ * alone would touch every row sharing that value. Explicitly listing the operation in
19
+ * `ITable.operations` for such a table throws at registration.
20
+ */
21
+ singlePkOnly?: boolean;
15
22
  method: 'GET' | 'POST' | 'PUT' | 'DELETE';
16
23
  /** URL builder receiving the table configuration to read `Schema.tableName`. */
17
24
  url: (tableConf: ITable) => string;
@@ -1 +1 @@
1
- {"version":3,"file":"route-helpers.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/route-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE7E,OAAO,KAAK,EAAE,MAAM,EAAE,mBAAmB,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAE5F,KAAK,WAAW,GAAG,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,GAAG,YAAY,CAAC,CAAC;AAElG,wBAAgB,eAAe,CAAC,OAAO,EAAE,mBAAmB,EAAE,SAAS,EAAE,MAAM,GAAG,WAAW,EAAE,CAE9F;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAQlG;AAED,UAAU,WAAW;IAEnB,IAAI,CAAC,EAAE,GAAG,CAAC;IAEX,MAAM,CAAC,EAAE,GAAG,CAAC;IAEb,WAAW,CAAC,EAAE,GAAG,CAAC;IAElB,QAAQ,CAAC,EAAE,GAAG,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,iGAAiG;IACjG,SAAS,EAAE,cAAc,CAAC;IAC1B,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,CAAC;IAC1C,gFAAgF;IAChF,GAAG,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC;IACnC,aAAa,EAAE,GAAG,GAAG,GAAG,CAAC;IACzB,wIAAwI;IACxI,OAAO,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,WAAW,CAAC;IACnF,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,KAAK,MAAM,CAAC;IACpF;2EACuE;IACvE,MAAM,EAAE,CACN,OAAO,EAAE,eAAe,EACxB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,cAAc,EACvB,KAAK,EAAE,YAAY,KAChB,OAAO,CAAC,OAAO,CAAC,CAAC;CACvB;AAED;;;;GAIG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,EAC5B,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC,IAAI,CAAC,CAqCf"}
1
+ {"version":3,"file":"route-helpers.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/route-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAG7E,OAAO,KAAK,EAAE,MAAM,EAAE,mBAAmB,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAE5F,KAAK,WAAW,GAAG,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,GAAG,YAAY,CAAC,CAAC;AAElG,wBAAgB,eAAe,CAAC,OAAO,EAAE,mBAAmB,EAAE,SAAS,EAAE,MAAM,GAAG,WAAW,EAAE,CAE9F;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAQlG;AAED,UAAU,WAAW;IAEnB,IAAI,CAAC,EAAE,GAAG,CAAC;IAEX,MAAM,CAAC,EAAE,GAAG,CAAC;IAEb,WAAW,CAAC,EAAE,GAAG,CAAC;IAElB,QAAQ,CAAC,EAAE,GAAG,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,iGAAiG;IACjG,SAAS,EAAE,cAAc,CAAC;IAC1B;;;;;OAKG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,CAAC;IAC1C,gFAAgF;IAChF,GAAG,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC;IACnC,aAAa,EAAE,GAAG,GAAG,GAAG,CAAC;IACzB,wIAAwI;IACxI,OAAO,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,WAAW,CAAC;IACnF,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,KAAK,MAAM,CAAC;IACpF;2EACuE;IACvE,MAAM,EAAE,CACN,OAAO,EAAE,eAAe,EACxB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,cAAc,EACvB,KAAK,EAAE,YAAY,KAChB,OAAO,CAAC,OAAO,CAAC,CAAC;CACvB;AAED;;;;GAIG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,EAC5B,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC,IAAI,CAAC,CAkDf"}
@@ -1,4 +1,5 @@
1
1
  import { ensureSqlApiDecorator } from '../../lib/sql-api-decorator.js';
2
+ import { isCompositePrimary } from '../../types.js';
2
3
  export function mergeOnRequests(options, tableConf) {
3
4
  return [...(options.onRequests || []), ...(tableConf.onRequests || [])];
4
5
  }
@@ -25,6 +26,16 @@ export async function registerForAllTables(fastify, options, spec) {
25
26
  // operations acts as a whitelist; when omitted every operation is exposed.
26
27
  if (tableConf.operations && !tableConf.operations.includes(spec.operation))
27
28
  continue;
29
+ // Single-PK routes cannot address composite-PK tables — skip them (default) or,
30
+ // when the consumer explicitly asked for the operation, fail loudly at startup.
31
+ if (spec.singlePkOnly && isCompositePrimary(tableConf.primary)) {
32
+ if (tableConf.operations?.includes(spec.operation)) {
33
+ throw new Error(`Table "${tableName}" has a composite primary key (${tableConf.primary.join(', ')}): ` +
34
+ `operation "${spec.operation}" addresses records by a single PK value and cannot be exposed. ` +
35
+ `Remove it from "operations", or handle the table via search/update or a custom route.`);
36
+ }
37
+ continue;
38
+ }
28
39
  const schemas = spec.schemas(DbTables, tableName, tableConf);
29
40
  // Only include schema keys that are actually defined: Fastify checks for key
30
41
  // *presence* (not undefined value) to decide whether to emit FSTWRN001.
@@ -1,4 +1,4 @@
1
- import { SearchTableBodyPost, SearchTableQueryString, SearchTableResponse } from '../../lib/schema/search.js';
1
+ import { SearchTableBodyPost, SearchTableQuery, SearchTableResponse } from '../../lib/schema/search.js';
2
2
  import { registerForAllTables } from './route-helpers.js';
3
3
  import { httpError } from '../../lib/errors.js';
4
4
  import { DEFAULT_MAX_ITEMS_PER_PAGE } from '../../types.js';
@@ -10,7 +10,7 @@ export default async function searchRoutes(fastify, options) {
10
10
  successStatus: 200,
11
11
  schemas: (db, table) => ({
12
12
  body: SearchTableBodyPost(db, table),
13
- querystring: SearchTableQueryString,
13
+ querystring: SearchTableQuery(options.maxItemsPerPage ?? DEFAULT_MAX_ITEMS_PER_PAGE),
14
14
  response: SearchTableResponse(db, table),
15
15
  }),
16
16
  summary: 'Search',
@@ -45,7 +45,7 @@ export default async function searchRoutes(fastify, options) {
45
45
  selectComputed: body.selectComputed,
46
46
  orderBy: query.orderBy,
47
47
  paginator: (query.page || query.itemsPerPage)
48
- ? { page: query.page || 1, itemsPerPage: query.itemsPerPage || 500 }
48
+ ? { page: query.page || 1, itemsPerPage: query.itemsPerPage || Math.min(500, maxRows) }
49
49
  : undefined,
50
50
  computeMin: query.computeMin,
51
51
  computeMax: query.computeMax,
@@ -28,6 +28,13 @@ export interface SqlApiPluginOptions {
28
28
  * Defaults to {@link DEFAULT_MAX_BULK_ITEMS}.
29
29
  */
30
30
  maxBulkItems?: number;
31
+ /**
32
+ * Register `GET /agent/manifest` (JSON) and `GET /agent/manifest.md` (markdown): a
33
+ * machine-readable description of every exposed table (fields, operations, join
34
+ * aliases, computed fields), for LLM/agent clients. Opt-in, like `swagger`. The
35
+ * routes run behind the same global `onRequests` hooks as the data routes.
36
+ */
37
+ agentManifest?: boolean;
31
38
  }
32
39
  /** Default row cap for search (page size and no-paginator LIMIT). Override via `maxItemsPerPage`. */
33
40
  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;CACvB;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;CACzB;AAED,qGAAqG;AACrG,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAE/C,8EAA8E;AAC9E,eAAO,MAAM,sBAAsB,OAAO,CAAC"}
@@ -128,5 +128,11 @@ export interface ITable<F extends Record<string, TSchema> = Record<string, TSche
128
128
  }
129
129
  export type DbTables = Record<string, ITable<any>>;
130
130
  export declare function primaryAsString(pk: string | string[]): string;
131
+ /**
132
+ * True when the primary key spans more than one column. Operations that address a
133
+ * record by a single PK value (get, delete, bulkDelete) cannot work on such tables:
134
+ * matching on the first column alone would hit every row sharing that value.
135
+ */
136
+ export declare function isCompositePrimary(pk: string | string[]): boolean;
131
137
  export declare function primaryAsCols(pk: string | string[], colFn: (f: string) => string): string | string[];
132
138
  //# sourceMappingURL=table.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"table.d.ts","sourceRoot":"","sources":["../../src/types/table.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"table.d.ts","sourceRoot":"","sources":["../../src/types/table.ts"],"names":[],"mappings":"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"}
@@ -2,6 +2,14 @@
2
2
  export function primaryAsString(pk) {
3
3
  return Array.isArray(pk) ? pk[0] : pk;
4
4
  }
5
+ /**
6
+ * True when the primary key spans more than one column. Operations that address a
7
+ * record by a single PK value (get, delete, bulkDelete) cannot work on such tables:
8
+ * matching on the first column alone would hit every row sharing that value.
9
+ */
10
+ export function isCompositePrimary(pk) {
11
+ return Array.isArray(pk) && pk.length > 1;
12
+ }
5
13
  export function primaryAsCols(pk, colFn) {
6
14
  return Array.isArray(pk) ? pk.map(colFn) : colFn(pk);
7
15
  }
@@ -0,0 +1,38 @@
1
+ # 0001. No ORM — raw SQL with minimal dependencies
2
+
3
+ - **Status**: accepted
4
+ - **Date**: 2026-07-23 (foundational decision, predates this record)
5
+
6
+ ## Context
7
+
8
+ Auto-generating CRUD APIs from database tables is the territory of ORMs and heavy
9
+ API-builder frameworks. Adopting one would have provided models, migrations and query
10
+ building for free — at the price of a large dependency tree, an abstraction layer between
11
+ the consumer and their SQL, and lock-in to the ORM's schema model.
12
+
13
+ ## Decision
14
+
15
+ The plugin talks to the database with raw SQL through thin driver adapters (`pg`,
16
+ `mysql2`), composing WHERE clauses with `node-condition-builder` and validating with
17
+ TypeBox. No ORM, no query-builder DSL, no migration system. Runtime dependencies are kept
18
+ to the minimum, and the plugin re-exports its building blocks (TypeBox, ConditionBuilder)
19
+ so consumers build on the same tools instead of adding parallel ones.
20
+
21
+ ## Alternatives considered
22
+
23
+ - **Build on an ORM (Prisma, Drizzle, Sequelize, …)** — rejected: large dependency
24
+ surface, a second schema definition to keep in sync with the DB, and the ORM's
25
+ abstractions leak into the public API. The CLI introspection (`information_schema`)
26
+ gives the same "schema from the DB" ergonomics without the layer.
27
+ - **Hand-rolled query-builder DSL** — rejected: reinvents an ORM incrementally.
28
+ `node-condition-builder` covers the one genuinely composable part (conditions);
29
+ everything else is plain SQL the maintainer can read and debug.
30
+
31
+ ## Consequences
32
+
33
+ - The engine owns dialect differences (placeholders, quoting, RETURNING vs insertId)
34
+ explicitly — see `src/lib/dialect.ts` and the adapters.
35
+ - Consumers get a framework approach: tools are provided (SqlApi, QueryClient,
36
+ ConditionBuilder), product structure is not imposed.
37
+ - Features an ORM would give for free (migrations, lazy relations, identity maps) are
38
+ intentionally out of scope.
@@ -0,0 +1,37 @@
1
+ # 0002. Open by default — no imposed auth model
2
+
3
+ - **Status**: accepted
4
+ - **Date**: 2026-07-23 (confirmed by the maintainer in June 2026)
5
+
6
+ ## Context
7
+
8
+ Registering the plugin with no configuration exposes every operation (read and write,
9
+ including bulk delete) on every table in `DbTables`. Security reviews repeatedly flag this
10
+ as a vulnerability and propose secure-by-default behavior: routes disabled until
11
+ explicitly enabled, or a mandatory auth hook.
12
+
13
+ ## Decision
14
+
15
+ The plugin stays open by default. Authentication and authorization are the consumer's
16
+ job, wired through the tools provided for it: `onRequests` (global or per-table request
17
+ hooks), `operations` (per-table whitelist of registered routes), and `tenantScope`
18
+ (tenant isolation). The README carries a prominent warning in its Security section.
19
+
20
+ ## Alternatives considered
21
+
22
+ - **Deny-by-default routes** (`operations` required, empty = nothing exposed) — rejected:
23
+ it turns the five-minute quick start into configuration boilerplate, and the plugin's
24
+ primary use case is rapid internal API scaffolding where the consumer adds auth at the
25
+ Fastify level anyway.
26
+ - **Mandatory auth callback** (refuse to register without an `onRequests`) — rejected:
27
+ imposes an auth model on consumers who legitimately handle auth outside the plugin
28
+ (gateway, network isolation, Fastify-level hooks on the parent scope).
29
+
30
+ ## Consequences
31
+
32
+ - **You must lock it down before exposing it.** The responsibility is documented, not
33
+ enforced.
34
+ - Security reviews will keep flagging this; the answer is this ADR plus the README
35
+ Security section, not a behavior change.
36
+ - `operations` gates HTTP routes only — the programmatic `sqlApi.*` methods remain
37
+ available to consumer code, by design.
@@ -0,0 +1,35 @@
1
+ # 0003. Bulk operations are not transactional
2
+
3
+ - **Status**: accepted
4
+ - **Date**: 2026-07-23 (confirmed by the maintainer in June 2026)
5
+
6
+ ## Context
7
+
8
+ Single insert and update run inside a transaction: main record, secondaries, deletions
9
+ and after-hooks are atomic (`QueryClient.withTransaction`, degrading gracefully when the
10
+ adapter has no `connect()`). Bulk upsert and bulk delete do not — a failure partway
11
+ through leaves the earlier items committed. Reviews periodically report this as an
12
+ inconsistency and propose wrapping bulk operations in a transaction.
13
+
14
+ ## Decision
15
+
16
+ Bulk operations stay non-transactional. They are a shortcut for issuing many single
17
+ operations efficiently, not an atomic batch primitive: "all or nothing" is explicitly not
18
+ part of their contract.
19
+
20
+ ## Alternatives considered
21
+
22
+ - **Wrap each bulk request in one transaction** — rejected: changes the semantics from
23
+ "N independent operations" to "one atomic batch", which is a different feature. Large
24
+ batches would also hold long transactions with the associated lock footprint.
25
+ - **Opt-in `transactional: true` flag** — rejected for now: adds API surface for a need
26
+ no consumer has expressed; a consumer needing atomicity can call the programmatic
27
+ `sqlApi.*` methods inside their own `withTransaction`.
28
+
29
+ ## Consequences
30
+
31
+ - A failed bulk request may have partially applied. Consumers that care must either
32
+ retry idempotently (upsert semantics make this natural) or manage their own
33
+ transaction via the programmatic API.
34
+ - Bulk delete reports the ids actually deleted (`afterBulkDelete` receives the actual
35
+ subset), which is the honest contract for a non-atomic batch.
@@ -0,0 +1,46 @@
1
+ # 0004. Updates are always open — no `excludeFromUpdate`
2
+
3
+ - **Status**: accepted
4
+ - **Date**: 2026-07-23
5
+
6
+ ## Context
7
+
8
+ `excludeFromCreation` strips client-supplied fields from inserts. A symmetric
9
+ `excludeFromUpdate` was proposed to statically block fields on the update path (e.g.
10
+ `createdAt`, `isAdmin`), partly to close the asymmetry where a field excluded from
11
+ creation can still be modified via `PUT`.
12
+
13
+ ## Decision
14
+
15
+ No `excludeFromUpdate`. Every Schema field is updatable by default, and
16
+ `excludeFromCreation` is defined as an **ergonomics tool for creation** (auto-generated
17
+ PKs, DB-default columns — its original purpose was handling autoincrement validation),
18
+ **not** a field-level security mechanism.
19
+
20
+ Field-level update rules are product logic the plugin must not decide: a super admin may
21
+ legitimately fix an `updatedAt`; in one product users may move themselves across tenants,
22
+ in another an admin moves others, in a third nobody moves anyone. A static list cannot
23
+ express any of this — real authorization is contextual (session, role, record state).
24
+ Consumers encode their rules in `beforeUpdate` (silent strip) or `validate` (loud 400),
25
+ or move privileged transitions to dedicated endpoints and keep them off the auto routes
26
+ via `operations`.
27
+
28
+ ## Alternatives considered
29
+
30
+ - **`excludeFromUpdate` list** — rejected: the main risk is the *false sense of
31
+ security*. It only covers "nobody, ever, via API", while the dangerous real-world cases
32
+ (`isAdmin`, roles, ownership) are session-dependent — someone would use the list for
33
+ them and believe the problem solved.
34
+ - **`immutable: [...]` / per-field `writable` modes** — rejected: a bigger redesign of
35
+ the write model for the same limited static semantics.
36
+
37
+ ## Consequences
38
+
39
+ - Sensitive flags in the Schema are updatable through the auto routes until the consumer
40
+ adds a hook. This is documented prominently (README Security → "Field-level update
41
+ rules are product logic", AGENTS_BACKEND.md pattern "Protect sensitive fields on
42
+ update").
43
+ - The tenant column is the one deliberate exception, and only under opt-in `tenantScope`:
44
+ tenant-scoped callers cannot re-tenant records (isolation contract), while admin
45
+ callers (`getTenantId` → `null`) remain unrestricted — consistent with "the plugin does
46
+ not decide product rules".
@@ -0,0 +1,51 @@
1
+ # 0005. Insert pipeline: client payload sanitized before `beforeInsert`
2
+
3
+ - **Status**: accepted
4
+ - **Date**: 2026-07-23
5
+
6
+ ## Context
7
+
8
+ Until v0.1.9 the insert pipeline applied `excludeFromCreation` *after* the `beforeInsert`
9
+ hook, so a value the hook assigned to an excluded field was silently stripped from the
10
+ INSERT. This broke the documented server-generated-id pattern (TEXT PK in
11
+ `excludeFromCreation`, id generated in `beforeInsert` → not-null violation) and
12
+ contradicted the documented contract that hook mutations propagate to the SQL. The same
13
+ ordering flaw let an excluded FK column erase the engine's own FK auto-fill on secondary
14
+ records.
15
+
16
+ ## Decision
17
+
18
+ Exclusion is a whitelist on **client input** and runs at the client boundary. The insert
19
+ pipeline order is, for both single insert and bulk upsert:
20
+
21
+ 1. `validate` — receives the payload **as sent by the client** (documented contract);
22
+ 2. `excludeFromCreation` strip;
23
+ 3. `beforeInsert` — hook mutations, including on excluded fields, reach the SQL;
24
+ 4. conversion to DB column format.
25
+
26
+ For secondary records, exclusion runs **before** the engine's FK auto-fill, so the
27
+ injected parent key always survives.
28
+
29
+ Server-side writers are ordered by trust: client input is sanitized first, then hooks
30
+ mutate freely, then engine-enforced values (tenant injection/validation) run last and
31
+ fail loudly on conflict rather than stripping silently.
32
+
33
+ ## Alternatives considered
34
+
35
+ - **Strip after the hook** (previous behavior) — rejected: silently discards
36
+ server-generated values; the only thing it "protected" was the ability of a hook to
37
+ accidentally write an excluded field, which is server code and trusted.
38
+ - **Strip before `validate` too** — rejected: `validate` is documented to receive the
39
+ record as sent by the client; changing its input would break that contract for no
40
+ gain.
41
+
42
+ ## Consequences
43
+
44
+ - The pattern `excludeFromCreation: ['id']` + id-generating `beforeInsert` works as
45
+ documented; consumers no longer need the `schemaOverrides` workaround.
46
+ - Behavior change (noted in the CHANGELOG): a hook that assigns an excluded field now
47
+ writes it, where it was previously discarded.
48
+ - `afterInsert` no longer sees client-supplied values on excluded fields — it sees what
49
+ was actually written.
50
+ - Locked by unit tests (single, bulk, upsert, secondaries, validate-ordering) and by a
51
+ dialect-parametric integration suite reproducing the original failure.
@@ -0,0 +1,34 @@
1
+ # 0006. Raw DB errors surface as 500 — no SQLSTATE→HTTP mapping
2
+
3
+ - **Status**: accepted
4
+ - **Date**: 2026-07-23 (confirmed by the maintainer in June 2026)
5
+
6
+ ## Context
7
+
8
+ Constraint violations (unique, foreign key, not-null) bubble up from the driver and reach
9
+ the client as a `500` carrying the raw database message. A review proposed mapping
10
+ SQLSTATE codes to HTTP semantics — unique violation → `409`, FK/not-null violation →
11
+ `400` — with sanitized messages.
12
+
13
+ ## Decision
14
+
15
+ No SQLSTATE mapping in the engine. Errors the plugin itself detects (validation, missing
16
+ PK, not found, tenant violations) throw proper `4xx` via `httpError`; everything coming
17
+ from the database is passed through unmapped.
18
+
19
+ ## Alternatives considered
20
+
21
+ - **SQLSTATE→HTTP mapping layer** — rejected: the mapping is dialect-specific
22
+ (PostgreSQL SQLSTATE vs MySQL errno), inevitably partial, and encodes product decisions
23
+ (is a duplicate a conflict to surface or an internal bug?) the plugin should not make.
24
+ - **Sanitizing DB messages** — rejected as a plugin default: hiding details is an
25
+ environment policy, and Fastify already gives consumers the hook for it.
26
+
27
+ ## Consequences
28
+
29
+ - Constraint violations look like server errors (`500`) to API clients, and driver
30
+ messages may leak schema details (table/column/constraint names). Consumers who care
31
+ set a Fastify `setErrorHandler` — one place, their policy, their status codes.
32
+ - A consumer-facing "friendly duplicate error" is expected to be handled in `validate`
33
+ (explicit pre-check) or in the consumer's error handler, not by the engine guessing
34
+ intent from SQLSTATE.
@@ -0,0 +1,39 @@
1
+ # 0007. Table generation is explicit — `--all` is a migration helper, not the default
2
+
3
+ - **Status**: accepted
4
+ - **Date**: 2026-07-23
5
+
6
+ ## Context
7
+
8
+ `sqlapi-generate-schema` with no arguments introspects every table, while
9
+ `sqlapi-generate-tables` with no arguments prints usage and exits, requiring `--all` or
10
+ explicit table names. A review flagged the opposite defaults as an inconsistency to
11
+ harmonize.
12
+
13
+ ## Decision
14
+
15
+ The asymmetry is deliberate and stays. The two generators have different roles:
16
+
17
+ - **Schemas** are disposable mirrors of the database — regenerating all of them is always
18
+ correct, so "all" is the natural default.
19
+ - **Table files are the consumer's working set**: which tables become API endpoints is a
20
+ deliberate choice, and it is legitimate to keep tables out of `DbTables`. The intended
21
+ flow is *"I added a table, I generate its Table file explicitly"*
22
+ (`sqlapi-generate-tables customer`). If the default were "all", every run would
23
+ resurrect Table files the consumer had deliberately deleted, forcing them to delete the
24
+ same files again and again. `--all` exists only as a bootstrap helper when introducing
25
+ the plugin into an existing codebase.
26
+
27
+ ## Alternatives considered
28
+
29
+ - **Default to `--all` (mirror generate-schema)** — rejected: re-creates deleted Table
30
+ files on every run, fighting the consumer's curation of the exposed surface.
31
+ - **Tracking deleted tables** (a skip-list) — rejected: state to maintain for a problem
32
+ explicit invocation solves for free.
33
+
34
+ ## Consequences
35
+
36
+ - `sqlapi-generate-tables` keeps requiring `--all` or table names; running it bare prints
37
+ usage and exits non-zero.
38
+ - The generator never overwrites existing Table files, so both the explicit and the
39
+ `--all` flow are safe to re-run.
@@ -0,0 +1,50 @@
1
+ # Architecture Decision Records
2
+
3
+ Deliberate design decisions of `fastify-auto-sqlapi`, with their rationale and the
4
+ alternatives that were considered and rejected.
5
+
6
+ **If you are an agent or a contributor about to "fix" one of the behaviors below: read the
7
+ matching ADR first.** These are not gaps or oversights — proposals that re-litigate a
8
+ decision recorded here should either reference new arguments the ADR does not address, or
9
+ be dropped.
10
+
11
+ ADRs are immutable: a decision is changed by adding a new ADR that supersedes the old one,
12
+ not by editing history.
13
+
14
+ ## Index
15
+
16
+ | # | Decision |
17
+ |---|----------|
18
+ | [0001](./0001-no-orm-raw-sql.md) | No ORM — raw SQL with minimal dependencies |
19
+ | [0002](./0002-open-by-default.md) | Open by default — no imposed auth model |
20
+ | [0003](./0003-bulk-not-transactional.md) | Bulk operations are not transactional |
21
+ | [0004](./0004-updates-always-open.md) | Updates are always open — no `excludeFromUpdate` |
22
+ | [0005](./0005-insert-pipeline-sanitize-before-hooks.md) | Insert pipeline: client payload sanitized before `beforeInsert` |
23
+ | [0006](./0006-raw-db-errors.md) | Raw DB errors surface as 500 — no SQLSTATE→HTTP mapping |
24
+ | [0007](./0007-explicit-table-generation.md) | Table generation is explicit — `--all` is a migration helper, not the default |
25
+
26
+ ## When to write one
27
+
28
+ Only when a decision was genuinely debated, or is counter-intuitive enough that a reasonable
29
+ outsider would mistake it for a bug. Plain conventions belong in [AGENTS.md](../../AGENTS.md).
30
+
31
+ ## Format
32
+
33
+ ```markdown
34
+ # NNNN. Title (imperative statement of the decision)
35
+
36
+ - **Status**: accepted | superseded by NNNN
37
+ - **Date**: YYYY-MM-DD
38
+
39
+ ## Context
40
+ What problem or tension forced a choice.
41
+
42
+ ## Decision
43
+ What was decided, stated plainly.
44
+
45
+ ## Alternatives considered
46
+ Each rejected option and the reason it lost.
47
+
48
+ ## Consequences
49
+ What follows — including the costs accepted knowingly.
50
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fastify-auto-sqlapi",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
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",
@@ -18,6 +18,7 @@
18
18
  },
19
19
  "files": [
20
20
  "dist",
21
+ "docs",
21
22
  "AGENTS.md",
22
23
  "AGENTS_BACKEND.md",
23
24
  "AGENTS_FRONTEND.md",