fastify-auto-sqlapi 0.1.1 → 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.
Files changed (73) hide show
  1. package/AGENTS_BACKEND.md +35 -1
  2. package/README.md +65 -4
  3. package/dist/bin/generate-schema.js +2 -2
  4. package/dist/index.d.ts +1 -1
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/lib/adapters/mysql-adapter.d.ts +4 -0
  7. package/dist/lib/adapters/mysql-adapter.d.ts.map +1 -1
  8. package/dist/lib/adapters/mysql-adapter.js +28 -17
  9. package/dist/lib/adapters/pg-adapter.d.ts +4 -0
  10. package/dist/lib/adapters/pg-adapter.d.ts.map +1 -1
  11. package/dist/lib/adapters/pg-adapter.js +18 -7
  12. package/dist/lib/cli/mysql-introspect.d.ts.map +1 -1
  13. package/dist/lib/cli/mysql-introspect.js +3 -1
  14. package/dist/lib/cli/pg-introspect.d.ts.map +1 -1
  15. package/dist/lib/cli/pg-introspect.js +13 -4
  16. package/dist/lib/cli/schema-codegen.d.ts +2 -1
  17. package/dist/lib/cli/schema-codegen.d.ts.map +1 -1
  18. package/dist/lib/cli/schema-codegen.js +17 -3
  19. package/dist/lib/cli/tables-codegen.d.ts +2 -0
  20. package/dist/lib/cli/tables-codegen.d.ts.map +1 -1
  21. package/dist/lib/cli/tables-codegen.js +43 -12
  22. package/dist/lib/db.d.ts +17 -0
  23. package/dist/lib/db.d.ts.map +1 -1
  24. package/dist/lib/db.js +47 -0
  25. package/dist/lib/engine/bulk/bulk-delete.d.ts.map +1 -1
  26. package/dist/lib/engine/bulk/bulk-delete.js +33 -10
  27. package/dist/lib/engine/bulk/bulk-upsert.d.ts.map +1 -1
  28. package/dist/lib/engine/bulk/bulk-upsert.js +9 -8
  29. package/dist/lib/engine/rest/delete.d.ts.map +1 -1
  30. package/dist/lib/engine/rest/delete.js +18 -7
  31. package/dist/lib/engine/rest/get.js +1 -1
  32. package/dist/lib/engine/rest/insert.d.ts.map +1 -1
  33. package/dist/lib/engine/rest/insert.js +38 -33
  34. package/dist/lib/engine/rest/update.d.ts.map +1 -1
  35. package/dist/lib/engine/rest/update.js +69 -55
  36. package/dist/lib/engine/search/search.d.ts.map +1 -1
  37. package/dist/lib/engine/search/search.js +49 -19
  38. package/dist/lib/sql-api.js +2 -2
  39. package/dist/lib/table-helpers.d.ts.map +1 -1
  40. package/dist/lib/table-helpers.js +10 -9
  41. package/dist/lib/tenant.d.ts +9 -2
  42. package/dist/lib/tenant.d.ts.map +1 -1
  43. package/dist/lib/tenant.js +31 -8
  44. package/dist/routes/auto/bulk-delete.routes.d.ts.map +1 -1
  45. package/dist/routes/auto/bulk-delete.routes.js +1 -0
  46. package/dist/routes/auto/bulk-upsert.routes.d.ts.map +1 -1
  47. package/dist/routes/auto/bulk-upsert.routes.js +1 -0
  48. package/dist/routes/auto/delete.routes.d.ts.map +1 -1
  49. package/dist/routes/auto/delete.routes.js +1 -0
  50. package/dist/routes/auto/get.routes.d.ts.map +1 -1
  51. package/dist/routes/auto/get.routes.js +1 -0
  52. package/dist/routes/auto/insert.routes.d.ts.map +1 -1
  53. package/dist/routes/auto/insert.routes.js +1 -0
  54. package/dist/routes/auto/plugin.d.ts.map +1 -1
  55. package/dist/routes/auto/plugin.js +40 -5
  56. package/dist/routes/auto/route-helpers.d.ts +3 -1
  57. package/dist/routes/auto/route-helpers.d.ts.map +1 -1
  58. package/dist/routes/auto/route-helpers.js +3 -0
  59. package/dist/routes/auto/search.routes.d.ts.map +1 -1
  60. package/dist/routes/auto/search.routes.js +1 -0
  61. package/dist/routes/auto/update.routes.d.ts.map +1 -1
  62. package/dist/routes/auto/update.routes.js +1 -0
  63. package/dist/types/cli.d.ts +6 -0
  64. package/dist/types/cli.d.ts.map +1 -1
  65. package/dist/types/db.d.ts +11 -0
  66. package/dist/types/db.d.ts.map +1 -1
  67. package/dist/types/dml.d.ts +2 -0
  68. package/dist/types/dml.d.ts.map +1 -1
  69. package/dist/types/schema.d.ts +2 -0
  70. package/dist/types/schema.d.ts.map +1 -1
  71. package/dist/types/table.d.ts +64 -2
  72. package/dist/types/table.d.ts.map +1 -1
  73. 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`, `beforeInsert`, `beforeUpdate`, and `afterInsert` all 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.
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** — `beforeInsert`, `beforeUpdate`, `afterInsert` for custom logic
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 (runs after validation) — all receive camelCase records (schema field names)
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`, `afterInsert`) use schema field names
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
@@ -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,EAAa,MAAM,gBAAgB,CAAC;AAE3D,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;CAC3E;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,cAAc,GAAG,SAAS,CAyB9D"}
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
- export function mysqlQueryable(pool) {
2
- return {
3
- async query(text, values) {
4
- const [result] = await pool.query(text, values);
5
- // SELECT returns an array of rows
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: header.affectedRows ?? 0,
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,EAAa,MAAM,gBAAgB,CAAC;AAE3D,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;CACpD;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,CAa1D"}
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
- return {
3
- async query(text, values) {
4
- const result = await client.query(text, values);
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
- rows: result.rows,
7
- affectedRows: result.rowCount ?? 0,
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,CAyBvB"}
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,CAqBvB"}
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
- FROM information_schema.columns
21
- WHERE table_schema = $1
22
- ORDER BY table_name, ordinal_position`, [schema]);
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>): 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,GACjE,MAAM,CAoDR;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,UAAU,EAAE,GAAG,QAAQ,CAqB1D;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,GAC7B,MAAM,CA2CR"}
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
- const hasDefault = columnInfo.column_default;
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;CACpC;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,CA2BpE;AAgBD,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"}
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: '${pk}',`);
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, ['${pk}'])),`);
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 !== pk && schema.fieldTypes[f]?.includes('String'));
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 => f !== pk) || schema.fields[0];
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: '${pk}',`);
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}, ['${pk}'])),`);
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 => f !== pk) || schema.fields[0];
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>[]>;