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
@@ -317,3 +317,66 @@ including `joinLeft.filters` / `joinLeft.conditions` and `orderBy`, which previo
317
317
  them with 400. They remain rejected in `selectComputed`, `computeMin/Max/Sum/Avg`,
318
318
  `joinGroup.aggregations.by` and `defaultOrder`, where the expression precedes the `WHERE`
319
319
  values in the parameter order.
320
+
321
+ # Breaking Change — by-single-id operations disabled for composite primary keys
322
+
323
+ Since **0.1.11**, tables whose `primary` is an array of more than one field no longer expose
324
+ the operations that address a record by a single PK value:
325
+
326
+ - `GET /rest/:table/:id` and `DELETE /rest/:table/:id` are **not registered** (404);
327
+ - `POST /bulk/:table/delete` is **not registered** (404);
328
+ - explicitly listing `get`, `delete` or `bulkDelete` in `ITable.operations` for such a table
329
+ now **throws at startup** with a descriptive error;
330
+ - the programmatic `sqlApi.get/delete/bulkDelete` reject with `400`.
331
+
332
+ Search, insert, update and bulk upsert are unaffected — they already matched every PK column.
333
+
334
+ ## Why this changed
335
+
336
+ These operations matched on the **first** PK column alone. On `primary: ['agentId', 'teamId']`:
337
+
338
+ - `DELETE /rest/t/1` deleted **every** row with `agent_id = 1` — not one record;
339
+ - `GET /rest/t/1` returned an arbitrary row among those sharing `agent_id = 1`;
340
+ - bulk delete removed every row matching each listed first-column value.
341
+
342
+ A single `:id` cannot address a composite key; silently matching a prefix of the key is data
343
+ loss waiting to happen, so the operations are refused loudly instead.
344
+
345
+ ## Who is affected
346
+
347
+ Only consumers with composite-PK tables **and** clients calling get/delete/bulk-delete on
348
+ them. If your composite table's first PK column happens to be unique on its own, these
349
+ endpoints previously behaved correctly for you — they are now gone and you must migrate.
350
+
351
+ ## Migration
352
+
353
+ - **Read one record** → `search` with every PK field in the filters:
354
+
355
+ ```typescript
356
+ await fetch('/auto/search/agent_team', {
357
+ method: 'POST',
358
+ body: JSON.stringify({ filters: { agentId: 1, teamId: 2 } }),
359
+ });
360
+ ```
361
+
362
+ - **Delete one record** → a custom route using the low-level `QueryClient`, matching the
363
+ full key:
364
+
365
+ ```typescript
366
+ import { createQueryClient, pgQueryable } from 'fastify-auto-sqlapi';
367
+
368
+ const db = createQueryClient(pgQueryable(app.pg), 'postgres');
369
+
370
+ app.delete('/agent-team/:agentId/:teamId', async (req) => {
371
+ const { agentId, teamId } = req.params;
372
+ await db.delete('agent_team', { agent_id: agentId, team_id: teamId });
373
+ return { main: { agentId, teamId } };
374
+ });
375
+ ```
376
+
377
+ - **Row removal as part of an edit flow** → `update` on the parent with `deletions` (write
378
+ joins), or bulk upsert — both match every PK column.
379
+
380
+ - **First PK column is truly unique on its own?** Then the composite declaration was
381
+ redundant: declare `primary: 'id'` (single) and keep the second column as a plain field —
382
+ every by-id operation comes back.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,106 @@ All notable changes to this project are documented here. This project follows
7
7
 
8
8
  Migration instructions for breaking changes live in **[BREAKING_CHANGES.md](./BREAKING_CHANGES.md)**.
9
9
 
