fastify-auto-sqlapi 0.2.0 → 0.2.2
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 +50 -0
- package/AGENTS_BACKEND.md +312 -9
- package/AGENTS_FRONTEND.md +1 -1
- package/BREAKING_CHANGES.md +132 -0
- package/CHANGELOG.md +170 -0
- package/README.md +255 -877
- package/dist/bin/generate-schema.js +49 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/agent/manifest.d.ts +2 -0
- package/dist/lib/agent/manifest.d.ts.map +1 -1
- package/dist/lib/agent/manifest.js +4 -0
- package/dist/lib/cli/mysql-introspect.d.ts.map +1 -1
- package/dist/lib/cli/mysql-introspect.js +15 -5
- package/dist/lib/cli/pg-introspect.d.ts.map +1 -1
- package/dist/lib/cli/pg-introspect.js +53 -14
- package/dist/lib/cli/schema-codegen.d.ts +2 -1
- package/dist/lib/cli/schema-codegen.d.ts.map +1 -1
- package/dist/lib/cli/schema-codegen.js +17 -4
- package/dist/lib/cli/tables-codegen.d.ts +4 -0
- package/dist/lib/cli/tables-codegen.d.ts.map +1 -1
- package/dist/lib/cli/tables-codegen.js +125 -27
- package/dist/lib/db.d.ts +6 -1
- package/dist/lib/db.d.ts.map +1 -1
- package/dist/lib/db.js +8 -2
- package/dist/lib/engine/bulk/bulk-upsert.d.ts.map +1 -1
- package/dist/lib/engine/bulk/bulk-upsert.js +6 -5
- package/dist/lib/engine/read-hooks.d.ts +19 -0
- package/dist/lib/engine/read-hooks.d.ts.map +1 -0
- package/dist/lib/engine/read-hooks.js +20 -0
- package/dist/lib/engine/rest/get.d.ts.map +1 -1
- package/dist/lib/engine/rest/get.js +10 -6
- package/dist/lib/engine/rest/insert.js +1 -1
- package/dist/lib/engine/rest/update.d.ts.map +1 -1
- package/dist/lib/engine/rest/update.js +15 -11
- package/dist/lib/engine/search/join-families.d.ts +11 -2
- package/dist/lib/engine/search/join-families.d.ts.map +1 -1
- package/dist/lib/engine/search/join-families.js +11 -13
- package/dist/lib/engine/search/search.d.ts.map +1 -1
- package/dist/lib/engine/search/search.js +14 -7
- package/dist/lib/engine/validate.d.ts +3 -3
- package/dist/lib/engine/validate.d.ts.map +1 -1
- package/dist/lib/engine/write-helpers.d.ts +7 -3
- package/dist/lib/engine/write-helpers.d.ts.map +1 -1
- package/dist/lib/engine/write-helpers.js +29 -30
- package/dist/lib/errors.d.ts +24 -0
- package/dist/lib/errors.d.ts.map +1 -1
- package/dist/lib/errors.js +59 -0
- package/dist/lib/schema/bulk-upsert.d.ts.map +1 -1
- package/dist/lib/schema/bulk-upsert.js +2 -1
- package/dist/lib/schema/get.d.ts +14 -0
- package/dist/lib/schema/get.d.ts.map +1 -0
- package/dist/lib/schema/get.js +18 -0
- package/dist/lib/schema/helpers.d.ts +21 -3
- package/dist/lib/schema/helpers.d.ts.map +1 -1
- package/dist/lib/schema/helpers.js +37 -5
- package/dist/lib/schema/insert.d.ts.map +1 -1
- package/dist/lib/schema/insert.js +7 -3
- package/dist/lib/schema/search.d.ts.map +1 -1
- package/dist/lib/schema/search.js +3 -11
- package/dist/lib/schema/update.d.ts.map +1 -1
- package/dist/lib/schema/update.js +7 -3
- package/dist/lib/sql-api.d.ts +9 -9
- package/dist/lib/sql-api.d.ts.map +1 -1
- package/dist/lib/sql-api.js +2 -1
- package/dist/lib/table-helpers.d.ts.map +1 -1
- package/dist/lib/table-helpers.js +169 -0
- package/dist/lib/tenant.d.ts +43 -8
- package/dist/lib/tenant.d.ts.map +1 -1
- package/dist/lib/tenant.js +169 -54
- package/dist/lib/write-access.d.ts +36 -0
- package/dist/lib/write-access.d.ts.map +1 -0
- package/dist/lib/write-access.js +58 -0
- package/dist/routes/auto/get.routes.d.ts.map +1 -1
- package/dist/routes/auto/get.routes.js +3 -2
- package/dist/routes/auto/plugin.d.ts.map +1 -1
- package/dist/routes/auto/plugin.js +8 -2
- package/dist/routes/auto/route-helpers.d.ts.map +1 -1
- package/dist/routes/auto/route-helpers.js +13 -4
- package/dist/routes/setup-swagger.d.ts.map +1 -0
- package/dist/{lib → routes}/sql-api-decorator.d.ts +1 -1
- package/dist/routes/sql-api-decorator.d.ts.map +1 -0
- package/dist/{lib → routes}/sql-api-decorator.js +4 -4
- package/dist/types/cli.d.ts +27 -1
- package/dist/types/cli.d.ts.map +1 -1
- package/dist/types/dml.d.ts +6 -4
- package/dist/types/dml.d.ts.map +1 -1
- package/dist/types/plugin.d.ts +22 -3
- package/dist/types/plugin.d.ts.map +1 -1
- package/dist/types/request.d.ts +24 -0
- package/dist/types/request.d.ts.map +1 -0
- package/dist/types/request.js +1 -0
- package/dist/types/schema.d.ts +15 -0
- package/dist/types/schema.d.ts.map +1 -1
- package/dist/types/search.d.ts +3 -0
- package/dist/types/search.d.ts.map +1 -1
- package/dist/types/table.d.ts +84 -11
- package/dist/types/table.d.ts.map +1 -1
- package/dist/types/tenant.d.ts +26 -2
- package/dist/types/tenant.d.ts.map +1 -1
- package/dist/types/validation.d.ts +3 -3
- package/dist/types/validation.d.ts.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -0
- package/docs/GLOSSARY.md +3 -1
- package/docs/adr/0004-updates-always-open.md +2 -1
- package/docs/adr/0006-raw-db-errors.md +2 -1
- package/docs/adr/0010-joins-do-not-run-route-guards.md +4 -1
- package/docs/adr/0013-sanitized-db-errors.md +114 -0
- package/docs/adr/0014-what-follows-a-write-join.md +84 -0
- package/docs/adr/0015-non-writable-columns.md +86 -0
- package/docs/adr/0016-a-view-is-a-table-config.md +92 -0
- package/docs/adr/0017-the-schema-is-the-contract.md +85 -0
- package/docs/adr/0018-one-seam-to-the-http-framework.md +68 -0
- package/docs/adr/README.md +6 -0
- package/package.json +1 -1
- package/dist/lib/setup-swagger.d.ts.map +0 -1
- package/dist/lib/sql-api-decorator.d.ts.map +0 -1
- /package/dist/{lib → routes}/setup-swagger.d.ts +0 -0
- /package/dist/{lib → routes}/setup-swagger.js +0 -0
|
@@ -37,10 +37,10 @@ function fileStatus(filename, content) {
|
|
|
37
37
|
function writeSchemaFiles(tableMap, schemasDir) {
|
|
38
38
|
const report = { created: false, touched: false, untouched: 0, files: new Set() };
|
|
39
39
|
for (const schemaName of Object.keys(tableMap)) {
|
|
40
|
-
const { name: tableName, fields, colMap, primary } = tableMap[schemaName];
|
|
40
|
+
const { name: tableName, fields, colMap, primary, generated, isView } = tableMap[schemaName];
|
|
41
41
|
const filename = path.join(schemasDir, `${schemaName}.ts`);
|
|
42
42
|
report.files.add(`${schemaName}.ts`);
|
|
43
|
-
const content = generateSchemaFile(schemaName, tableName, fields, colMap, primary);
|
|
43
|
+
const content = generateSchemaFile(schemaName, tableName, fields, colMap, primary, generated, isView);
|
|
44
44
|
const status = fileStatus(filename, content);
|
|
45
45
|
if (status === 'created')
|
|
46
46
|
report.created = true;
|
|
@@ -71,6 +71,51 @@ function removeOrphanSchemas(schemasDir, generated) {
|
|
|
71
71
|
}
|
|
72
72
|
return removed;
|
|
73
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* Name the columns the database computes, table by table.
|
|
76
|
+
*
|
|
77
|
+
* These are the columns an INSERT or an UPDATE may not mention — both engines reject the
|
|
78
|
+
* statement — so the generator leaves them out of every write body. Saying so here matters
|
|
79
|
+
* more than the comment it also writes into the file: the generated source is not always
|
|
80
|
+
* read, whereas this output is what whoever ran the command actually sees.
|
|
81
|
+
*/
|
|
82
|
+
function reportGeneratedColumns(tableMap) {
|
|
83
|
+
const withGenerated = Object.keys(tableMap)
|
|
84
|
+
.map((schemaName) => tableMap[schemaName])
|
|
85
|
+
.filter((t) => t.generated.length > 0);
|
|
86
|
+
if (withGenerated.length === 0)
|
|
87
|
+
return;
|
|
88
|
+
console.log('');
|
|
89
|
+
display('Database-computed columns found (GENERATED ALWAYS AS ...):', CONSOLE_COLORS.yellow);
|
|
90
|
+
for (const table of withGenerated) {
|
|
91
|
+
display(` ${table.name}: ${table.generated.join(', ')}`, CONSOLE_COLORS.yellow);
|
|
92
|
+
}
|
|
93
|
+
display(' They are readable but rejected by every write, so they are excluded from the insert, ' +
|
|
94
|
+
'update and bulk bodies. Do not send them: they are computed by the database.', CONSOLE_COLORS.gray);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Name the relations that are views.
|
|
98
|
+
*
|
|
99
|
+
* The generated `Table*.ts` says the same thing in a comment, but a generated source file is
|
|
100
|
+
* not always read — this output is what whoever ran the command actually sees, and the two
|
|
101
|
+
* assumptions worth contesting (which column is the key, whether writes make sense) are both
|
|
102
|
+
* assumptions the generator had to make without the database's help.
|
|
103
|
+
*/
|
|
104
|
+
function reportViews(tableMap) {
|
|
105
|
+
const views = Object.keys(tableMap)
|
|
106
|
+
.map((schemaName) => tableMap[schemaName])
|
|
107
|
+
.filter((t) => t.isView);
|
|
108
|
+
if (views.length === 0)
|
|
109
|
+
return;
|
|
110
|
+
console.log('');
|
|
111
|
+
display(`Views found: ${views.map((v) => v.name).join(', ')}`, CONSOLE_COLORS.yellow);
|
|
112
|
+
display(' A view is a table config like any other, but the database reports no PRIMARY KEY for ' +
|
|
113
|
+
'one. The table generator will NOT invent one: it uses "id" when the view has it and says ' +
|
|
114
|
+
'so, and otherwise writes a TODO placeholder. Check the key is really unique in the view ' +
|
|
115
|
+
'before exposing get/update/delete — those address rows by it.', CONSOLE_COLORS.gray);
|
|
116
|
+
display(' Writes are left off by default: only a view simple enough for the engine to make ' +
|
|
117
|
+
'updatable accepts one, and an aggregating view rejects every INSERT and UPDATE.', CONSOLE_COLORS.gray);
|
|
118
|
+
}
|
|
74
119
|
function reportOutcome(report) {
|
|
75
120
|
console.log('');
|
|
76
121
|
if (report.untouched > 0) {
|
|
@@ -117,6 +162,8 @@ await runCli('fastify-auto-sqlapi: generating schemas', async () => {
|
|
|
117
162
|
display(`No matching tables found for: ${tableNames.join(', ')}`, CONSOLE_COLORS.magenta);
|
|
118
163
|
return;
|
|
119
164
|
}
|
|
165
|
+
reportViews(tableMap);
|
|
166
|
+
reportGeneratedColumns(tableMap);
|
|
120
167
|
const report = writeSchemaFiles(tableMap, schemasDir);
|
|
121
168
|
if (!tableNames.length && removeOrphanSchemas(schemasDir, report.files)) {
|
|
122
169
|
report.touched = true;
|
package/dist/index.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export { resolveTenant } from './lib/tenant.js';
|
|
|
12
12
|
export { SqlApi, createSqlApi } from './lib/sql-api.js';
|
|
13
13
|
export type { SqlApiOptions, SqlApiSearchParams, SqlApiInsertParams, SqlApiUpdateParams } from './lib/sql-api.js';
|
|
14
14
|
export { default as fastifyAutoSqlApi } from './routes/auto/plugin.js';
|
|
15
|
-
export { setupSwagger } from './
|
|
15
|
+
export { setupSwagger } from './routes/setup-swagger.js';
|
|
16
16
|
export { searchEngine } from './lib/engine/search/search.js';
|
|
17
17
|
export { SearchTableBodyPost, SearchTableQuery, SearchTableQueryString, SearchTableResponse } from './lib/schema/search.js';
|
|
18
18
|
export { default as searchRoutes } from './routes/auto/search.routes.js';
|
|
@@ -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, TenantContext, ValidationError, ValidatorFn, BulkValidatorItem, BulkValidatorFn, } from './types.js';
|
|
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';
|
|
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":"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,
|
|
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,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"}
|
package/dist/index.js
CHANGED
|
@@ -21,7 +21,7 @@ export { SqlApi, createSqlApi } from './lib/sql-api.js';
|
|
|
21
21
|
// Plugin
|
|
22
22
|
export { default as fastifyAutoSqlApi } from './routes/auto/plugin.js';
|
|
23
23
|
// Swagger
|
|
24
|
-
export { setupSwagger } from './
|
|
24
|
+
export { setupSwagger } from './routes/setup-swagger.js';
|
|
25
25
|
// Search
|
|
26
26
|
export { searchEngine } from './lib/engine/search/search.js';
|
|
27
27
|
export { SearchTableBodyPost, SearchTableQuery, SearchTableQueryString, SearchTableResponse } from './lib/schema/search.js';
|
|
@@ -7,6 +7,8 @@ interface ManifestField {
|
|
|
7
7
|
nullable?: boolean;
|
|
8
8
|
/** Present (true) when the field is writable but never returned by reads (readExclude). */
|
|
9
9
|
writeOnly?: boolean;
|
|
10
|
+
/** Present (true) when the field is returned by reads but refused by every write. */
|
|
11
|
+
readOnly?: boolean;
|
|
10
12
|
}
|
|
11
13
|
interface ManifestJoin {
|
|
12
14
|
alias: string;
|
|
@@ -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;
|
|
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;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,aAAa,GAAG,MAAM,CAkBrE"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isCompositePrimary } from '../../types.js';
|
|
2
|
+
import { writeExcludedFields } from '../write-access.js';
|
|
2
3
|
const ALL_OPERATIONS = [
|
|
3
4
|
'search', 'get', 'insert', 'update', 'delete', 'bulkUpsert', 'bulkDelete',
|
|
4
5
|
];
|
|
@@ -45,6 +46,7 @@ function manifestFields(tableConf) {
|
|
|
45
46
|
const schema = tableConf.Schema;
|
|
46
47
|
const required = new Set((schema.validation.required) ?? []);
|
|
47
48
|
const readExcluded = new Set(tableConf.readExclude ?? []);
|
|
49
|
+
const writeExcluded = writeExcludedFields(tableConf, schema);
|
|
48
50
|
const fields = {};
|
|
49
51
|
for (const [name, fieldSchema] of Object.entries(schema.fields)) {
|
|
50
52
|
const f = { type: shortType(fieldSchema) };
|
|
@@ -54,6 +56,8 @@ function manifestFields(tableConf) {
|
|
|
54
56
|
f.nullable = true;
|
|
55
57
|
if (readExcluded.has(name))
|
|
56
58
|
f.writeOnly = true;
|
|
59
|
+
if (writeExcluded.has(name))
|
|
60
|
+
f.readOnly = true;
|
|
57
61
|
fields[name] = f;
|
|
58
62
|
}
|
|
59
63
|
return fields;
|
|
@@ -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;AAsCD,wBAAgB,0BAA0B,IAAI,qBAAqB,CAmBlE;AAuDD,wBAAsB,qBAAqB,CACzC,gBAAgB,EAAE,qBAAqB,EACvC,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,UAAU,EAAE,CAAC,CAkCvB"}
|
|
@@ -65,14 +65,21 @@ function mapMysqlType(dataType) {
|
|
|
65
65
|
return 'varchar';
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
|
+
/** `EXTRA`, lowercased — MySQL and MariaDB disagree on the case of both key and value. */
|
|
69
|
+
function extra(row) {
|
|
70
|
+
return String(row.EXTRA || row.extra || '').toLowerCase();
|
|
71
|
+
}
|
|
68
72
|
export async function introspectMysqlTables(connectionConfig, schema) {
|
|
69
73
|
const mysql2 = loadOptionalDependency('mysql2/promise', 'npm install mysql2');
|
|
70
74
|
const connection = await mysql2.createConnection(connectionConfig);
|
|
71
75
|
try {
|
|
72
|
-
const [rows] = await connection.query(`SELECT table_name, column_name, data_type, column_default, is_nullable,
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
+
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
|
|
78
|
+
FROM information_schema.columns c
|
|
79
|
+
JOIN information_schema.tables t
|
|
80
|
+
ON t.table_schema = c.table_schema AND t.table_name = c.table_name
|
|
81
|
+
WHERE c.table_schema = ?
|
|
82
|
+
ORDER BY c.table_name, c.ordinal_position`, [schema]);
|
|
76
83
|
// `||` is kept as-is from the untyped version: this change types the driver, it does
|
|
77
84
|
// not alter behaviour. The trailing fallbacks only satisfy ColumnInfo, which cannot
|
|
78
85
|
// hold `undefined`.
|
|
@@ -83,7 +90,10 @@ export async function introspectMysqlTables(connectionConfig, schema) {
|
|
|
83
90
|
column_default: row.COLUMN_DEFAULT || row.column_default || null,
|
|
84
91
|
is_nullable: row.IS_NULLABLE || row.is_nullable || '',
|
|
85
92
|
is_primary: (row.COLUMN_KEY || row.column_key) === 'PRI',
|
|
86
|
-
is_auto_increment:
|
|
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',
|
|
87
97
|
}));
|
|
88
98
|
}
|
|
89
99
|
finally {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pg-introspect.d.ts","sourceRoot":"","sources":["../../../src/lib/cli/pg-introspect.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAmBjD,wBAAgB,qBAAqB,IAAI,MAAM,CAY9C;
|
|
1
|
+
{"version":3,"file":"pg-introspect.d.ts","sourceRoot":"","sources":["../../../src/lib/cli/pg-introspect.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAmBjD,wBAAgB,qBAAqB,IAAI,MAAM,CAY9C;AAcD,wBAAsB,gBAAgB,CACpC,gBAAgB,EAAE,MAAM,EACxB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,UAAU,EAAE,CAAC,CAmEvB"}
|
|
@@ -15,20 +15,59 @@ export async function introspectTables(connectionString, schema) {
|
|
|
15
15
|
const client = new pg.Client({ connectionString });
|
|
16
16
|
try {
|
|
17
17
|
await client.connect();
|
|
18
|
-
const result = await client.query(
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
18
|
+
const result = await client.query(
|
|
19
|
+
// The UNION picks up materialized views, which `information_schema` does not describe at
|
|
20
|
+
// all: they live only in `pg_catalog`, and without this half they were silently absent
|
|
21
|
+
// from every generation run. Their columns are cast to the same shapes the standard view
|
|
22
|
+
// returns, so the two halves stay union-compatible.
|
|
23
|
+
`SELECT * FROM (
|
|
24
|
+
SELECT c.table_name::text, c.column_name::text, c.udt_name::text,
|
|
25
|
+
c.column_default::text, c.is_nullable::text,
|
|
26
|
+
c.is_identity::text, c.is_generated::text,
|
|
27
|
+
(pk.column_name IS NOT NULL) AS is_primary,
|
|
28
|
+
(t.table_type = 'VIEW') AS is_view,
|
|
29
|
+
c.ordinal_position::int AS ordinal_position
|
|
30
|
+
FROM information_schema.columns c
|
|
31
|
+
JOIN information_schema.tables t
|
|
32
|
+
ON t.table_schema = c.table_schema AND t.table_name = c.table_name
|
|
33
|
+
LEFT JOIN (
|
|
34
|
+
SELECT kcu.table_name, kcu.column_name
|
|
35
|
+
FROM information_schema.table_constraints tc
|
|
36
|
+
JOIN information_schema.key_column_usage kcu
|
|
37
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
38
|
+
AND tc.table_schema = kcu.table_schema
|
|
39
|
+
WHERE tc.constraint_type = 'PRIMARY KEY' AND tc.table_schema = $1
|
|
40
|
+
) pk ON pk.table_name = c.table_name AND pk.column_name = c.column_name
|
|
41
|
+
WHERE c.table_schema = $1
|
|
42
|
+
UNION ALL
|
|
43
|
+
SELECT mc.relname::text, ma.attname::text, mt.typname::text,
|
|
44
|
+
NULL::text, CASE WHEN ma.attnotnull THEN 'NO' ELSE 'YES' END,
|
|
45
|
+
'NO'::text, 'NEVER'::text,
|
|
46
|
+
false, true, ma.attnum::int
|
|
47
|
+
FROM pg_class mc
|
|
48
|
+
JOIN pg_namespace mn ON mn.oid = mc.relnamespace
|
|
49
|
+
JOIN pg_attribute ma
|
|
50
|
+
ON ma.attrelid = mc.oid AND ma.attnum > 0 AND NOT ma.attisdropped
|
|
51
|
+
JOIN pg_type mt ON mt.oid = ma.atttypid
|
|
52
|
+
WHERE mc.relkind = 'm' AND mn.nspname = $1
|
|
53
|
+
) cols
|
|
54
|
+
ORDER BY table_name, ordinal_position`, [schema]);
|
|
55
|
+
// The query's own columns are spelled out rather than spread: `is_identity` and
|
|
56
|
+
// `is_generated` arrive as the standard's string enums and `ordinal_position` only exists
|
|
57
|
+
// to order the union, so none of the three belongs in the `ColumnInfo` handed back.
|
|
58
|
+
return result.rows.map((row) => ({
|
|
59
|
+
table_name: row.table_name,
|
|
60
|
+
column_name: row.column_name,
|
|
61
|
+
udt_name: row.udt_name,
|
|
62
|
+
column_default: row.column_default,
|
|
63
|
+
is_nullable: row.is_nullable,
|
|
64
|
+
is_primary: row.is_primary,
|
|
65
|
+
is_view: row.is_view,
|
|
66
|
+
// An identity column has no `column_default`, so without this flag it is emitted as a
|
|
67
|
+
// mandatory field and the table cannot be inserted into at all.
|
|
68
|
+
...(row.is_identity === 'YES' ? { is_auto_increment: true } : {}),
|
|
69
|
+
...(row.is_generated === 'ALWAYS' ? { is_generated: true } : {}),
|
|
70
|
+
}));
|
|
32
71
|
}
|
|
33
72
|
finally {
|
|
34
73
|
await client.end();
|
|
@@ -6,6 +6,7 @@ export declare function convertColType(udtName: string, columnInfo: {
|
|
|
6
6
|
column_default: string | null;
|
|
7
7
|
is_nullable: string;
|
|
8
8
|
is_auto_increment?: boolean;
|
|
9
|
+
is_generated?: boolean;
|
|
9
10
|
}): string;
|
|
10
11
|
export declare function buildTableMap(rows: ColumnInfo[]): TableMap;
|
|
11
12
|
/**
|
|
@@ -15,5 +16,5 @@ export declare function buildTableMap(rows: ColumnInfo[]): TableMap;
|
|
|
15
16
|
* Returns the names of the excluded tables, in map order.
|
|
16
17
|
*/
|
|
17
18
|
export declare function applyExcludeTables(tableMap: TableMap, excludeTables?: string[]): string[];
|
|
18
|
-
export declare function generateSchemaFile(schemaName: string, tableName: string, fields: Record<string, string>, colMap: Record<string, string>, primary?: string[]): string;
|
|
19
|
+
export declare function generateSchemaFile(schemaName: string, tableName: string, fields: Record<string, string>, colMap: Record<string, string>, primary?: string[], generated?: string[], isView?: boolean): string;
|
|
19
20
|
//# 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;
|
|
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;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,EAClB,SAAS,CAAC,EAAE,MAAM,EAAE,EACpB,MAAM,CAAC,EAAE,OAAO,GACf,MAAM,CAoDR"}
|
|
@@ -3,8 +3,10 @@ import { toCamelCase, toSchemaName } from '../naming.js';
|
|
|
3
3
|
* @testonly Exported only so unit tests can exercise it directly.
|
|
4
4
|
*/
|
|
5
5
|
export function convertColType(udtName, columnInfo) {
|
|
6
|
-
// AUTO_INCREMENT (mysql)
|
|
7
|
-
|
|
6
|
+
// AUTO_INCREMENT (mysql) and IDENTITY (postgres) behave like a default: the DB generates
|
|
7
|
+
// the value. A generated column goes further — it refuses one — so it can never be a
|
|
8
|
+
// mandatory field either, whatever its nullability says.
|
|
9
|
+
const hasDefault = columnInfo.column_default || columnInfo.is_auto_increment || columnInfo.is_generated;
|
|
8
10
|
const isNullable = columnInfo.is_nullable === 'YES';
|
|
9
11
|
const wrap = (t) => {
|
|
10
12
|
// Nullable columns need the type-array form (`Nullable(T)` → type: [..., 'null']):
|
|
@@ -69,6 +71,8 @@ export function buildTableMap(rows) {
|
|
|
69
71
|
fields: {},
|
|
70
72
|
colMap: {},
|
|
71
73
|
primary: [],
|
|
74
|
+
generated: [],
|
|
75
|
+
isView: row.is_view === true,
|
|
72
76
|
};
|
|
73
77
|
}
|
|
74
78
|
const fieldName = toCamelCase(row.column_name);
|
|
@@ -85,6 +89,9 @@ export function buildTableMap(rows) {
|
|
|
85
89
|
if (row.is_primary) {
|
|
86
90
|
acc[schemaName].primary.push(fieldName);
|
|
87
91
|
}
|
|
92
|
+
if (row.is_generated) {
|
|
93
|
+
acc[schemaName].generated.push(fieldName);
|
|
94
|
+
}
|
|
88
95
|
return acc;
|
|
89
96
|
}, {});
|
|
90
97
|
}
|
|
@@ -113,10 +120,16 @@ export function applyExcludeTables(tableMap, excludeTables) {
|
|
|
113
120
|
}
|
|
114
121
|
return excluded;
|
|
115
122
|
}
|
|
116
|
-
export function generateSchemaFile(schemaName, tableName, fields, colMap, primary) {
|
|
123
|
+
export function generateSchemaFile(schemaName, tableName, fields, colMap, primary, generated, isView) {
|
|
117
124
|
const fieldsStr = JSON.stringify(fields, null, 2).replace(/"(.*?)"\s*:\s*"Type(.*?)"/g, '$1: Type$2');
|
|
118
125
|
const colMapStr = JSON.stringify(colMap, null, 2);
|
|
119
126
|
const primaryKeyLine = primary?.length ? `\n primaryKey: ${JSON.stringify(primary)},` : '';
|
|
127
|
+
// Published so the table generator — and the runtime — know which columns the database
|
|
128
|
+
// computes: those are readable but reject every write.
|
|
129
|
+
const generatedLine = generated?.length ? `\n generatedFields: ${JSON.stringify(generated)},` : '';
|
|
130
|
+
// A view has no PRIMARY KEY constraint to introspect, so the table generator has to be told
|
|
131
|
+
// it may not read the absence of one as "no key exists on this data".
|
|
132
|
+
const isViewLine = isView ? `\n isView: true,` : '';
|
|
120
133
|
const usesNullable = Object.values(fields).some((t) => t.includes('Nullable('));
|
|
121
134
|
const valueImports = usesNullable ? 'Type, Nullable' : 'Type';
|
|
122
135
|
return `// Auto-generated by fastify-auto-sqlapi. Do not edit.
|
|
@@ -144,7 +157,7 @@ export const Schema = {
|
|
|
144
157
|
fields: _Schema,
|
|
145
158
|
validation,
|
|
146
159
|
tableName: "${tableName}",
|
|
147
|
-
partialValidation,${primaryKeyLine}
|
|
160
|
+
partialValidation,${primaryKeyLine}${generatedLine}${isViewLine}
|
|
148
161
|
};
|
|
149
162
|
export const ${schemaName} = Schema;
|
|
150
163
|
export type TypeSchema = Static<typeof Schema.validation>;
|
|
@@ -5,6 +5,10 @@ export interface ParsedSchema {
|
|
|
5
5
|
fieldTypes: Record<string, string>;
|
|
6
6
|
/** PRIMARY KEY fields declared in the schema file (`primaryKey: [...]`), when present. */
|
|
7
7
|
primary?: string[];
|
|
8
|
+
/** Database-computed fields declared in the schema file (`generatedFields: [...]`). */
|
|
9
|
+
generated?: string[];
|
|
10
|
+
/** True when the schema file declares `isView: true` — the relation is a view. */
|
|
11
|
+
isView?: boolean;
|
|
8
12
|
}
|
|
9
13
|
interface DetectedRelation {
|
|
10
14
|
parentSchemaName: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tables-codegen.d.ts","sourceRoot":"","sources":["../../../src/lib/cli/tables-codegen.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,0FAA0F;IAC1F,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"tables-codegen.d.ts","sourceRoot":"","sources":["../../../src/lib/cli/tables-codegen.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,0FAA0F;IAC1F,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,uFAAuF;IACvF,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,kFAAkF;IAClF,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,UAAU,gBAAgB;IACxB,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;CACpB;AAID,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAoCpE;AA4GD;;GAEG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,gBAAgB,EAAE,CAwB3E;AAgID,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,GAAG,MAAM,CAwFhG;AAID,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,MAAM,CAqBrE;AAqDD;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,MAAM,CAoClE"}
|
|
@@ -20,43 +20,91 @@ export function parseSchemaFile(content) {
|
|
|
20
20
|
}
|
|
21
21
|
if (fields.length === 0)
|
|
22
22
|
return null;
|
|
23
|
-
// primaryKey: ["code"] — emitted by generate-schema from
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
.split(',')
|
|
29
|
-
.map((s) => s.trim().replace(/^["']|["']$/g, ''))
|
|
30
|
-
.filter(Boolean);
|
|
31
|
-
if (primary.length === 0)
|
|
32
|
-
primary = undefined;
|
|
33
|
-
}
|
|
23
|
+
// primaryKey: ["code"] / generatedFields: ["total"] — emitted by generate-schema from
|
|
24
|
+
// DB introspection. Both are absent on hand-written schema files.
|
|
25
|
+
const primary = parseFieldList(content, 'primaryKey');
|
|
26
|
+
const generated = parseFieldList(content, 'generatedFields');
|
|
27
|
+
const isView = /isView:\s*true/.test(content);
|
|
34
28
|
return {
|
|
35
29
|
schemaName: exportMatch[1],
|
|
36
30
|
tableName: tableNameMatch[1],
|
|
37
31
|
fields,
|
|
38
32
|
fieldTypes,
|
|
39
33
|
primary,
|
|
34
|
+
generated,
|
|
35
|
+
...(isView ? { isView: true } : {}),
|
|
40
36
|
};
|
|
41
37
|
}
|
|
38
|
+
/** Read a `<key>: ["a", "b"]` list out of a generated schema file. */
|
|
39
|
+
function parseFieldList(content, key) {
|
|
40
|
+
const match = new RegExp(`${key}:\\s*\\[([^\\]]*)\\]`).exec(content);
|
|
41
|
+
if (!match)
|
|
42
|
+
return undefined;
|
|
43
|
+
const values = match[1]
|
|
44
|
+
.split(',')
|
|
45
|
+
.map((s) => s.trim().replace(/^["']|["']$/g, ''))
|
|
46
|
+
.filter(Boolean);
|
|
47
|
+
return values.length ? values : undefined;
|
|
48
|
+
}
|
|
42
49
|
// ─── Detection ───────────────────────────────────────────────
|
|
50
|
+
/** Stands in for a primary key nobody can infer. `defineTable` refuses it beyond search. */
|
|
51
|
+
const PK_PLACEHOLDER = 'TODO_pick_a_unique_column';
|
|
43
52
|
function detectPrimaryKey(schema) {
|
|
44
53
|
// Preferred source: the PK introspected from the DB (primaryKey in the schema file).
|
|
45
54
|
if (schema.primary?.length) {
|
|
46
55
|
const pk = schema.primary.length === 1 ? schema.primary[0] : schema.primary;
|
|
47
56
|
const first = schema.primary[0];
|
|
48
|
-
return {
|
|
57
|
+
return {
|
|
58
|
+
pk,
|
|
59
|
+
autoIncrement: schema.fieldTypes[first]?.includes('Optional') ?? false,
|
|
60
|
+
source: 'declared',
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
// A view carries no PRIMARY KEY constraint, so an absent one says nothing about the data.
|
|
64
|
+
// Only `id` is a strong enough signal to act on; the type-based fallback below would take
|
|
65
|
+
// any integer column — a count, a year — and hand back a key that is not unique, which
|
|
66
|
+
// `get/:id`, update and delete would then address rows by. Guessing is the one thing that
|
|
67
|
+
// must not happen quietly here.
|
|
68
|
+
if (schema.isView) {
|
|
69
|
+
return schema.fields.includes('id')
|
|
70
|
+
? { pk: 'id', autoIncrement: false, source: 'inferred' }
|
|
71
|
+
: { pk: PK_PLACEHOLDER, autoIncrement: false, source: 'unresolved' };
|
|
49
72
|
}
|
|
50
73
|
// Heuristic fallback for hand-written/legacy schema files without primaryKey.
|
|
51
74
|
if (schema.fields.includes('id')) {
|
|
52
|
-
return {
|
|
75
|
+
return {
|
|
76
|
+
pk: 'id',
|
|
77
|
+
autoIncrement: schema.fieldTypes['id']?.includes('Optional') ?? false,
|
|
78
|
+
source: 'inferred',
|
|
79
|
+
};
|
|
53
80
|
}
|
|
54
81
|
for (const field of schema.fields) {
|
|
55
82
|
if (schema.fieldTypes[field]?.includes('Integer')) {
|
|
56
|
-
return {
|
|
83
|
+
return {
|
|
84
|
+
pk: field,
|
|
85
|
+
autoIncrement: schema.fieldTypes[field].includes('Optional'),
|
|
86
|
+
source: 'inferred',
|
|
87
|
+
};
|
|
57
88
|
}
|
|
58
89
|
}
|
|
59
|
-
return { pk: schema.fields[0], autoIncrement: false };
|
|
90
|
+
return { pk: schema.fields[0], autoIncrement: false, source: 'inferred' };
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The `writeExclude` slot, plus the list of columns the database computes for this table.
|
|
94
|
+
*
|
|
95
|
+
* Those are excluded from every write by the runtime already — the Schema publishes them in
|
|
96
|
+
* `generatedFields` — so the line stays commented: it is here to say *why* the fields are
|
|
97
|
+
* missing from the write bodies, and to give a place to add fields of your own.
|
|
98
|
+
*/
|
|
99
|
+
function writeExcludeLines(schema) {
|
|
100
|
+
if (!schema.generated?.length) {
|
|
101
|
+
return [' // writeExclude: [],'];
|
|
102
|
+
}
|
|
103
|
+
return [
|
|
104
|
+
` // Computed by the database (GENERATED ALWAYS AS ...): ${schema.generated.join(', ')}`,
|
|
105
|
+
' // — readable, never writable. Already excluded from every write body.',
|
|
106
|
+
' // writeExclude: [],',
|
|
107
|
+
];
|
|
60
108
|
}
|
|
61
109
|
/** First PK field: used for defaultOrder, relations and example snippets. */
|
|
62
110
|
function pkFirst(pk) {
|
|
@@ -157,9 +205,48 @@ function allowedReadJoinsLines(parentRels) {
|
|
|
157
205
|
lines.push(` // ],`);
|
|
158
206
|
return lines;
|
|
159
207
|
}
|
|
208
|
+
// ─── Views ───────────────────────────────────────────────────
|
|
209
|
+
/**
|
|
210
|
+
* The header a view's config gets: what the generator could not read from the database, and
|
|
211
|
+
* what it therefore assumed.
|
|
212
|
+
*/
|
|
213
|
+
function viewNotice(schema, source) {
|
|
214
|
+
if (!schema.isView)
|
|
215
|
+
return [];
|
|
216
|
+
const lines = [
|
|
217
|
+
`// "${schema.tableName}" is a VIEW. It is a table config like any other, but the database`,
|
|
218
|
+
`// reports no PRIMARY KEY for it and only a view the engine can make updatable accepts a`,
|
|
219
|
+
`// write — an aggregating one rejects every INSERT and UPDATE outright.`,
|
|
220
|
+
];
|
|
221
|
+
if (source === 'inferred') {
|
|
222
|
+
lines.push(`// The primary key below was inferred from the column names, NOT read from the database:`, `// check that it is actually unique in this view before exposing get/update/delete.`);
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
lines.push(`// No column was a plausible primary key, so none was invented. As generated this config`, `// serves search only, which never needs one. To expose get/update/delete, replace the`, `// placeholder with a column that is unique in this view and widen \`operations\`.`);
|
|
226
|
+
}
|
|
227
|
+
return lines;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* A view's route whitelist. Reads only: writing through a view works solely when the engine
|
|
231
|
+
* can make that view updatable, which is a property of the view's own SQL rather than of
|
|
232
|
+
* anything declared here — so the generated starting point is the one that always works, and
|
|
233
|
+
* widening it is a deliberate edit. This is not the runtime default (ADR 0002), which stays
|
|
234
|
+
* open: it is what the generator writes into a file the developer then owns.
|
|
235
|
+
*/
|
|
236
|
+
function viewOperationsLine(schema, source) {
|
|
237
|
+
if (!schema.isView)
|
|
238
|
+
return [];
|
|
239
|
+
return source === 'unresolved'
|
|
240
|
+
? [` operations: ['search'],`]
|
|
241
|
+
: [` operations: ['search', 'get'],`];
|
|
242
|
+
}
|
|
243
|
+
/** Ordering must not fall back to a primary key that does not exist. */
|
|
244
|
+
function defaultOrderField(schema, pk, source) {
|
|
245
|
+
return source === 'unresolved' ? schema.fields[0] : pkFirst(pk);
|
|
246
|
+
}
|
|
160
247
|
export function generateSingleTableFile(schema, allSchemas) {
|
|
161
248
|
const { byParent, byChild } = indexRelations(detectRelations(allSchemas));
|
|
162
|
-
const { pk, autoIncrement } = detectPrimaryKey(schema);
|
|
249
|
+
const { pk, autoIncrement, source } = detectPrimaryKey(schema);
|
|
163
250
|
const tableVarName = 'Table' + schema.schemaName.replace(/^Schema/, '');
|
|
164
251
|
const parentRels = byParent.get(schema.schemaName) ?? [];
|
|
165
252
|
const childRels = byChild.get(schema.schemaName) ?? [];
|
|
@@ -180,6 +267,7 @@ export function generateSingleTableFile(schema, allSchemas) {
|
|
|
180
267
|
lines.push(`// });`);
|
|
181
268
|
// Table definition
|
|
182
269
|
lines.push(``);
|
|
270
|
+
lines.push(...viewNotice(schema, source));
|
|
183
271
|
lines.push(`// Fields: ${schema.fields.join(', ')}`);
|
|
184
272
|
lines.push(`export const ${tableVarName} = defineTable({`);
|
|
185
273
|
lines.push(` primary: ${formatPrimary(pk)},`);
|
|
@@ -194,15 +282,21 @@ export function generateSingleTableFile(schema, allSchemas) {
|
|
|
194
282
|
lines.push(` // if (filters.q) condition.isILike(\`\${Schema.tableName}.\${Schema.col('${schema.fields.includes('name') ? 'name' : schema.fields[0]}')}\`, \`%\${filters.q}%\`);`);
|
|
195
283
|
lines.push(` // }`);
|
|
196
284
|
lines.push(` // ),`);
|
|
197
|
-
lines.push(` defaultOrder: '${
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
285
|
+
lines.push(` defaultOrder: '${defaultOrderField(schema, pk, source)}',`);
|
|
286
|
+
lines.push(...viewOperationsLine(schema, source));
|
|
287
|
+
// A view is never inserted into by the auto routes as generated, so the creation whitelist
|
|
288
|
+
// would be a line about an operation that is not exposed.
|
|
289
|
+
if (!schema.isView) {
|
|
290
|
+
if (autoIncrement) {
|
|
291
|
+
lines.push(` excludeFromCreation: ['${pkFirst(pk)}'],`);
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
lines.push(` // excludeFromCreation: [],`);
|
|
295
|
+
}
|
|
203
296
|
}
|
|
204
297
|
// Hide columns from every read (writes still accept them, e.g. a password hash)
|
|
205
298
|
lines.push(` // readExclude: [],`);
|
|
299
|
+
lines.push(...writeExcludeLines(schema));
|
|
206
300
|
lines.push(...allowedReadJoinsLines(parentRels));
|
|
207
301
|
const upsertPk = pkFields(pk).map((p) => `'${p}'`).join(', ');
|
|
208
302
|
lines.push(` // upsertMap: buildUpsertRules(buildUpsertRule(Schema, [${upsertPk}])),`);
|
|
@@ -243,22 +337,26 @@ export function generateDbTablesIndex(schemas) {
|
|
|
243
337
|
// ─── Code Generation (legacy) ───────────────────────────────
|
|
244
338
|
/** One `defineTable({...})` block for the legacy single-file generator. */
|
|
245
339
|
function legacyTableBlock(schema, parentRels, childRels) {
|
|
246
|
-
const { pk, autoIncrement } = detectPrimaryKey(schema);
|
|
340
|
+
const { pk, autoIncrement, source } = detectPrimaryKey(schema);
|
|
247
341
|
const tableVarName = 'Table' + schema.schemaName.replace(/^Schema/, '');
|
|
248
342
|
const upsertPk = pkFields(pk).map((p) => `'${p}'`).join(', ');
|
|
249
343
|
const exampleField = schema.fields.find((f) => !pkFields(pk).includes(f)) || schema.fields[0];
|
|
250
344
|
const lines = [
|
|
251
345
|
``,
|
|
252
346
|
`// ─── ${schema.tableName} ──────────────────────────────`,
|
|
347
|
+
...viewNotice(schema, source),
|
|
253
348
|
`// Fields: ${schema.fields.join(', ')}`,
|
|
254
349
|
`const ${tableVarName} = defineTable({`,
|
|
255
350
|
` primary: ${formatPrimary(pk)},`,
|
|
256
351
|
` ...exportTableInfo(${schema.schemaName}),`,
|
|
257
|
-
` defaultOrder: '${
|
|
258
|
-
|
|
259
|
-
? ` excludeFromCreation: ['${pkFirst(pk)}'],`
|
|
260
|
-
: ` // excludeFromCreation: [],`,
|
|
352
|
+
` defaultOrder: '${defaultOrderField(schema, pk, source)}',`,
|
|
353
|
+
...viewOperationsLine(schema, source),
|
|
261
354
|
];
|
|
355
|
+
if (!schema.isView) {
|
|
356
|
+
lines.push(autoIncrement
|
|
357
|
+
? ` excludeFromCreation: ['${pkFirst(pk)}'],`
|
|
358
|
+
: ` // excludeFromCreation: [],`);
|
|
359
|
+
}
|
|
262
360
|
lines.push(...allowedReadJoinsLines(parentRels));
|
|
263
361
|
lines.push(` // upsertMap: buildUpsertRules(buildUpsertRule(${schema.schemaName}, [${upsertPk}])),`, ` // schemaOverrides: {},`, ` validate: async (db, req, main, secondaries) => {`, ` const errors: ValidationError[] = [];`, ` // if (!main.${exampleField}) errors.push(['${exampleField}', 'required']);`, ` return errors;`, ` },`, ` // beforeInsert: async (db, req, record) => {},`, ` // beforeUpdate: async (db, req, fields) => {},`, tenantScopeLine(childRels), `});`);
|
|
264
362
|
return lines;
|
package/dist/lib/db.d.ts
CHANGED
|
@@ -37,7 +37,12 @@ export declare class QueryClient {
|
|
|
37
37
|
insert(table: string, record: DbRecord, pkCol: string | string[]): Promise<Record<string, unknown>>;
|
|
38
38
|
insertOrUpdate(table: string, record: DbRecord, conflictKeys: string[], pkCol: string | string[]): Promise<Record<string, unknown>>;
|
|
39
39
|
bulkInsert(table: string, records: DbRecord[], pkCol: string | string[], chunkSize?: number): Promise<Record<string, unknown>[]>;
|
|
40
|
-
|
|
40
|
+
/**
|
|
41
|
+
* `immutableCols` are written when the row is inserted but left untouched when the statement
|
|
42
|
+
* lands on an existing row — like the conflict keys, which are never in the update set either.
|
|
43
|
+
* Used by tenant isolation to keep an upsert from re-assigning a row's owner.
|
|
44
|
+
*/
|
|
45
|
+
bulkInsertOrUpdate(table: string, records: DbRecord[], conflictKeys: string[], pkCol: string | string[], chunkSize?: number, immutableCols?: string[]): Promise<Record<string, unknown>[]>;
|
|
41
46
|
update(table: string, record: DbRecord, where: DbRecord, extraCondition?: ConditionBuilder): Promise<number>;
|
|
42
47
|
delete(table: string, where: DbRecord, extraCondition?: ConditionBuilder): Promise<number>;
|
|
43
48
|
select<T = Record<string, unknown>>({ tableName, columns, where, values, limit, orderBy, joins, distinct, }: SelectOptions): Promise<T[]>;
|