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.
- package/AGENTS.md +3 -1
- package/AGENTS_BACKEND.md +102 -3
- package/AGENTS_FRONTEND.md +69 -545
- package/BREAKING_CHANGES.md +63 -0
- package/CHANGELOG.md +100 -0
- package/README.md +190 -25
- 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/engine/write-helpers.d.ts +18 -4
- package/dist/lib/engine/write-helpers.d.ts.map +1 -1
- package/dist/lib/engine/write-helpers.js +31 -10
- 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/0001-no-orm-raw-sql.md +38 -0
- package/docs/adr/0002-open-by-default.md +37 -0
- package/docs/adr/0003-bulk-not-transactional.md +35 -0
- package/docs/adr/0004-updates-always-open.md +46 -0
- package/docs/adr/0005-insert-pipeline-sanitize-before-hooks.md +51 -0
- package/docs/adr/0006-raw-db-errors.md +34 -0
- package/docs/adr/0007-explicit-table-generation.md +39 -0
- package/docs/adr/README.md +50 -0
- package/package.json +2 -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,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
|
|
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/
|
|
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
|
|
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
|
|
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/
|
|
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'], //
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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';
|
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"}
|