fastify-auto-sqlapi 0.1.12 → 0.1.14

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_BACKEND.md CHANGED
@@ -357,6 +357,8 @@ buildRelation(mainSchema, mainField, joinSchema, joinField, options?)
357
357
  // unique?: boolean; // default false.
358
358
  // // false → 1:N (child→main). Allowed in joinMustExist/joinMultiple/joinGroup.
359
359
  // // true → N:1 (parent→main). Allowed in joinLeft only.
360
+ // fields?: string[]; // allowlist of the target's fields reachable through this relation.
361
+ // // Read joins only. Must include joinField. Everything else is 400.
360
362
  // }
361
363
  ```
362
364
 
@@ -367,6 +369,27 @@ buildRelation(mainSchema, mainField, joinSchema, joinField, options?)
367
369
 
368
370
  **When to declare an explicit `alias`**: only when (a) you join the same table multiple times in the same `allowedReadJoins`/`allowedWriteJoins`, or (b) you want a friendlier name in the public API surface than the SQL table name (e.g. `'orders'` instead of `'customer_order'`). Otherwise omit it — the default is `joinSchema.tableName`.
369
371
 
372
+ **⚠️ A relation in `allowedReadJoins` is a read grant on the target table.** The join runs inside the query of the *host* table's route, so the target's `onRequests` never fire and its `operations` whitelist does not apply — a table with `operations: []` is still fully readable through any relation pointing at it. What does cross a join: `readExclude`, `tenantScope`, and the schema the relation was declared with.
373
+
374
+ To expose a table broadly but keep some of its columns narrow, give the relation a `fields` allowlist:
375
+
376
+ ```typescript
377
+ allowedReadJoins: [
378
+ buildRelation(SchemaAgent, 'userId', SchemaUser, 'id', { unique: true, fields: ['id', 'name'] }),
379
+ ],
380
+ ```
381
+
382
+ The relation is declared against a schema narrowed to that list, so every read surface validates against it — `selection`, `filters`, `conditions`, `orderBy`, aggregations, and the generated request/response schemas. A field outside the list is `400 Unknown field` everywhere, not just missing from the projection, and the default `'*'` selection is spelled out into the allowed columns rather than emitting a real `SELECT *`.
383
+
384
+ Rules, all enforced at declaration time:
385
+
386
+ - **Must include the join field** — it is what correlates the fetched rows with the main ones.
387
+ - **Not allowed on `allowedWriteJoins`** — the allowlist restricts reading only; write paths resolve `upsertMap` by schema identity and must write every column the caller sent. Declare two relations if the same pair is joined for reading and for writing.
388
+ - **Computed fields on the target resolve against the narrowed schema** — one that reads a field outside the list is a `400`, so a computed cannot be used to reach past the allowlist. One that stays inside keeps working.
389
+ - **Fail-closed** — a column added to the target later is not reachable through the relation until it is added to `fields`.
390
+
391
+ `fields` is not a replacement for `readExclude` (which hides a column from the owning table's own routes too). See [ADR 0010](./docs/adr/0010-joins-do-not-run-route-guards.md) and [ADR 0011](./docs/adr/0011-join-fields-allowlist.md).
392
+
370
393
  **Choosing `unique`**: if `joinField` is the PK (or part of the composite PK) of `joinSchema`, the relation is N:1 — you almost certainly want `unique: true` so the alias is usable in `joinLeft`.
371
394
 
372
395
  **Owned child tables (translations, `*_info` details): use a writeJoin, not a standalone table.** A table that only exists as a child of a parent — e.g. `product_info` with composite PK `(product_id, lang)` — should be an `allowedWriteJoins` on the parent, not its own `DbTables` entry. The engine auto-fills the FK (`product_id`); add it to `upsertMap` (conflict key = the composite PK) to upsert children passing only their own fields. Avoids redundant endpoints/validators.
@@ -431,6 +454,10 @@ export const TableCustomer = defineTable({
431
454
 
432
455
  `extraFilters` accepts either a `Type.Object({...})` or a plain `Record<string, TSchema>`. Extra filter fields appear in Swagger but are NOT auto-applied as `WHERE col = value` — they are handled exclusively by the `extendedCondition` callback. The `filters` parameter in the callback is fully typed with autocomplete for all schema fields + extra filter keys.
433
456
 
457
+ **Not available on `joinLeft`.** `extendedCondition` writes its own column references, which cannot be qualified with the `LEFT JOIN` alias the parent gets there, so `buildLeftJoinClauses` handles schema and computed fields only. The generated body schema does not advertise the parent's `extraFilters` under `joinLeft.<alias>.filters`, and sending one is a `400` rather than a silent no-op. Use `joinMustExist` on the same relation when you need an extra filter on the parent.
458
+
459
+ **Unknown filter keys are a `400`** (`Unknown filter field: <key>`), on the main table and on every join. A filter that does not match a schema field, an `extraFilters` key or a computed field is rejected instead of dropped: silently ignoring it would return *more* rows than the caller asked for. The check lives in the engine, so it applies to `sqlApi.search()` too, and it runs before any query — the outcome never depends on whether the main query matched rows.
460
+
434
461
  ---
435
462
 
436
463
  ## Computed Fields (extension system)
@@ -777,7 +804,7 @@ Tables without `tenantScope` are unaffected — no filtering regardless of `getT
777
804
  - **All fields Optional in response**: `RETURNING *` may return any subset. Response schemas use `Type.Partial`.
