fastify-auto-sqlapi 0.1.10 → 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.
- package/AGENTS.md +1 -1
- package/AGENTS_BACKEND.md +76 -2
- package/AGENTS_FRONTEND.md +69 -545
- package/BREAKING_CHANGES.md +63 -0
- package/CHANGELOG.md +69 -0
- package/README.md +98 -11
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/lib/agent/manifest.d.ts +53 -0
- package/dist/lib/agent/manifest.d.ts.map +1 -0
- package/dist/lib/agent/manifest.js +153 -0
- package/dist/lib/agent/tool-schemas.d.ts +36 -0
- package/dist/lib/agent/tool-schemas.d.ts.map +1 -0
- package/dist/lib/agent/tool-schemas.js +47 -0
- package/dist/lib/cli/schema-codegen.d.ts.map +1 -1
- package/dist/lib/cli/schema-codegen.js +9 -1
- package/dist/lib/engine/bulk/bulk-delete.d.ts.map +1 -1
- package/dist/lib/engine/bulk/bulk-delete.js +5 -1
- package/dist/lib/engine/rest/delete.d.ts.map +1 -1
- package/dist/lib/engine/rest/delete.js +4 -1
- package/dist/lib/engine/rest/get.d.ts.map +1 -1
- package/dist/lib/engine/rest/get.js +4 -1
- package/dist/lib/nullable.d.ts +17 -0
- package/dist/lib/nullable.d.ts.map +1 -0
- package/dist/lib/nullable.js +24 -0
- package/dist/lib/schema/search.d.ts +11 -9
- package/dist/lib/schema/search.d.ts.map +1 -1
- package/dist/lib/schema/search.js +27 -10
- package/dist/lib/table-helpers.d.ts.map +1 -1
- package/dist/lib/table-helpers.js +12 -2
- package/dist/routes/auto/agent-manifest.routes.d.ts +12 -0
- package/dist/routes/auto/agent-manifest.routes.d.ts.map +1 -0
- package/dist/routes/auto/agent-manifest.routes.js +30 -0
- package/dist/routes/auto/bulk-delete.routes.d.ts.map +1 -1
- package/dist/routes/auto/bulk-delete.routes.js +1 -0
- package/dist/routes/auto/delete.routes.d.ts.map +1 -1
- package/dist/routes/auto/delete.routes.js +1 -0
- package/dist/routes/auto/get.routes.d.ts.map +1 -1
- package/dist/routes/auto/get.routes.js +1 -0
- package/dist/routes/auto/plugin.d.ts.map +1 -1
- package/dist/routes/auto/plugin.js +4 -0
- package/dist/routes/auto/route-helpers.d.ts +7 -0
- package/dist/routes/auto/route-helpers.d.ts.map +1 -1
- package/dist/routes/auto/route-helpers.js +11 -0
- package/dist/routes/auto/search.routes.js +3 -3
- package/dist/types/plugin.d.ts +7 -0
- package/dist/types/plugin.d.ts.map +1 -1
- package/dist/types/table.d.ts +6 -0
- package/dist/types/table.d.ts.map +1 -1
- package/dist/types/table.js +8 -0
- package/docs/adr/0007-explicit-table-generation.md +39 -0
- package/docs/adr/README.md +1 -0
- package/package.json +1 -1
package/BREAKING_CHANGES.md
CHANGED
|
@@ -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,75 @@ 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
|
+
|
|
10
79
|
## [0.1.10]
|
|
11
80
|
|
|
12
81
|
### Added
|
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,6 +24,38 @@ 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
|
|
@@ -513,6 +536,13 @@ Search with filters, advanced conditions, pagination, ordering, joins, and aggre
|
|
|
513
536
|
|
|
514
537
|
Returns `{ main: { ... } }` or 404.
|
|
515
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
|
+
|
|
516
546
|
### POST /rest/{table}
|
|
517
547
|
|
|
518
548
|
**Body:**
|
|
@@ -540,7 +570,7 @@ Returns `{ main: { ... } }` or 404.
|
|
|
540
570
|
> ),
|
|
541
571
|
> // → PUT /rest/product { "main": {...}, "secondaries": { "translations": [{ "lang": "en", "name": "Bike" }] } }
|
|
542
572
|
> ```
|
|
543
|
-
> 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
|
|
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).
|
|
544
574
|
|
|
545
575
|
**Response (201):** `{ main: { ... }, secondaries: { ... } }`
|
|
546
576
|
|
|
@@ -691,6 +721,60 @@ await app.register(async (instance) => {
|
|
|
691
721
|
}, { prefix: '/admin' });
|
|
692
722
|
```
|
|
693
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
|
+
|
|
694
778
|
## Design Decisions
|
|
695
779
|
|
|
696
780
|
Deliberate, non-obvious choices — open-by-default, non-transactional bulk operations,
|
|
@@ -707,6 +791,8 @@ proposes changing one of these behaviors.
|
|
|
707
791
|
- **`joinMustExist` / `joinMultiple` / `joinGroup`** are 1:N (child→main) and use side queries / EXISTS / correlated subqueries — no row duplication
|
|
708
792
|
- **`joinLeft`** is N:1 (parent→main) and adds a real `LEFT JOIN` on demand (only when filtering/ordering by parent)
|
|
709
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
|
|
710
796
|
|
|
711
797
|
## Re-exports
|
|
712
798
|
|
|
@@ -725,6 +811,7 @@ import {
|
|
|
725
811
|
buildRelation,
|
|
726
812
|
buildUpsertRule,
|
|
727
813
|
buildUpsertRules,
|
|
814
|
+
Nullable, // type-array nullable schema (see Conventions)
|
|
728
815
|
|
|
729
816
|
// DB layer
|
|
730
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';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,eAAe,EACf,eAAe,GAChB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAG1E,OAAO,EAAE,UAAU,EAAE,KAAK,UAAU,EAAE,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAGjF,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAGjE,OAAO,EAAE,eAAe,EAAE,WAAW,EAAE,aAAa,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;
|
|
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"}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { isCompositePrimary } from '../../types.js';
|
|
2
|
+
const ALL_OPERATIONS = [
|
|
3
|
+
'search', 'get', 'insert', 'update', 'delete', 'bulkUpsert', 'bulkDelete',
|
|
4
|
+
];
|
|
5
|
+
const SINGLE_PK_OPERATIONS = new Set(['get', 'delete', 'bulkDelete']);
|
|
6
|
+
function shortType(schema) {
|
|
7
|
+
if (!schema)
|
|
8
|
+
return 'any';
|
|
9
|
+
const t = schema.type;
|
|
10
|
+
if (!t)
|
|
11
|
+
return 'any';
|
|
12
|
+
const types = (Array.isArray(t) ? t : [t]).filter((x) => x !== 'null');
|
|
13
|
+
return types[0] ?? 'any';
|
|
14
|
+
}
|
|
15
|
+
function isNullable(schema) {
|
|
16
|
+
const t = schema?.type;
|
|
17
|
+
return Array.isArray(t) && t.includes('null');
|
|
18
|
+
}
|
|
19
|
+
function computedTypes(tableConf) {
|
|
20
|
+
if (!tableConf.computedFields)
|
|
21
|
+
return undefined;
|
|
22
|
+
const out = {};
|
|
23
|
+
for (const [name, fn] of Object.entries(tableConf.computedFields)) {
|
|
24
|
+
try {
|
|
25
|
+
const stub = fn({
|
|
26
|
+
db: { qi: (s) => s, dialectName: 'postgres' },
|
|
27
|
+
qiCol: () => '""',
|
|
28
|
+
});
|
|
29
|
+
out[name] = shortType(stub.type);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
out[name] = 'any';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return Object.keys(out).length ? out : undefined;
|
|
36
|
+
}
|
|
37
|
+
export function tableOperations(tableConf) {
|
|
38
|
+
const requested = tableConf.operations ?? ALL_OPERATIONS;
|
|
39
|
+
if (!isCompositePrimary(tableConf.primary))
|
|
40
|
+
return [...requested];
|
|
41
|
+
return requested.filter((op) => !SINGLE_PK_OPERATIONS.has(op));
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Build the machine-readable description of every table exposed by this deployment:
|
|
45
|
+
* fields (type/required/nullable), enabled operations, join aliases, computed fields,
|
|
46
|
+
* extra filters, tenant scoping. Derived entirely from `DbTables`, so it is always in
|
|
47
|
+
* sync with the running configuration. Pairs with AGENTS_FRONTEND.md (the request grammar)
|
|
48
|
+
* to give an LLM client everything it needs to drive the API.
|
|
49
|
+
*/
|
|
50
|
+
export function buildAgentManifest(dbTables) {
|
|
51
|
+
const tables = {};
|
|
52
|
+
for (const [key, tableConf] of Object.entries(dbTables)) {
|
|
53
|
+
const schema = tableConf.Schema;
|
|
54
|
+
const required = new Set((schema.validation.required) ?? []);
|
|
55
|
+
const readExcluded = new Set(tableConf.readExclude ?? []);
|
|
56
|
+
const fields = {};
|
|
57
|
+
for (const [name, fieldSchema] of Object.entries(schema.fields)) {
|
|
58
|
+
const f = { type: shortType(fieldSchema) };
|
|
59
|
+
if (required.has(name))
|
|
60
|
+
f.required = true;
|
|
61
|
+
if (isNullable(fieldSchema))
|
|
62
|
+
f.nullable = true;
|
|
63
|
+
if (readExcluded.has(name))
|
|
64
|
+
f.writeOnly = true;
|
|
65
|
+
fields[name] = f;
|
|
66
|
+
}
|
|
67
|
+
const readJoins = (tableConf.allowedReadJoins ?? []).map((j) => ({
|
|
68
|
+
alias: j.alias,
|
|
69
|
+
table: j.joinSchema.tableName,
|
|
70
|
+
kind: j.unique ? 'N:1' : '1:N',
|
|
71
|
+
}));
|
|
72
|
+
const writeJoins = (tableConf.allowedWriteJoins ?? []).map((j) => ({
|
|
73
|
+
alias: j.alias,
|
|
74
|
+
table: j.joinSchema.tableName,
|
|
75
|
+
kind: j.unique ? 'N:1' : '1:N',
|
|
76
|
+
}));
|
|
77
|
+
const extraFilters = Object.keys(tableConf.extraFilters ?? {}).length
|
|
78
|
+
? Object.fromEntries(Object.entries(tableConf.extraFilters).map(([n, s]) => [n, shortType(s)]))
|
|
79
|
+
: undefined;
|
|
80
|
+
const entry = {
|
|
81
|
+
table: schema.tableName,
|
|
82
|
+
primary: tableConf.primary,
|
|
83
|
+
operations: tableOperations(tableConf),
|
|
84
|
+
fields,
|
|
85
|
+
};
|
|
86
|
+
if (tableConf.excludeFromCreation?.length)
|
|
87
|
+
entry.serverGenerated = [...tableConf.excludeFromCreation];
|
|
88
|
+
const computed = computedTypes(tableConf);
|
|
89
|
+
if (computed)
|
|
90
|
+
entry.computed = computed;
|
|
91
|
+
if (extraFilters)
|
|
92
|
+
entry.extraFilters = extraFilters;
|
|
93
|
+
if (readJoins.length)
|
|
94
|
+
entry.readJoins = readJoins;
|
|
95
|
+
if (writeJoins.length)
|
|
96
|
+
entry.writeJoins = writeJoins;
|
|
97
|
+
if (tableConf.tenantScope)
|
|
98
|
+
entry.tenantScoped = true;
|
|
99
|
+
if (tableConf.defaultOrder)
|
|
100
|
+
entry.defaultOrder = tableConf.defaultOrder;
|
|
101
|
+
tables[key] = entry;
|
|
102
|
+
}
|
|
103
|
+
return { tables };
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Compact markdown rendering of the manifest, designed for an LLM system prompt.
|
|
107
|
+
* Notation: `!` required on insert, `?` nullable, `(writeOnly)` never returned by reads.
|
|
108
|
+
*/
|
|
109
|
+
export function renderAgentManifestMd(manifest) {
|
|
110
|
+
const lines = [
|
|
111
|
+
'# Tables',
|
|
112
|
+
'',
|
|
113
|
+
'Notation: `!`=required on insert, `?`=nullable, `(writeOnly)`=writable, never in reads.',
|
|
114
|
+
'serverGenerated fields: omit on insert (values from client are ignored).',
|
|
115
|
+
'readJoins aliases: `1:N` → joinMustExist/joinMultiple/joinGroup; `N:1` → joinLeft.',
|
|
116
|
+
'writeJoins aliases: keys for `secondaries`/`deletions` in writes.',
|
|
117
|
+
'',
|
|
118
|
+
];
|
|
119
|
+
for (const t of Object.values(manifest.tables)) {
|
|
120
|
+
const pk = Array.isArray(t.primary) ? t.primary.join('+') : t.primary;
|
|
121
|
+
const flags = t.tenantScoped ? ' [tenant-scoped]' : '';
|
|
122
|
+
lines.push(`## ${t.table} PK:${pk} ops:${t.operations.join(',')}${flags}`);
|
|
123
|
+
const fieldParts = Object.entries(t.fields).map(([name, f]) => {
|
|
124
|
+
let s = `${name}:${f.type}`;
|
|
125
|
+
if (f.required)
|
|
126
|
+
s += '!';
|
|
127
|
+
if (f.nullable)
|
|
128
|
+
s += '?';
|
|
129
|
+
if (f.writeOnly)
|
|
130
|
+
s += '(writeOnly)';
|
|
131
|
+
return s;
|
|
132
|
+
});
|
|
133
|
+
lines.push(`fields: ${fieldParts.join(', ')}`);
|
|
134
|
+
if (t.serverGenerated)
|
|
135
|
+
lines.push(`serverGenerated: ${t.serverGenerated.join(', ')}`);
|
|
136
|
+
if (t.computed) {
|
|
137
|
+
lines.push(`computed: ${Object.entries(t.computed).map(([n, ty]) => `${n}:${ty}`).join(', ')}`);
|
|
138
|
+
}
|
|
139
|
+
if (t.extraFilters) {
|
|
140
|
+
lines.push(`extraFilters: ${Object.entries(t.extraFilters).map(([n, ty]) => `${n}:${ty}`).join(', ')}`);
|
|
141
|
+
}
|
|
142
|
+
if (t.readJoins) {
|
|
143
|
+
lines.push(`readJoins: ${t.readJoins.map((j) => `${j.alias}→${j.table}(${j.kind})`).join(', ')}`);
|
|
144
|
+
}
|
|
145
|
+
if (t.writeJoins) {
|
|
146
|
+
lines.push(`writeJoins: ${t.writeJoins.map((j) => `${j.alias}→${j.table}`).join(', ')}`);
|
|
147
|
+
}
|
|
148
|
+
if (t.defaultOrder)
|
|
149
|
+
lines.push(`defaultOrder: ${t.defaultOrder}`);
|
|
150
|
+
lines.push('');
|
|
151
|
+
}
|
|
152
|
+
return lines.join('\n');
|
|
153
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { DbTables } from '../../types.js';
|
|
2
|
+
export interface AgentToolSchemas {
|
|
3
|
+
/** POST /search/{table}: `body` + `querystring` (orderBy/page/itemsPerPage/compute*). */
|
|
4
|
+
search?: {
|
|
5
|
+
body: object;
|
|
6
|
+
querystring: object;
|
|
7
|
+
};
|
|
8
|
+
insert?: {
|
|
9
|
+
body: object;
|
|
10
|
+
};
|
|
11
|
+
update?: {
|
|
12
|
+
body: object;
|
|
13
|
+
};
|
|
14
|
+
bulkUpsert?: {
|
|
15
|
+
body: object;
|
|
16
|
+
};
|
|
17
|
+
bulkDelete?: {
|
|
18
|
+
body: object;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Ready-to-use JSON Schemas for exposing a table's operations as strict LLM tools —
|
|
23
|
+
* the same TypeBox schemas the HTTP routes validate with (TypeBox objects ARE plain
|
|
24
|
+
* JSON Schema). Only the operations actually enabled for the table are included
|
|
25
|
+
* (`operations` whitelist, composite-PK rules). `get` and `delete` take a single PK
|
|
26
|
+
* path param and need no schema.
|
|
27
|
+
*
|
|
28
|
+
* Strict tools trade prompt size for provider-side validation; the lighter
|
|
29
|
+
* alternative is a loose tool plus the plugin's structured 400 as the retry signal
|
|
30
|
+
* (see "LLM / agent clients" in the README).
|
|
31
|
+
*/
|
|
32
|
+
export declare function agentToolSchemas(dbTables: DbTables, tableName: string, opts?: {
|
|
33
|
+
maxItemsPerPage?: number;
|
|
34
|
+
maxBulkItems?: number;
|
|
35
|
+
}): AgentToolSchemas;
|
|
36
|
+
//# sourceMappingURL=tool-schemas.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-schemas.d.ts","sourceRoot":"","sources":["../../../src/lib/agent/tool-schemas.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE/C,MAAM,WAAW,gBAAgB;IAC/B,yFAAyF;IACzF,MAAM,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/C,MAAM,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1B,MAAM,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1B,UAAU,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9B,UAAU,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CAC/B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,QAAQ,EAClB,SAAS,EAAE,MAAM,EACjB,IAAI,CAAC,EAAE;IAAE,eAAe,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,GACzD,gBAAgB,CA6BlB"}
|