fastify-auto-sqlapi 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/AGENTS_BACKEND.md +9 -6
- package/AGENTS_FRONTEND.md +2 -2
- package/BREAKING_CHANGES.md +105 -0
- package/CHANGELOG.md +227 -69
- package/README.md +10 -4
- package/dist/bin/generate-schema.js +7 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/lib/agent/manifest.d.ts +2 -1
- package/dist/lib/agent/manifest.d.ts.map +1 -1
- package/dist/lib/agent/manifest.js +11 -3
- package/dist/lib/cli/mysql-introspect.d.ts +28 -0
- package/dist/lib/cli/mysql-introspect.d.ts.map +1 -1
- package/dist/lib/cli/mysql-introspect.js +39 -16
- package/dist/lib/cli/schema-codegen.d.ts +14 -0
- package/dist/lib/cli/schema-codegen.d.ts.map +1 -1
- package/dist/lib/cli/schema-codegen.js +67 -4
- package/dist/lib/table-helpers.d.ts.map +1 -1
- package/dist/lib/table-helpers.js +2 -1
- package/dist/lib/tenant.d.ts.map +1 -1
- package/dist/lib/tenant.js +7 -1
- package/dist/types/cli.d.ts +10 -0
- package/dist/types/cli.d.ts.map +1 -1
- package/dist/types/plugin.d.ts +21 -0
- package/dist/types/plugin.d.ts.map +1 -1
- package/docs/adr/0017-the-schema-is-the-contract.md +2 -1
- package/docs/adr/0019-column-exclusion-is-generation-time-config.md +62 -0
- package/docs/adr/README.md +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -62,6 +62,8 @@ And multi-tenancy is one plugin option (`getTenantId: (req) => req.user?.organiz
|
|
|
62
62
|
// → 403 Forbidden — cross-tenant references are rejected server-side
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
+
> ⚠️ **`getTenantId` returning `null`/`undefined` means _admin_ — no filtering, full cross-tenant access.** So "unauthenticated" and "admin" collapse to the same value: if your auth hook is missing or fails, `req.user` is `undefined` and the `?? null` above serves the request as admin. Nothing fails closed. In production, make `getTenantId` (or the auth hook) **reject when there is no authenticated principal**, and reserve `null` for a real superadmin — see the `getTenantId` option docs.
|
|
66
|
+
|
|
65
67
|
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.
|
|
66
68
|
|
|
67
69
|
## What you get
|
|
@@ -213,7 +215,7 @@ each way they need to differ:
|
|
|
213
215
|
|---|---|
|
|
214
216
|
| a column must never be read | `readExclude` |
|
|
215
217
|
| a column must never be written | `writeExclude` |
|
|
216
|
-
| a column must not exist for the API at all |
|
|
218
|
+
| a column must not exist for the API at all | `excludeColumns` in `sqlapi.config.ts` |
|
|
217
219
|
| the API type must be stricter than the column | `schemaOverrides` |
|
|
218
220
|
| the value is derived, not stored | `computedFields` |
|
|
219
221
|
| the stored representation is not the API one | `afterRead` (decrypt, unpack, rescale) |
|
|
@@ -291,9 +293,12 @@ mapping is product logic, for your `setErrorHandler`). Set `exposeDebugInfo: tru
|
|
|
291
293
|
developing to get the driver detail on the wire, additively.
|
|
292
294
|
([ADR 0013](./docs/adr/0013-sanitized-db-errors.md))
|
|
293
295
|
|
|
294
|
-
Two caps are on by default: `maxItemsPerPage` (1000) bounds a search — and is
|
|
295
|
-
`LIMIT` even with no paginator, so an empty-body search cannot dump a table — and
|
|
296
|
-
(1000) bounds a bulk array.
|
|
296
|
+
Two caps are on by default and configurable: `maxItemsPerPage` (1000) bounds a search — and is
|
|
297
|
+
applied as the `LIMIT` even with no paginator, so an empty-body search cannot dump a table — and
|
|
298
|
+
`maxBulkItems` (1000) bounds a bulk array. Two more are fixed and bound the *work* a search asks
|
|
299
|
+
for rather than the rows it returns: 100 `conditions` and 20 `orderBy` parts, each over-limit
|
|
300
|
+
request answered with a 400. Those two are enforced in the engine, so `sqlApi.search()` gets them
|
|
301
|
+
too. Write bodies are `additionalProperties: false`, so the Schema is the
|
|
297
302
|
write whitelist and there is no mass assignment; note that Fastify *strips* an unknown field by
|
|
298
303
|
default rather than rejecting it.
|
|
299
304
|
|
|
@@ -327,3 +332,4 @@ request says exactly what to fix.
|
|
|
327
332
|
## License
|
|
328
333
|
|
|
329
334
|
MIT
|
|
335
|
+
|
|
@@ -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 { applyExcludeTables, buildTableMap, generateSchemaFile } from '../lib/cli/schema-codegen.js';
|
|
7
|
+
import { applyExcludeColumns, applyExcludeTables, buildTableMap, generateSchemaFile, } from '../lib/cli/schema-codegen.js';
|
|
8
8
|
import { loadEnvFile, CONSOLE_COLORS, display, displayAsTableRow, parseArgs, runCli } from './utils.js';
|
|
9
9
|
const STATUS_COLOR = {
|
|
10
10
|
created: CONSOLE_COLORS.green,
|
|
@@ -157,6 +157,12 @@ await runCli('fastify-auto-sqlapi: generating schemas', async () => {
|
|
|
157
157
|
if (excluded.length) {
|
|
158
158
|
display(`Excluded by config (excludeTables): ${excluded.join(', ')}`, CONSOLE_COLORS.gray);
|
|
159
159
|
}
|
|
160
|
+
// Runs after the table blacklist so a column pattern is never checked against a table the
|
|
161
|
+
// generator will not see anyway (its primary key included).
|
|
162
|
+
const excludedColumns = applyExcludeColumns(tableMap, config.excludeColumns);
|
|
163
|
+
if (excludedColumns.length) {
|
|
164
|
+
display(`Excluded by config (excludeColumns): ${excludedColumns.join(', ')}`, CONSOLE_COLORS.gray);
|
|
165
|
+
}
|
|
160
166
|
const tableNames = cliArgs.tables;
|
|
161
167
|
if (!keepOnlyRequestedTables(tableMap, tableNames)) {
|
|
162
168
|
display(`No matching tables found for: ${tableNames.join(', ')}`, CONSOLE_COLORS.magenta);
|
package/dist/index.d.ts
CHANGED
|
@@ -34,5 +34,5 @@ export { BulkDeleteTableBody, BulkDeleteTableResponse } from './lib/schema/bulk-
|
|
|
34
34
|
export { default as bulkDeleteRoutes } from './routes/auto/bulk-delete.routes.js';
|
|
35
35
|
export { Type, type Static } from '@sinclair/typebox';
|
|
36
36
|
export { ConditionBuilder, Expression } from 'node-condition-builder';
|
|
37
|
-
export type { SqlApiConfig, ColumnInfo, TableMap, Queryable, TransactionQueryable, SqlResult, DbRecord, DbRecordValue, SelectOptions, SchemaDefinition, JoinDefinition, ITable, TableOperation, DbTables, SqlApiPluginOptions, SwaggerOptions, SearchParams, SearchResult, Paginator, PaginationResult, TableFilterFn, ExtendedConditionFn, FilterRecord, AggregationRequest, JoinGroupRequest, ComputedFieldFn, ComputedFieldContext, ComputedFieldExpr, GetParams, GetResult, DeleteParams, DeleteResult, InsertParams, InsertResult, BulkUpsertItem, BulkUpsertParams, BulkUpsertResult, BulkDeleteParams, BulkDeleteResult, ConditionMethod, SearchCondition, UpdateParams, UpdateResult, TenantId, TenantScope, TenantScopeDirect, TenantScopeIndirect, TenantScopeAnyOf, TenantContext, ValidationError, ValidatorFn, BulkValidatorItem, BulkValidatorFn, } from './types.js';
|
|
37
|
+
export type { ApiRequest, SqlApiConfig, ColumnInfo, TableMap, Queryable, TransactionQueryable, SqlResult, DbRecord, DbRecordValue, SelectOptions, SchemaDefinition, JoinDefinition, ITable, TableOperation, DbTables, SqlApiPluginOptions, SwaggerOptions, SearchParams, SearchResult, Paginator, PaginationResult, TableFilterFn, ExtendedConditionFn, FilterRecord, AggregationRequest, JoinGroupRequest, ComputedFieldFn, ComputedFieldContext, ComputedFieldExpr, GetParams, GetResult, DeleteParams, DeleteResult, InsertParams, InsertResult, BulkUpsertItem, BulkUpsertParams, BulkUpsertResult, BulkDeleteParams, BulkDeleteResult, ConditionMethod, SearchCondition, UpdateParams, UpdateResult, TenantId, TenantScope, TenantScopeDirect, TenantScopeIndirect, TenantScopeAnyOf, TenantContext, ValidationError, ValidatorFn, BulkValidatorItem, BulkValidatorFn, } from './types.js';
|
|
38
38
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
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;AACxH,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAG7C,OAAO,EACL,kBAAkB,EAClB,qBAAqB,EACrB,KAAK,aAAa,EAClB,KAAK,aAAa,GACnB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,gBAAgB,EAAE,KAAK,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AACtF,OAAO,EAAE,OAAO,IAAI,mBAAmB,EAAE,MAAM,wCAAwC,CAAC;AAGxF,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAGhD,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACxD,YAAY,EAAE,aAAa,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAIlH,OAAO,EAAE,OAAO,IAAI,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAGvE,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAGzD,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAC7D,OAAO,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAC5H,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAGzE,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAC3D,OAAO,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAC9E,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAGzE,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AACrD,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAGnE,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAC3D,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAGzE,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAC3D,OAAO,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAC9E,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAGzE,OAAO,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AACpE,OAAO,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,MAAM,6BAA6B,CAAC;AAC3F,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,qCAAqC,CAAC;AAGlF,OAAO,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AACpE,OAAO,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,MAAM,6BAA6B,CAAC;AAC3F,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,qCAAqC,CAAC;AAGlF,OAAO,EAAE,IAAI,EAAE,KAAK,MAAM,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAGtE,YAAY,EACV,UAAU,EACV,YAAY,EACZ,UAAU,EACV,QAAQ,EACR,SAAS,EACT,oBAAoB,EACpB,SAAS,EACT,QAAQ,EACR,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,cAAc,EACd,MAAM,EACN,cAAc,EACd,QAAQ,EACR,mBAAmB,EACnB,cAAc,EACd,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,gBAAgB,EAChB,aAAa,EACb,mBAAmB,EACnB,YAAY,EACZ,kBAAkB,EAClB,gBAAgB,EAChB,eAAe,EACf,oBAAoB,EACpB,iBAAiB,EACjB,SAAS,EACT,SAAS,EACT,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,cAAc,EACd,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,EACf,eAAe,EACf,YAAY,EACZ,YAAY,EACZ,QAAQ,EACR,WAAW,EACX,iBAAiB,EACjB,mBAAmB,EACnB,gBAAgB,EAChB,aAAa,EACb,eAAe,EACf,WAAW,EACX,iBAAiB,EACjB,eAAe,GAChB,MAAM,YAAY,CAAC"}
|
|
@@ -49,7 +49,8 @@ export declare function tableOperations(tableConf: ITable): TableOperation[];
|
|
|
49
49
|
export declare function buildAgentManifest(dbTables: DbTables): AgentManifest;
|
|
50
50
|
/**
|
|
51
51
|
* Compact markdown rendering of the manifest, designed for an LLM system prompt.
|
|
52
|
-
* Notation: `!` required on insert, `?` nullable, `(writeOnly)` never returned by reads
|
|
52
|
+
* Notation: `!` required on insert, `?` nullable, `(writeOnly)` never returned by reads,
|
|
53
|
+
* `(readOnly)` returned by reads and refused by every write.
|
|
53
54
|
*/
|
|
54
55
|
export declare function renderAgentManifestMd(manifest: AgentManifest): string;
|
|
55
56
|
export {};
|
|
@@ -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,EAAkB,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAQvF,UAAU,aAAa;IACrB,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;IACpB,qFAAqF;IACrF,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,UAAU,YAAY;IACpB,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;AA4DD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,QAAQ,GAAG,aAAa,CAMpE;AAED
|
|
1
|
+
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../../src/lib/agent/manifest.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAkB,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAQvF,UAAU,aAAa;IACrB,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;IACpB,qFAAqF;IACrF,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,UAAU,YAAY;IACpB,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;AA4DD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,QAAQ,GAAG,aAAa,CAMpE;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,aAAa,GAAG,MAAM,CAmBrE"}
|
|
@@ -119,13 +119,15 @@ export function buildAgentManifest(dbTables) {
|
|
|
119
119
|
}
|
|
120
120
|
/**
|
|
121
121
|
* Compact markdown rendering of the manifest, designed for an LLM system prompt.
|
|
122
|
-
* Notation: `!` required on insert, `?` nullable, `(writeOnly)` never returned by reads
|
|
122
|
+
* Notation: `!` required on insert, `?` nullable, `(writeOnly)` never returned by reads,
|
|
123
|
+
* `(readOnly)` returned by reads and refused by every write.
|
|
123
124
|
*/
|
|
124
125
|
export function renderAgentManifestMd(manifest) {
|
|
125
126
|
const lines = [
|
|
126
127
|
'# Tables',
|
|
127
128
|
'',
|
|
128
|
-
'Notation: `!`=required on insert, `?`=nullable, `(writeOnly)`=writable, never in reads
|
|
129
|
+
'Notation: `!`=required on insert, `?`=nullable, `(writeOnly)`=writable, never in reads, ' +
|
|
130
|
+
'`(readOnly)`=returned by reads, refused by every write (omit it from write payloads).',
|
|
129
131
|
'serverGenerated fields: omit on insert (values from client are ignored).',
|
|
130
132
|
'readJoins aliases: `1:N` → joinMustExist/joinMultiple/joinGroup; `N:1` → joinLeft.',
|
|
131
133
|
'writeJoins aliases: keys for `secondaries`/`deletions` in writes.',
|
|
@@ -142,7 +144,11 @@ export function renderAgentManifestMd(manifest) {
|
|
|
142
144
|
function namedTypes(map) {
|
|
143
145
|
return Object.entries(map).map(([n, ty]) => `${n}:${ty}`).join(', ');
|
|
144
146
|
}
|
|
145
|
-
/**
|
|
147
|
+
/**
|
|
148
|
+
* A field's type with its notation suffixes: `!` required, `?` nullable, `(writeOnly)`,
|
|
149
|
+
* `(readOnly)`. The two exclusion suffixes are mutually exclusive — `defineTable` rejects a
|
|
150
|
+
* field that is both — so at most one of them is ever appended.
|
|
151
|
+
*/
|
|
146
152
|
function fieldNotation(f) {
|
|
147
153
|
let s = f.type;
|
|
148
154
|
if (f.required)
|
|
@@ -151,6 +157,8 @@ function fieldNotation(f) {
|
|
|
151
157
|
s += '?';
|
|
152
158
|
if (f.writeOnly)
|
|
153
159
|
s += '(writeOnly)';
|
|
160
|
+
if (f.readOnly)
|
|
161
|
+
s += '(readOnly)';
|
|
154
162
|
return s;
|
|
155
163
|
}
|
|
156
164
|
function renderTableMd(t) {
|
|
@@ -6,6 +6,34 @@ export interface MysqlConnectionConfig {
|
|
|
6
6
|
password: string;
|
|
7
7
|
database: string;
|
|
8
8
|
}
|
|
9
|
+
/** information_schema.columns row. MySQL and MariaDB differ on column-name case. */
|
|
10
|
+
interface MysqlColumnRow {
|
|
11
|
+
TABLE_NAME?: string;
|
|
12
|
+
table_name?: string;
|
|
13
|
+
COLUMN_NAME?: string;
|
|
14
|
+
column_name?: string;
|
|
15
|
+
DATA_TYPE?: string;
|
|
16
|
+
data_type?: string;
|
|
17
|
+
COLUMN_DEFAULT?: string | null;
|
|
18
|
+
column_default?: string | null;
|
|
19
|
+
IS_NULLABLE?: string;
|
|
20
|
+
is_nullable?: string;
|
|
21
|
+
COLUMN_KEY?: string;
|
|
22
|
+
column_key?: string;
|
|
23
|
+
EXTRA?: string;
|
|
24
|
+
extra?: string;
|
|
25
|
+
GENERATION_EXPRESSION?: string | null;
|
|
26
|
+
generation_expression?: string | null;
|
|
27
|
+
TABLE_TYPE?: string;
|
|
28
|
+
table_type?: string;
|
|
29
|
+
}
|
|
9
30
|
export declare function buildMysqlConnectionConfig(): MysqlConnectionConfig;
|
|
31
|
+
/**
|
|
32
|
+
* One `information_schema.columns` row as the generator sees it.
|
|
33
|
+
*
|
|
34
|
+
* @testonly Exported only so unit tests can exercise it directly.
|
|
35
|
+
*/
|
|
36
|
+
export declare function mapMysqlColumnRow(row: MysqlColumnRow): ColumnInfo;
|
|
10
37
|
export declare function introspectMysqlTables(connectionConfig: MysqlConnectionConfig, schema: string): Promise<ColumnInfo[]>;
|
|
38
|
+
export {};
|
|
11
39
|
//# sourceMappingURL=mysql-introspect.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mysql-introspect.d.ts","sourceRoot":"","sources":["../../../src/lib/cli/mysql-introspect.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEjD,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;
|
|
1
|
+
{"version":3,"file":"mysql-introspect.d.ts","sourceRoot":"","sources":["../../../src/lib/cli/mysql-introspect.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEjD,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAkBD,oFAAoF;AACpF,UAAU,cAAc;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qBAAqB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,qBAAqB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,wBAAgB,0BAA0B,IAAI,qBAAqB,CAmBlE;AAyED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,cAAc,GAAG,UAAU,CAejE;AAED,wBAAsB,qBAAqB,CACzC,gBAAgB,EAAE,qBAAqB,EACvC,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,UAAU,EAAE,CAAC,CAoBvB"}
|
|
@@ -69,32 +69,55 @@ function mapMysqlType(dataType) {
|
|
|
69
69
|
function extra(row) {
|
|
70
70
|
return String(row.EXTRA || row.extra || '').toLowerCase();
|
|
71
71
|
}
|
|
72
|
+
/** The two `EXTRA` markers that mean a real `GENERATED ALWAYS AS (...)` column. */
|
|
73
|
+
const GENERATED_EXTRA = /\b(?:virtual|stored|persistent) generated\b/;
|
|
74
|
+
/**
|
|
75
|
+
* True only for a column the database computes from an expression — never writable.
|
|
76
|
+
*
|
|
77
|
+
* `EXTRA` cannot be matched on the word 'generated': MySQL writes `DEFAULT_GENERATED` there
|
|
78
|
+
* for any column with a default *expression*, `DEFAULT CURRENT_TIMESTAMP` included, so a
|
|
79
|
+
* substring match takes every such timestamp column out of the write bodies. Only
|
|
80
|
+
* `GENERATION_EXPRESSION` separates the two, and it is non-empty for generated columns alone.
|
|
81
|
+
* `EXTRA` stays as a fallback, matched on the exact markers, for a driver or an older server
|
|
82
|
+
* that does not return the expression.
|
|
83
|
+
*/
|
|
84
|
+
function isGenerated(row) {
|
|
85
|
+
const expression = String(row.GENERATION_EXPRESSION ?? row.generation_expression ?? '').trim();
|
|
86
|
+
return expression !== '' || GENERATED_EXTRA.test(extra(row));
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* One `information_schema.columns` row as the generator sees it.
|
|
90
|
+
*
|
|
91
|
+
* @testonly Exported only so unit tests can exercise it directly.
|
|
92
|
+
*/
|
|
93
|
+
export function mapMysqlColumnRow(row) {
|
|
94
|
+
// `||` is kept as-is from the untyped version: this change types the driver, it does
|
|
95
|
+
// not alter behaviour. The trailing fallbacks only satisfy ColumnInfo, which cannot
|
|
96
|
+
// hold `undefined`.
|
|
97
|
+
return {
|
|
98
|
+
table_name: row.TABLE_NAME || row.table_name || '',
|
|
99
|
+
column_name: row.COLUMN_NAME || row.column_name || '',
|
|
100
|
+
udt_name: mapMysqlType(row.DATA_TYPE || row.data_type || ''),
|
|
101
|
+
column_default: row.COLUMN_DEFAULT || row.column_default || null,
|
|
102
|
+
is_nullable: row.IS_NULLABLE || row.is_nullable || '',
|
|
103
|
+
is_primary: (row.COLUMN_KEY || row.column_key) === 'PRI',
|
|
104
|
+
is_auto_increment: extra(row).includes('auto_increment'),
|
|
105
|
+
is_generated: isGenerated(row),
|
|
106
|
+
is_view: (row.TABLE_TYPE || row.table_type) === 'VIEW',
|
|
107
|
+
};
|
|
108
|
+
}
|
|
72
109
|
export async function introspectMysqlTables(connectionConfig, schema) {
|
|
73
110
|
const mysql2 = loadOptionalDependency('mysql2/promise', 'npm install mysql2');
|
|
74
111
|
const connection = await mysql2.createConnection(connectionConfig);
|
|
75
112
|
try {
|
|
76
113
|
const [rows] = await connection.query(`SELECT c.table_name, c.column_name, c.data_type, c.column_default, c.is_nullable,
|
|
77
|
-
c.column_key, c.extra, t.table_type
|
|
114
|
+
c.column_key, c.extra, c.generation_expression, t.table_type
|
|
78
115
|
FROM information_schema.columns c
|
|
79
116
|
JOIN information_schema.tables t
|
|
80
117
|
ON t.table_schema = c.table_schema AND t.table_name = c.table_name
|
|
81
118
|
WHERE c.table_schema = ?
|
|
82
119
|
ORDER BY c.table_name, c.ordinal_position`, [schema]);
|
|
83
|
-
|
|
84
|
-
// not alter behaviour. The trailing fallbacks only satisfy ColumnInfo, which cannot
|
|
85
|
-
// hold `undefined`.
|
|
86
|
-
return rows.map((row) => ({
|
|
87
|
-
table_name: row.TABLE_NAME || row.table_name || '',
|
|
88
|
-
column_name: row.COLUMN_NAME || row.column_name || '',
|
|
89
|
-
udt_name: mapMysqlType(row.DATA_TYPE || row.data_type || ''),
|
|
90
|
-
column_default: row.COLUMN_DEFAULT || row.column_default || null,
|
|
91
|
-
is_nullable: row.IS_NULLABLE || row.is_nullable || '',
|
|
92
|
-
is_primary: (row.COLUMN_KEY || row.column_key) === 'PRI',
|
|
93
|
-
is_auto_increment: extra(row).includes('auto_increment'),
|
|
94
|
-
// 'VIRTUAL GENERATED' / 'STORED GENERATED': computed from an expression, never writable.
|
|
95
|
-
is_generated: extra(row).includes('generated'),
|
|
96
|
-
is_view: (row.TABLE_TYPE || row.table_type) === 'VIEW',
|
|
97
|
-
}));
|
|
120
|
+
return rows.map(mapMysqlColumnRow);
|
|
98
121
|
}
|
|
99
122
|
finally {
|
|
100
123
|
await connection.end();
|
|
@@ -16,5 +16,19 @@ export declare function buildTableMap(rows: ColumnInfo[]): TableMap;
|
|
|
16
16
|
* Returns the names of the excluded tables, in map order.
|
|
17
17
|
*/
|
|
18
18
|
export declare function applyExcludeTables(tableMap: TableMap, excludeTables?: string[]): string[];
|
|
19
|
+
/**
|
|
20
|
+
* Remove from every table in `tableMap` the columns matching any of the `excludeColumns`
|
|
21
|
+
* config patterns. A pattern is `<table>.<column>`, both parts matched against the DB names
|
|
22
|
+
* (not the generated field names) with `*` as the only wildcard on either side.
|
|
23
|
+
*
|
|
24
|
+
* An excluded column does not exist for the API at all: it leaves the Schema's fields, its
|
|
25
|
+
* `colMap` and its `generatedFields`, so every read, write, filter and manifest built from the
|
|
26
|
+
* Schema is unaware of it. That is also why the primary key is refused — every engine
|
|
27
|
+
* identifies rows with it — and why a table that would end up empty is refused too: that
|
|
28
|
+
* is `excludeTables`' job.
|
|
29
|
+
*
|
|
30
|
+
* Returns the excluded columns as `table.column`, in map order.
|
|
31
|
+
*/
|
|
32
|
+
export declare function applyExcludeColumns(tableMap: TableMap, excludeColumns?: string[]): string[];
|
|
19
33
|
export declare function generateSchemaFile(schemaName: string, tableName: string, fields: Record<string, string>, colMap: Record<string, string>, primary?: string[], generated?: string[], isView?: boolean): string;
|
|
20
34
|
//# 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;;GAEG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,UAAU,EAAE;IACV,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB,GACA,MAAM,CA6DR;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,UAAU,EAAE,GAAG,QAAQ,CA0C1D;
|
|
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;;GAEG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,UAAU,EAAE;IACV,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB,GACA,MAAM,CA6DR;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,UAAU,EAAE,GAAG,QAAQ,CA0C1D;AAaD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,QAAQ,EAAE,aAAa,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAgBzF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,cAAc,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAiD3F;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,EAClB,SAAS,CAAC,EAAE,MAAM,EAAE,EACpB,MAAM,CAAC,EAAE,OAAO,GACf,MAAM,CAoDR"}
|
|
@@ -95,6 +95,15 @@ export function buildTableMap(rows) {
|
|
|
95
95
|
return acc;
|
|
96
96
|
}, {});
|
|
97
97
|
}
|
|
98
|
+
/** Compile a config glob — `*` is the only wildcard, everything else is literal. */
|
|
99
|
+
function globToRegExp(pattern) {
|
|
100
|
+
return new RegExp(`^${pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replaceAll('\\*', '.*')}$`);
|
|
101
|
+
}
|
|
102
|
+
function assertPatternList(option, value) {
|
|
103
|
+
if (!Array.isArray(value) || value.some((p) => typeof p !== 'string')) {
|
|
104
|
+
throw new Error(`Invalid config: "${option}" must be an array of strings.`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
98
107
|
/**
|
|
99
108
|
* Remove from `tableMap` the tables matching any of the `excludeTables` config patterns.
|
|
100
109
|
* Patterns are matched against the DB table name (not the generated schema name);
|
|
@@ -104,12 +113,10 @@ export function buildTableMap(rows) {
|
|
|
104
113
|
export function applyExcludeTables(tableMap, excludeTables) {
|
|
105
114
|
if (excludeTables === undefined)
|
|
106
115
|
return [];
|
|
107
|
-
|
|
108
|
-
throw new Error('Invalid config: "excludeTables" must be an array of strings.');
|
|
109
|
-
}
|
|
116
|
+
assertPatternList('excludeTables', excludeTables);
|
|
110
117
|
if (excludeTables.length === 0)
|
|
111
118
|
return [];
|
|
112
|
-
const regexes = excludeTables.map(
|
|
119
|
+
const regexes = excludeTables.map(globToRegExp);
|
|
113
120
|
const excluded = [];
|
|
114
121
|
for (const schemaName of Object.keys(tableMap)) {
|
|
115
122
|
const tableName = tableMap[schemaName].name;
|
|
@@ -120,6 +127,62 @@ export function applyExcludeTables(tableMap, excludeTables) {
|
|
|
120
127
|
}
|
|
121
128
|
return excluded;
|
|
122
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* Remove from every table in `tableMap` the columns matching any of the `excludeColumns`
|
|
132
|
+
* config patterns. A pattern is `<table>.<column>`, both parts matched against the DB names
|
|
133
|
+
* (not the generated field names) with `*` as the only wildcard on either side.
|
|
134
|
+
*
|
|
135
|
+
* An excluded column does not exist for the API at all: it leaves the Schema's fields, its
|
|
136
|
+
* `colMap` and its `generatedFields`, so every read, write, filter and manifest built from the
|
|
137
|
+
* Schema is unaware of it. That is also why the primary key is refused — every engine
|
|
138
|
+
* identifies rows with it — and why a table that would end up empty is refused too: that
|
|
139
|
+
* is `excludeTables`' job.
|
|
140
|
+
*
|
|
141
|
+
* Returns the excluded columns as `table.column`, in map order.
|
|
142
|
+
*/
|
|
143
|
+
export function applyExcludeColumns(tableMap, excludeColumns) {
|
|
144
|
+
if (excludeColumns === undefined)
|
|
145
|
+
return [];
|
|
146
|
+
assertPatternList('excludeColumns', excludeColumns);
|
|
147
|
+
if (excludeColumns.length === 0)
|
|
148
|
+
return [];
|
|
149
|
+
const matchers = excludeColumns.map((pattern) => {
|
|
150
|
+
const dot = pattern.indexOf('.');
|
|
151
|
+
if (dot <= 0 || dot === pattern.length - 1) {
|
|
152
|
+
throw new Error(`Invalid config: excludeColumns entry "${pattern}" must be in the form "table.column" ` +
|
|
153
|
+
`(a "*" wildcard is allowed on either side, e.g. "*.password_hash").`);
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
table: globToRegExp(pattern.slice(0, dot)),
|
|
157
|
+
column: globToRegExp(pattern.slice(dot + 1)),
|
|
158
|
+
};
|
|
159
|
+
});
|
|
160
|
+
const excluded = [];
|
|
161
|
+
for (const schemaName of Object.keys(tableMap)) {
|
|
162
|
+
const table = tableMap[schemaName];
|
|
163
|
+
const tableMatchers = matchers.filter((m) => m.table.test(table.name));
|
|
164
|
+
if (tableMatchers.length === 0)
|
|
165
|
+
continue;
|
|
166
|
+
for (const field of Object.keys(table.fields)) {
|
|
167
|
+
const columnName = table.colMap[field];
|
|
168
|
+
if (!tableMatchers.some((m) => m.column.test(columnName)))
|
|
169
|
+
continue;
|
|
170
|
+
if (table.primary.includes(field)) {
|
|
171
|
+
throw new Error(`Invalid config: excludeColumns cannot exclude the primary key column ` +
|
|
172
|
+
`"${table.name}.${columnName}" — every route identifies rows with it.`);
|
|
173
|
+
}
|
|
174
|
+
delete table.fields[field];
|
|
175
|
+
delete table.colMap[field];
|
|
176
|
+
table.generated = table.generated.filter((f) => f !== field);
|
|
177
|
+
excluded.push(`${table.name}.${columnName}`);
|
|
178
|
+
}
|
|
179
|
+
if (Object.keys(table.fields).length === 0) {
|
|
180
|
+
throw new Error(`Invalid config: excludeColumns leaves table "${table.name}" with no columns. ` +
|
|
181
|
+
`To hide a whole table use excludeTables instead.`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return excluded;
|
|
185
|
+
}
|
|
123
186
|
export function generateSchemaFile(schemaName, tableName, fields, colMap, primary, generated, isView) {
|
|
124
187
|
const fieldsStr = JSON.stringify(fields, null, 2).replace(/"(.*?)"\s*:\s*"Type(.*?)"/g, '$1: Type$2');
|
|
125
188
|
const colMapStr = JSON.stringify(colMap, null, 2);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"table-helpers.d.ts","sourceRoot":"","sources":["../../src/lib/table-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,KAAK,yBAAyB,EAAiC,MAAM,wBAAwB,CAAC;AACzH,OAAO,EAAQ,KAAK,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,mBAAmB,CAAC;AACrE,OAAO,KAAK,EACV,gBAAgB,EAChB,cAAc,EACd,aAAa,EACb,MAAM,EAEP,MAAM,aAAa,CAAC;AAcrB,KAAK,UAAU,CAAC,EAAE,IAAI,EAAE,SAAS,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,EAAE,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAExH,wBAAgB,eAAe,CAC7B,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,EAAE,SAAS,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,EAErE,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC3B,YAAY,GAAE,EAAa,EAC3B,iBAAiB,CAAC,EAAE,CAClB,SAAS,EAAE,gBAAgB,EAC3B,OAAO,EAAE;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,MAAM,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,yBAAyB;CAAE,KAC3E,IAAI,GACR;IAAE,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,CAAC;IAAC,OAAO,EAAE,aAAa,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAsChG;AAED,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC3D,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,GAChB,MAAM,CAAC,CAAC,CAAC,CAWX;
|
|
1
|
+
{"version":3,"file":"table-helpers.d.ts","sourceRoot":"","sources":["../../src/lib/table-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,KAAK,yBAAyB,EAAiC,MAAM,wBAAwB,CAAC;AACzH,OAAO,EAAQ,KAAK,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,mBAAmB,CAAC;AACrE,OAAO,KAAK,EACV,gBAAgB,EAChB,cAAc,EACd,aAAa,EACb,MAAM,EAEP,MAAM,aAAa,CAAC;AAcrB,KAAK,UAAU,CAAC,EAAE,IAAI,EAAE,SAAS,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,EAAE,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAExH,wBAAgB,eAAe,CAC7B,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,EAAE,SAAS,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,EAErE,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC3B,YAAY,GAAE,EAAa,EAC3B,iBAAiB,CAAC,EAAE,CAClB,SAAS,EAAE,gBAAgB,EAC3B,OAAO,EAAE;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,MAAM,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,yBAAyB;CAAE,KAC3E,IAAI,GACR;IAAE,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,CAAC;IAAC,OAAO,EAAE,aAAa,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAsChG;AAED,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC3D,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,GAChB,MAAM,CAAC,CAAC,CAAC,CAWX;AAiSD,MAAM,WAAW,oBAAoB;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;;;;;;;;;;OAaG;IACH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAgDD,wBAAgB,aAAa,CAC3B,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAEjC,UAAU,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC/B,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC,EAAE,EAClD,UAAU,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC/B,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC,EAC3B,OAAO,CAAC,EAAE,oBAAoB,GAC7B,cAAc,CAWhB;AAED,wBAAgB,eAAe,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/D,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC3B,OAAO,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC,EAAE,GAC5B,CAAC,gBAAgB,EAAE,MAAM,EAAE,CAAC,CAE9B;AAED,wBAAgB,gBAAgB,CAC9B,GAAG,KAAK,EAAE,CAAC,gBAAgB,EAAE,MAAM,EAAE,CAAC,EAAE,GACvC,GAAG,CAAC,gBAAgB,EAAE,MAAM,EAAE,CAAC,CAEjC"}
|
|
@@ -250,7 +250,8 @@ function validateWriteExclude(config) {
|
|
|
250
250
|
}
|
|
251
251
|
if (config.readExclude?.includes(field)) {
|
|
252
252
|
throw new Error(`defineTable: field '${field}' on table '${table}' is in both readExclude and ` +
|
|
253
|
-
`writeExclude, leaving it neither readable nor writable.
|
|
253
|
+
`writeExclude, leaving it neither readable nor writable. Exclude it at generation time ` +
|
|
254
|
+
`with excludeColumns in sqlapi.config.ts instead.`);
|
|
254
255
|
}
|
|
255
256
|
}
|
|
256
257
|
}
|
package/dist/lib/tenant.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tenant.d.ts","sourceRoot":"","sources":["../../src/lib/tenant.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,EAAE,gBAAgB,EAAE,KAAK,cAAc,EAAE,KAAK,yBAAyB,EAAE,MAAM,wBAAwB,CAAC;AAE/G,OAAO,KAAK,EAEV,MAAM,EACN,mBAAmB,EACnB,QAAQ,EACR,WAAW,EACX,iBAAiB,EACjB,mBAAmB,EACnB,gBAAgB,EAChB,aAAa,EACd,MAAM,aAAa,CAAC;AAwCrB,wBAAsB,aAAa,CACjC,OAAO,EAAE,mBAAmB,EAC5B,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,UAAU,GAClB,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAWpC;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,EAAE,EAAE,WAAW,EACf,KAAK,EAAE,WAAW,EAClB,SAAS,EAAE,QAAQ,EAAE,EACrB,QAAQ,EAAE,MAAM,GACf,gBAAgB,CAgBlB;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,aAAa,GAAG,SAAS,EACjC,SAAS,EAAE,MAAM,GAAG,SAAS,GAC5B,aAAa,GAAG,SAAS,CAI3B;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CACjC,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,aAAa,GAAG,SAAS,EACjC,QAAQ,EAAE,MAAM,GACf,gBAAgB,GAAG,SAAS,CAoB9B;AAED,wBAAgB,eAAe,CAC7B,EAAE,EAAE,WAAW,EACf,KAAK,EAAE,mBAAmB,EAC1B,aAAa,EAAE,MAAM,GACpB,MAAM,CAMR;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,KAAK,EAAE,gBAAgB,EACvB,SAAS,EAAE,QAAQ,EAAE,GACpB,IAAI,CAYN;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,KAAK,EAAE,iBAAiB,GAAG,mBAAmB,EAC9C,SAAS,EAAE,QAAQ,EAAE,GACpB,IAAI,CAiBN;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,WAAW,EACf,KAAK,EAAE,mBAAmB,EAC1B,SAAS,EAAE,QAAQ,EAAE,EACrB,QAAQ,EAAE,OAAO,EAAE,GAClB,OAAO,CAAC,IAAI,CAAC,CAyBf;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,WAAW,EACf,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,yBAAyB,GAAG,cAAc,EAAE,EACrD,MAAM,EAAE,aAAa,GACpB;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,EAAE,CAAA;CAAE,CAwBtC;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,yBAAyB,CAC7C,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,aAAa,GAAG,SAAS,EACjC,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,EAAE,EACtB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GACjC,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"tenant.d.ts","sourceRoot":"","sources":["../../src/lib/tenant.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/tenant.js
CHANGED
|
@@ -283,10 +283,16 @@ export async function assertTenantOwnsConflicts(db, tenant, tableName, conflictC
|
|
|
283
283
|
};
|
|
284
284
|
let mismatchSql;
|
|
285
285
|
if (tenantCols.length === 1) {
|
|
286
|
-
|
|
286
|
+
const col = tenantCols[0];
|
|
287
|
+
const notIn = `${col} NOT IN (${tenant.ids.map((id) => {
|
|
287
288
|
values.push(id);
|
|
288
289
|
return db.ph(values.length);
|
|
289
290
|
}).join(', ')})`;
|
|
291
|
+
// A NULL owner makes `col NOT IN (...)` return NULL, not TRUE, so an unowned row would slip
|
|
292
|
+
// past the probe and be claimable/overwritten by the upsert. Treat NULL as "not mine" — the
|
|
293
|
+
// same way the multi-column branch (NOT COALESCE(..., FALSE)) and the read side already do —
|
|
294
|
+
// so a NULL-owner conflict row is flagged as foreign.
|
|
295
|
+
mismatchSql = `(${col} IS NULL OR ${notIn})`;
|
|
290
296
|
}
|
|
291
297
|
else {
|
|
292
298
|
// A NULL party makes `IN` return NULL, and `NOT NULL` is not TRUE — a plain negation would
|
package/dist/types/cli.d.ts
CHANGED
|
@@ -10,6 +10,16 @@ export interface SqlApiConfig {
|
|
|
10
10
|
* Excluded schemas already on disk are removed as orphans on the next full run.
|
|
11
11
|
*/
|
|
12
12
|
excludeTables?: string[];
|
|
13
|
+
/**
|
|
14
|
+
* Columns to leave out of the generated Schemas (`sqlapi-generate-schema`), as
|
|
15
|
+
* `"<table>.<column>"` entries matched against the DB names; `*` is a wildcard on either
|
|
16
|
+
* side (e.g. "user.password_hash", "*.internal_*"). An excluded column does not exist for
|
|
17
|
+
* the API at all — no read, no write, no filter — unlike `readExclude` / `writeExclude`
|
|
18
|
+
* on the table config, which hide one side of a column that stays in the Schema. A primary
|
|
19
|
+
* key column cannot be excluded, and a table left with no columns is refused: use
|
|
20
|
+
* `excludeTables` for that.
|
|
21
|
+
*/
|
|
22
|
+
excludeColumns?: string[];
|
|
13
23
|
}
|
|
14
24
|
export interface ColumnInfo {
|
|
15
25
|
table_name: string;
|
package/dist/types/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/types/cli.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/types/cli.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,UAAU;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,yFAAyF;IACzF,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B;;;;;OAKG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,QAAQ;IACvB,CAAC,UAAU,EAAE,MAAM,GAAG;QACpB,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAC/B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAC/B,iFAAiF;QACjF,OAAO,EAAE,MAAM,EAAE,CAAC;QAClB,mFAAmF;QACnF,SAAS,EAAE,MAAM,EAAE,CAAC;QACpB,oFAAoF;QACpF,MAAM,EAAE,OAAO,CAAC;KACjB,CAAC;CACH"}
|
package/dist/types/plugin.d.ts
CHANGED
|
@@ -14,6 +14,27 @@ export interface SqlApiPluginOptions {
|
|
|
14
14
|
prefix?: string;
|
|
15
15
|
swagger?: boolean | SwaggerOptions;
|
|
16
16
|
dialect?: DialectName;
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the caller's tenant id(s) for this request. Returning one id (or an array, for a
|
|
19
|
+
* caller who belongs to several tenants) scopes every read and write to rows that tenant owns.
|
|
20
|
+
*
|
|
21
|
+
* ⚠️ **Returning `null`/`undefined` means ADMIN — no tenant filtering at all, i.e. full
|
|
22
|
+
* cross-tenant access.** "Unauthenticated" and "admin" therefore collapse to the same value.
|
|
23
|
+
* The tenant identity is read from the request (typically `request.user`, populated by your own
|
|
24
|
+
* auth hook in {@link onRequests}), which this plugin does not set. If that hook is missing,
|
|
25
|
+
* mis-ordered, or fails, `request.user` is `undefined` and the common `?? null` pattern yields
|
|
26
|
+
* `null` → the request is served as admin. Nothing here fails closed.
|
|
27
|
+
*
|
|
28
|
+
* To fail closed, make this callback (or the auth hook) **reject when there is no authenticated
|
|
29
|
+
* principal** instead of returning `null`, and reserve `null` for a real, authorized superadmin:
|
|
30
|
+
*
|
|
31
|
+
* ```ts
|
|
32
|
+
* getTenantId: (req) => {
|
|
33
|
+
* if (!req.user) throw app.httpErrors.unauthorized(); // no principal → deny, never admin
|
|
34
|
+
* return req.user.isSuperadmin ? null : req.user.organizationId;
|
|
35
|
+
* }
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
17
38
|
getTenantId?: (request: ApiRequest) => TenantId | TenantId[] | null | undefined | Promise<TenantId | TenantId[] | null | undefined>;
|
|
18
39
|
debug?: boolean;
|
|
19
40
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../src/types/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AACzD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,QAAQ,CAAC;IACnB,UAAU,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,KAAK,OAAO,CAAC,IAAI,GAAG,QAAQ,CAAC,CAAC,EAAE,CAAC;IACpF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,GAAG,cAAc,CAAC;IACnC,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,UAAU,KAAK,QAAQ,GAAG,QAAQ,EAAE,GAAG,IAAI,GAAG,SAAS,GAC3E,OAAO,CAAC,QAAQ,GAAG,QAAQ,EAAE,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;IACtD,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;;;;;;;;;;;;;;;OAiBG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,qGAAqG;AACrG,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAE/C,8EAA8E;AAC9E,eAAO,MAAM,sBAAsB,OAAO,CAAC"}
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../src/types/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AACzD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,QAAQ,CAAC;IACnB,UAAU,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,KAAK,OAAO,CAAC,IAAI,GAAG,QAAQ,CAAC,CAAC,EAAE,CAAC;IACpF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,GAAG,cAAc,CAAC;IACnC,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,UAAU,KAAK,QAAQ,GAAG,QAAQ,EAAE,GAAG,IAAI,GAAG,SAAS,GAC3E,OAAO,CAAC,QAAQ,GAAG,QAAQ,EAAE,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;IACtD,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;;;;;;;;;;;;;;;OAiBG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,qGAAqG;AACrG,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAE/C,8EAA8E;AAC9E,eAAO,MAAM,sBAAsB,OAAO,CAAC"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# 0017. The database schema is the API contract — by default, and only by default
|
|
2
2
|
|
|
3
|
-
- **Status**: accepted
|
|
3
|
+
- **Status**: accepted (the "trim it out of the Schema" lever below is `excludeColumns` since
|
|
4
|
+
[0019](./0019-column-exclusion-is-generation-time-config.md) — the Schema file is never edited)
|
|
4
5
|
- **Date**: 2026-08-28
|
|
5
6
|
|
|
6
7
|
## Context
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# 0019. Removing a column from the API is generation-time config — `excludeColumns`, never a Schema edit
|
|
2
|
+
|
|
3
|
+
- **Status**: accepted
|
|
4
|
+
- **Date**: 2026-09-13
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
[ADR 0017](./0017-the-schema-is-the-contract.md) lists one lever per way the exposed shape may
|
|
9
|
+
differ from the stored one. For *"a column must not exist for the API at all"* it said: trim it
|
|
10
|
+
out of the Schema. That lever did not exist. Schema files are disposable mirrors regenerated on
|
|
11
|
+
every run ([ADR 0007](./0007-explicit-table-generation.md), [ADR 0009](./0009-schema-exclusion-config-blacklist.md)),
|
|
12
|
+
so a trimmed Schema survived exactly until the next `sqlapi-generate-schema`. The two runtime
|
|
13
|
+
levers do not cover the case either: `readExclude` hides one side, `writeExclude` the other, and
|
|
14
|
+
`defineTable` refuses a field in both — because a column that is neither readable nor writable
|
|
15
|
+
still sits in the validation schema, the Swagger output and the manifest, where it is a lie.
|
|
16
|
+
|
|
17
|
+
## Decision
|
|
18
|
+
|
|
19
|
+
Column exclusion is a generation-time config, next to `excludeTables`: `excludeColumns:
|
|
20
|
+
string[]` in `sqlapi.config.ts`, entries of the form `"<table>.<column>"`, both parts matched
|
|
21
|
+
against the DB names with `*` as the only wildcard. A matched column is removed from the
|
|
22
|
+
generated Schema — its fields, its `colMap`, its `generatedFields` — so everything built from
|
|
23
|
+
the Schema (routes, filters, ordering, joins, Swagger, manifest, TypeScript types) is unaware of
|
|
24
|
+
it. There is nothing to hide at runtime because there is nothing there.
|
|
25
|
+
|
|
26
|
+
Two refusals, both loud at generation time:
|
|
27
|
+
|
|
28
|
+
- **A primary key column cannot be excluded.** Every route identifies rows with it; the table
|
|
29
|
+
generator would either invent a key or write a placeholder, and either is silent damage.
|
|
30
|
+
- **A table cannot be left with no columns.** That is what `excludeTables` is for, and one
|
|
31
|
+
mechanism per intent keeps the config readable.
|
|
32
|
+
|
|
33
|
+
`excludeColumns` runs after `excludeTables`, so a column pattern is never checked against a
|
|
34
|
+
table the generator will not see — a `*.id` glob does not trip on the primary key of a table
|
|
35
|
+
that is excluded anyway.
|
|
36
|
+
|
|
37
|
+
## Alternatives considered
|
|
38
|
+
|
|
39
|
+
- **Editing the generated Schema file** — the documented answer until now, rejected: the file
|
|
40
|
+
is overwritten on every run, so the edit is a hand-maintained exception under `schemas/`,
|
|
41
|
+
exactly what ADR 0009 refused to support for whole tables.
|
|
42
|
+
- **A runtime `exclude` key on `defineTable`** — rejected: the Schema is imported by the
|
|
43
|
+
consumer's code before `defineTable` runs, so its TypeScript type, its `validation` object and
|
|
44
|
+
the `$id`-registered JSON Schema would still carry the column. A runtime lever can only hide;
|
|
45
|
+
the column would remain visible in Swagger, in the manifest and in the `TypeSchema` type, and
|
|
46
|
+
every reader of those would have to know about the second layer.
|
|
47
|
+
- **Allowing `readExclude` + `writeExclude` on the same field** — rejected for the same reason:
|
|
48
|
+
it is the runtime lever again, with the added cost of a rule that means "does not exist" but
|
|
49
|
+
reads as two independent hidings.
|
|
50
|
+
- **A per-column option inside the Schema file** — rejected: the Schema file carries no config,
|
|
51
|
+
by design; `sqlapi.config.ts` is where the generator's inputs already live.
|
|
52
|
+
|
|
53
|
+
## Consequences
|
|
54
|
+
|
|
55
|
+
- Adding an entry to `excludeColumns` changes the generated Schema on the next run and, for
|
|
56
|
+
TypeScript consumers, fails compilation wherever the column was referenced. Intended: that is
|
|
57
|
+
ADR 0017's "checked, not implied" at work.
|
|
58
|
+
- The relation-level narrowing described in [ADR 0010](./0010-joins-do-not-run-route-guards.md)
|
|
59
|
+
— a relation declared with a narrower Schema object — is a different tool for a different
|
|
60
|
+
scope (per relation, not per API) and is unaffected.
|
|
61
|
+
- An `excludeColumns` entry that is not `table.column`, that reaches a primary key, or that
|
|
62
|
+
empties a table fails the run with an explicit error rather than being ignored.
|
package/docs/adr/README.md
CHANGED
|
@@ -33,6 +33,7 @@ not by editing history.
|
|
|
33
33
|
| [0016](./0016-a-view-is-a-table-config.md) | A view is a table config — the generator refuses to invent its primary key |
|
|
34
34
|
| [0017](./0017-the-schema-is-the-contract.md) | The database schema is the API contract — by default, and only by default |
|
|
35
35
|
| [0018](./0018-one-seam-to-the-http-framework.md) | The HTTP framework is reachable through one seam — `ApiRequest` |
|
|
36
|
+
| [0019](./0019-column-exclusion-is-generation-time-config.md) | Removing a column from the API is generation-time config — `excludeColumns`, never a Schema edit |
|
|
36
37
|
|
|
37
38
|
## When to write one
|
|
38
39
|
|