778
805
  - **Nullable columns use `Nullable(T)` (type-array form)**: the generator emits `Type.Optional(Nullable(T))` for nullable columns — `Nullable` (exported by the package) produces `{ type: ['integer', 'null'] }`. Do NOT replace it with `Type.Union([T, Type.Null()])`: under Fastify's default Ajv `coerceTypes` a union corrupts values through its branches (`null` → `0` with the Null branch last, `0`/`""` → `null` with it first), while the type array validates and serializes NULL correctly. A bare `Type.Optional(T)` is also wrong: NULL serializes as `0`/`""`. In `filters`, an explicit `null` on a schema field builds `WHERE col IS NULL`.
779
806
  - **`excludeFromCreation`**: **IMPORTANT** — auto-increment PKs (e.g. `id` serial/auto_increment) MUST be listed here, otherwise INSERT will try to send them and fail. The CLI auto-detects this and adds it by default. Also useful for `createdAt`/`updatedAt` columns managed by DB defaults or hooks. It strips **client-supplied** values only: the payload is sanitized before `beforeInsert` runs, so a value the hook assigns to an excluded field (e.g. a server-generated TEXT id) DOES reach the INSERT. Same for the engine's FK auto-fill on secondaries — an excluded FK column does not suppress it. It is an **ergonomics tool for creation, NOT a field-level security mechanism**: it does not apply to updates — every Schema field is updatable by default, by design. Field-level update rules (`isAdmin`, roles, owner/tenant columns, state fields) are product logic: enforce them in `beforeUpdate` (silent strip) or `validate` (loud 400), or move the privileged transition to a dedicated endpoint and keep it off the auto routes via `operations`.
