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
package/AGENTS.md CHANGED
@@ -12,7 +12,9 @@ The plugin generates REST endpoints (search, get, insert, update, delete, bulk u
12
12
 
13
13
  - **[AGENTS_BACKEND.md](./AGENTS_BACKEND.md)** — Setup workflow, CLI, schema/table generation, `defineTable()` complete reference, `buildRelation` (alias + unique), `extraFilters` + `extendedCondition`, **SqlApi** (programmatic high-level API for custom routes), ConditionBuilder API, QueryClient API, Swagger, multi-tenant configuration, **validation** (`validate` + `validateBulk`), hooks, key conventions, common backend patterns, FAQ/gotchas, dialect differences.
14
14
 
15
- - **[AGENTS_FRONTEND.md](./AGENTS_FRONTEND.md)** — Generated endpoints overview, search (POST with body filters, conditions/advanced filters using ConditionBuilder methods, query params for ordering/pagination/aggregation), `joinMustExist` (EXISTS-based filtering by related tables), `joinMultiple` (fetch child rows), `joinGroup` (aggregations), `joinLeft` (embed N:1 parent inline), GET, INSERT (with secondaries), UPDATE (with secondaries + deletions), DELETE, bulk upsert, bulk delete, response shapes (PK-only), **validation errors** (structured field-level 400 responses), pagination, computed values.
15
+ - **[AGENTS_FRONTEND.md](./AGENTS_FRONTEND.md)** — Compact client-side request grammar (LLM-oriented, also the system-prompt reference for runtime agent clients): all endpoints, search with the four join families (`joinMustExist`, `joinMultiple`, `joinGroup`, `joinLeft`), conditions methods, ordering (including dotted notations), pagination, writes with secondaries/deletions, structured 400 error shape. Pair with `GET {prefix}/agent/manifest.md` (when `agentManifest` is enabled) for this deployment's tables/fields/aliases.
16
+
17
+ - **[docs/adr/](./docs/adr/README.md)** — Architecture Decision Records. **Read before proposing a "fix" to a deliberate behavior**: open-by-default, non-transactional bulk, always-updatable fields, raw DB errors, insert-pipeline ordering. These are recorded decisions with rationale, not gaps.
16
18
 
17
19
  ## Quick Reference
18
20
 
package/AGENTS_BACKEND.md CHANGED
@@ -227,6 +227,8 @@ export const TableAgentTeamLink = defineTable({
227
227
 
228
228
  When `primary` is an array, RETURNING clauses include all PK columns, and response shapes contain all PK fields.
229
229
 
230
+ **By-single-id operations are not available for composite PKs**: `get`, `delete` and `bulkDelete` address a record by one PK value, so their routes are skipped for composite-PK tables (matching on the first column alone could hit many rows — destructively for the deletes). Explicitly listing one of them in `operations` throws at startup, and the programmatic `sqlApi.get/delete/bulkDelete` reject with 400. Use `search` (all PK fields as filters), `update` (matches every PK column), or a custom route.
231
+
230
232
  ### All keys
231
233
 
232
234
  ```typescript
@@ -380,7 +382,7 @@ upsertMap: buildUpsertRules(
380
382
  // → PUT /rest/product { "main": {...}, "secondaries": { "translations": [{ "lang": "en", "name": "Bike" }] } }
381
383
  ```
382
384
 
383
- 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.
385
+ 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.
384
386
 
385
387
  **Aliasing the same table twice**: declare two `buildRelation` entries with different `alias`. Example: a `session` table referencing `user` for both `createdBy` and `updatedBy`:
386
388
 
@@ -527,7 +529,7 @@ For `joinLeft` specifically, the computed expr is automatically alias-prefixed b
527
529
 
528
530
  ## ConditionBuilder API
529
531
 
530
- Used in `extendedCondition` callbacks, hooks, and exposed via the `conditions` field in the search API (see [AGENTS_FRONTEND.md](./AGENTS_FRONTEND.md#conditions-advanced-filters)).
532
+ Used in `extendedCondition` callbacks, hooks, and exposed via the `conditions` field in the search API (methods and params arity in [AGENTS_FRONTEND.md](./AGENTS_FRONTEND.md)).
531
533
 
532
534
  ```typescript
533
535
  const cb = new ConditionBuilder('AND'); // or 'OR'
@@ -773,7 +775,8 @@ Tables without `tenantScope` are unaffected — no filtering regardless of `getT
773
775
 
774
776
  - **camelCase in API, any case in DB**: all request/response fields are camelCase. The plugin converts via `col()` and `colMap`. For snake_case DB columns (default), conversion is automatic. For camelCase DB columns (e.g. betterauth), the CLI generates a `colMap` that preserves the original column names — no conversion needed. Manual schemas without `colMap` fall back to `toUnderscore()`.
775
777
  - **All fields Optional in response**: `RETURNING *` may return any subset. Response schemas use `Type.Partial`.
776
- - **`excludeFromCreation`**: **IMPORTANT** — auto-increment PKs (e.g. `id` serial/auto_increment) MUST be listed here, otherwise INSERT will try to send them and fail. The CLI auto-detects this and adds it by default. Also useful for `createdAt`/`updatedAt` columns managed by DB defaults or hooks.
778
+ - **Nullable columns use `Nullable(T)` (type-array form)**: the generator emits `Type.Optional(Nullable(T))` for nullable columns — `Nullable` (exported by the package) produces `{ type: ['integer', 'null'] }`. Do NOT replace it with `Type.Union([T, Type.Null()])`: under Fastify's default Ajv `coerceTypes` a union corrupts values through its branches (`null` → `0` with the Null branch last, `0`/`""` → `null` with it first), while the type array validates and serializes NULL correctly. A bare `Type.Optional(T)` is also wrong: NULL serializes as `0`/`""`. In `filters`, an explicit `null` on a schema field builds `WHERE col IS NULL`.
779
+ - **`excludeFromCreation`**: **IMPORTANT** — auto-increment PKs (e.g. `id` serial/auto_increment) MUST be listed here, otherwise INSERT will try to send them and fail. The CLI auto-detects this and adds it by default. Also useful for `createdAt`/`updatedAt` columns managed by DB defaults or hooks. It strips **client-supplied** values only: the payload is sanitized before `beforeInsert` runs, so a value the hook assigns to an excluded field (e.g. a server-generated TEXT id) DOES reach the INSERT. Same for the engine's FK auto-fill on secondaries — an excluded FK column does not suppress it. It is an **ergonomics tool for creation, NOT a field-level security mechanism**: it does not apply to updates — every Schema field is updatable by default, by design. Field-level update rules (`isAdmin`, roles, owner/tenant columns, state fields) are product logic: enforce them in `beforeUpdate` (silent strip) or `validate` (loud 400), or move the privileged transition to a dedicated endpoint and keep it off the auto routes via `operations`.
777
780
  - **`readExclude`**: fields hidden from every read — not projected by search/get, omitted from read response schemas and from this table's default (`*`) join selection when it is a join target. Referencing one from `filters`, `conditions`, `orderBy`, `joinGroup` aggregations or an explicit join `selection` returns 400: hiding a field from output while allowing it to be filtered would leak its value by bisection. **Writes are unaffected** — insert/update/bulk still accept the field, so a column can be writable but never readable (password hash, access token). Primary keys cannot be excluded. Complementary to trimming the Schema, which removes the column from reads *and* writes.
778
781
  - **`upsertMap`**: when present for a schema, INSERT becomes upsert. PostgreSQL: `ON CONFLICT (...) DO UPDATE`. MySQL/MariaDB: `ON DUPLICATE KEY UPDATE`. Applies to both main and secondary tables.
779
782
  - **`schemaOverrides`**: override auto-generated schema fields with stricter TypeBox types (e.g. `{ email: Type.String({ format: 'email' }) }`). Overrides are merged into the body schema for insert, update, and bulk-upsert. The original Schema file is never modified. In updates, overridden fields are still wrapped in Optional (validates only when present). Overrides appear in Swagger.
@@ -894,6 +897,78 @@ const TableCustomer = defineTable({
894
897
  });
895
898
  ```
896
899
 
900
+ ### PostgreSQL driver type parsers (numeric/int8 as strings, timestamp shifts)
901
+
902
+ The `pg` driver — not the plugin — returns `numeric`/`int8` as **strings** (precision
903
+ safety), parses `date` into a JS `Date` at local midnight, and interprets
904
+ `timestamp without time zone` in the server's local timezone. The plugin deliberately does
905
+ not touch `pg.types` (global process state — see the design principles). If your API should
906
+ return numbers and stable date strings, configure the parsers once at startup, before
907
+ creating the pool:
908
+
909
+ ```typescript
910
+ import pg from 'pg';
911
+
912
+ pg.types.setTypeParser(pg.types.builtins.INT8, (v) => parseInt(v, 10));
913
+ pg.types.setTypeParser(pg.types.builtins.NUMERIC, (v) => parseFloat(v));
914
+ // Keep dates/timestamps as verbatim strings (no local-timezone Date conversion).
915
+ // Recommended when the DB stores UTC in `timestamp without time zone` (several ERPs do).
916
+ pg.types.setTypeParser(pg.types.builtins.DATE, (v) => v);
917
+ pg.types.setTypeParser(pg.types.builtins.TIMESTAMP, (v) => v);
918
+ ```
919
+
920
+ Note: HTTP responses already coerce a numeric string like `"42"` to a number when the
921
+ schema says integer — the parsers matter for programmatic `sqlApi.*` results, arithmetic in
922
+ hooks, and timestamp correctness.
923
+
924
+ ### Translated jsonb fields (multi-language columns)
925
+
926
+ Some applications (several ERPs among them) store translatable columns as jsonb:
927
+ `{"en_US": "Bike", "it_IT": "Bici"}`. Expose the translation as a computed field — usable
928
+ in `filters`, `conditions`, `orderBy` and (opt-in) projected via `selectComputed`:
929
+
930
+ ```typescript
931
+ const LANG = process.env.APP_LANG || 'en_US';
932
+
933
+ const TableProductTemplate = defineTable({
934
+ primary: 'id',
935
+ ...exportTableInfo(SchemaProductTemplate),
936
+ computedFields: {
937
+ displayName: ({ qiCol }) => ({
938
+ expr: `${qiCol('name')}->>'${LANG}'`,
939
+ type: Type.String(),
940
+ }),
941
+ },
942
+ });
943
+ // → POST /search/product_template { "filters": { "displayName": "Bike" } }
944
+ // or body { "selectComputed": ["displayName"] } to project it in the response
945
+ ```
946
+
947
+ ### Protect sensitive fields on update (isAdmin, roles, ownership)
948
+
949
+ Every Schema field is updatable by default — the plugin does not impose field-level rules
950
+ (see `excludeFromCreation` in Key Conventions). Encode yours in the hooks:
951
+
952
+ ```typescript
953
+ const TableUser = defineTable({
954
+ primary: 'id',
955
+ ...exportTableInfo(SchemaUser),
956
+ // Silent strip: non-admins simply cannot touch the flag
957
+ beforeUpdate: async (db, req, fields) => {
958
+ if (!req.user.isAdmin) delete fields.isAdmin;
959
+ },
960
+ // Or loud 400 via validate:
961
+ // validate: async (db, req, main) => {
962
+ // if (main.isAdmin !== undefined && !req.user.isAdmin)
963
+ // return [['isAdmin', 'forbidden']];
964
+ // return [];
965
+ // },
966
+ });
967
+ ```
968
+
969
+ For privileged transitions (promote to admin, move across tenants) prefer a dedicated
970
+ endpoint with its own auth/audit, and keep the operation off the auto routes via `operations`.
971
+
897
972
  ### Full-text search filter
898
973
 
899
974
  ```typescript
@@ -978,6 +1053,30 @@ await app.register(async (instance) => {
978
1053
  }, { prefix: '/admin' });
979
1054
  ```
980
1055
 
1056
+ ### LLM / agent surface (read-only + manifest)
1057
+
1058
+ Give a chat agent direct data access with the plugin as enforcement layer (tenant scoping,
1059
+ `readExclude`, caps still apply to whatever the model invents). Read-only surface + the
1060
+ manifest endpoint, agent-specific auth:
1061
+
1062
+ ```typescript
1063
+ import { searchRoutes, getRoutes, agentManifestRoutes } from 'fastify-auto-sqlapi';
1064
+
1065
+ await app.register(async (instance) => {
1066
+ const opts = { DbTables: dbTables, onRequests: [agentAuth] };
1067
+ await instance.register(searchRoutes, opts);
1068
+ await instance.register(getRoutes, opts);
1069
+ await instance.register(agentManifestRoutes, opts); // GET /agent/manifest(.md)
1070
+ }, { prefix: '/agent' });
1071
+ ```
1072
+
1073
+ System prompt = `AGENTS_FRONTEND.md` (request grammar, ships in the package) + the output of
1074
+ `GET /agent/manifest.md` (this deployment's tables/fields/aliases). On the main plugin the
1075
+ manifest is enabled with the `agentManifest: true` option. Validation strategy: either a
1076
+ loose generic tool (`table` enum from the manifest, free body) with the structured 400
1077
+ `fields[]` as the retry signal, or strict provider-side tools via
1078
+ `agentToolSchemas(dbTables, table)` — the exact JSON Schemas the routes validate with.
1079
+
981
1080
  ---
982
1081
 
983
1082
  ## FAQ / Gotchas