fastify-auto-sqlapi 0.1.11 → 0.1.12
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/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,33 @@ 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.12]
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **Security: `aggregations.by` bypassed `readExclude`.** Every other reference to a
|
|
15
|
+
read-excluded field (filters, conditions, orderBy, aggregation fields, join selections)
|
|
16
|
+
is rejected with 400, but the `joinGroup` GROUP BY field was resolved without the check —
|
|
17
|
+
`aggregations: { by: '<excludedField>', count: [...] }` returned the hidden field's
|
|
18
|
+
distinct values verbatim in `rows[].by`. The field now goes through the same
|
|
19
|
+
`validateSchemaField` guard as aggregation fields (400).
|
|
20
|
+
- **Security: HAVING-style conditions and 3-part aggregation `orderBy` could probe
|
|
21
|
+
read-excluded fields.** `buildAggOrderExpr` validated the aggregation field without the
|
|
22
|
+
join table's config, skipping the `readExclude` check. The downstream validation in the
|
|
23
|
+
joinGroup execution masked it — except when the main result set was empty, where that
|
|
24
|
+
validation is skipped entirely: a condition like `player.sum.<hiddenField> > X` answered
|
|
25
|
+
400 when at least one row matched and 200 when none did, a binary oracle allowing
|
|
26
|
+
bisection of the hidden field's aggregates. The check now fires at condition-build time.
|
|
27
|
+
Found by a full audit of every field-referencing input against `readExclude` (all other
|
|
28
|
+
paths verified guarded: filters, conditions, orderBy 1/2-part, selections, aggregation
|
|
29
|
+
fields, compute*, GET).
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- ADR 0008: agent client interface — static grammar (`AGENTS_FRONTEND.md`, same filename by
|
|
34
|
+
design) + runtime manifest split, density as a feature, loose validation with the
|
|
35
|
+
structured 400 as the default tool strategy.
|
|
36
|
+
|
|
10
37
|
## [0.1.11]
|
|
11
38
|
|
|
12
39
|
### Breaking
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/search/search.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EACV,QAAQ,EACR,YAAY,EACZ,YAAY,EAcb,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/search/search.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EACV,QAAQ,EACR,YAAY,EACZ,YAAY,EAcb,MAAM,mBAAmB,CAAC;AA8lC3B,wBAAsB,YAAY,CAChC,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,YAAY,CAAC,CA4HvB"}
|
|
@@ -219,7 +219,10 @@ function buildAggOrderExpr(db, dbTables, tableConf, alias, fn, field, joinGroup)
|
|
|
219
219
|
if (groupReq.aggregations.by && groupReq.aggregations.by !== joinField) {
|
|
220
220
|
err400(`Cannot order by aggregation on joinGroup with 'by' clause on non-FK column: ${alias} (grouped by '${groupReq.aggregations.by}', correlation FK is '${joinField}')`);
|
|
221
221
|
}
|
|
222
|
-
|
|
222
|
+
// Pass the join table's conf: without it validateSchemaField skips the readExclude
|
|
223
|
+
// check, and a HAVING-style condition on a hidden field becomes a 400-vs-200 oracle
|
|
224
|
+
// (executeJoinGroup's own validation is skipped when the main result set is empty).
|
|
225
|
+
const fieldCol = validateSchemaField(field, joinSchema, dbTables[joinSchema.tableName]);
|
|
223
226
|
const refs = extractJoinRefs(db, tableConf, joinDef);
|
|
224
227
|
// The subquery FROM is aliased so the correlation to the outer table survives a
|
|
225
228
|
// self-referencing relation; everything inside references the alias.
|
|
@@ -555,7 +558,10 @@ function buildByExpression(by, joinSchema, joinTableConf, db) {
|
|
|
555
558
|
err400(`Invalid 'by' specification: expected a field or computed name`);
|
|
556
559
|
}
|
|
557
560
|
if (by in joinSchema.fields) {
|
|
558
|
-
|
|
561
|
+
// validateSchemaField also enforces readExclude: GROUP BY on a hidden field
|
|
562
|
+
// would return its distinct values verbatim in `rows[].by`.
|
|
563
|
+
const col = validateSchemaField(by, joinSchema, joinTableConf);
|
|
564
|
+
return `${db.qi(joinSchema.tableName)}.${db.qi(col)}`;
|
|
559
565
|
}
|
|
560
566
|
const fn = joinTableConf?.computedFields?.[by];
|
|
561
567
|
if (fn) {
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# 0008. Agent client interface: static grammar + runtime manifest
|
|
2
|
+
|
|
3
|
+
- **Status**: accepted
|
|
4
|
+
- **Date**: 2026-07-26
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
The plugin doubles as the enforcement layer for LLM clients that drive the API at runtime
|
|
9
|
+
(a chat agent operating on backoffice data: whatever requests the model invents, tenant
|
|
10
|
+
scoping, `readExclude`, `operations` and caps still apply). Such a client needs to be
|
|
11
|
+
told how to call the API — but an LLM prompt has different constraints than human
|
|
12
|
+
documentation: every line costs tokens on every call, and anything deployment-specific
|
|
13
|
+
goes stale the moment the schema changes.
|
|
14
|
+
|
|
15
|
+
## Decision
|
|
16
|
+
|
|
17
|
+
The client-facing documentation is split into two halves with different lifecycles:
|
|
18
|
+
|
|
19
|
+
- **Grammar** — `AGENTS_FRONTEND.md`: HOW to call the API (endpoints, search with the four
|
|
20
|
+
join families, condition methods with params arity, ordering notations, pagination,
|
|
21
|
+
writes, error shapes). Static, identical for every deployment, ~110 dense lines designed
|
|
22
|
+
for a system prompt. It deliberately contains **no tables, fields or aliases**.
|
|
23
|
+
- **Vocabulary** — the manifest (`agentManifest: true` → `GET {prefix}/agent/manifest(.md)`,
|
|
24
|
+
or `buildAgentManifest()` programmatically): WHAT this deployment exposes — tables,
|
|
25
|
+
fields with type/required/nullable, enabled operations, join aliases with direction,
|
|
26
|
+
computed fields. Generated from `DbTables` at startup, so it can never drift from the
|
|
27
|
+
running configuration.
|
|
28
|
+
|
|
29
|
+
Three deliberate properties:
|
|
30
|
+
|
|
31
|
+
1. **The grammar file replaced the old tutorial-style `AGENTS_FRONTEND.md` under the same
|
|
32
|
+
filename.** The path `node_modules/fastify-auto-sqlapi/AGENTS_FRONTEND.md` is known to
|
|
33
|
+
consumers' coding agents; a new filename would have broken every existing reference for
|
|
34
|
+
zero benefit. Do not rename it.
|
|
35
|
+
2. **Density is a feature, not a documentation gap.** The file lives in LLM system
|
|
36
|
+
prompts: jsonc examples with terse end-of-line comments, no rationale, no repetition —
|
|
37
|
+
each concept appears exactly once. Normative rules (what returns 400) belong in it;
|
|
38
|
+
explanations do not. Expanding it with prose and tutorials would revert this decision.
|
|
39
|
+
3. **Loose validation with the structured 400 as retry signal is the default strategy**
|
|
40
|
+
for LLM tools; strict per-table JSON-Schema tools are opt-in via `agentToolSchemas()`.
|
|
41
|
+
The plugin's 400 (`fields: [{path, code, message}]`) was designed machine-readable, so
|
|
42
|
+
a loose generic tool self-corrects in one round trip. This is also why the plugin does
|
|
43
|
+
not ship an opinionated MCP/tool layer: it provides the pieces (grammar, manifest,
|
|
44
|
+
schemas), the consumer composes them.
|
|
45
|
+
|
|
46
|
+
## Alternatives considered
|
|
47
|
+
|
|
48
|
+
- **One complete document (grammar + tables of a given deployment)** — rejected: the
|
|
49
|
+
deployment-specific half goes stale on every schema change; the manifest endpoint is
|
|
50
|
+
the only source that cannot drift.
|
|
51
|
+
- **A new `AGENT_CLIENT.md` alongside the old `AGENTS_FRONTEND.md`** — rejected: near-total
|
|
52
|
+
content overlap, proven drift risk (a fix had to be applied to both files during the
|
|
53
|
+
same session that created the second one), and known-path breakage on removal.
|
|
54
|
+
- **Strict JSON-Schema tools as the default** — rejected: the full search schema per
|
|
55
|
+
table is enormous (conditions, four join families, per-alias shapes); across N tables
|
|
56
|
+
it explodes prompt size and degrades model performance. Kept as opt-in for hot tables.
|
|
57
|
+
|
|
58
|
+
## Consequences
|
|
59
|
+
|
|
60
|
+
- `AGENTS_FRONTEND.md` serves both coding agents (writing client code) and runtime LLM
|
|
61
|
+
clients; the verbose tutorial content it replaced is covered by the grammar plus
|
|
62
|
+
Swagger.
|
|
63
|
+
- Contributions that add tables/fields/examples-with-prose to the grammar file, or rename
|
|
64
|
+
it, should be declined with a pointer to this ADR.
|
|
65
|
+
- New request-shape features must update the grammar (a rule line, not a tutorial) and,
|
|
66
|
+
when they touch per-table capabilities, the manifest builder.
|
package/docs/adr/README.md
CHANGED
|
@@ -22,6 +22,7 @@ not by editing history.
|
|
|
22
22
|
| [0005](./0005-insert-pipeline-sanitize-before-hooks.md) | Insert pipeline: client payload sanitized before `beforeInsert` |
|
|
23
23
|
| [0006](./0006-raw-db-errors.md) | Raw DB errors surface as 500 — no SQLSTATE→HTTP mapping |
|
|
24
24
|
| [0007](./0007-explicit-table-generation.md) | Table generation is explicit — `--all` is a migration helper, not the default |
|
|
25
|
+
| [0008](./0008-agent-client-interface.md) | Agent client interface: static grammar + runtime manifest |
|
|
25
26
|
|
|
26
27
|
## When to write one
|
|
27
28
|
|
package/package.json
CHANGED