fastify-auto-sqlapi 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS_BACKEND.md +35 -1
- package/README.md +65 -4
- package/dist/bin/generate-schema.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/lib/adapters/mysql-adapter.d.ts +4 -0
- package/dist/lib/adapters/mysql-adapter.d.ts.map +1 -1
- package/dist/lib/adapters/mysql-adapter.js +28 -17
- package/dist/lib/adapters/pg-adapter.d.ts +4 -0
- package/dist/lib/adapters/pg-adapter.d.ts.map +1 -1
- package/dist/lib/adapters/pg-adapter.js +18 -7
- package/dist/lib/cli/mysql-introspect.d.ts.map +1 -1
- package/dist/lib/cli/mysql-introspect.js +3 -1
- package/dist/lib/cli/pg-introspect.d.ts.map +1 -1
- package/dist/lib/cli/pg-introspect.js +13 -4
- 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 -3
- package/dist/lib/cli/tables-codegen.d.ts +2 -0
- package/dist/lib/cli/tables-codegen.d.ts.map +1 -1
- package/dist/lib/cli/tables-codegen.js +43 -12
- package/dist/lib/db.d.ts +17 -0
- package/dist/lib/db.d.ts.map +1 -1
- package/dist/lib/db.js +47 -0
- package/dist/lib/engine/bulk/bulk-delete.d.ts.map +1 -1
- package/dist/lib/engine/bulk/bulk-delete.js +25 -8
- package/dist/lib/engine/bulk/bulk-upsert.d.ts.map +1 -1
- package/dist/lib/engine/bulk/bulk-upsert.js +9 -8
- package/dist/lib/engine/rest/delete.d.ts.map +1 -1
- package/dist/lib/engine/rest/delete.js +9 -5
- package/dist/lib/engine/rest/get.js +1 -1
- package/dist/lib/engine/rest/insert.d.ts.map +1 -1
- package/dist/lib/engine/rest/insert.js +38 -33
- package/dist/lib/engine/rest/update.d.ts.map +1 -1
- package/dist/lib/engine/rest/update.js +69 -55
- package/dist/lib/engine/search/search.d.ts.map +1 -1
- package/dist/lib/engine/search/search.js +49 -19
- package/dist/lib/table-helpers.d.ts.map +1 -1
- package/dist/lib/table-helpers.js +10 -9
- package/dist/lib/tenant.d.ts +2 -2
- package/dist/lib/tenant.d.ts.map +1 -1
- package/dist/lib/tenant.js +11 -9
- package/dist/routes/auto/bulk-delete.routes.d.ts.map +1 -1
- package/dist/routes/auto/bulk-delete.routes.js +1 -0
- package/dist/routes/auto/bulk-upsert.routes.d.ts.map +1 -1
- package/dist/routes/auto/bulk-upsert.routes.js +1 -0
- package/dist/routes/auto/delete.routes.d.ts.map +1 -1
- package/dist/routes/auto/delete.routes.js +1 -0
- package/dist/routes/auto/get.routes.d.ts.map +1 -1
- package/dist/routes/auto/get.routes.js +1 -0
- package/dist/routes/auto/insert.routes.d.ts.map +1 -1
- package/dist/routes/auto/insert.routes.js +1 -0
- package/dist/routes/auto/route-helpers.d.ts +3 -1
- package/dist/routes/auto/route-helpers.d.ts.map +1 -1
- package/dist/routes/auto/route-helpers.js +3 -0
- package/dist/routes/auto/search.routes.d.ts.map +1 -1
- package/dist/routes/auto/search.routes.js +1 -0
- package/dist/routes/auto/update.routes.d.ts.map +1 -1
- package/dist/routes/auto/update.routes.js +1 -0
- package/dist/types/cli.d.ts +6 -0
- package/dist/types/cli.d.ts.map +1 -1
- package/dist/types/db.d.ts +11 -0
- package/dist/types/db.d.ts.map +1 -1
- package/dist/types/schema.d.ts +2 -0
- package/dist/types/schema.d.ts.map +1 -1
- package/dist/types/table.d.ts +40 -2
- package/dist/types/table.d.ts.map +1 -1
- package/package.json +1 -1
package/AGENTS_BACKEND.md
CHANGED
|
@@ -305,6 +305,25 @@ export const TableCustomer = defineTable({
|
|
|
305
305
|
// Mutate fields before UPDATE. camelCase. PK is present for reference but excluded from UPDATE SET.
|
|
306
306
|
fields.updatedBy = req.user.id;
|
|
307
307
|
},
|
|
308
|
+
afterUpdate: async (db, req, record, secondaryRecords, deletionRecords) => {
|
|
309
|
+
// Called after UPDATE + secondaries + deletions, INSIDE the write transaction: throwing rolls back.
|
|
310
|
+
},
|
|
311
|
+
beforeDelete: async (db, req, id) => {
|
|
312
|
+
// Throw to abort the single delete. For tenant-scoped tables runs only after ownership is verified.
|
|
313
|
+
},
|
|
314
|
+
afterDelete: async (db, req, id) => {
|
|
315
|
+
// Called after a successful single delete (not on 404).
|
|
316
|
+
},
|
|
317
|
+
beforeBulkDelete: async (db, req, ids) => {
|
|
318
|
+
// Called ONCE with all ids before a bulk delete. Throw to abort the whole batch.
|
|
319
|
+
},
|
|
320
|
+
afterBulkDelete: async (db, req, deletedIds) => {
|
|
321
|
+
// Called ONCE with the ids ACTUALLY deleted (may be a subset of the requested ids).
|
|
322
|
+
},
|
|
323
|
+
|
|
324
|
+
// OPERATIONS — whitelist of auto-generated HTTP routes for this table.
|
|
325
|
+
// Omitted = ALL operations exposed (default-open!). Does not affect programmatic sqlApi.*.
|
|
326
|
+
// operations: ['search', 'get'],
|
|
308
327
|
|
|
309
328
|
// AUTH — per-table request hooks
|
|
310
329
|
onRequests: [
|
|
@@ -340,6 +359,21 @@ buildRelation(mainSchema, mainField, joinSchema, joinField, options?)
|
|
|
340
359
|
|
|
341
360
|
**Choosing `unique`**: if `joinField` is the PK (or part of the composite PK) of `joinSchema`, the relation is N:1 — you almost certainly want `unique: true` so the alias is usable in `joinLeft`.
|
|
342
361
|
|
|
362
|
+
**Owned child tables (translations, `*_info` details): use a writeJoin, not a standalone table.** A table that only exists as a child of a parent — e.g. `product_info` with composite PK `(product_id, lang)` — should be an `allowedWriteJoins` on the parent, not its own `DbTables` entry. The engine auto-fills the FK (`product_id`); add it to `upsertMap` (conflict key = the composite PK) to upsert children passing only their own fields. Avoids redundant endpoints/validators.
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
// on the parent (product) table:
|
|
366
|
+
allowedWriteJoins: [
|
|
367
|
+
buildRelation(SchemaProduct, 'id', SchemaProductInfo, 'productId', { alias: 'translations' }),
|
|
368
|
+
],
|
|
369
|
+
upsertMap: buildUpsertRules(
|
|
370
|
+
buildUpsertRule(SchemaProductInfo, ['productId', 'lang']), // composite conflict key
|
|
371
|
+
),
|
|
372
|
+
// → PUT /rest/product { "main": {...}, "secondaries": { "translations": [{ "lang": "en", "name": "Bike" }] } }
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
Expose a composite-PK table as a standalone CRUD table only when it stands on its own (M:N link tables, natural keys) — composite PKs are fully supported there too.
|
|
376
|
+
|
|
343
377
|
**Aliasing the same table twice**: declare two `buildRelation` entries with different `alias`. Example: a `session` table referencing `user` for both `createdBy` and `updatedBy`:
|
|
344
378
|
|
|
345
379
|
```typescript
|
|
@@ -724,7 +758,7 @@ Tables without `tenantScope` are unaffected — no filtering regardless of `getT
|
|
|
724
758
|
- **`schemaOverrides`**: override auto-generated schema fields with stricter TypeBox types (e.g. `{ email: Type.String({ format: 'email' }) }`). Overrides are merged into the body schema for insert, update, and bulk-upsert. The original Schema file is never modified. In updates, overridden fields are still wrapped in Optional (validates only when present). Overrides appear in Swagger.
|
|
725
759
|
- **Validation receives camelCase**: `validate` receives the original camelCase record (as sent by the client) and secondaries. Field names match the schema definition, with full TypeScript inference (`main.startDate`, not `main.start_date`). It returns `ValidationError[]` — tuples of `[field, code]` or `[field, code, message]`. If any errors are returned, the request is rejected with 400 before hooks or SQL execute.
|
|
726
760
|
- **`validateBulk` replaces `validate` in bulk**: when `validateBulk` is defined, it is called once with all items and per-item `validate` is skipped. This allows optimized batch queries instead of N individual checks. When only `validate` is defined, it runs per-item as fallback.
|
|
727
|
-
- **All hooks and validators receive camelCase records**: `validate
|
|
761
|
+
- **All hooks and validators receive camelCase records**: `validate` and the whole hook matrix (`beforeInsert`/`afterInsert`, `beforeUpdate`/`afterUpdate`, `beforeDelete`/`afterDelete`, `beforeBulkDelete`/`afterBulkDelete`) get records keyed by schema field names (camelCase). Mutations propagate to the SQL (plugin converts to DB column format via `colMap` after the hook). The engine internally uses `snakecaseRecord(..., schema)` after user mutations to map field names to actual DB columns.
|
|
728
762
|
- **Filters validation**: TypeBox schemas use `additionalProperties: false`. By default Fastify strips unknown fields silently. For 400 errors on unknown filters: `Fastify({ ajv: { customOptions: { removeAdditional: false } } })`.
|
|
729
763
|
- **Tenant filtering**: when `tenantScope` is set on a table and `getTenantId` is provided in plugin options, all CRUD operations are automatically scoped to the tenant. `getTenantId` returning `null`/`undefined` = admin (no filter). Returning an array = multi-tenant user (IN clause).
|
|
730
764
|
|
package/README.md
CHANGED
|
@@ -14,7 +14,8 @@ Point it at your database, and get a full REST API with search, advanced conditi
|
|
|
14
14
|
- **Bulk operations** — batch insert/upsert/delete in single queries
|
|
15
15
|
- **Multi-tenant** — automatic row-level isolation, zero code in handlers
|
|
16
16
|
- **Validation** — structured field-level validation with cross-entity support
|
|
17
|
-
- **Hooks** —
|
|
17
|
+
- **Hooks** — full before/after matrix (insert, update, delete, bulk delete) for custom logic
|
|
18
|
+
- **Transactions** — insert/update with secondaries run atomically (rollback on failure)
|
|
18
19
|
- **Swagger UI** — optional, auto-configured from your schemas
|
|
19
20
|
- **Composable** — register all routes or pick only what you need
|
|
20
21
|
|
|
@@ -183,10 +184,16 @@ const TableCustomer = defineTable({
|
|
|
183
184
|
return [];
|
|
184
185
|
},
|
|
185
186
|
|
|
186
|
-
// Hooks (
|
|
187
|
+
// Hooks (run after validation) — all receive camelCase records (schema field names).
|
|
188
|
+
// after* hooks for insert/update run INSIDE the write transaction: throwing rolls back.
|
|
187
189
|
beforeInsert: async (db, req, record) => { /* camelCase; mutations propagate to INSERT */ },
|
|
188
|
-
beforeUpdate: async (db, req, fields) => { /* camelCase; PK included for reference, excluded from UPDATE SET */ },
|
|
189
190
|
afterInsert: async (db, req, record, secondaryRecords) => { /* camelCase; input merged with generated PK */ },
|
|
191
|
+
beforeUpdate: async (db, req, fields) => { /* camelCase; PK included for reference, excluded from UPDATE SET */ },
|
|
192
|
+
afterUpdate: async (db, req, record, secondaryRecords, deletionRecords) => { /* after UPDATE + secondaries + deletions */ },
|
|
193
|
+
beforeDelete: async (db, req, id) => { /* throw to abort the deletion */ },
|
|
194
|
+
afterDelete: async (db, req, id) => { /* after a successful single delete */ },
|
|
195
|
+
beforeBulkDelete: async (db, req, ids) => { /* called ONCE with all ids; throw to abort the batch */ },
|
|
196
|
+
afterBulkDelete: async (db, req, deletedIds) => { /* called ONCE with the ids ACTUALLY deleted */ },
|
|
190
197
|
|
|
191
198
|
// Auth (per-table)
|
|
192
199
|
onRequests: [
|
|
@@ -247,6 +254,46 @@ await app.register(fastifyAutoSqlApi, {
|
|
|
247
254
|
|
|
248
255
|
For MySQL/MariaDB, register `mysql2/promise` pool instead of `@fastify/postgres` and pass `dialect: 'mysql'` or `'mariadb'`.
|
|
249
256
|
|
|
257
|
+
## Security
|
|
258
|
+
|
|
259
|
+
> ⚠️ **The plugin is open by default.** Registering it without any configuration exposes
|
|
260
|
+
> **all operations on all tables in `DbTables`** (read AND write, including bulk delete) to
|
|
261
|
+
> anyone who can reach the server. This is intentional — the plugin provides the tools and
|
|
262
|
+
> does not impose an auth model — but it means **you** are responsible for locking it down
|
|
263
|
+
> before exposing it.
|
|
264
|
+
|
|
265
|
+
Three layers are available, combinable:
|
|
266
|
+
|
|
267
|
+
**1. Authentication / authorization hooks** — `onRequests` runs before every auto-generated
|
|
268
|
+
route (globally or per table):
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
await app.register(fastifyAutoSqlApi, {
|
|
272
|
+
DbTables: dbTables,
|
|
273
|
+
onRequests: [async (request, reply) => {
|
|
274
|
+
await request.jwtVerify(); // or any auth check; throw/reply to block
|
|
275
|
+
}],
|
|
276
|
+
});
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
**2. Per-table operation whitelist** — `operations` limits which routes are registered for a
|
|
280
|
+
table. Omitted = all operations (default). Unlisted operations are not registered at all
|
|
281
|
+
(they answer 404):
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
const TableAuditLog = defineTable({
|
|
285
|
+
primary: 'id',
|
|
286
|
+
...exportTableInfo(SchemaAuditLog),
|
|
287
|
+
operations: ['search', 'get'], // read-only over HTTP: no insert/update/delete/bulk
|
|
288
|
+
});
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Note: `operations` gates the HTTP routes only. The programmatic `fastify.sqlApi.*` methods
|
|
292
|
+
are always available to your own code.
|
|
293
|
+
|
|
294
|
+
**3. Multi-tenancy** — `getTenantId` + `tenantScope` filter every query by tenant (see
|
|
295
|
+
[Multi-Tenant](#multi-tenant)).
|
|
296
|
+
|
|
250
297
|
## API Reference
|
|
251
298
|
|
|
252
299
|
### POST /search/{table}
|
|
@@ -356,6 +403,20 @@ Returns `{ main: { ... } }` or 404.
|
|
|
356
403
|
|
|
357
404
|
`secondaries` keys are the **alias** declared in `allowedWriteJoins`. FK fields are auto-filled from the inserted main record.
|
|
358
405
|
|
|
406
|
+
> **Owned child tables (translations, `*_info` details): use a writeJoin, not a standalone table.**
|
|
407
|
+
> A table that only exists as a child of a parent — e.g. `product_info` with composite PK `(product_id, lang)` — should be an `allowedWriteJoins` on the parent, not its own `DbTables` entry. The engine auto-fills the FK (`product_id`); add it to `upsertMap` (conflict key = the composite PK) to upsert children passing only their own fields:
|
|
408
|
+
> ```typescript
|
|
409
|
+
> // on the parent (product) table:
|
|
410
|
+
> allowedWriteJoins: [
|
|
411
|
+
> buildRelation(SchemaProduct, 'id', SchemaProductInfo, 'productId', { alias: 'translations' }),
|
|
412
|
+
> ],
|
|
413
|
+
> upsertMap: buildUpsertRules(
|
|
414
|
+
> buildUpsertRule(SchemaProductInfo, ['productId', 'lang']), // composite conflict key
|
|
415
|
+
> ),
|
|
416
|
+
> // → PUT /rest/product { "main": {...}, "secondaries": { "translations": [{ "lang": "en", "name": "Bike" }] } }
|
|
417
|
+
> ```
|
|
418
|
+
> Expose a composite-PK table as a standalone CRUD table only when it stands on its own (M:N link tables, natural keys) — composite PKs are fully supported there too.
|
|
419
|
+
|
|
359
420
|
**Response (201):** `{ main: { ... }, secondaries: { ... } }`
|
|
360
421
|
|
|
361
422
|
### PUT /rest/{table}
|
|
@@ -495,7 +556,7 @@ await app.register(async (instance) => {
|
|
|
495
556
|
|
|
496
557
|
## Conventions
|
|
497
558
|
|
|
498
|
-
- **camelCase everywhere in the API** — requests, responses, `validate`, and all hooks (`beforeInsert`, `beforeUpdate`, `
|
|
559
|
+
- **camelCase everywhere in the API** — requests, responses, `validate`, and all hooks (`beforeInsert`/`afterInsert`, `beforeUpdate`/`afterUpdate`, `beforeDelete`/`afterDelete`, `beforeBulkDelete`/`afterBulkDelete`) use schema field names
|
|
499
560
|
- **Conversion to DB column format is automatic** via `colMap` — supports both snake_case and camelCase DB columns (e.g. betterauth-style)
|
|
500
561
|
- **Aliases identify joins** — declared in `buildRelation({ alias })`, used as keys in request/response/`secondaries`/dotted notation
|
|
501
562
|
- **`joinMustExist` / `joinMultiple` / `joinGroup`** are 1:N (child→main) and use side queries / EXISTS / correlated subqueries — no row duplication
|
|
@@ -54,10 +54,10 @@ await runCli('fastify-auto-sqlapi: generating schemas', async () => {
|
|
|
54
54
|
let untouched = 0;
|
|
55
55
|
const generatedFiles = new Set();
|
|
56
56
|
for (const schemaName of Object.keys(tableMap)) {
|
|
57
|
-
const { name: tableName, fields, colMap } = tableMap[schemaName];
|
|
57
|
+
const { name: tableName, fields, colMap, primary } = tableMap[schemaName];
|
|
58
58
|
const filename = path.join(schemasDir, `${schemaName}.ts`);
|
|
59
59
|
generatedFiles.add(`${schemaName}.ts`);
|
|
60
|
-
const content = generateSchemaFile(schemaName, tableName, fields, colMap);
|
|
60
|
+
const content = generateSchemaFile(schemaName, tableName, fields, colMap, primary);
|
|
61
61
|
let status = 'created';
|
|
62
62
|
let color = CONSOLE_COLORS.green;
|
|
63
63
|
if (fs.existsSync(filename)) {
|
package/dist/index.d.ts
CHANGED
|
@@ -30,5 +30,5 @@ export { BulkDeleteTableBody, BulkDeleteTableResponse } from './lib/schema/bulk-
|
|
|
30
30
|
export { default as bulkDeleteRoutes } from './routes/auto/bulk-delete.routes.js';
|
|
31
31
|
export { Type, type Static } from '@sinclair/typebox';
|
|
32
32
|
export { ConditionBuilder, Expression } from 'node-condition-builder';
|
|
33
|
-
export type { SqlApiConfig, ColumnInfo, TableMap, Queryable, SqlResult, DbRecord, DbRecordValue, SelectOptions, SchemaDefinition, JoinDefinition, ITable, 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';
|
|
33
|
+
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';
|
|
34
34
|
//# 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":"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"}
|
|
@@ -4,6 +4,10 @@ export interface MysqlQueryable {
|
|
|
4
4
|
affectedRows?: number;
|
|
5
5
|
insertId?: number;
|
|
6
6
|
}, unknown]>;
|
|
7
|
+
/** Present on mysql2 promise pools (and @fastify/mysql): checks out a dedicated connection. */
|
|
8
|
+
getConnection?(): Promise<MysqlQueryable & {
|
|
9
|
+
release(): void;
|
|
10
|
+
}>;
|
|
7
11
|
}
|
|
8
12
|
export declare function mysqlQueryable(pool: MysqlQueryable): Queryable;
|
|
9
13
|
//# sourceMappingURL=mysql-adapter.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mysql-adapter.d.ts","sourceRoot":"","sources":["../../../src/lib/adapters/mysql-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,
|
|
1
|
+
{"version":3,"file":"mysql-adapter.d.ts","sourceRoot":"","sources":["../../../src/lib/adapters/mysql-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAmC,MAAM,gBAAgB,CAAC;AAEjF,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,GAAG,EAAE,MAAM,EACX,MAAM,CAAC,EAAE,OAAO,EAAE,GACjB,OAAO,CAAC,CAAC,CAAC,EAAE,GAAG;QAAE,YAAY,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,OAAO,CAAC,CAAC,CAAC;IAC1E,+FAA+F;IAC/F,aAAa,CAAC,IAAI,OAAO,CAAC,cAAc,GAAG;QAAE,OAAO,IAAI,IAAI,CAAA;KAAE,CAAC,CAAC;CACjE;AA2BD,wBAAgB,cAAc,CAAC,IAAI,EAAE,cAAc,GAAG,SAAS,CAc9D"}
|
|
@@ -1,21 +1,32 @@
|
|
|
1
|
-
|
|
2
|
-
return {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
if (Array.isArray(result) && !('affectedRows' in result)) {
|
|
7
|
-
return {
|
|
8
|
-
rows: result,
|
|
9
|
-
affectedRows: result.length,
|
|
10
|
-
};
|
|
11
|
-
}
|
|
12
|
-
// INSERT/UPDATE/DELETE returns ResultSetHeader-like
|
|
13
|
-
const header = result;
|
|
1
|
+
function wrapQuery(client) {
|
|
2
|
+
return async (text, values) => {
|
|
3
|
+
const [result] = await client.query(text, values);
|
|
4
|
+
// SELECT returns an array of rows
|
|
5
|
+
if (Array.isArray(result) && !('affectedRows' in result)) {
|
|
14
6
|
return {
|
|
15
|
-
rows:
|
|
16
|
-
affectedRows:
|
|
17
|
-
insertId: header.insertId,
|
|
7
|
+
rows: result,
|
|
8
|
+
affectedRows: result.length,
|
|
18
9
|
};
|
|
19
|
-
}
|
|
10
|
+
}
|
|
11
|
+
// INSERT/UPDATE/DELETE returns ResultSetHeader-like
|
|
12
|
+
const header = result;
|
|
13
|
+
return {
|
|
14
|
+
rows: [],
|
|
15
|
+
affectedRows: header.affectedRows ?? 0,
|
|
16
|
+
insertId: header.insertId,
|
|
17
|
+
};
|
|
20
18
|
};
|
|
21
19
|
}
|
|
20
|
+
export function mysqlQueryable(pool) {
|
|
21
|
+
const queryable = { query: wrapQuery(pool) };
|
|
22
|
+
if (typeof pool.getConnection === 'function') {
|
|
23
|
+
queryable.connect = async () => {
|
|
24
|
+
const conn = await pool.getConnection();
|
|
25
|
+
return {
|
|
26
|
+
query: wrapQuery(conn),
|
|
27
|
+
release: () => conn.release(),
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return queryable;
|
|
32
|
+
}
|
|
@@ -4,6 +4,10 @@ export interface PgQueryable {
|
|
|
4
4
|
rows: T[];
|
|
5
5
|
rowCount: number | null;
|
|
6
6
|
}>;
|
|
7
|
+
/** Present on pg.Pool (and @fastify/postgres): checks out a dedicated client. */
|
|
8
|
+
connect?(): Promise<PgQueryable & {
|
|
9
|
+
release(): void;
|
|
10
|
+
}>;
|
|
7
11
|
}
|
|
8
12
|
export declare function pgQueryable(client: PgQueryable): Queryable;
|
|
9
13
|
//# sourceMappingURL=pg-adapter.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pg-adapter.d.ts","sourceRoot":"","sources":["../../../src/lib/adapters/pg-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,
|
|
1
|
+
{"version":3,"file":"pg-adapter.d.ts","sourceRoot":"","sources":["../../../src/lib/adapters/pg-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAmC,MAAM,gBAAgB,CAAC;AAEjF,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,OAAO,EAAE,GACjB,OAAO,CAAC;QAAE,IAAI,EAAE,CAAC,EAAE,CAAC;QAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;IACnD,iFAAiF;IACjF,OAAO,CAAC,IAAI,OAAO,CAAC,WAAW,GAAG;QAAE,OAAO,IAAI,IAAI,CAAA;KAAE,CAAC,CAAC;CACxD;AAeD,wBAAgB,WAAW,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,CAc1D"}
|
|
@@ -1,11 +1,22 @@
|
|
|
1
|
+
function wrapQuery(client) {
|
|
2
|
+
return async (text, values) => {
|
|
3
|
+
const result = await client.query(text, values);
|
|
4
|
+
return {
|
|
5
|
+
rows: result.rows,
|
|
6
|
+
affectedRows: result.rowCount ?? 0,
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
}
|
|
1
10
|
export function pgQueryable(client) {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
11
|
+
const queryable = { query: wrapQuery(client) };
|
|
12
|
+
if (typeof client.connect === 'function') {
|
|
13
|
+
queryable.connect = async () => {
|
|
14
|
+
const conn = await client.connect();
|
|
5
15
|
return {
|
|
6
|
-
|
|
7
|
-
|
|
16
|
+
query: wrapQuery(conn),
|
|
17
|
+
release: () => conn.release(),
|
|
8
18
|
};
|
|
9
|
-
}
|
|
10
|
-
}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
return queryable;
|
|
11
22
|
}
|
|
@@ -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,wBAAgB,0BAA0B,IAAI;IAC5C,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,CAmBA;AAkDD,wBAAsB,qBAAqB,CACzC,gBAAgB,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,EAClG,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,UAAU,EAAE,CAAC,
|
|
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,wBAAgB,0BAA0B,IAAI;IAC5C,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,CAmBA;AAkDD,wBAAsB,qBAAqB,CACzC,gBAAgB,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,EAClG,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,UAAU,EAAE,CAAC,CA2BvB"}
|
|
@@ -70,7 +70,7 @@ export async function introspectMysqlTables(connectionConfig, schema) {
|
|
|
70
70
|
const mysql2 = loadOptionalDependency('mysql2/promise', 'npm install mysql2');
|
|
71
71
|
const connection = await mysql2.createConnection(connectionConfig);
|
|
72
72
|
try {
|
|
73
|
-
const [rows] = await connection.query(`SELECT table_name, column_name, data_type, column_default, is_nullable
|
|
73
|
+
const [rows] = await connection.query(`SELECT table_name, column_name, data_type, column_default, is_nullable, column_key, extra
|
|
74
74
|
FROM information_schema.columns
|
|
75
75
|
WHERE table_schema = ?
|
|
76
76
|
ORDER BY table_name, ordinal_position`, [schema]);
|
|
@@ -81,6 +81,8 @@ export async function introspectMysqlTables(connectionConfig, schema) {
|
|
|
81
81
|
udt_name: mapMysqlType(row.DATA_TYPE || row.data_type),
|
|
82
82
|
column_default: row.COLUMN_DEFAULT || row.column_default,
|
|
83
83
|
is_nullable: row.IS_NULLABLE || row.is_nullable,
|
|
84
|
+
is_primary: (row.COLUMN_KEY || row.column_key) === 'PRI',
|
|
85
|
+
is_auto_increment: String(row.EXTRA || row.extra || '').includes('auto_increment'),
|
|
84
86
|
}));
|
|
85
87
|
}
|
|
86
88
|
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;AAEjD,wBAAgB,qBAAqB,IAAI,MAAM,CAY9C;AAED,wBAAsB,gBAAgB,CACpC,gBAAgB,EAAE,MAAM,EACxB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,UAAU,EAAE,CAAC,
|
|
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;AAEjD,wBAAgB,qBAAqB,IAAI,MAAM,CAY9C;AAED,wBAAsB,gBAAgB,CACpC,gBAAgB,EAAE,MAAM,EACxB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,UAAU,EAAE,CAAC,CA8BvB"}
|
|
@@ -16,10 +16,19 @@ export async function introspectTables(connectionString, schema) {
|
|
|
16
16
|
const client = new pg.Client({ connectionString });
|
|
17
17
|
try {
|
|
18
18
|
await client.connect();
|
|
19
|
-
const result = await client.query(`SELECT table_name, column_name, udt_name, column_default, is_nullable
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
const result = await client.query(`SELECT c.table_name, c.column_name, c.udt_name, c.column_default, c.is_nullable,
|
|
20
|
+
(pk.column_name IS NOT NULL) AS is_primary
|
|
21
|
+
FROM information_schema.columns c
|
|
22
|
+
LEFT JOIN (
|
|
23
|
+
SELECT kcu.table_name, kcu.column_name
|
|
24
|
+
FROM information_schema.table_constraints tc
|
|
25
|
+
JOIN information_schema.key_column_usage kcu
|
|
26
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
27
|
+
AND tc.table_schema = kcu.table_schema
|
|
28
|
+
WHERE tc.constraint_type = 'PRIMARY KEY' AND tc.table_schema = $1
|
|
29
|
+
) pk ON pk.table_name = c.table_name AND pk.column_name = c.column_name
|
|
30
|
+
WHERE c.table_schema = $1
|
|
31
|
+
ORDER BY c.table_name, c.ordinal_position`, [schema]);
|
|
23
32
|
return result.rows;
|
|
24
33
|
}
|
|
25
34
|
finally {
|
|
@@ -2,7 +2,8 @@ import type { ColumnInfo, TableMap } from '../../types.js';
|
|
|
2
2
|
export declare function convertColType(udtName: string, columnInfo: {
|
|
3
3
|
column_default: string | null;
|
|
4
4
|
is_nullable: string;
|
|
5
|
+
is_auto_increment?: boolean;
|
|
5
6
|
}): string;
|
|
6
7
|
export declare function buildTableMap(rows: ColumnInfo[]): TableMap;
|
|
7
|
-
export declare function generateSchemaFile(schemaName: string, tableName: string, fields: Record<string, string>, colMap: Record<string, string
|
|
8
|
+
export declare function generateSchemaFile(schemaName: string, tableName: string, fields: Record<string, string>, colMap: Record<string, string>, primary?: string[]): string;
|
|
8
9
|
//# sourceMappingURL=schema-codegen.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema-codegen.d.ts","sourceRoot":"","sources":["../../../src/lib/cli/schema-codegen.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE3D,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,UAAU,EAAE;IAAE,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,
|
|
1
|
+
{"version":3,"file":"schema-codegen.d.ts","sourceRoot":"","sources":["../../../src/lib/cli/schema-codegen.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE3D,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,UAAU,EAAE;IAAE,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAAE,GAC9F,MAAM,CAqDR;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,UAAU,EAAE,GAAG,QAAQ,CAqC1D;AAED,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,OAAO,CAAC,EAAE,MAAM,EAAE,GACjB,MAAM,CA4CR"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { toCamelCase, toSchemaName } from '../naming.js';
|
|
2
2
|
export function convertColType(udtName, columnInfo) {
|
|
3
|
-
|
|
3
|
+
// AUTO_INCREMENT (mysql) behaves like a default: the DB generates the value.
|
|
4
|
+
const hasDefault = columnInfo.column_default || columnInfo.is_auto_increment;
|
|
4
5
|
const isNullable = columnInfo.is_nullable === 'YES';
|
|
5
6
|
const wrap = (t) => {
|
|
6
7
|
return hasDefault || isNullable ? `Type.Optional(${t})` : t;
|
|
@@ -58,17 +59,30 @@ export function buildTableMap(rows) {
|
|
|
58
59
|
name: row.table_name,
|
|
59
60
|
fields: {},
|
|
60
61
|
colMap: {},
|
|
62
|
+
primary: [],
|
|
61
63
|
};
|
|
62
64
|
}
|
|
63
65
|
const fieldName = toCamelCase(row.column_name);
|
|
66
|
+
// Two columns mapping to the same camelCase field (e.g. user_name + userName) would
|
|
67
|
+
// silently overwrite each other in the colMap, dropping one column from the API.
|
|
68
|
+
const existing = acc[schemaName].colMap[fieldName];
|
|
69
|
+
if (existing !== undefined && existing !== row.column_name) {
|
|
70
|
+
throw new Error(`Naming collision on table "${row.table_name}": columns "${existing}" and ` +
|
|
71
|
+
`"${row.column_name}" both map to the API field "${fieldName}". ` +
|
|
72
|
+
`Rename one of the columns (or exclude the table) before generating schemas.`);
|
|
73
|
+
}
|
|
64
74
|
acc[schemaName].fields[fieldName] = convertColType(row.udt_name, row);
|
|
65
75
|
acc[schemaName].colMap[fieldName] = row.column_name;
|
|
76
|
+
if (row.is_primary) {
|
|
77
|
+
acc[schemaName].primary.push(fieldName);
|
|
78
|
+
}
|
|
66
79
|
return acc;
|
|
67
80
|
}, {});
|
|
68
81
|
}
|
|
69
|
-
export function generateSchemaFile(schemaName, tableName, fields, colMap) {
|
|
82
|
+
export function generateSchemaFile(schemaName, tableName, fields, colMap, primary) {
|
|
70
83
|
const fieldsStr = JSON.stringify(fields, null, 2).replace(/"(.*?)"\s*:\s*"Type(.*?)"/g, '$1: Type$2');
|
|
71
84
|
const colMapStr = JSON.stringify(colMap, null, 2);
|
|
85
|
+
const primaryKeyLine = primary?.length ? `\n primaryKey: ${JSON.stringify(primary)},` : '';
|
|
72
86
|
return `// Auto-generated by fastify-auto-sqlapi. Do not edit.
|
|
73
87
|
import { Type } from "fastify-auto-sqlapi";
|
|
74
88
|
import type { Static } from "fastify-auto-sqlapi";
|
|
@@ -94,7 +108,7 @@ export const Schema = {
|
|
|
94
108
|
fields: _Schema,
|
|
95
109
|
validation,
|
|
96
110
|
tableName: "${tableName}",
|
|
97
|
-
partialValidation
|
|
111
|
+
partialValidation,${primaryKeyLine}
|
|
98
112
|
};
|
|
99
113
|
export const ${schemaName} = Schema;
|
|
100
114
|
export type TypeSchema = Static<typeof Schema.validation>;
|
|
@@ -3,6 +3,8 @@ export interface ParsedSchema {
|
|
|
3
3
|
tableName: string;
|
|
4
4
|
fields: string[];
|
|
5
5
|
fieldTypes: Record<string, string>;
|
|
6
|
+
/** PRIMARY KEY fields declared in the schema file (`primaryKey: [...]`), when present. */
|
|
7
|
+
primary?: string[];
|
|
6
8
|
}
|
|
7
9
|
export interface DetectedRelation {
|
|
8
10
|
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;
|
|
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;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,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,CAuCpE;AAuCD,wBAAgB,eAAe,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,gBAAgB,EAAE,CAwB3E;AAID,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,GAAG,MAAM,CAwHhG;AAID,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,MAAM,CAqBrE;AAID,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,MAAM,CA6FlE"}
|
|
@@ -20,15 +20,34 @@ export function parseSchemaFile(content) {
|
|
|
20
20
|
}
|
|
21
21
|
if (fields.length === 0)
|
|
22
22
|
return null;
|
|
23
|
+
// primaryKey: ["code"] — emitted by generate-schema from DB introspection
|
|
24
|
+
let primary;
|
|
25
|
+
const pkMatch = content.match(/primaryKey:\s*\[([^\]]*)\]/);
|
|
26
|
+
if (pkMatch) {
|
|
27
|
+
primary = pkMatch[1]
|
|
28
|
+
.split(',')
|
|
29
|
+
.map((s) => s.trim().replace(/^["']|["']$/g, ''))
|
|
30
|
+
.filter(Boolean);
|
|
31
|
+
if (primary.length === 0)
|
|
32
|
+
primary = undefined;
|
|
33
|
+
}
|
|
23
34
|
return {
|
|
24
35
|
schemaName: exportMatch[1],
|
|
25
36
|
tableName: tableNameMatch[1],
|
|
26
37
|
fields,
|
|
27
38
|
fieldTypes,
|
|
39
|
+
primary,
|
|
28
40
|
};
|
|
29
41
|
}
|
|
30
42
|
// ─── Detection ───────────────────────────────────────────────
|
|
31
43
|
function detectPrimaryKey(schema) {
|
|
44
|
+
// Preferred source: the PK introspected from the DB (primaryKey in the schema file).
|
|
45
|
+
if (schema.primary?.length) {
|
|
46
|
+
const pk = schema.primary.length === 1 ? schema.primary[0] : schema.primary;
|
|
47
|
+
const first = schema.primary[0];
|
|
48
|
+
return { pk, autoIncrement: schema.fieldTypes[first]?.includes('Optional') ?? false };
|
|
49
|
+
}
|
|
50
|
+
// Heuristic fallback for hand-written/legacy schema files without primaryKey.
|
|
32
51
|
if (schema.fields.includes('id')) {
|
|
33
52
|
return { pk: 'id', autoIncrement: schema.fieldTypes['id']?.includes('Optional') ?? false };
|
|
34
53
|
}
|
|
@@ -39,6 +58,18 @@ function detectPrimaryKey(schema) {
|
|
|
39
58
|
}
|
|
40
59
|
return { pk: schema.fields[0], autoIncrement: false };
|
|
41
60
|
}
|
|
61
|
+
/** First PK field: used for defaultOrder, relations and example snippets. */
|
|
62
|
+
function pkFirst(pk) {
|
|
63
|
+
return Array.isArray(pk) ? pk[0] : pk;
|
|
64
|
+
}
|
|
65
|
+
/** Render the `primary:` value for the defineTable template. */
|
|
66
|
+
function formatPrimary(pk) {
|
|
67
|
+
return Array.isArray(pk) ? `[${pk.map((p) => `'${p}'`).join(', ')}]` : `'${pk}'`;
|
|
68
|
+
}
|
|
69
|
+
/** All PK fields as an array, for "non-PK field" lookups in example snippets. */
|
|
70
|
+
function pkFields(pk) {
|
|
71
|
+
return Array.isArray(pk) ? pk : [pk];
|
|
72
|
+
}
|
|
42
73
|
export function detectRelations(schemas) {
|
|
43
74
|
const tableBySnakeName = new Map();
|
|
44
75
|
for (const schema of schemas) {
|
|
@@ -55,7 +86,7 @@ export function detectRelations(schemas) {
|
|
|
55
86
|
const { pk } = detectPrimaryKey(parent);
|
|
56
87
|
relations.push({
|
|
57
88
|
parentSchemaName: parent.schemaName,
|
|
58
|
-
parentField: pk,
|
|
89
|
+
parentField: pkFirst(pk),
|
|
59
90
|
childSchemaName: schema.schemaName,
|
|
60
91
|
childField: field,
|
|
61
92
|
});
|
|
@@ -113,7 +144,7 @@ export function generateSingleTableFile(schema, allSchemas) {
|
|
|
113
144
|
lines.push(``);
|
|
114
145
|
lines.push(`// Fields: ${schema.fields.join(', ')}`);
|
|
115
146
|
lines.push(`export const ${tableVarName} = defineTable({`);
|
|
116
|
-
lines.push(` primary:
|
|
147
|
+
lines.push(` primary: ${formatPrimary(pk)},`);
|
|
117
148
|
lines.push(` ...exportTableInfo(Schema),`);
|
|
118
149
|
// Commented example with extraFilters + extendedCondition
|
|
119
150
|
lines.push(` // ...exportTableInfo(`);
|
|
@@ -123,9 +154,9 @@ export function generateSingleTableFile(schema, allSchemas) {
|
|
|
123
154
|
lines.push(` // if (filters.q) condition.isILike(Schema.col('${schema.fields.includes('name') ? 'name' : schema.fields[0]}'), \`%\${filters.q}%\`);`);
|
|
124
155
|
lines.push(` // }`);
|
|
125
156
|
lines.push(` // ),`);
|
|
126
|
-
lines.push(` defaultOrder: '${pk}',`);
|
|
157
|
+
lines.push(` defaultOrder: '${pkFirst(pk)}',`);
|
|
127
158
|
if (autoIncrement) {
|
|
128
|
-
lines.push(` excludeFromCreation: ['${pk}'],`);
|
|
159
|
+
lines.push(` excludeFromCreation: ['${pkFirst(pk)}'],`);
|
|
129
160
|
}
|
|
130
161
|
else {
|
|
131
162
|
lines.push(` // excludeFromCreation: [],`);
|
|
@@ -140,14 +171,14 @@ export function generateSingleTableFile(schema, allSchemas) {
|
|
|
140
171
|
else {
|
|
141
172
|
lines.push(` // allowedReadJoins: [],`);
|
|
142
173
|
}
|
|
143
|
-
lines.push(` // upsertMap: buildUpsertRules(buildUpsertRule(Schema, ['${
|
|
174
|
+
lines.push(` // upsertMap: buildUpsertRules(buildUpsertRule(Schema, [${pkFields(pk).map((p) => `'${p}'`).join(', ')}])),`);
|
|
144
175
|
// schemaOverrides: suggest email format if email field exists, otherwise minLength on first string field
|
|
145
176
|
const emailField = schema.fields.find(f => f.toLowerCase().includes('email'));
|
|
146
177
|
if (emailField) {
|
|
147
178
|
lines.push(` // schemaOverrides: { ${emailField}: Type.String({ format: 'email' }) },`);
|
|
148
179
|
}
|
|
149
180
|
else {
|
|
150
|
-
const stringField = schema.fields.find(f => f
|
|
181
|
+
const stringField = schema.fields.find(f => !pkFields(pk).includes(f) && schema.fieldTypes[f]?.includes('String'));
|
|
151
182
|
if (stringField) {
|
|
152
183
|
lines.push(` // schemaOverrides: { ${stringField}: Type.String({ minLength: 1 }) },`);
|
|
153
184
|
}
|
|
@@ -156,7 +187,7 @@ export function generateSingleTableFile(schema, allSchemas) {
|
|
|
156
187
|
}
|
|
157
188
|
}
|
|
158
189
|
// validate: use first non-PK field for example
|
|
159
|
-
const exampleField = schema.fields.find(f =>
|
|
190
|
+
const exampleField = schema.fields.find(f => !pkFields(pk).includes(f)) || schema.fields[0];
|
|
160
191
|
lines.push(` validate: async (db, req, main, secondaries) => {`);
|
|
161
192
|
lines.push(` const errors: ValidationError[] = [];`);
|
|
162
193
|
lines.push(` // if (!main.${exampleField}) errors.push(['${exampleField}', 'required']);`);
|
|
@@ -225,11 +256,11 @@ export function generateTablesFile(schemas) {
|
|
|
225
256
|
lines.push(`// ─── ${schema.tableName} ──────────────────────────────`);
|
|
226
257
|
lines.push(`// Fields: ${schema.fields.join(', ')}`);
|
|
227
258
|
lines.push(`const ${tableVarName} = defineTable({`);
|
|
228
|
-
lines.push(` primary:
|
|
259
|
+
lines.push(` primary: ${formatPrimary(pk)},`);
|
|
229
260
|
lines.push(` ...exportTableInfo(${schema.schemaName}),`);
|
|
230
|
-
lines.push(` defaultOrder: '${pk}',`);
|
|
261
|
+
lines.push(` defaultOrder: '${pkFirst(pk)}',`);
|
|
231
262
|
if (autoIncrement) {
|
|
232
|
-
lines.push(` excludeFromCreation: ['${pk}'],`);
|
|
263
|
+
lines.push(` excludeFromCreation: ['${pkFirst(pk)}'],`);
|
|
233
264
|
}
|
|
234
265
|
else {
|
|
235
266
|
lines.push(` // excludeFromCreation: [],`);
|
|
@@ -244,9 +275,9 @@ export function generateTablesFile(schemas) {
|
|
|
244
275
|
else {
|
|
245
276
|
lines.push(` // allowedReadJoins: [],`);
|
|
246
277
|
}
|
|
247
|
-
lines.push(` // upsertMap: buildUpsertRules(buildUpsertRule(${schema.schemaName}, ['${
|
|
278
|
+
lines.push(` // upsertMap: buildUpsertRules(buildUpsertRule(${schema.schemaName}, [${pkFields(pk).map((p) => `'${p}'`).join(', ')}])),`);
|
|
248
279
|
lines.push(` // schemaOverrides: {},`);
|
|
249
|
-
const exampleFieldLegacy = schema.fields.find(f =>
|
|
280
|
+
const exampleFieldLegacy = schema.fields.find(f => !pkFields(pk).includes(f)) || schema.fields[0];
|
|
250
281
|
lines.push(` validate: async (db, req, main, secondaries) => {`);
|
|
251
282
|
lines.push(` const errors: ValidationError[] = [];`);
|
|
252
283
|
lines.push(` // if (!main.${exampleFieldLegacy}) errors.push(['${exampleFieldLegacy}', 'required']);`);
|
package/dist/lib/db.d.ts
CHANGED
|
@@ -13,10 +13,27 @@ export declare class QueryClient {
|
|
|
13
13
|
get qi(): (id: string) => string;
|
|
14
14
|
get ph(): (i: number) => string;
|
|
15
15
|
get dialectName(): string;
|
|
16
|
+
/** ConditionBuilder dialect ('postgres' | 'mysql') for per-instance builders. */
|
|
17
|
+
get cbDialect(): SqlDialect['cbDialect'];
|
|
18
|
+
/** Whether the dialect supports `RETURNING` (postgres/mariadb: yes, mysql: no). */
|
|
19
|
+
get supportsReturning(): boolean;
|
|
20
|
+
/** Build the ` RETURNING <quoted pk>` suffix ('' when unsupported). */
|
|
21
|
+
get returningPk(): SqlDialect['returningPk'];
|
|
16
22
|
get dateTrunc(): SqlDialect['dateTrunc'];
|
|
17
23
|
setDebug(mode: boolean): void;
|
|
18
24
|
expression(value: string): Expression;
|
|
19
25
|
query<T = Record<string, unknown>>(text: string, values?: unknown[]): Promise<SqlResult<T>>;
|
|
26
|
+
/**
|
|
27
|
+
* Run `fn` inside a DB transaction on a dedicated connection (BEGIN/COMMIT, with
|
|
28
|
+
* ROLLBACK + rethrow on error). Requires the underlying Queryable to expose
|
|
29
|
+
* `connect()`; when it does not (legacy custom adapters), `fn` runs directly on
|
|
30
|
+
* this client without transactional guarantees.
|
|
31
|
+
*
|
|
32
|
+
* Nested calls join the outer transaction: the tx QueryClient wraps a dedicated
|
|
33
|
+
* connection that has no `connect()`, so an inner `withTransaction` is a no-op
|
|
34
|
+
* wrapper around the same connection.
|
|
35
|
+
*/
|
|
36
|
+
withTransaction<T>(fn: (tx: QueryClient) => Promise<T>): Promise<T>;
|
|
20
37
|
insert(table: string, record: DbRecord, pkCol: string | string[]): Promise<Record<string, unknown>>;
|
|
21
38
|
insertOrUpdate(table: string, record: DbRecord, conflictKeys: string[], pkCol: string | string[]): Promise<Record<string, unknown>>;
|
|
22
39
|
bulkInsert(table: string, records: DbRecord[], pkCol: string | string[], chunkSize?: number): Promise<Record<string, unknown>[]>;
|