780
- - **`readExclude`**: fields hidden from every read — not projected by search/get, omitted from read response schemas and from this table's default (`*`) join selection when it is a join target. Referencing one from `filters`, `conditions`, `orderBy`, `joinGroup` aggregations or an explicit join `selection` returns 400: hiding a field from output while allowing it to be filtered would leak its value by bisection. **Writes are unaffected** — insert/update/bulk still accept the field, so a column can be writable but never readable (password hash, access token). Primary keys cannot be excluded. Complementary to trimming the Schema, which removes the column from reads *and* writes.
807
+ - **`readExclude`**: fields hidden from every read — not projected by search/get, omitted from read response schemas and from this table's default (`*`) join selection when it is a join target. Referencing one from `filters`, `conditions`, `orderBy`, `joinGroup` aggregations or an explicit join `selection` returns 400: hiding a field from output while allowing it to be filtered would leak its value by bisection. **Writes are unaffected** — insert/update/bulk still accept the field, so a column can be writable but never readable (password hash, access token). Primary keys cannot be excluded. Complementary to trimming the Schema, which removes the column from reads *and* writes. It is **per table and static**: a column hidden here is hidden from every caller, including admins. To keep a column readable on the table's own routes but not through a relation, trim the Schema the *relation* is declared with instead — see the read-grant warning under [buildRelation](#buildrelation-signature).
781
808
  - **`upsertMap`**: when present for a schema, INSERT becomes upsert. PostgreSQL: `ON CONFLICT (...) DO UPDATE`. MySQL/MariaDB: `ON DUPLICATE KEY UPDATE`. Applies to both main and secondary tables.
782
809
  - **`schemaOverrides`**: override auto-generated schema fields with stricter TypeBox types (e.g. `{ email: Type.String({ format: 'email' }) }`). Overrides are merged into the body schema for insert, update, and bulk-upsert. The original Schema file is never modified. In updates, overridden fields are still wrapped in Optional (validates only when present). Overrides appear in Swagger.
783
810
  - **Validation receives camelCase**: `validate` receives the original camelCase record (as sent by the client) and secondaries. Field names match the schema definition, with full TypeScript inference (`main.startDate`, not `main.start_date`). It returns `ValidationError[]` — tuples of `[field, code]` or `[field, code, message]`. If any errors are returned, the request is rejected with 400 before hooks or SQL execute.
@@ -380,3 +380,78 @@ endpoints previously behaved correctly for you — they are now gone and you mus
380
380
  - **First PK column is truly unique on its own?** Then the composite declaration was
381
381
  redundant: declare `primary: 'id'` (single) and keep the second column as a plain field —
382
382
  every by-id operation comes back.
383
+
384
+ ---
385
+
386
+ # Breaking Change — unknown filter keys are rejected
387
+
388
+ `filters` keys that match nothing are now a `400`, on the main table and on every join family.
389
+ Previously they were dropped without a word.
390
+
391
+ ## Why this changed
392
+
393
+ The engine only ever visited filter keys matching a schema field, an `extraFilters` entry or a
394
+ computed field. Anything else — a typo, a renamed column, a field that exists on the table but
395
+ not on the trimmed Schema a relation was declared with — was never looked at, and the query ran
396
+ as if the filter had not been sent.
397
+
398
+ That fails in the dangerous direction. A rejected filter returns *more* rows than the caller
399
+ asked for, and nothing in the response says so; in an application where filters are also how
400
+ visibility is narrowed, a typo becomes a data exposure. It was also the odd one out: an unknown
401
+ field in `selection`, `conditions`, `orderBy` or a `joinGroup` aggregation has always been a
402
+ 400.
403
+
404
+ ## What changed
405
+
406
+ | Request | Before | After |
407
+ |---|---|---|
408
+ | `filters: { nosuchfield: 'x' }` | `200`, filter ignored | `400 Unknown filter field: nosuchfield` |
409
+ | `joinMultiple.<alias>.filters: { nosuchfield: 1 }` | `200`, filter ignored | `400 Unknown filter field: nosuchfield` |
410
+ | `joinLeft.<alias>.filters: { <extraFilter> }` | `200`, filter ignored | `400 Filter '<key>' is an extraFilter: …` |
411
+ | `filters: { knownField: undefined }` | ignored | ignored (unchanged) |
412
+ | `filters: { knownField: null }` | `IS NULL` | `IS NULL` (unchanged) |
413
+
414
+ Two related fixes ship with it:
415
+
416
+ - The check runs in the engine, not in the route schema, so `sqlApi.search()` behaves the same
417
+ as the HTTP routes. (A closed `additionalProperties: false` schema would not have worked:
418
+ Fastify runs Ajv with `removeAdditional: true`, so it would have *stripped* the key and
419
+ reproduced the same silent behaviour one layer up.)
420
+ - Join filters are validated **before any query runs**. They used to be validated inside the
421
+ side query, which is skipped entirely when the main result set is empty — so the same request
422
+ answered 400 or 200 depending on the data it matched.
423
+
424
+ ## Who is affected
425
+
426
+ Callers that send filter keys the target table does not declare. Over-sending is the common
427
+ case: a frontend that spreads a whole form state into `filters`, or reuses one filter object
428
+ across two tables.
429
+
430
+ ## Migration
431
+
432
+ Send only keys the table declares. `GET /agent/manifest` and the Swagger body schema both list
433
+ them per table (schema fields + `extraFilters` + computed fields).
434
+
435
+ For a form-state object, filter it before sending rather than relying on the server to ignore
436
+ the extras:
437
+
438
+ ```typescript
439
+ const ALLOWED = ['name', 'status', 'createdFrom'] as const;
440
+ const filters = Object.fromEntries(
441
+ Object.entries(formState).filter(([k, v]) => ALLOWED.includes(k) && v !== '')
442
+ );
443
+ ```
444
+
445
+ Passing `undefined` for an absent value is still safe and needs no filtering: only keys with a
446
+ defined value are validated.
447
+
448
+ ## joinLeft and extraFilters
449
+
450
+ `joinLeft.<alias>.filters` used to advertise the parent table's `extraFilters` in Swagger and
451
+ accept them at runtime, applying nothing: `buildLeftJoinClauses` builds its condition inline and
452
+ never runs the target's `extendedCondition`, whose column references cannot be qualified with
453
+ the `LEFT JOIN` alias the parent gets there. They are no longer advertised, and sending one
454
+ returns a `400` naming the reason.
455
+
456
+ Use `joinMustExist` on the same relation when you need an extra filter on the parent — that path
457
+ delegates to the target's own `filters()` and does run `extendedCondition`.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,61 @@ 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
+ ## [Unreleased]
11
+
12
+ ### Changed
13
+
14
+ - **Unknown `filters` keys are now rejected with `400 Unknown filter field: <key>`** instead
15
+ of being dropped in silence — on the main table and on every join family. The engine only
16
+ ever visited keys matching a schema field, an `extraFilters` entry or a computed field, so a
17
+ mistyped filter name came back as an unfiltered (wider) result set with no error: the
18
+ dangerous direction to fail in, and inconsistent with `selection`, `conditions`, `orderBy`
19
+ and aggregations, which have always answered 400. The check lives in the engine, so it
20
+ covers `sqlApi.search()` as well as the HTTP routes. `undefined` values still mean "filter
21
+ not supplied" and are ignored; an explicit `null` still filters by `IS NULL`. See
22
+ [BREAKING_CHANGES.md](./BREAKING_CHANGES.md#breaking-change--unknown-filter-keys-are-rejected).
23
+ - **`joinLeft.<alias>.filters` no longer advertises the parent's `extraFilters`.** They were
24
+ present in the generated body schema and in Swagger, but `buildLeftJoinClauses` never runs
25
+ the target's `extendedCondition` (its column references cannot be qualified with the
26
+ `LEFT JOIN` alias), so the filter applied nothing. They are now rejected with a `400` naming
27
+ the reason and pointing at `joinMustExist` on the same relation.
28
+
29
+ ### Fixed
30
+
31
+ - **Join validation no longer depends on the main result set.** `joinMultiple` / `joinLeft` /
32
+ `joinGroup` skip their side query entirely when the main query matched no rows, so everything
33
+ validated inside it was skipped with it — the same request answered 400 or 200 depending on
34
+ the data. This covered filter keys and `readExclude` on join filters, an explicit `selection`
35
+ naming an unknown field, and `joinGroup` aggregation fields including `aggregations.by`. Join
36
+ filters are now validated up front, and the selection and aggregation lists are resolved
37
+ before the empty-result bail-out.
38
+
39
+ ### Added
40
+
41
+ - **`fields` allowlist on `buildRelation`** — restricts which of the target table's fields are
42
+ reachable through one relation, without hiding them from the table's own routes the way
43
+ `readExclude` would. The relation is declared against a schema narrowed to the list, so
44
+ `selection`, `filters`, `conditions`, `orderBy`, aggregations and the generated
45
+ request/response schemas all reject anything outside it with `400 Unknown field`; the default
46
+ `'*'` selection is spelled out into the allowed columns instead of emitting a real `SELECT *`;
47
+ and computed fields on the target resolve against the narrowed schema, so one reading an
48
+ excluded column is a 400 rather than a way around the list. Declaration-time checks: the list
49
+ must include the join field, and is rejected on `allowedWriteJoins` (write paths resolve
50
+ `upsertMap` by schema identity and must write every column the caller sent). Fail-closed by
51
+ design — see [ADR 0011](./docs/adr/0011-join-fields-allowlist.md) for allowlist vs blocklist.
52
+ - **[ADR 0010](./docs/adr/0010-joins-do-not-run-route-guards.md)** — a declared join is a read
53
+ grant: `onRequests` and `operations` are route-level and do not follow a join, so
54
+ `allowedReadJoins` grants read access to the target table under the *host* table's
55
+ authorization. Documents what does cross a join (`readExclude`, `tenantScope`, the relation's
56
+ schema), the trimmed-schema + explicit-`selection` pattern for column-level narrowing, and
57
+ the rejected alternatives (`canBeJoined` hook, `selection` as a ceiling).
58
+
59
+ - **`excludeTables` config option** — blacklist for `sqlapi-generate-schema`. Tables
60
+ listed in `excludeTables` in `sqlapi.config.ts` (exact names or `*` globs, e.g.
61
+ `knex_*`) are skipped during schema generation; their previously generated schema files
62
+ are removed as orphans on the next full run, like those of dropped tables (ADR 0009).
63
+ Does not apply to `sqlapi-generate-tables`, which is already explicit (ADR 0007).
64
+
10
65
  ## [0.1.12]
11
66
 
12
67
  ### Fixed
package/README.md CHANGED
@@ -1,14 +1,63 @@
1
1
  # fastify-auto-sqlapi
2
2
 
3
- Automatic CRUD API generation from PostgreSQL, MySQL, and MariaDB tables for [Fastify](https://fastify.dev/). No ORM, no magic — just raw SQL, with TypeBox validation and Swagger docs out of the box.
3
+ **Your database already knows its tables, columns and relations. This plugin turns that knowledge into a complete REST API inside your [Fastify](https://fastify.dev/) app.**
4
4
 
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.
5
+ Two CLI commands introspect PostgreSQL / MySQL / MariaDB and generate typed schemas; one `register()` call turns each table *you choose to expose* into seven endpoints: search with filters, advanced conditions, relation joins and aggregations; validated writes with nested child records; bulk upsert/delete; multi-tenant isolation; Swagger docs. Related tables don't need endpoints of their own — reads reach them through joins, writes through nested children — so a whole subgraph can live behind a single exposed table. What's left to write is what makes your product yours — auth, business rules, custom logic — as plain TypeScript hooks next to your other routes.
6
+
7
+ One request, to show the point — *"active customers with over 500 total in completed orders, with their order stats, biggest spenders first"*:
8
+
9
+ ```jsonc
10
+ // POST /api/search/customer?orderBy=orders.sum.total DESC&page=1&itemsPerPage=20
11
+ {
12
+ "filters": { "status": "active" },
13
+ "joinGroup": {
14
+ "orders": {
15
+ "filters": { "status": "completed" },
16
+ "aggregations": { "sum": ["total"], "count": ["id"] }
17
+ }
18
+ },
19
+ "conditions": [{ "field": "orders.sum.total", "method": "isGreater", "params": [500] }]
20
+ }
21
+ ```
22
+
23
+ Under the hood this is a fixed, request-shaped number of set-based queries — the main select plus one per requested join alias (EXISTS clauses and correlated subqueries ride inside them), **never one per returned row**. There are no lazy relations to accidentally loop over: the N+1 problem is structurally absent, not something you remember to avoid.
24
+
25
+ Writes read the same way — *"sync these clients and their tags: update the ones that exist, insert the missing ones, don't duplicate a tag"*:
26
+
27
+ ```jsonc
28
+ // PUT /api/bulk/customer — all mains in one SQL; child FKs auto-filled from each parent
29
+ [
30
+ { "main": { "email": "mario@acme.it", "name": "Mario" },
31
+ "secondaries": { "tags": [{ "tag": "vip" }, { "tag": "newsletter" }] } },
32
+ { "main": { "email": "luigi@acme.it", "name": "Luigi" } }
33
+ ]
34
+ // → 200 [{ "main": { "id": 1 }, "secondaries": { "tags": [{ "id": 10 }, { "id": 11 }] } },
35
+ // { "main": { "id": 2 } }]
36
+ ```
37
+
38
+ Conflict keys are declared server-side (`upsertMap`: here customer on `email`, tag on `(customerId, tag)`), so re-sending the same payload updates rows instead of duplicating them — client-side sync without hand-written diffing.
39
+
40
+ And multi-tenancy is one plugin option (`getTenantId: (req) => req.user.organizationId`), enforced on reads *and* writes with zero code in handlers. Rows of other tenants are simply invisible (`404`); here a caller in org B tries to re-point one of their own orders to a customer belonging to org A:
41
+
42
+ ```jsonc
43
+ // PUT /api/rest/order — order 7 is mine, customer 41 belongs to another tenant
44
+ { "main": { "id": 7, "customerId": 41 } }
45
+ // → 403 Forbidden — cross-tenant references are rejected server-side
46
+ ```
47
+
48
+ No endpoint written by hand, no resolvers, no query language on the server — and these requests are schema-validated, Swagger-documented, size-capped and tenant-isolated like every other one.
49
+
50
+ ## Not an ORM, not GraphQL — a third thing
51
+
52
+ - **Not an ORM.** An ORM is a library *your* code uses to talk to the database — you still hand-write every endpoint on top of it. This plugin generates the endpoints themselves. And there is no ORM underneath either: no models, no migrations, no second schema to keep in sync. The database is the single source of truth (the CLI reads it from `information_schema`), and every request runs as plain parameterized SQL you can read with `debug: true`.
53
+ - **Not GraphQL.** GraphQL buys client-driven flexibility with a heavy toolchain: a schema layer, hand-written resolvers (and their N+1 traps), mutations written one by one, client libraries, and query-cost analysis to stop hostile requests. This plugin covers what most projects actually reach for GraphQL for — filter, paginate, join related data, aggregate, in one round trip — with a fixed JSON grammar over plain REST: curl-able, Swagger-documented, bounded by design (join depth is fixed, page and bulk sizes are capped). The write side — nested children, upserts, bulk — is generated too, which GraphQL never gives you for free. What you give up: arbitrarily deep nesting, per-field selection on the main table, subscriptions. If you need those, you need GraphQL; most CRUD backends don't.
54
+ - **Not a hosted black box.** PostgREST / Hasura / Supabase give you an instant API as a separate service, configured from the outside. This is a plugin inside your own app: hooks, validation and auth are TypeScript functions in your codebase, and you can always drop down to `app.sqlApi.*` or raw SQL in a custom route — same engine, no lock-in.
6
55
 
7
56
  ## Features
8
57
 
58
+ - **Zero boilerplate** — 7 endpoints per exposed table; related tables can stay behind joins and nested writes instead of getting endpoints of their own
59
+ - **No ORM** — raw SQL via `pg` + parameterized queries; set-based joins, so no N+1 by construction
9
60
  - **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
10
- - **Zero boilerplate** — define your tables, get 7 endpoints each
11
- - **No ORM** — raw SQL via `pg` + parameterized queries
12
61
  - **TypeBox validation** — request/response schemas auto-generated from your DB
13
62
  - **Joins** — four explicit families (`joinMustExist`, `joinMultiple`, `joinGroup`, `joinLeft`) with alias support
14
63
  - **Computed fields** — declare virtual fields as SQL expressions in `defineTable`; usable like schema fields in filters/orderBy/conditions, and opt-in projected in the response via `selectComputed`. Covers JSON extraction, derived columns, dialect-aware date bucketing
@@ -25,37 +74,6 @@ Point it at your database, and get a full REST API with search, advanced conditi
25
74
  > with bound values, read the 0.1.6 entry — earlier versions could return wrong rows.
26
75
 
27
76
 
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
-
59
77
  ## Quick Start
60
78
 
61
79
  ### 1. Install
@@ -76,6 +94,7 @@ export default {
76
94
  schema: 'public', // PostgreSQL schema to introspect (default: 'public')
77
95
  // dialect: 'mysql', // 'postgres' (default) | 'mysql' | 'mariadb'
78
96
  // envFile: '.env.local', // env file to load (default: '.env')
97
+ // excludeTables: ['knex_*'], // tables to skip in schema generation ('*' = wildcard)
79
98
  };
80
99
  ```
81
100
 
@@ -113,6 +132,12 @@ This introspects your tables and generates one TypeBox schema file per table in
113
132
  `<outputDir>/schemas/` (e.g. `SchemaCustomer.ts`, `SchemaOrder.ts`). These files are
114
133
  auto-generated — don't edit them; re-run the command after a DB change.
115
134
 
135
+ To keep some tables out of schema generation (migration bookkeeping, PostGIS internals, …)
136
+ list them in `excludeTables` in the config — exact table names or `*` globs (e.g.
137
+ `knex_*`). Excluded tables are invisible to the generator: their schemas are not created,
138
+ and any previously generated schema file is removed as an orphan on the next full run,
139
+ like that of a dropped table.
140
+
116
141
  ### 4. Generate the tables template
117
142
 
118
143
  ```bash
@@ -161,6 +186,37 @@ That's it. For a table called `customer`, you now have:
161
186
 
162
187
  > **Note:** Search uses `POST` because filters are passed as JSON in the request body.
163
188
 
189
+ ## Working with coding agents
190
+
191
+ The library is documented for two readers. Humans get this README; agents get task-oriented
192
+ files that ship in the published package, so they are already on disk after `npm install`:
193
+
194
+ | File | Contents |
195
+ | -------------------------------------- | ----------------------------------------------------------------- |
196
+ | `AGENTS.md` | Entry point: what the library does, which file to read for what |
197
+ | `AGENTS_BACKEND.md` | `defineTable` reference, hooks, tenancy, computed fields, CLI |
198
+ | `AGENTS_FRONTEND.md` | Request/response shapes for every endpoint |
199
+ | `docs/adr/` | Why the non-obvious behaviours are what they are |
200
+
201
+ Point your agent at them once:
202
+
203
+ > Read `node_modules/fastify-auto-sqlapi/AGENTS.md` before touching anything under `src/db/`.
204
+
205
+ Three design choices make the generated code predictable enough for an agent to write it
206
+ unsupervised:
207
+
208
+ - **Configuration is declarative and local.** A table is one `defineTable()` call — nothing to
209
+ wire across files, so a diff is reviewable at a glance.
210
+ - **One naming convention.** camelCase in requests, responses, hooks and validators; the
211
+ mapping to DB columns is automatic. There is no second convention to remember, and no
212
+ layer where the agent has to guess which one applies.
213
+ - **Errors are machine-readable.** A 400 carries `fields: [{ path, code, message }]`, so a
214
+ failing request tells the agent exactly what to fix instead of requiring a guess.
215
+
216
+ The ADRs matter more than they look: they answer the "why is it like this?" questions
217
+ (open-by-default, non-transactional bulk, always-updatable fields) that an agent would
218
+ otherwise resolve by inventing a workaround — or by proposing to change intentional behaviour.
219
+
164
220
  ## Table Configuration
165
221
 
166
222
  Tables are configured with `defineTable()`. The only required fields are `primary` and the output of `exportTableInfo()`:
@@ -210,6 +266,8 @@ const TableCustomer = defineTable({
210
266
  buildRelation(SchemaCustomer, 'id', SchemaOrder, 'customerId'), // alias = 'order'
211
267
  buildRelation(SchemaSession, 'userId', SchemaUser, 'id', { unique: true }), // alias = 'user', N:1
212
268
  buildRelation(SchemaSession, 'updatedBy', SchemaUser, 'id', { alias: 'updater', unique: true }),
269
+ // `fields` restricts what this relation exposes of the target — read joins only
270
+ buildRelation(SchemaAgent, 'userId', SchemaUser, 'id', { unique: true, fields: ['id', 'name'] }),
213
271
  ],
214
272
  allowedWriteJoins: [
215
273
  buildRelation(SchemaCustomer, 'id', SchemaOrder, 'customerId'), // alias = 'order'
@@ -367,6 +425,43 @@ are always available to your own code.
367
425
  **3. Multi-tenancy** — `getTenantId` + `tenantScope` filter every query by tenant (see
368
426
  [Multi-Tenant](#multi-tenant)).
369
427
 
428
+ ### A declared join is a read grant
429
+
430
+ ⚠️ **Neither of the first two layers follows a join.** `onRequests` and `operations` are
431
+ properties of the routes generated *for* a table; a join is resolved inside the query of the
432
+ **host** table's route, so the target's hooks never run and its `operations` whitelist does not
433
+ apply. A table with `operations: []` — no HTTP routes at all — is still fully readable through
434
+ any relation pointing at it.
435
+
436
+ So `allowedReadJoins` is a security decision: declaring a relation grants read access to the
437
+ target table under the **host** table's authorization.
438
+
439
+ What does follow a join: `readExclude`, `tenantScope`, and the schema the relation was declared
440
+ with. To expose a table broadly while keeping some columns narrow, give the relation a `fields`
441
+ allowlist:
442
+
443
+ ```typescript
444
+ allowedReadJoins: [
445
+ buildRelation(SchemaAgent, 'userId', SchemaUser, 'id', {
446
+ unique: true,
447
+ fields: ['id', 'name'], // everything else on `user` is unreachable here
448
+ }),
449
+ ],
450
+ ```
451
+
452
+ Every read surface validates against it — `selection`, `filters`, `conditions`, `orderBy`,
453
+ aggregations, and the generated request/response schemas — so `email` is `400 Unknown field`
454
+ everywhere, not just missing from the projection. It is fail-closed: a column added to `user`
455
+ later is not reachable through this relation until it is added to the list.
456
+
457
+ `fields` restricts **reading** only, and only through that relation. It is not a replacement for
458
+ `readExclude` (which hides a column from the table's own routes too) or for `onRequests` (which
459
+ decides who may call them).
460
+
461
+ Full rationale and the rejected alternatives in
462
+ [ADR 0010](./docs/adr/0010-joins-do-not-run-route-guards.md) and
463
+ [ADR 0011](./docs/adr/0011-join-fields-allowlist.md).
464
+
370
465
  ### Request limits
371
466
 
372
467
  Two hard caps bound how much work a single request can trigger (defense against accidental or
@@ -383,11 +478,14 @@ capped — they run your own trusted code.
383
478
 
384
479
  ### Write whitelist
385
480
 
386
- Insert/update/bulk-upsert bodies reject **unknown properties** (`additionalProperties: false`):
387
- only columns present in the generated Schema (as narrowed by `schemaOverrides` /
388
- `excludeFromCreation`) can be written. An unexpected field is a `400`, never a silently-written
389
- column — the Schema is the write whitelist. Trim the generated Schema (or use `excludeFromCreation`)
390
- to keep sensitive columns out of it.
481
+ Insert/update/bulk-upsert bodies are `additionalProperties: false`: only columns present in the
482
+ generated Schema (as narrowed by `schemaOverrides` / `excludeFromCreation`) can be written. The
483
+ Schema is the write whitelist. Trim the generated Schema (or use `excludeFromCreation`) to keep
484
+ sensitive columns out of it.
485
+
486
+ An unexpected field is **stripped, not rejected**: Fastify runs Ajv with `removeAdditional: true`,
487
+ so the request succeeds and the column is simply never written. Mass assignment is prevented
488
+ either way, but do not expect a `400` to tell a client its extra field was ignored.
391
489
 
392
490
  ### Field-level update rules are product logic — use the hooks
393
491
 
@@ -487,12 +585,12 @@ Search with filters, advanced conditions, pagination, ordering, joins, and aggre
487
585
  }
488
586
  ```
489
587
 
490
- - **`filters`** — equality-based, flat key/value. Supports schema fields + `extraFilters`.
588
+ - **`filters`** — equality-based, flat key/value. Supports schema fields, `extraFilters` and computed fields. A key that is none of those is a `400 Unknown filter field: <key>` — a mistyped filter must never come back as a wider result set. An explicit `null` filters by `IS NULL`; `undefined` means "not supplied" and is ignored.
491
589
  - **`conditions`** — array of `{ field, method, params }`. Methods: `isEqual`, `isNotEqual`, `isGreater`, `isGreaterOrEqual`, `isLess`, `isLessOrEqual`, `isLike`, `isILike`, `isIn`, `isNotIn`, `isBetween`, `isNotBetween`, `isNull`, `isNotNull`.
492
590
  - **`joinMustExist`** — EXISTS-based filter: "main rows where at least one related row matches". Accepts `{ filters, conditions }` (both optional). Aliases must come from `allowedReadJoins` declarations with `unique: false`.
493
591
  - **`joinMultiple`** — fetches related child rows in a side query. Accepts `{ filters, conditions, selection }`. Same `unique: false` aliases.
494
592
  - **`joinGroup`** — aggregations on the related table. Supports `sum`, `min`, `max`, `avg`, `count`, `distinctCount`, and optional `by` for GROUP BY (a schema field name or a computed-field name declared on the join table — e.g. for date bucketing declare a computed using `db.dateTrunc('month', qiCol('orderDate'))`). Accepts `{ filters, conditions }`. Same `unique: false` aliases.
495
- - **`joinLeft`** — embeds an N:1 parent. Real `LEFT JOIN` is added on demand (only when the request has `filters`/`conditions` on the parent or uses 2-parti `orderBy` on this alias). Aliases must be declared with `unique: true`. Accepts `{ filters, conditions, selection }`.
593
+ - **`joinLeft`** — embeds an N:1 parent. Real `LEFT JOIN` is added on demand (only when the request has `filters`/`conditions` on the parent or uses 2-parti `orderBy` on this alias). Aliases must be declared with `unique: true`. Accepts `{ filters, conditions, selection }`. Its `filters` accept schema and computed fields only: the parent's `extraFilters` need an alias-aware `extendedCondition`, so they are rejected with a `400` here instead of being silently ignored — use `joinMustExist` on the same relation when you need one.
496
594
 
497
595
  **Dot-notation in `orderBy` and `conditions`**:
498
596
 
@@ -4,7 +4,7 @@ import * as path from 'node:path';
4
4
  import { loadConfig } from '../lib/cli/config.js';
5
5
  import { buildConnectionString, introspectTables } from '../lib/cli/pg-introspect.js';
6
6
  import { buildMysqlConnectionConfig, introspectMysqlTables } from '../lib/cli/mysql-introspect.js';
7
- import { buildTableMap, generateSchemaFile } from '../lib/cli/schema-codegen.js';
7
+ import { applyExcludeTables, buildTableMap, generateSchemaFile } from '../lib/cli/schema-codegen.js';
8
8
  import { loadEnvFile, CONSOLE_COLORS, display, displayAsTableRow, parseArgs, runCli } from './utils.js';
9
9
  await runCli('fastify-auto-sqlapi: generating schemas', async () => {
10
10
  const cliArgs = parseArgs({
@@ -36,6 +36,14 @@ await runCli('fastify-auto-sqlapi: generating schemas', async () => {
36
36
  return;
37
37
  }
38
38
  const tableMap = buildTableMap(rows);
39
+ // Config blacklist: excluded tables are invisible to the generator, so their
40
+ // schema files (if any) fall through to the orphan cleanup below.
41
+ const excluded = applyExcludeTables(tableMap, config.excludeTables);
42
+ if (excluded.length) {
43
+ display(`Excluded by config (excludeTables): ${excluded.join(', ')}`, CONSOLE_COLORS.gray);
44
+ }
45
+ // Note: no early return when everything is excluded — the orphan cleanup below
46
+ // must still run to remove the schema files of freshly excluded tables.
39
47
  // Filter tables if --tables flag is provided
40
48
  const tableNames = cliArgs.tables;
41
49
  if (tableNames.length) {
@@ -1 +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"}
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,CA6CrE"}
@@ -114,6 +114,8 @@ export function renderAgentManifestMd(manifest) {
114
114
  'serverGenerated fields: omit on insert (values from client are ignored).',
115
115
  'readJoins aliases: `1:N` → joinMustExist/joinMultiple/joinGroup; `N:1` → joinLeft.',
116
116
  'writeJoins aliases: keys for `secondaries`/`deletions` in writes.',
117
+ 'filters: unknown keys are rejected (400), never ignored. extraFilters do not apply ' +
118
+ 'under joinLeft — use joinMustExist on the same relation.',
117
119
  '',
118
120
  ];
119
121
  for (const t of Object.values(manifest.tables)) {
@@ -5,5 +5,12 @@ export declare function convertColType(udtName: string, columnInfo: {
5
5
  is_auto_increment?: boolean;
6
6
  }): string;
7
7
  export declare function buildTableMap(rows: ColumnInfo[]): TableMap;
8
+ /**
9
+ * Remove from `tableMap` the tables matching any of the `excludeTables` config patterns.
10
+ * Patterns are matched against the DB table name (not the generated schema name);
11
+ * `*` is the only wildcard — everything else is literal.
12
+ * Returns the names of the excluded tables, in map order.
13
+ */
14
+ export declare function applyExcludeTables(tableMap: TableMap, excludeTables?: string[]): string[];
8
15
  export declare function generateSchemaFile(schemaName: string, tableName: string, fields: Record<string, string>, colMap: Record<string, string>, primary?: string[]): string;
9
16
  //# sourceMappingURL=schema-codegen.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"schema-codegen.d.ts","sourceRoot":"","sources":["../../../src/lib/cli/schema-codegen.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE3D,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,UAAU,EAAE;IAAE,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAAE,GAC9F,MAAM,CA0DR;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,UAAU,EAAE,GAAG,QAAQ,CAqC1D;AAED,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,OAAO,CAAC,EAAE,MAAM,EAAE,GACjB,MAAM,CA8CR"}
1
+ {"version":3,"file":"schema-codegen.d.ts","sourceRoot":"","sources":["../../../src/lib/cli/schema-codegen.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE3D,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,UAAU,EAAE;IAAE,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAAE,GAC9F,MAAM,CA0DR;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,UAAU,EAAE,GAAG,QAAQ,CAqC1D;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,QAAQ,EAAE,aAAa,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAoBzF;AAED,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,OAAO,CAAC,EAAE,MAAM,EAAE,GACjB,MAAM,CA8CR"}
@@ -85,6 +85,31 @@ export function buildTableMap(rows) {
85
85
  return acc;
86
86
  }, {});
87
87
  }
88
+ /**
89
+ * Remove from `tableMap` the tables matching any of the `excludeTables` config patterns.
90
+ * Patterns are matched against the DB table name (not the generated schema name);
91
+ * `*` is the only wildcard — everything else is literal.
92
+ * Returns the names of the excluded tables, in map order.
93
+ */
94
+ export function applyExcludeTables(tableMap, excludeTables) {
95
+ if (excludeTables === undefined)
96
+ return [];
97
+ if (!Array.isArray(excludeTables) || excludeTables.some((p) => typeof p !== 'string')) {
98
+ throw new Error('Invalid config: "excludeTables" must be an array of strings.');
99
+ }
100
+ if (excludeTables.length === 0)
101
+ return [];
102
+ const regexes = excludeTables.map((pattern) => new RegExp(`^${pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replaceAll('\\*', '.*')}$`));
103
+ const excluded = [];
104
+ for (const schemaName of Object.keys(tableMap)) {
105
+ const tableName = tableMap[schemaName].name;
106
+ if (regexes.some((re) => re.test(tableName))) {
107
+ excluded.push(tableName);
108
+ delete tableMap[schemaName];
109
+ }
110
+ }
111
+ return excluded;
112
+ }
88
113
  export function generateSchemaFile(schemaName, tableName, fields, colMap, primary) {
89
114
  const fieldsStr = JSON.stringify(fields, null, 2).replace(/"(.*?)"\s*:\s*"Type(.*?)"/g, '$1: Type$2');
90
115
  const colMapStr = JSON.stringify(colMap, null, 2);
@@ -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;AA8lC3B,wBAAsB,YAAY,CAChC,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,YAAY,CAAC,CA4HvB"}
1
+ {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/search/search.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EACV,QAAQ,EACR,YAAY,EACZ,YAAY,EAeb,MAAM,mBAAmB,CAAC;AAqoC3B,wBAAsB,YAAY,CAChC,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,YAAY,CAAC,CAqIvB"}