10
+ ## [0.1.11]
11
+
12
+ ### Breaking
13
+
14
+ - **⚠️ By-single-id operations are disabled for composite-PK tables.** `GET /rest/:id`,
15
+ `DELETE /rest/:id` and `POST /bulk/:table/delete` matched on the **first** PK column alone:
16
+ on a table with `primary: ['agentId', 'teamId']`, `DELETE /rest/t/1` deleted **every** row
17
+ with `agentId = 1`, not one record. These routes are no longer registered for composite-PK
18
+ tables (404); explicitly listing one of them in `operations` now throws at startup, and the
19
+ programmatic `sqlApi.get/delete/bulkDelete` reject with 400. Search, insert, update and
20
+ bulk upsert are unaffected — they already handled every PK column. If your composite table's
21
+ first PK column happens to be unique, these endpoints previously worked for you and are now
22
+ gone — see
23
+ [BREAKING_CHANGES.md](./BREAKING_CHANGES.md#breaking-change--by-single-id-operations-disabled-for-composite-primary-keys)
24
+ for the migration paths.
25
+
26
+ ### Fixed
27
+
28
+ - **⚠️ NULL values were serialized as `0` / `""` in read responses.** The generator mapped
29
+ nullable columns to `Type.Optional(T)`, which means "key may be absent" — not "value may
30
+ be null" — so fast-json-stringify coerced every NULL to the type's zero value (a NULL FK
31
+ was served as `0`). Nullable columns are now generated as `Type.Optional(Nullable(T))`,
32
+ where the new exported `Nullable()` helper emits the JSON-Schema type-array form
33
+ (`type: ['integer', 'null']`). **Regenerate your schemas** (`sqlapi-generate-schema`) to
34
+ pick up the fix. Note: the type-array form is deliberate — a `Type.Union([T, Type.Null()])`
35
+ would NOT fix it, because Fastify's default Ajv `coerceTypes` corrupts values through
36
+ union branches (`null` → `0` with the Null branch last, `0`/`""` → `null` with it first);
37
+ with a type array no coercion happens. As a side effect, writes now accept an explicit
38
+ `null` to set a column to NULL.
39
+ - **`maxItemsPerPage` below 500 broke every request without an explicit `itemsPerPage`.**
40
+ The search querystring schema declared a fixed `default: 500`; Ajv injects defaults before
41
+ the cap check runs, so any lower cap rejected the injected default with 400. The schema is
42
+ now parameterized: `default = min(500, maxItemsPerPage)` and the cap doubles as the schema
43
+ `maximum` (structured 400 instead of a plain error). The `SearchTableQuery(cap)` builder is
44
+ exported; `SearchTableQueryString` remains as the default-cap variant.
45
+
46
+ ### Changed
47
+
48
+ - **`filters` with an explicit `null` now filter by `IS NULL`.** Previously a null filter
49
+ was silently dropped — a request asking "field is null" returned unfiltered results. An
50
+ explicit `null` on a schema field now builds `WHERE col IS NULL` (undefined/absent keys
51
+ are still ignored). `conditions` + `isNull` keeps working as before.
52
+
53
+ ### Added
54
+
55
+ - **LLM / agent client support.** The plugin doubles as the enforcement layer for a chat
56
+ agent operating on your data (tenant scoping, `readExclude`, `operations`, caps apply to
57
+ whatever the model invents). New pieces:
58
+ - `AGENTS_FRONTEND.md` rewritten as a compact LLM-oriented request grammar (search with
59
+ all join families, ordering, pagination, writes, error shapes) — usable both by coding
60
+ agents and as the system prompt of a runtime agent client. Same filename and package
61
+ path as before, so existing references keep working; the verbose tutorial-style
62
+ content it replaces is fully covered by the grammar plus Swagger.
63
+ - `agentManifest: true` plugin option → `GET {prefix}/agent/manifest` (JSON) and
64
+ `/agent/manifest.md` (markdown for the system prompt): per-table fields with
65
+ type/required/nullable, enabled operations, join aliases with direction, computed
66
+ fields, extra filters — always in sync with the running config, behind the same
67
+ global `onRequests`. Also exported programmatically (`buildAgentManifest`,
68
+ `renderAgentManifestMd`) and as a granular route plugin (`agentManifestRoutes`).
69
+ - `agentToolSchemas(dbTables, table)` — the exact JSON Schemas the routes validate
70
+ with, per enabled operation, for strict provider-side tool definitions.
71
+ - README "LLM / agent clients" section and AGENTS_BACKEND.md pattern: read-only agent
72
+ surface via granular composition, loose-tool + structured-400 retry loop vs strict
73
+ tools.
74
+ - `Nullable(schema)` export: marks a TypeBox schema nullable via the JSON-Schema type-array
75
+ form — the only representation safe under Fastify's default Ajv/fast-json-stringify on
76
+ both input validation and response serialization.
77
+ - ADR 0007: table generation is explicit — `--all` is a migration helper, not the default.
78
+
79
+ ## [0.1.10]
80
+
81
+ ### Added
82
+
83
+ - **Architecture Decision Records** in `docs/adr/` (shipped in the npm package so agents
84
+ consuming the library can read them): no-ORM/raw-SQL, open-by-default, non-transactional
85
+ bulk operations, always-updatable fields (no `excludeFromUpdate`), insert-pipeline
86
+ ordering, raw DB errors. Linked from README ("Design Decisions") and AGENTS.md.
87
+
88
+ ### Fixed
89
+
90
+ - **`excludeFromCreation` no longer strips values set by `beforeInsert`.** The exclusion list
91
+ was applied after the hook, so a server-generated value assigned in `beforeInsert` (the
92
+ documented pattern for TEXT primary keys, `createdAt`, audit columns) was silently removed
93
+ from the INSERT — a table with `excludeFromCreation: ['id']` and an id-generating hook
94
+ failed with a not-null violation. The client payload is now sanitized *before* the hook
95
+ runs, in both the single insert and the bulk upsert path: client-supplied values on
96
+ excluded fields are still ignored, hook-assigned ones reach the SQL. For the same reason,
97
+ secondary records now drop excluded fields *before* the engine's FK auto-fill, so listing
98
+ the FK column in a secondary table's `excludeFromCreation` no longer erases the injected
99
+ parent key.
100
+
101
+ ### Documentation
102
+
103
+ - Clarified that `excludeFromCreation` is an ergonomics tool for creation, not a field-level
104
+ security mechanism: it deliberately does not apply to updates, where every Schema field is
105
+ writable by default. Field-level update rules (sensitive flags, roles, ownership/tenant
106
+ columns) are product logic to enforce via `beforeUpdate`/`validate` or dedicated endpoints —
107
+ see the new "Field-level update rules" section in the README and the matching pattern in
108
+ AGENTS_BACKEND.md.
109
+
10
110
  ## [0.1.7]
11
111
 
12
112
  ### Fixed
package/README.md CHANGED
@@ -4,18 +4,9 @@ Automatic CRUD API generation from PostgreSQL, MySQL, and MariaDB tables for [Fa
4
4
 
5
5
  Point it at your database, and get a full REST API with search, advanced conditions, pagination, joins, aggregations, bulk operations, multi-tenant isolation, validation, and hooks.
6
6
 
7
- ## What LLMs Are Saying
8
-
9
- > API: 5 new read-only tables — and this is where your plugin really shined.
10
-
11
- > Yes, it genuinely saved me a lot of work, and the numbers make that obvious: I deleted about 350 lines of hand-written API repository and route code, replacing them with roughly 150 lines of declarative configuration. And it wasn't a one-for-one trade. In return, I gained features I simply didn't have before: complete Swagger documentation, the conditions DSL (previously I only had sorting and fixed filters), pagination with total counts, structured 400 errors, and consistent camelCase conversion.
12
-
13
- > The moment I thought, "Okay, this is well designed," was when I used computedFields: the case of "attributes stored inside a jsonb column that still need to be sortable and filterable" is exactly the kind of problem that usually forces you to step outside the framework, yet it fit in cleanly. It worked end-to-end with the role-based CASE expression, remained sortable, could be projected with selectComputed, and required zero SQL in the routes.
14
-
15
- > The AGENTS files deserve a mention: I configured everything without ever opening the source code—CLI schema, defineTable, buildRelation, computed, extraFilters, and the frontend request/response shapes. As "documentation for agents," they genuinely work, and that's more than I can say for most libraries.
16
-
17
7
  ## Features
18
8
 
9
+ - **Agent-ready** — `AGENTS.md` and the ADRs ship *inside* the npm package, so a coding agent can configure the library without reading the source or guessing intent
19
10
  - **Zero boilerplate** — define your tables, get 7 endpoints each
20
11
  - **No ORM** — raw SQL via `pg` + parameterized queries
21
12
  - **TypeBox validation** — request/response schemas auto-generated from your DB
@@ -33,54 +24,105 @@ Point it at your database, and get a full REST API with search, advanced conditi
33
24
  > **[BREAKING_CHANGES.md](./BREAKING_CHANGES.md)** for migration guides. If you use `computedFields`
34
25
  > with bound values, read the 0.1.6 entry — earlier versions could return wrong rows.
35
26
 
27
+
28
+ ## Working with coding agents
29
+
30
+ The library is documented for two readers. Humans get this README; agents get task-oriented
31
+ files that ship in the published package, so they are already on disk after `npm install`:
32
+
33
+ | File | Contents |
34
+ | -------------------------------------- | ----------------------------------------------------------------- |
35
+ | `AGENTS.md` | Entry point: what the library does, which file to read for what |
36
+ | `AGENTS_BACKEND.md` | `defineTable` reference, hooks, tenancy, computed fields, CLI |
37
+ | `AGENTS_FRONTEND.md` | Request/response shapes for every endpoint |
38
+ | `docs/adr/` | Why the non-obvious behaviours are what they are |
39
+
40
+ Point your agent at them once:
41
+
42
+ > Read `node_modules/fastify-auto-sqlapi/AGENTS.md` before touching anything under `src/db/`.
43
+
44
+ Three design choices make the generated code predictable enough for an agent to write it
45
+ unsupervised:
46
+
47
+ - **Configuration is declarative and local.** A table is one `defineTable()` call — nothing to
48
+ wire across files, so a diff is reviewable at a glance.
49
+ - **One naming convention.** camelCase in requests, responses, hooks and validators; the
50
+ mapping to DB columns is automatic. There is no second convention to remember, and no
51
+ layer where the agent has to guess which one applies.
52
+ - **Errors are machine-readable.** A 400 carries `fields: [{ path, code, message }]`, so a
53
+ failing request tells the agent exactly what to fix instead of requiring a guess.
54
+
55
+ The ADRs matter more than they look: they answer the "why is it like this?" questions
56
+ (open-by-default, non-transactional bulk, always-updatable fields) that an agent would
57
+ otherwise resolve by inventing a workaround — or by proposing to change intentional behaviour.
58
+
36
59
  ## Quick Start
37
60
 
38
61
  ### 1. Install
39
62
 
40
63
  ```bash
41
64
  npm install fastify-auto-sqlapi fastify @fastify/postgres
65
+ # MySQL/MariaDB: npm install fastify-auto-sqlapi fastify mysql2
42
66
  ```
43
67
 
44
68
  ### 2. Create the config file
45
69
 
46
- Create `sqlapi.config.ts` in your project root. This is used only by the CLI generators, not at runtime.
70
+ Create `sqlapi.config.ts` (or `.mjs`/`.js`) in your project root. This is used only by the
71
+ CLI generators, not at runtime.
47
72
 
48
73
  ```typescript
49
74
  export default {
50
- outputDir: './src/schemas', // where to generate Schema files
51
- schema: 'public', // PostgreSQL schema to introspect
75
+ outputDir: './src/db', // base dir for generated files (default: './src/db')
76
+ schema: 'public', // PostgreSQL schema to introspect (default: 'public')
77
+ // dialect: 'mysql', // 'postgres' (default) | 'mysql' | 'mariadb'
78
+ // envFile: '.env.local', // env file to load (default: '.env')
52
79
  };
53
80
  ```
54
81
 
55
- The DB connection is read from environment variables:
82
+ The DB connection is read from environment variables (a `.env` file in the project root is
83
+ loaded automatically, without overriding variables already set):
56
84
 
57
85
  ```bash
58
86
  # Either a full connection string:
59
- DATABASE_URL=postgres://user:pass@localhost:5432/mydb
87
+ DATABASE_URL=postgres://user:pass@localhost:5432/mydb # or mysql://...
60
88
 
61
- # Or individual vars:
89
+ # Or individual vars (PostgreSQL):
62
90
  POSTGRES_HOST=127.0.0.1
63
91
  POSTGRES_PORT=5432
64
92
  POSTGRES_USER=myuser
65
93
  POSTGRES_PASSWORD=mypassword
66
94
  POSTGRES_DB=mydb
95
+
96
+ # Or individual vars (MySQL/MariaDB):
97
+ MYSQL_HOST=127.0.0.1
98
+ MYSQL_PORT=3306
99
+ MYSQL_USER=myuser
100
+ MYSQL_PASSWORD=mypassword
101
+ MYSQL_DB=mydb
67
102
  ```
68
103
 
69
104
  ### 3. Generate schemas from your database
70
105
 
71
106
  ```bash
72
- npx sqlapi-generate-schema
107
+ npx sqlapi-generate-schema # all tables
108
+ npx sqlapi-generate-schema --tables customer,order # only some
109
+ npx sqlapi-generate-schema --dialect mysql # override the config dialect
73
110
  ```
74
111
 
75
- This introspects your tables and generates one TypeBox schema file per table (e.g. `SchemaCustomer.ts`, `SchemaOrder.ts`). These files are auto-generated — don't edit them.
112
+ This introspects your tables and generates one TypeBox schema file per table in
113
+ `<outputDir>/schemas/` (e.g. `SchemaCustomer.ts`, `SchemaOrder.ts`). These files are
114
+ auto-generated — don't edit them; re-run the command after a DB change.
76
115
 
77
116
  ### 4. Generate the tables template
78
117
 
79
118
  ```bash
80
- npx sqlapi-generate-tables
119
+ npx sqlapi-generate-tables --all # or: npx sqlapi-generate-tables customer order
81
120
  ```
82
121
 
83
- This creates a `tables.ts` file with a `defineTable()` call for each table. It auto-detects primary keys, foreign key relations, and lays out all available options as commented code. **This file is yours to customize.**
122
+ This creates in `<outputDir>/tables/` one `Table*.ts` file per table — a `defineTable()`
123
+ call with auto-detected primary keys, foreign key relations, and all available options as
124
+ commented code — plus a `dbTables.ts` index. **These files are yours to customize**: the
125
+ generator never overwrites an existing file, so re-running it only adds files for new tables.
84
126
 
85
127
  ### 5. Register the plugin
86
128
 
@@ -88,7 +130,7 @@ This creates a `tables.ts` file with a `defineTable()` call for each table. It a
88
130
  import Fastify from 'fastify';
89
131
  import fastifyPostgres from '@fastify/postgres';
90
132
  import { fastifyAutoSqlApi } from 'fastify-auto-sqlapi';
91
- import { dbTables } from './src/schemas/tables.js';
133
+ import { dbTables } from './src/db/tables/dbTables.js';
92
134
 
93
135
  const app = Fastify();
94
136
 
@@ -156,7 +198,8 @@ const TableCustomer = defineTable({
156
198
 
157
199
  // Ordering & filtering
158
200
  defaultOrder: 'name', // default ORDER BY (camelCase fields map to DB columns)
159
- excludeFromCreation: ['id'], // omit from INSERT (e.g. auto-increment)
201
+ excludeFromCreation: ['id'], // client-sent values ignored on INSERT (auto-increment,
202
+ // DB defaults); beforeInsert can still set them
160
203
  readExclude: ['passwordHash'], // hide from all reads (writes unaffected)
161
204
  distinctResults: true, // SELECT DISTINCT
162
205
 
@@ -269,7 +312,20 @@ await app.register(fastifyAutoSqlApi, {
269
312
  | `maxBulkItems` | `number` | No | Max number of items accepted by the bulk endpoints (default: `1000`) |
270
313
  | `debug` | `boolean` | No | Log all SQL queries to console |
271
314
 
272
- For MySQL/MariaDB, register `mysql2/promise` pool instead of `@fastify/postgres` and pass `dialect: 'mysql'` or `'mariadb'`.
315
+ The plugin picks up the connection from the Fastify instance: `fastify.pg` for PostgreSQL
316
+ (what `@fastify/postgres` decorates) or `fastify.mysql` for MySQL/MariaDB. For MySQL,
317
+ decorate a `mysql2/promise` pool (e.g. via `@fastify/mysql` with `promise: true`, or manually)
318
+ and pass `dialect: 'mysql'` or `'mariadb'`:
319
+
320
+ ```typescript
321
+ import mysql from 'mysql2/promise';
322
+
323
+ const pool = mysql.createPool({ host: '127.0.0.1', user: 'myuser', password: '...', database: 'mydb' });
324
+ app.decorate('mysql', pool);
325
+ app.addHook('onClose', async () => { await pool.end(); });
326
+
327
+ await app.register(fastifyAutoSqlApi, { DbTables: dbTables, dialect: 'mysql' });
328
+ ```
273
329
 
274
330
  ## Security
275
331
 
@@ -333,6 +389,42 @@ only columns present in the generated Schema (as narrowed by `schemaOverrides` /
333
389
  column — the Schema is the write whitelist. Trim the generated Schema (or use `excludeFromCreation`)
334
390
  to keep sensitive columns out of it.
335
391
 
392
+ ### Field-level update rules are product logic — use the hooks
393
+
394
+ `excludeFromCreation` is an **ergonomics tool for creation**, not a security mechanism: it
395
+ exists so auto-generated values (serial PKs, DB-default timestamps) don't have to be sent —
396
+ or validated — on insert. It deliberately does **not** apply to updates: every field in the
397
+ Schema is updatable by default, because whether a field may change is a product decision the
398
+ plugin cannot make for you (a super admin may legitimately fix an `updatedAt`; in one product
399
+ users may move themselves across tenants, in another nobody may).
400
+
401
+ So watch out for **sensitive flags and ownership fields** — `isAdmin`, `role`, tenant/owner
402
+ columns, state fields: if they are in the Schema, the auto-generated update route will accept
403
+ them. Encode your rules with the tools made for it:
404
+
405
+ ```typescript
406
+ // Silent strip: non-admins simply cannot touch the flag
407
+ beforeUpdate: async (db, req, fields) => {
408
+ if (!req.user.isAdmin) delete fields.isAdmin;
409
+ },
410
+
411
+ // Loud 400: touching the flag without permission is an error
412
+ validate: async (db, req, main) => {
413
+ if (main.isAdmin !== undefined && !req.user.isAdmin)
414
+ return [['isAdmin', 'forbidden']];
415
+ return [];
416
+ },
417
+ ```
418
+
419
+ For privileged state transitions (promoting an admin, moving a record across tenants) consider
420
+ a dedicated endpoint with its own auth and audit instead of the auto-generated CRUD route —
421
+ `operations` lets you keep the sensitive operation off the auto routes entirely.
422
+
423
+ Note on tenants: when `tenantScope` is active, tenant-scoped callers can never move a record to
424
+ another tenant (the tenant column is enforced server-side); admin callers (`getTenantId` →
425
+ `null`) are unrestricted. That is the isolation contract of the opt-in tenancy feature, not a
426
+ field-level rule.
427
+
336
428
  ### Read visibility
337
429
 
338
430
  `readExclude` hides columns from every read while leaving writes untouched — the case for a
@@ -444,6 +536,13 @@ Search with filters, advanced conditions, pagination, ordering, joins, and aggre
444
536
 
445
537
  Returns `{ main: { ... } }` or 404.
446
538
 
539
+ > **Composite primary keys:** `GET /rest/:id`, `DELETE /rest/:id` and `POST /bulk/:table/delete`
540
+ > address a record by a single PK value, so they are **not registered** for tables whose
541
+ > primary key spans multiple columns (a match on the first column alone could hit many rows —
542
+ > for the deletes, destructively). Explicitly listing one of these operations in `operations`
543
+ > for such a table throws at startup. Use `search` with all PK fields in the filters, `update`
544
+ > (which matches every PK column), or a custom route.
545
+
447
546
  ### POST /rest/{table}
448
547
 
449
548
  **Body:**
@@ -471,7 +570,7 @@ Returns `{ main: { ... } }` or 404.
471
570
  > ),
472
571
  > // → PUT /rest/product { "main": {...}, "secondaries": { "translations": [{ "lang": "en", "name": "Bike" }] } }
473
572
  > ```
474
- > Expose a composite-PK table as a standalone CRUD table only when it stands on its own (M:N link tables, natural keys) — composite PKs are fully supported there too.
573
+ > Expose a composite-PK table as a standalone CRUD table only when it stands on its own (M:N link tables, natural keys) — search, insert, update and bulk upsert fully support composite PKs; the by-single-id routes (get, delete, bulkDelete) are skipped for them (see the note under GET).
475
574
 
476
575
  **Response (201):** `{ main: { ... }, secondaries: { ... } }`
477
576
 
@@ -491,7 +590,8 @@ Returns `{ main: { ... } }` or 404.
491
590
 
492
591
  ### DELETE /rest/{table}/:id
493
592
 
494
- Returns the deleted record or 404.
593
+ **Response (200):** `{ main: { <pk>: ... } }` (PK-only, like all write responses), or `404` if
594
+ the record does not exist.
495
595
 
496
596
  ### PUT /bulk/{table}
497
597
 
@@ -621,6 +721,68 @@ await app.register(async (instance) => {
621
721
  }, { prefix: '/admin' });
622
722
  ```
623
723
 
724
+ ## LLM / agent clients
725
+
726
+ The plugin is a natural enforcement layer for an agent that operates on your data through
727
+ chat: whatever requests the LLM invents, they pass through the same tenant scoping,
728
+ `readExclude`, `operations` whitelist, validation and request caps as any HTTP client —
729
+ direct-DB firepower, backoffice constraints. Three pieces make this practical:
730
+
731
+ **1. The grammar** — [`AGENTS_FRONTEND.md`](./AGENTS_FRONTEND.md) (ships in the npm package)
732
+ is a compact, LLM-oriented reference of how to call the API: search with all four join
733
+ families, ordering, pagination, writes, error shapes. Put it in the system prompt.
734
+
735
+ **2. The vocabulary** — enable the manifest endpoint to describe *this deployment's* tables:
736
+
737
+ ```typescript
738
+ await app.register(fastifyAutoSqlApi, {
739
+ DbTables: dbTables,
740
+ agentManifest: true, // GET {prefix}/agent/manifest (JSON) + /agent/manifest.md
741
+ });
742
+ ```
743
+
744
+ `GET /agent/manifest.md` returns a compact markdown block per table — fields with
745
+ type/required/nullable, enabled operations, join aliases (with direction), computed fields,
746
+ extra filters — always in sync with the running config, behind the same `onRequests` auth.
747
+ Fetch it at session start: grammar + vocabulary is everything the model needs. The same data
748
+ is available programmatically via `buildAgentManifest(dbTables)` /
749
+ `renderAgentManifestMd(manifest)`.
750
+
751
+ **3. The guardrails** — writes are often too dangerous to hand to a model. Register a
752
+ read-only surface with granular composition and give the agent only that:
753
+
754
+ ```typescript
755
+ import { searchRoutes, getRoutes, agentManifestRoutes } from 'fastify-auto-sqlapi';
756
+
757
+ await app.register(async (instance) => {
758
+ const opts = { DbTables: dbTables, onRequests: [agentAuth] };
759
+ await instance.register(searchRoutes, opts);
760
+ await instance.register(getRoutes, opts);
761
+ await instance.register(agentManifestRoutes, opts);
762
+ }, { prefix: '/agent' }); // reads only: no insert/update/delete routes exist here
763
+ ```
764
+
765
+ **Validation strategy** — two options, per table or per deployment:
766
+
767
+ - *Loose tool + retry loop* (recommended default): expose a generic tool
768
+ (`table` as enum from the manifest, `body` as free object) and let the plugin validate.
769
+ A failed request returns a structured 400 with `fields: [{path, code, message}]` — feed
770
+ it back to the model and it self-corrects in one round trip. Cheap prompts, no schema
771
+ duplication.
772
+ - *Strict tools*: `agentToolSchemas(dbTables, 'customer')` returns the exact JSON Schemas
773
+ the routes validate with (body + querystring for search, bodies for writes, only for the
774
+ operations enabled on that table) — plug them into provider-side tool definitions when
775
+ you want invalid calls rejected before they leave the model. Costs prompt size; best for
776
+ a few hot tables.
777
+
778
+ ## Design Decisions
779
+
780
+ Deliberate, non-obvious choices — open-by-default, non-transactional bulk operations,
781
+ always-updatable fields, raw DB errors, insert-pipeline ordering — are recorded as
782
+ [Architecture Decision Records in `docs/adr/`](./docs/adr/README.md), each with its
783
+ rationale and the alternatives that were rejected. Read them before filing an issue that
784
+ proposes changing one of these behaviors.
785
+
624
786
  ## Conventions
625
787
 
626
788
  - **camelCase everywhere in the API** — requests, responses, `validate`, and all hooks (`beforeInsert`/`afterInsert`, `beforeUpdate`/`afterUpdate`, `beforeDelete`/`afterDelete`, `beforeBulkDelete`/`afterBulkDelete`) use schema field names
@@ -629,6 +791,8 @@ await app.register(async (instance) => {
629
791
  - **`joinMustExist` / `joinMultiple` / `joinGroup`** are 1:N (child→main) and use side queries / EXISTS / correlated subqueries — no row duplication
630
792
  - **`joinLeft`** is N:1 (parent→main) and adds a real `LEFT JOIN` on demand (only when filtering/ordering by parent)
631
793
  - **All response fields are Optional** — response schemas use `Type.Partial` since `RETURNING *` may return any subset
794
+ - **Nullable columns use `Nullable(T)`** — the generator emits the JSON-Schema type-array form (`type: ['integer', 'null']`) via the exported `Nullable()` helper. This is deliberate: a bare `Type.Optional(T)` serializes NULL as `0`/`""`, and a `Type.Union([T, Type.Null()])` gets corrupted by Ajv's default type coercion (`null` ↔ `0`/`""` through the branches). In `filters`, an explicit `null` filters by `IS NULL`
795
+ - **The `pg` driver returns `numeric`/`int8` as strings** and parses timestamps into local-timezone `Date`s — driver defaults the plugin deliberately does not override (global state). See "PostgreSQL driver type parsers" in AGENTS_BACKEND.md for the recommended one-time setup
632
796
 
633
797
  ## Re-exports
634
798
 
@@ -647,6 +811,7 @@ import {
647
811
  buildRelation,
648
812
  buildUpsertRule,
649
813
  buildUpsertRules,
814
+ Nullable, // type-array nullable schema (see Conventions)
650
815
 
651
816
  // DB layer
652
817
  QueryClient, // raw SQL query helper
package/dist/index.d.ts CHANGED
@@ -4,13 +4,17 @@ export { getDialect, type SqlDialect, type DialectName } from './lib/dialect.js'
4
4
  export { pgQueryable } from './lib/adapters/pg-adapter.js';
5
5
  export { mysqlQueryable } from './lib/adapters/mysql-adapter.js';
6
6
  export { exportTableInfo, defineTable, buildRelation, buildUpsertRule, buildUpsertRules } from './lib/table-helpers.js';
7
+ export { Nullable } from './lib/nullable.js';
8
+ export { buildAgentManifest, renderAgentManifestMd, type AgentManifest, type ManifestTable, } from './lib/agent/manifest.js';
9
+ export { agentToolSchemas, type AgentToolSchemas } from './lib/agent/tool-schemas.js';
10
+ export { default as agentManifestRoutes } from './routes/auto/agent-manifest.routes.js';
7
11
  export { resolveTenant } from './lib/tenant.js';
8
12
  export { SqlApi, createSqlApi } from './lib/sql-api.js';
9
13
  export type { SqlApiOptions, SqlApiSearchParams, SqlApiInsertParams, SqlApiUpdateParams } from './lib/sql-api.js';
10
14
  export { default as fastifyAutoSqlApi } from './routes/auto/plugin.js';
11
15
  export { setupSwagger } from './lib/setup-swagger.js';
12
16
  export { searchEngine } from './lib/engine/search/search.js';
13
- export { SearchTableBodyPost, SearchTableQueryString, SearchTableResponse } from './lib/schema/search.js';
17
+ export { SearchTableBodyPost, SearchTableQuery, SearchTableQueryString, SearchTableResponse } from './lib/schema/search.js';
14
18
  export { default as searchRoutes } from './routes/auto/search.routes.js';
15
19
  export { insertEngine } from './lib/engine/rest/insert.js';
16
20
  export { InsertTableBody, InsertTableResponse } from './lib/schema/insert.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -9,6 +9,11 @@ export { pgQueryable } from './lib/adapters/pg-adapter.js';
9
9
  export { mysqlQueryable } from './lib/adapters/mysql-adapter.js';
10
10
  // Table helpers
11
11
  export { exportTableInfo, defineTable, buildRelation, buildUpsertRule, buildUpsertRules } from './lib/table-helpers.js';
12
+ export { Nullable } from './lib/nullable.js';
13
+ // Agent / LLM client helpers
14
+ export { buildAgentManifest, renderAgentManifestMd, } from './lib/agent/manifest.js';
15
+ export { agentToolSchemas } from './lib/agent/tool-schemas.js';
16
+ export { default as agentManifestRoutes } from './routes/auto/agent-manifest.routes.js';
12
17
  // Tenant
13
18
  export { resolveTenant } from './lib/tenant.js';
14
19
  // SqlApi — programmatic high-level API
@@ -19,7 +24,7 @@ export { default as fastifyAutoSqlApi } from './routes/auto/plugin.js';
19
24
  export { setupSwagger } from './lib/setup-swagger.js';
20
25
  // Search
21
26
  export { searchEngine } from './lib/engine/search/search.js';
22
- export { SearchTableBodyPost, SearchTableQueryString, SearchTableResponse } from './lib/schema/search.js';
27
+ export { SearchTableBodyPost, SearchTableQuery, SearchTableQueryString, SearchTableResponse } from './lib/schema/search.js';
23
28
  export { default as searchRoutes } from './routes/auto/search.routes.js';
24
29
  // Insert
25
30
  export { insertEngine } from './lib/engine/rest/insert.js';
@@ -0,0 +1,53 @@
1
+ import type { DbTables, ITable, TableOperation } from '../../types.js';
2
+ export interface ManifestField {
3
+ type: string;
4
+ /** Present (true) when the field must be sent on insert. */
5
+ required?: boolean;
6
+ /** Present (true) when the column accepts null. */
7
+ nullable?: boolean;
8
+ /** Present (true) when the field is writable but never returned by reads (readExclude). */
9
+ writeOnly?: boolean;
10
+ }
11
+ export interface ManifestJoin {
12
+ alias: string;
13
+ table: string;
14
+ /** '1:N' → usable in joinMustExist/joinMultiple/joinGroup; 'N:1' → usable in joinLeft. */
15
+ kind: '1:N' | 'N:1';
16
+ }
17
+ export interface ManifestTable {
18
+ /** URL segment for this table's routes. */
19
+ table: string;
20
+ primary: string | string[];
21
+ operations: TableOperation[];
22
+ fields: Record<string, ManifestField>;
23
+ /** Fields stripped from client input on insert (server-generated: PKs, timestamps). */
24
+ serverGenerated?: string[];
25
+ /** Computed (virtual) fields: usable in filters/conditions/orderBy/selectComputed. */
26
+ computed?: Record<string, string>;
27
+ /** Extra filter names accepted in `filters` beyond schema fields. */
28
+ extraFilters?: Record<string, string>;
29
+ readJoins?: ManifestJoin[];
30
+ /** Aliases accepted as `secondaries`/`deletions` keys in writes. */
31
+ writeJoins?: ManifestJoin[];
32
+ /** Present (true) when rows are automatically scoped to the caller's tenant. */
33
+ tenantScoped?: boolean;
34
+ defaultOrder?: string;
35
+ }
36
+ export interface AgentManifest {
37
+ tables: Record<string, ManifestTable>;
38
+ }
39
+ export declare function tableOperations(tableConf: ITable): TableOperation[];
40
+ /**
41
+ * Build the machine-readable description of every table exposed by this deployment:
42
+ * fields (type/required/nullable), enabled operations, join aliases, computed fields,
43
+ * extra filters, tenant scoping. Derived entirely from `DbTables`, so it is always in
44
+ * sync with the running configuration. Pairs with AGENTS_FRONTEND.md (the request grammar)
45
+ * to give an LLM client everything it needs to drive the API.
46
+ */
47
+ export declare function buildAgentManifest(dbTables: DbTables): AgentManifest;
48
+ /**
49
+ * Compact markdown rendering of the manifest, designed for an LLM system prompt.
50
+ * Notation: `!` required on insert, `?` nullable, `(writeOnly)` never returned by reads.
51
+ */
52
+ export declare function renderAgentManifestMd(manifest: AgentManifest): string;
53
+ //# sourceMappingURL=manifest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../../src/lib/agent/manifest.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAOvE,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,mDAAmD;IACnD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,2FAA2F;IAC3F,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,0FAA0F;IAC1F,IAAI,EAAE,KAAK,GAAG,KAAK,CAAC;CACrB;AAED,MAAM,WAAW,aAAa;IAC5B,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC3B,UAAU,EAAE,cAAc,EAAE,CAAC;IAC7B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IACtC,uFAAuF;IACvF,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,sFAAsF;IACtF,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,qEAAqE;IACrE,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,SAAS,CAAC,EAAE,YAAY,EAAE,CAAC;IAC3B,oEAAoE;IACpE,UAAU,CAAC,EAAE,YAAY,EAAE,CAAC;IAC5B,gFAAgF;IAChF,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;CACvC;AAgCD,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,cAAc,EAAE,CAInE;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,QAAQ,GAAG,aAAa,CAuDpE;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,aAAa,GAAG,MAAM,CA2CrE"}