fastify-auto-sqlapi 0.0.4 → 0.1.1

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 (127) hide show
  1. package/AGENTS.md +15 -2
  2. package/AGENTS_BACKEND.md +160 -32
  3. package/AGENTS_FRONTEND.md +220 -49
  4. package/README.md +147 -27
  5. package/dist/bin/generate-schema.js +12 -29
  6. package/dist/bin/generate-tables.js +10 -33
  7. package/dist/bin/utils.d.ts +29 -1
  8. package/dist/bin/utils.d.ts.map +1 -1
  9. package/dist/bin/utils.js +71 -2
  10. package/dist/index.d.ts +1 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/lib/cli/load-dependency.d.ts +8 -0
  13. package/dist/lib/cli/load-dependency.d.ts.map +1 -0
  14. package/dist/lib/cli/load-dependency.js +21 -0
  15. package/dist/lib/cli/mysql-introspect.d.ts.map +1 -1
  16. package/dist/lib/cli/mysql-introspect.js +2 -11
  17. package/dist/lib/cli/pg-introspect.d.ts.map +1 -1
  18. package/dist/lib/cli/pg-introspect.js +2 -17
  19. package/dist/lib/cli/schema-codegen.d.ts +1 -1
  20. package/dist/lib/cli/schema-codegen.d.ts.map +1 -1
  21. package/dist/lib/cli/schema-codegen.js +11 -4
  22. package/dist/lib/cli/tables-codegen.d.ts.map +1 -1
  23. package/dist/lib/cli/tables-codegen.js +4 -3
  24. package/dist/lib/db.d.ts +1 -0
  25. package/dist/lib/db.d.ts.map +1 -1
  26. package/dist/lib/db.js +33 -49
  27. package/dist/lib/dialect.d.ts +17 -0
  28. package/dist/lib/dialect.d.ts.map +1 -1
  29. package/dist/lib/dialect.js +32 -0
  30. package/dist/lib/engine/bulk/bulk-upsert.d.ts.map +1 -1
  31. package/dist/lib/engine/bulk/bulk-upsert.js +26 -50
  32. package/dist/lib/engine/rest/delete.d.ts.map +1 -1
  33. package/dist/lib/engine/rest/delete.js +5 -10
  34. package/dist/lib/engine/rest/get.d.ts.map +1 -1
  35. package/dist/lib/engine/rest/get.js +4 -6
  36. package/dist/lib/engine/rest/insert.d.ts.map +1 -1
  37. package/dist/lib/engine/rest/insert.js +20 -39
  38. package/dist/lib/engine/rest/update.d.ts.map +1 -1
  39. package/dist/lib/engine/rest/update.js +35 -67
  40. package/dist/lib/engine/search/search.d.ts.map +1 -1
  41. package/dist/lib/engine/search/search.js +650 -130
  42. package/dist/lib/engine/write-helpers.d.ts +39 -2
  43. package/dist/lib/engine/write-helpers.d.ts.map +1 -1
  44. package/dist/lib/engine/write-helpers.js +58 -15
  45. package/dist/lib/errors.d.ts +11 -0
  46. package/dist/lib/errors.d.ts.map +1 -0
  47. package/dist/lib/errors.js +12 -0
  48. package/dist/lib/naming.d.ts +11 -2
  49. package/dist/lib/naming.d.ts.map +1 -1
  50. package/dist/lib/naming.js +33 -5
  51. package/dist/lib/schema/bulk-upsert.d.ts.map +1 -1
  52. package/dist/lib/schema/bulk-upsert.js +9 -25
  53. package/dist/lib/schema/helpers.d.ts +28 -1
  54. package/dist/lib/schema/helpers.d.ts.map +1 -1
  55. package/dist/lib/schema/helpers.js +38 -0
  56. package/dist/lib/schema/insert.d.ts.map +1 -1
  57. package/dist/lib/schema/insert.js +9 -19
  58. package/dist/lib/schema/search.d.ts.map +1 -1
  59. package/dist/lib/schema/search.js +147 -53
  60. package/dist/lib/schema/update.d.ts.map +1 -1
  61. package/dist/lib/schema/update.js +9 -25
  62. package/dist/lib/sql-api.d.ts +6 -6
  63. package/dist/lib/sql-api.d.ts.map +1 -1
  64. package/dist/lib/sql-api.js +8 -8
  65. package/dist/lib/table-helpers.d.ts +6 -1
  66. package/dist/lib/table-helpers.d.ts.map +1 -1
  67. package/dist/lib/table-helpers.js +51 -3
  68. package/dist/lib/tenant.d.ts +23 -0
  69. package/dist/lib/tenant.d.ts.map +1 -1
  70. package/dist/lib/tenant.js +57 -9
  71. package/dist/routes/auto/bulk-delete.routes.d.ts.map +1 -1
  72. package/dist/routes/auto/bulk-delete.routes.js +17 -23
  73. package/dist/routes/auto/bulk-upsert.routes.d.ts.map +1 -1
  74. package/dist/routes/auto/bulk-upsert.routes.js +16 -22
  75. package/dist/routes/auto/delete.routes.d.ts.map +1 -1
  76. package/dist/routes/auto/delete.routes.js +18 -24
  77. package/dist/routes/auto/get.routes.d.ts.map +1 -1
  78. package/dist/routes/auto/get.routes.js +16 -23
  79. package/dist/routes/auto/insert.routes.d.ts.map +1 -1
  80. package/dist/routes/auto/insert.routes.js +19 -26
  81. package/dist/routes/auto/route-helpers.d.ts +27 -2
  82. package/dist/routes/auto/route-helpers.d.ts.map +1 -1
  83. package/dist/routes/auto/route-helpers.js +39 -1
  84. package/dist/routes/auto/search.routes.d.ts.map +1 -1
  85. package/dist/routes/auto/search.routes.js +47 -47
  86. package/dist/routes/auto/update.routes.d.ts.map +1 -1
  87. package/dist/routes/auto/update.routes.js +20 -27
  88. package/dist/types/cli.d.ts +22 -0
  89. package/dist/types/cli.d.ts.map +1 -0
  90. package/dist/types/cli.js +1 -0
  91. package/dist/types/computed.d.ts +29 -0
  92. package/dist/types/computed.d.ts.map +1 -0
  93. package/dist/types/computed.js +1 -0
  94. package/dist/types/conditions.d.ts +16 -0
  95. package/dist/types/conditions.d.ts.map +1 -0
  96. package/dist/types/conditions.js +1 -0
  97. package/dist/types/db.d.ts +22 -0
  98. package/dist/types/db.d.ts.map +1 -0
  99. package/dist/types/db.js +1 -0
  100. package/dist/types/dml.d.ts +64 -0
  101. package/dist/types/dml.d.ts.map +1 -0
  102. package/dist/types/dml.js +1 -0
  103. package/dist/types/join.d.ts +44 -0
  104. package/dist/types/join.d.ts.map +1 -0
  105. package/dist/types/join.js +1 -0
  106. package/dist/types/plugin.d.ts +20 -0
  107. package/dist/types/plugin.d.ts.map +1 -0
  108. package/dist/types/plugin.js +1 -0
  109. package/dist/types/schema.d.ts +10 -0
  110. package/dist/types/schema.d.ts.map +1 -0
  111. package/dist/types/schema.js +1 -0
  112. package/dist/types/search.d.ts +45 -0
  113. package/dist/types/search.d.ts.map +1 -0
  114. package/dist/types/search.js +1 -0
  115. package/dist/types/table.d.ts +52 -0
  116. package/dist/types/table.d.ts.map +1 -0
  117. package/dist/types/table.js +7 -0
  118. package/dist/types/tenant.d.ts +19 -0
  119. package/dist/types/tenant.d.ts.map +1 -0
  120. package/dist/types/tenant.js +1 -0
  121. package/dist/types/validation.d.ts +21 -0
  122. package/dist/types/validation.d.ts.map +1 -0
  123. package/dist/types/validation.js +1 -0
  124. package/dist/types.d.ts +12 -258
  125. package/dist/types.d.ts.map +1 -1
  126. package/dist/types.js +12 -7
  127. package/package.json +8 -5
@@ -1,4 +1,4 @@
1
- import { createRequire } from 'node:module';
1
+ import { loadOptionalDependency } from './load-dependency.js';
2
2
  export function buildMysqlConnectionConfig() {
3
3
  if (process.env.DATABASE_URL) {
4
4
  const url = new URL(process.env.DATABASE_URL);
@@ -66,17 +66,8 @@ function mapMysqlType(dataType) {
66
66
  }
67
67
  }
68
68
  export async function introspectMysqlTables(connectionConfig, schema) {
69
- // Dynamic import: mysql2 is optional peer dependency.
70
- // Use createRequire from cwd so it works with npm link.
71
69
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
72
- let mysql2;
73
- try {
74
- const require = createRequire(process.cwd() + '/noop.js');
75
- mysql2 = require('mysql2/promise');
76
- }
77
- catch {
78
- throw new Error('mysql2 is required for MySQL/MariaDB introspection. Install it with: npm install mysql2');
79
- }
70
+ const mysql2 = loadOptionalDependency('mysql2/promise', 'npm install mysql2');
80
71
  const connection = await mysql2.createConnection(connectionConfig);
81
72
  try {
82
73
  const [rows] = await connection.query(`SELECT table_name, column_name, data_type, column_default, is_nullable
@@ -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,CAkCvB"}
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,4 +1,4 @@
1
- import { createRequire } from 'node:module';
1
+ import { loadOptionalDependency } from './load-dependency.js';
2
2
  export function buildConnectionString() {
3
3
  if (process.env.DATABASE_URL) {
4
4
  return process.env.DATABASE_URL;
@@ -11,23 +11,8 @@ export function buildConnectionString() {
11
11
  return `postgres://${user}:${password}@${host}:${port}/${db}`;
12
12
  }
13
13
  export async function introspectTables(connectionString, schema) {
14
- // Dynamic require from cwd so it works with npm link
15
14
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
- let pg;
17
- try {
18
- const require = createRequire(process.cwd() + '/noop.js');
19
- pg = require('pg');
20
- }
21
- catch {
22
- try {
23
- // Fallback: resolve from the package's own node_modules
24
- const require = createRequire(import.meta.url);
25
- pg = require('pg');
26
- }
27
- catch {
28
- throw new Error('pg is required for PostgreSQL introspection. Install it with: npm install pg');
29
- }
30
- }
15
+ const pg = loadOptionalDependency('pg', 'npm install pg');
31
16
  const client = new pg.Client({ connectionString });
32
17
  try {
33
18
  await client.connect();
@@ -4,5 +4,5 @@ export declare function convertColType(udtName: string, columnInfo: {
4
4
  is_nullable: string;
5
5
  }): string;
6
6
  export declare function buildTableMap(rows: ColumnInfo[]): TableMap;
7
- export declare function generateSchemaFile(schemaName: string, tableName: string, fields: Record<string, string>): string;
7
+ export declare function generateSchemaFile(schemaName: string, tableName: string, fields: Record<string, string>, colMap: Record<string, string>): string;
8
8
  //# 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,CAkB1D;AAED,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC7B,MAAM,CAsCR"}
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"}
@@ -57,20 +57,26 @@ export function buildTableMap(rows) {
57
57
  acc[schemaName] = {
58
58
  name: row.table_name,
59
59
  fields: {},
60
+ colMap: {},
60
61
  };
61
62
  }
62
- acc[schemaName].fields[toCamelCase(row.column_name)] = convertColType(row.udt_name, row);
63
+ const fieldName = toCamelCase(row.column_name);
64
+ acc[schemaName].fields[fieldName] = convertColType(row.udt_name, row);
65
+ acc[schemaName].colMap[fieldName] = row.column_name;
63
66
  return acc;
64
67
  }, {});
65
68
  }
66
- export function generateSchemaFile(schemaName, tableName, fields) {
69
+ export function generateSchemaFile(schemaName, tableName, fields, colMap) {
67
70
  const fieldsStr = JSON.stringify(fields, null, 2).replace(/"(.*?)"\s*:\s*"Type(.*?)"/g, '$1: Type$2');
71
+ const colMapStr = JSON.stringify(colMap, null, 2);
68
72
  return `// Auto-generated by fastify-auto-sqlapi. Do not edit.
69
- import { Type, toUnderscore } from "fastify-auto-sqlapi";
73
+ import { Type } from "fastify-auto-sqlapi";
70
74
  import type { Static } from "fastify-auto-sqlapi";
71
75
 
72
76
  const _Schema = ${fieldsStr};
73
77
 
78
+ const _colMap: Record<string, string> = ${colMapStr};
79
+
74
80
  const validation = Type.Object(_Schema, { $id: "${schemaName}" });
75
81
  const partialValidation = Type.Object(
76
82
  Object.fromEntries(
@@ -83,7 +89,8 @@ const partialValidation = Type.Object(
83
89
  );
84
90
 
85
91
  export const Schema = {
86
- col: (f: keyof typeof _Schema) => toUnderscore(f),
92
+ col: (f: keyof typeof _Schema) => _colMap[f as string] ?? f,
93
+ colMap: _colMap,
87
94
  fields: _Schema,
88
95
  validation,
89
96
  tableName: "${tableName}",
@@ -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,CAuHhG;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;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"}
@@ -98,6 +98,7 @@ export function generateSingleTableFile(schema, allSchemas) {
98
98
  const lines = [];
99
99
  // Imports
100
100
  lines.push(`import {defineTable, exportTableInfo, Type} from 'fastify-auto-sqlapi';`);
101
+ lines.push(`import type {ValidationError} from 'fastify-auto-sqlapi';`);
101
102
  lines.push(`import {${schema.schemaName} as Schema} from '../schemas/${schema.schemaName}';`);
102
103
  // Commented imports for related schemas
103
104
  for (const relSchema of relatedSchemas) {
@@ -157,7 +158,7 @@ export function generateSingleTableFile(schema, allSchemas) {
157
158
  // validate: use first non-PK field for example
158
159
  const exampleField = schema.fields.find(f => f !== pk) || schema.fields[0];
159
160
  lines.push(` validate: async (db, req, main, secondaries) => {`);
160
- lines.push(` const errors = [];`);
161
+ lines.push(` const errors: ValidationError[] = [];`);
161
162
  lines.push(` // if (!main.${exampleField}) errors.push(['${exampleField}', 'required']);`);
162
163
  lines.push(` return errors;`);
163
164
  lines.push(` },`);
@@ -209,7 +210,7 @@ export function generateTablesFile(schemas) {
209
210
  const lines = [];
210
211
  // Imports
211
212
  lines.push(`import {defineTable, exportTableInfo, Type} from 'fastify-auto-sqlapi';`);
212
- lines.push(`import type { DbTables } from 'fastify-auto-sqlapi';`);
213
+ lines.push(`import type { DbTables, ValidationError } from 'fastify-auto-sqlapi';`);
213
214
  lines.push(``);
214
215
  // Schema imports
215
216
  for (const schema of schemas) {
@@ -247,7 +248,7 @@ export function generateTablesFile(schemas) {
247
248
  lines.push(` // schemaOverrides: {},`);
248
249
  const exampleFieldLegacy = schema.fields.find(f => f !== pk) || schema.fields[0];
249
250
  lines.push(` validate: async (db, req, main, secondaries) => {`);
250
- lines.push(` const errors = [];`);
251
+ lines.push(` const errors: ValidationError[] = [];`);
251
252
  lines.push(` // if (!main.${exampleFieldLegacy}) errors.push(['${exampleFieldLegacy}', 'required']);`);
252
253
  lines.push(` return errors;`);
253
254
  lines.push(` },`);
package/dist/lib/db.d.ts CHANGED
@@ -13,6 +13,7 @@ export declare class QueryClient {
13
13
  get qi(): (id: string) => string;
14
14
  get ph(): (i: number) => string;
15
15
  get dialectName(): string;
16
+ get dateTrunc(): SqlDialect['dateTrunc'];
16
17
  setDebug(mode: boolean): void;
17
18
  expression(value: string): Expression;
18
19
  query<T = Record<string, unknown>>(text: string, values?: unknown[]): Promise<SqlResult<T>>;
@@ -1 +1 @@
1
- {"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/lib/db.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAK/C,sCAAsC;AACtC,eAAO,MAAM,WAAW,GAAI,GAAG,MAAM,WAA0B,CAAC;AAEhE,qBAAa,WAAW;;IACtB,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,OAAO,CAAa;IAC5B,OAAO,CAAC,KAAK,CAAS;gBAEV,MAAM,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,UAAU;IAKnD,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,CAE/B;IAED,IAAI,EAAE,IAAI,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAE9B;IAED,IAAI,WAAW,IAAI,MAAM,CAExB;IAED,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI;IAI7B,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU;IAIrC,KAAK,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,OAAO,EAAE,GACjB,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;IA6FlB,MAAM,CACV,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,QAAQ,EAChB,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,GACvB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAoB7B,cAAc,CAClB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,QAAQ,EAChB,YAAY,EAAE,MAAM,EAAE,EACtB,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,GACvB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAuB7B,UAAU,CACd,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,QAAQ,EAAE,EACnB,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,EACxB,SAAS,SAAqB,GAC7B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IA2B/B,kBAAkB,CACtB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,QAAQ,EAAE,EACnB,YAAY,EAAE,MAAM,EAAE,EACtB,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,EACxB,SAAS,SAAqB,GAC7B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IA+B/B,MAAM,CACV,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,QAAQ,EAChB,KAAK,EAAE,QAAQ,EACf,cAAc,CAAC,EAAE,gBAAgB,GAChC,OAAO,CAAC,MAAM,CAAC;IAuBZ,MAAM,CACV,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,QAAQ,GACd,OAAO,CAAC,MAAM,CAAC;IAYZ,MAAM,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EACxC,SAAS,EACT,OAAa,EACb,KAAK,EACL,MAAM,EACN,KAAY,EACZ,OAAY,EACZ,KAAU,EACV,QAAgB,GACjB,EAAE,aAAa,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC;CAchC;AAED,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,SAAS,EACf,WAAW,CAAC,EAAE,MAAM,GACnB,WAAW,CAGb"}
1
+ {"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/lib/db.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAQ/C,sCAAsC;AACtC,eAAO,MAAM,WAAW,GAAI,GAAG,MAAM,WAA0B,CAAC;AAEhE,qBAAa,WAAW;;IACtB,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,OAAO,CAAa;IAC5B,OAAO,CAAC,KAAK,CAAS;gBAEV,MAAM,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,UAAU;IAKnD,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,CAE/B;IAED,IAAI,EAAE,IAAI,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAE9B;IAED,IAAI,WAAW,IAAI,MAAM,CAExB;IAED,IAAI,SAAS,IAAI,UAAU,CAAC,WAAW,CAAC,CAEvC;IAED,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI;IAI7B,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU;IAIrC,KAAK,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,OAAO,EAAE,GACjB,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;IAmGlB,MAAM,CACV,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,QAAQ,EAChB,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,GACvB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAgB7B,cAAc,CAClB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,QAAQ,EAChB,YAAY,EAAE,MAAM,EAAE,EACtB,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,GACvB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAkB7B,UAAU,CACd,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,QAAQ,EAAE,EACnB,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,EACxB,SAAS,SAAqB,GAC7B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IAoB/B,kBAAkB,CACtB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,QAAQ,EAAE,EACnB,YAAY,EAAE,MAAM,EAAE,EACtB,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,EACxB,SAAS,SAAqB,GAC7B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IAuB/B,MAAM,CACV,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,QAAQ,EAChB,KAAK,EAAE,QAAQ,EACf,cAAc,CAAC,EAAE,gBAAgB,GAChC,OAAO,CAAC,MAAM,CAAC;IAuBZ,MAAM,CACV,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,QAAQ,GACd,OAAO,CAAC,MAAM,CAAC;IAYZ,MAAM,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EACxC,SAAS,EACT,OAAa,EACb,KAAK,EACL,MAAM,EACN,KAAY,EACZ,OAAY,EACZ,KAAU,EACV,QAAgB,GACjB,EAAE,aAAa,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC;CAchC;AAED,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,SAAS,EACf,WAAW,CAAC,EAAE,MAAM,GACnB,WAAW,CAGb"}
package/dist/lib/db.js CHANGED
@@ -1,5 +1,8 @@
1
1
  import { Expression } from 'node-condition-builder';
2
2
  import { getDialect } from './dialect.js';
3
+ // Default chunk size for bulk INSERT/UPSERT. Picked to keep parameter count well under
4
+ // PostgreSQL's hard limit of 65535 bind parameters per statement (≈ 130 columns × 500 rows).
5
+ // Override per-call via the `chunkSize` argument when working with extremely wide tables.
3
6
  const DEFAULT_CHUNK_SIZE = 500;
4
7
  /** @deprecated Use db.qi() instead */
5
8
  export const escapeIdent = (f) => f.replace(/"/g, '""');
@@ -20,6 +23,9 @@ export class QueryClient {
20
23
  get dialectName() {
21
24
  return this.dialect.name;
22
25
  }
26
+ get dateTrunc() {
27
+ return this.dialect.dateTrunc.bind(this.dialect);
28
+ }
23
29
  setDebug(mode) {
24
30
  this.debug = mode;
25
31
  }
@@ -73,26 +79,14 @@ export class QueryClient {
73
79
  }
74
80
  return { values, rows: rowPlaceholders.join(', ') };
75
81
  }
76
- // Build the update SET for upsert: dialect-specific
77
- #upsertUpdateSet(fields, conflictKeys) {
78
- const updateCols = fields.filter((f) => !conflictKeys.includes(f));
79
- if (!updateCols.length)
80
- return '';
81
- if (this.dialect.name === 'postgres') {
82
- return updateCols
83
- .map((f) => `${this.dialect.qi(f)} = EXCLUDED.${this.dialect.qi(f)}`)
84
- .join(', ');
85
- }
86
- // MySQL/MariaDB: VALUES(col) syntax
87
- return updateCols
88
- .map((f) => `${this.dialect.qi(f)} = VALUES(${this.dialect.qi(f)})`)
89
- .join(', ');
90
- }
91
82
  #quotedPkCols(pkCol) {
92
83
  const cols = Array.isArray(pkCol) ? pkCol : [pkCol];
93
84
  return cols.map((c) => this.dialect.qi(c)).join(', ');
94
85
  }
95
- #mysqlPkResult(pkCol, record, insertId) {
86
+ #insertHead(table, fields) {
87
+ return `INSERT INTO ${this.dialect.qi(table)} (${this.#q(fields)})`;
88
+ }
89
+ #mysqlPkRow(pkCol, record, insertId) {
96
90
  const cols = Array.isArray(pkCol) ? pkCol : [pkCol];
97
91
  const result = {};
98
92
  for (const col of cols) {
@@ -106,53 +100,51 @@ export class QueryClient {
106
100
  return { [pkCol]: insertId };
107
101
  return result;
108
102
  }
103
+ /**
104
+ * Single source of truth for "extract PK rows from an insert/upsert result". When the
105
+ * dialect supports RETURNING (postgres/mariadb), returns `result.rows` directly. Otherwise
106
+ * (mysql) synthesizes PK rows from `insertId` + the input records (assumes
107
+ * `auto_increment_increment=1` for multi-row inserts).
108
+ */
109
+ #extractPkRows(result, pkCol, inputs) {
110
+ if (this.dialect.supportsReturning)
111
+ return result.rows;
112
+ const firstId = result.insertId;
113
+ return inputs.map((rec, j) => this.#mysqlPkRow(pkCol, rec, typeof firstId === 'number' ? firstId + j : undefined));
114
+ }
109
115
  async insert(table, record, pkCol) {
110
116
  const values = [];
111
117
  const { fields, placeholders } = this.#params(record, values);
112
118
  if (!fields.length)
113
119
  throw new Error('Cannot execute empty insert');
114
120
  const returning = this.dialect.returningPk(this.#quotedPkCols(pkCol));
115
- const result = await this.query(`INSERT INTO ${this.dialect.qi(table)} (${this.#q(fields)})
121
+ const result = await this.query(`${this.#insertHead(table, fields)}
116
122
  VALUES (${placeholders.join(', ')})${returning}`, values);
117
- if (this.dialect.supportsReturning) {
118
- return result.rows[0];
119
- }
120
- return this.#mysqlPkResult(pkCol, record, result.insertId);
123
+ return this.#extractPkRows(result, pkCol, [record])[0];
121
124
  }
122
125
  async insertOrUpdate(table, record, conflictKeys, pkCol) {
123
126
  const values = [];
124
127
  const { fields, placeholders } = this.#params(record, values);
125
128
  if (!fields.length)
126
129
  throw new Error('Cannot execute empty insert');
127
- const updateSet = this.#upsertUpdateSet(fields, conflictKeys);
128
- const upsertClause = this.dialect.upsertSql(this.#q(conflictKeys), updateSet);
130
+ const upsertClause = this.dialect.upsertSql(this.#q(conflictKeys), this.dialect.upsertUpdateSet(fields, conflictKeys));
129
131
  const returning = this.dialect.returningPk(this.#quotedPkCols(pkCol));
130
- const result = await this.query(`INSERT INTO ${this.dialect.qi(table)} (${this.#q(fields)})
132
+ const result = await this.query(`${this.#insertHead(table, fields)}
131
133
  VALUES (${placeholders.join(', ')})
132
134
  ${upsertClause}${returning}`, values);
133
- if (this.dialect.supportsReturning) {
134
- return result.rows[0];
135
- }
136
- return this.#mysqlPkResult(pkCol, record, result.insertId);
135
+ return this.#extractPkRows(result, pkCol, [record])[0];
137
136
  }
138
137
  async bulkInsert(table, records, pkCol, chunkSize = DEFAULT_CHUNK_SIZE) {
139
138
  if (!records.length)
140
139
  return [];
141
140
  const fields = Object.keys(records[0]);
142
- const results = [];
143
141
  const returning = this.dialect.returningPk(this.#quotedPkCols(pkCol));
142
+ const results = [];
144
143
  for (let i = 0; i < records.length; i += chunkSize) {
145
144
  const chunk = records.slice(i, i + chunkSize);
146
145
  const { values, rows } = this.#bulkValues(fields, chunk);
147
- const result = await this.query(`INSERT INTO ${this.dialect.qi(table)} (${this.#q(fields)}) VALUES ${rows}${returning}`, values);
148
- if (this.dialect.supportsReturning) {
149
- results.push(...result.rows);
150
- }
151
- else {
152
- for (const rec of chunk) {
153
- results.push(this.#mysqlPkResult(pkCol, rec, result.insertId));
154
- }
155
- }
146
+ const result = await this.query(`${this.#insertHead(table, fields)} VALUES ${rows}${returning}`, values);
147
+ results.push(...this.#extractPkRows(result, pkCol, chunk));
156
148
  }
157
149
  return results;
158
150
  }
@@ -160,24 +152,16 @@ export class QueryClient {
160
152
  if (!records.length)
161
153
  return [];
162
154
  const fields = Object.keys(records[0]);
163
- const updateSet = this.#upsertUpdateSet(fields, conflictKeys);
164
- const upsertClause = this.dialect.upsertSql(this.#q(conflictKeys), updateSet);
155
+ const upsertClause = this.dialect.upsertSql(this.#q(conflictKeys), this.dialect.upsertUpdateSet(fields, conflictKeys));
165
156
  const returning = this.dialect.returningPk(this.#quotedPkCols(pkCol));
166
157
  const results = [];
167
158
  for (let i = 0; i < records.length; i += chunkSize) {
168
159
  const chunk = records.slice(i, i + chunkSize);
169
160
  const { values, rows } = this.#bulkValues(fields, chunk);
170
- const result = await this.query(`INSERT INTO ${this.dialect.qi(table)} (${this.#q(fields)})
161
+ const result = await this.query(`${this.#insertHead(table, fields)}
171
162
  VALUES ${rows}
172
163
  ${upsertClause}${returning}`, values);
173
- if (this.dialect.supportsReturning) {
174
- results.push(...result.rows);
175
- }
176
- else {
177
- for (const rec of chunk) {
178
- results.push(this.#mysqlPkResult(pkCol, rec, result.insertId));
179
- }
180
- }
164
+ results.push(...this.#extractPkRows(result, pkCol, chunk));
181
165
  }
182
166
  return results;
183
167
  }
@@ -1,4 +1,5 @@
1
1
  export type DialectName = 'postgres' | 'mysql' | 'mariadb';
2
+ export type TruncateUnit = 'year' | 'quarter' | 'month' | 'day' | 'hour';
2
3
  export interface SqlDialect {
3
4
  name: DialectName;
4
5
  /** Quote identifier: "id" (postgres) or `id` (mysql/mariadb) */
@@ -12,6 +13,22 @@ export interface SqlDialect {
12
13
  upsertSql(quotedConflictKeys: string, quotedUpdateSet: string): string;
13
14
  /** RETURNING pk_col suffix or empty string */
14
15
  returningPk(quotedPkCol: string): string;
16
+ /**
17
+ * Build the SET body for an upsert: the column list (separated by ', ') that the dialect
18
+ * uses to copy "incoming row" values into a conflicting row.
19
+ * - postgres: `"col" = EXCLUDED."col"`
20
+ * - mysql/mariadb: `` `col` = VALUES(`col`) ``
21
+ * Returns '' when there is nothing to update (i.e. all `fields` are conflict keys).
22
+ */
23
+ upsertUpdateSet(fields: string[], conflictKeys: string[]): string;
24
+ /**
25
+ * Truncate a timestamp/date column to the start of the given unit and
26
+ * return it as an ISO-style string. The output is always a STRING:
27
+ * - year/quarter/month/day → 'YYYY-MM-DD' (e.g. '2026-04-01')
28
+ * - hour → 'YYYY-MM-DDTHH:00:00'
29
+ * `qualifiedCol` is a fully-qualified, already-quoted column expression.
30
+ */
31
+ dateTrunc(unit: TruncateUnit, qualifiedCol: string): string;
15
32
  }
16
33
  export declare function getDialect(name: DialectName): SqlDialect;
17
34
  //# sourceMappingURL=dialect.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dialect.d.ts","sourceRoot":"","sources":["../../src/lib/dialect.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,OAAO,GAAG,SAAS,CAAC;AAE3D,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,WAAW,CAAC;IAClB,gEAAgE;IAChE,EAAE,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,sDAAsD;IACtD,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAC1B,oCAAoC;IACpC,SAAS,EAAE,UAAU,GAAG,OAAO,CAAC;IAChC,iBAAiB,EAAE,OAAO,CAAC;IAC3B,4CAA4C;IAC5C,SAAS,CAAC,kBAAkB,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,MAAM,CAAC;IACvE,8CAA8C;IAC9C,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAAC;CAC1C;AAqDD,wBAAgB,UAAU,CAAC,IAAI,EAAE,WAAW,GAAG,UAAU,CAIxD"}
1
+ {"version":3,"file":"dialect.d.ts","sourceRoot":"","sources":["../../src/lib/dialect.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,OAAO,GAAG,SAAS,CAAC;AAE3D,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,GAAG,KAAK,GAAG,MAAM,CAAC;AAEzE,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,WAAW,CAAC;IAClB,gEAAgE;IAChE,EAAE,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,sDAAsD;IACtD,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAC1B,oCAAoC;IACpC,SAAS,EAAE,UAAU,GAAG,OAAO,CAAC;IAChC,iBAAiB,EAAE,OAAO,CAAC;IAC3B,4CAA4C;IAC5C,SAAS,CAAC,kBAAkB,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,MAAM,CAAC;IACvE,8CAA8C;IAC9C,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAAC;IACzC;;;;;;OAMG;IACH,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC;IAClE;;;;;;OAMG;IACH,SAAS,CAAC,IAAI,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAAC;CAC7D;AAqFD,wBAAgB,UAAU,CAAC,IAAI,EAAE,WAAW,GAAG,UAAU,CAIxD"}
@@ -9,10 +9,36 @@ const PostgresDialect = {
9
9
  return `ON CONFLICT (${quotedConflictKeys}) DO NOTHING`;
10
10
  return `ON CONFLICT (${quotedConflictKeys}) DO UPDATE SET ${quotedUpdateSet}`;
11
11
  },
12
+ upsertUpdateSet(fields, conflictKeys) {
13
+ const updateCols = fields.filter((f) => !conflictKeys.includes(f));
14
+ if (!updateCols.length)
15
+ return '';
16
+ return updateCols.map((f) => `${this.qi(f)} = EXCLUDED.${this.qi(f)}`).join(', ');
17
+ },
12
18
  returningPk(quotedPkCol) {
13
19
  return ` RETURNING ${quotedPkCol}`;
14
20
  },
21
+ dateTrunc(unit, col) {
22
+ const fmt = unit === 'hour' ? `'YYYY-MM-DD"T"HH24:00:00'` : `'YYYY-MM-DD'`;
23
+ // DATE_TRUNC accepts a literal unit string, never user input — `unit` is whitelisted upstream.
24
+ return `TO_CHAR(DATE_TRUNC('${unit}', ${col}), ${fmt})`;
25
+ },
15
26
  };
27
+ function mysqlDateTrunc(unit, col) {
28
+ switch (unit) {
29
+ case 'year': return `DATE_FORMAT(${col}, '%Y-01-01')`;
30
+ case 'quarter': return `CONCAT(YEAR(${col}), '-', LPAD((QUARTER(${col})-1)*3+1, 2, '0'), '-01')`;
31
+ case 'month': return `DATE_FORMAT(${col}, '%Y-%m-01')`;
32
+ case 'day': return `DATE_FORMAT(${col}, '%Y-%m-%d')`;
33
+ case 'hour': return `DATE_FORMAT(${col}, '%Y-%m-%dT%H:00:00')`;
34
+ }
35
+ }
36
+ function mysqlUpsertUpdateSet(fields, conflictKeys) {
37
+ const updateCols = fields.filter((f) => !conflictKeys.includes(f));
38
+ if (!updateCols.length)
39
+ return '';
40
+ return updateCols.map((f) => `${this.qi(f)} = VALUES(${this.qi(f)})`).join(', ');
41
+ }
16
42
  const MysqlDialect = {
17
43
  name: 'mysql',
18
44
  qi: (id) => `\`${id.replace(/`/g, '``')}\``,
@@ -20,13 +46,17 @@ const MysqlDialect = {
20
46
  cbDialect: 'mysql',
21
47
  supportsReturning: false,
22
48
  upsertSql(_quotedConflictKeys, quotedUpdateSet) {
49
+ // MySQL requires at least one assignment in ON DUPLICATE KEY UPDATE; when there's nothing
50
+ // user-meaningful to update, assign the first conflict key to itself (no-op).
23
51
  if (!quotedUpdateSet)
24
52
  return `ON DUPLICATE KEY UPDATE ${_quotedConflictKeys.split(', ')[0]} = ${_quotedConflictKeys.split(', ')[0]}`;
25
53
  return `ON DUPLICATE KEY UPDATE ${quotedUpdateSet}`;
26
54
  },
55
+ upsertUpdateSet: mysqlUpsertUpdateSet,
27
56
  returningPk() {
28
57
  return '';
29
58
  },
59
+ dateTrunc: mysqlDateTrunc,
30
60
  };
31
61
  const MariadbDialect = {
32
62
  name: 'mariadb',
@@ -39,9 +69,11 @@ const MariadbDialect = {
39
69
  return `ON DUPLICATE KEY UPDATE ${_quotedConflictKeys.split(', ')[0]} = ${_quotedConflictKeys.split(', ')[0]}`;
40
70
  return `ON DUPLICATE KEY UPDATE ${quotedUpdateSet}`;
41
71
  },
72
+ upsertUpdateSet: mysqlUpsertUpdateSet,
42
73
  returningPk(quotedPkCol) {
43
74
  return ` RETURNING ${quotedPkCol}`;
44
75
  },
76
+ dateTrunc: mysqlDateTrunc,
45
77
  };
46
78
  const dialects = {
47
79
  postgres: PostgresDialect,
@@ -1 +1 @@
1
- {"version":3,"file":"bulk-upsert.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/bulk/bulk-upsert.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,gBAAgB,EAChB,gBAAgB,EAGjB,MAAM,mBAAmB,CAAC;AAE3B,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAgG5F"}
1
+ {"version":3,"file":"bulk-upsert.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/bulk/bulk-upsert.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,gBAAgB,EAChB,gBAAgB,EAEjB,MAAM,mBAAmB,CAAC;AAE3B,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAkF5F"}
@@ -1,80 +1,56 @@
1
- import { camelcaseObject, snakecaseRecord } from '../../naming.js';
2
- import { removeExcludedFields, processSecondaries, processDeletions } from '../write-helpers.js';
3
- import { injectTenantValue, validateTenantFK } from '../../tenant.js';
4
- import { runValidation, runBulkValidation } from '../validate.js';
1
+ import { processSecondaries, processDeletions, prepareInsertRecord } from '../write-helpers.js';
2
+ import { enforceTenantOnWrites } from '../../tenant.js';
3
+ import { runBulkValidation } from '../validate.js';
5
4
  import { primaryAsString } from '../../../types.js';
6
5
  export async function bulkUpsertEngine(params) {
7
6
  const { db, tableConf, dbTables, request, items, tenant } = params;
8
7
  if (!items.length)
9
8
  return [];
9
+ const schema = tableConf.Schema;
10
10
  const pk = primaryAsString(tableConf.primary);
11
- const pkCol = tableConf.Schema.col(pk);
12
- // 1. Prepare all main records
13
- const preparedMains = items.map((item) => {
14
- let rec = snakecaseRecord(item.main);
15
- rec = removeExcludedFields(rec, tableConf);
16
- return rec;
17
- });
18
- // 1b. Tenant: inject or validate FK
19
- if (tenant) {
20
- if ('through' in tenant.scope) {
21
- // Indirect: collect all FK values, validate in batch
22
- const scope = tenant.scope;
23
- const fkCol = scope.through.localField;
24
- const fkValues = preparedMains.map((rec) => rec[fkCol]);
25
- await validateTenantFK(db, scope, tenant.ids, fkValues);
26
- }
27
- else {
28
- for (const rec of preparedMains) {
29
- injectTenantValue(rec, tenant.scope, tenant.ids);
30
- }
31
- }
32
- }
33
- // 2. Custom validation (receives original camelCase records)
11
+ const pkCol = schema.col(pk);
12
+ // 1. Bulk validation runs once for all items (skips per-item validate). Otherwise per-item
13
+ // validation happens inside prepareInsertRecord below.
34
14
  if (tableConf.validateBulk) {
35
15
  await runBulkValidation(db, request, tableConf, items);
36
16
  }
37
- else {
38
- for (let i = 0; i < items.length; i++) {
39
- await runValidation(db, request, tableConf, items[i].main, items[i].secondaries);
40
- }
41
- }
42
- // 3. beforeInsert hook per record
43
- if (tableConf.beforeInsert) {
44
- for (const rec of preparedMains) {
45
- await tableConf.beforeInsert(db, request, rec);
46
- }
47
- }
17
+ // 2. For each item: validate (unless validateBulk ran) → beforeInsert → snake → remove excluded.
18
+ const prepared = await Promise.all(items.map((item) => prepareInsertRecord({ db, tableConf, request }, item.main, item.secondaries, { skipValidate: !!tableConf.validateBulk })));
19
+ const inputMains = prepared.map((p) => p.camel);
20
+ const preparedMains = prepared.map((p) => p.snake);
21
+ // 3. Tenant: inject (direct) or batch-validate FK (indirect) — enforced after user mutations
22
+ await enforceTenantOnWrites(db, tenant, preparedMains);
48
23
  // 4. Bulk upsert all mains in one query → returns PK-only
49
- const upsertKeys = tableConf.upsertMap?.get(tableConf.Schema);
24
+ const upsertKeys = tableConf.upsertMap?.get(schema);
50
25
  let pkRows;
51
26
  if (upsertKeys) {
52
- const conflictCols = upsertKeys.map((k) => tableConf.Schema.col(k));
53
- pkRows = await db.bulkInsertOrUpdate(tableConf.Schema.tableName, preparedMains, conflictCols, pkCol);
27
+ const conflictCols = upsertKeys.map((k) => schema.col(k));
28
+ pkRows = await db.bulkInsertOrUpdate(schema.tableName, preparedMains, conflictCols, pkCol);
54
29
  }
55
30
  else {
56
- pkRows = await db.bulkInsert(tableConf.Schema.tableName, preparedMains, pkCol);
31
+ pkRows = await db.bulkInsert(schema.tableName, preparedMains, pkCol);
57
32
  }
58
- const pksCamel = pkRows.map((r) => camelcaseObject(r));
59
- // 5. Process secondaries and deletions per item
33
+ // 5. Process secondaries, deletions, afterInsert per item (all camelCase)
60
34
  const results = [];
61
35
  for (let i = 0; i < items.length; i++) {
62
36
  const item = items[i];
63
- const mainPk = pksCamel[i];
64
- const result = { main: mainPk };
65
- // For FK auto-fill in secondaries, merge input record + PK
66
- const mainForFK = { ...camelcaseObject(preparedMains[i]), ...mainPk };
37
+ const mainPk = pkRows[i]; // PK-only (already in camelCase field name since PK uses camelCase field)
38
+ // pkRows entries come from SQL RETURNING or insertId — keys are DB column names.
39
+ // For simple PK named the same in API and DB (like 'id') this is transparent.
40
+ const result = { main: { [pk]: mainPk[pkCol] ?? mainPk[pk] } };
41
+ // FK auto-fill: input camelCase + generated PK
42
+ const mainForFK = { ...inputMains[i], [pk]: mainPk[pkCol] ?? mainPk[pk] };
67
43
  if (item.secondaries && Object.keys(item.secondaries).length > 0) {
68
44
  const sec = await processSecondaries(db, tableConf, dbTables, mainForFK, item.secondaries);
69
45
  if (Object.keys(sec).length > 0)
70
46
  result.secondaries = sec;
71
47
  }
72
48
  if (item.deletions && Object.keys(item.deletions).length > 0) {
73
- const del = await processDeletions(db, tableConf, item.deletions);
49
+ const del = await processDeletions(db, tableConf, mainForFK, item.deletions);
74
50
  if (Object.keys(del).length > 0)
75
51
  result.deletions = del;
76
52
  }
77
- // 6. afterInsert hook per item
53
+ // 6. afterInsert hook per item (camelCase)
78
54
  if (tableConf.afterInsert) {
79
55
  await tableConf.afterInsert(db, request, mainForFK, result.secondaries);
80
56
  }
@@ -1 +1 @@
1
- {"version":3,"file":"delete.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/delete.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAY,MAAM,mBAAmB,CAAC;AAE9E,wBAAsB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CA8B9E"}
1
+ {"version":3,"file":"delete.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/delete.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAY,MAAM,mBAAmB,CAAC;AAE9E,wBAAsB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAsB9E"}
@@ -1,4 +1,5 @@
1
1
  import { buildTenantDeleteWhere } from '../../tenant.js';
2
+ import { httpError } from '../../errors.js';
2
3
  import { primaryAsString } from '../../../types.js';
3
4
  export async function deleteEngine(params) {
4
5
  const { db, tableConf, id, tenant } = params;
@@ -7,19 +8,13 @@ export async function deleteEngine(params) {
7
8
  const tableName = tableConf.Schema.tableName;
8
9
  if (!tenant) {
9
10
  const affectedRows = await db.delete(tableName, { [pkCol]: id });
10
- if (affectedRows === 0) {
11
- const err = new Error(`Record not found: ${id}`);
12
- err.statusCode = 404;
13
- throw err;
14
- }
11
+ if (affectedRows === 0)
12
+ throw httpError(404, `Record not found: ${id}`);
15
13
  return { main: { [pk]: id } };
16
14
  }
17
15
  const { where, values } = buildTenantDeleteWhere(db, tableName, pkCol, id, tenant);
18
16
  const result = await db.query(`DELETE FROM ${db.qi(tableName)} WHERE ${where}`, values);
19
- if (result.affectedRows === 0) {
20
- const err = new Error(`Record not found: ${id}`);
21
- err.statusCode = 404;
22
- throw err;
23
- }
17
+ if (result.affectedRows === 0)
18
+ throw httpError(404, `Record not found: ${id}`);
24
19
  return { main: { [pk]: id } };
25
20
  }
@@ -1 +1 @@
1
- {"version":3,"file":"get.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/get.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAuB,MAAM,mBAAmB,CAAC;AAEnF,wBAAsB,SAAS,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC,CAiCrE"}
1
+ {"version":3,"file":"get.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/get.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAuB,MAAM,mBAAmB,CAAC;AAEnF,wBAAsB,SAAS,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC,CA6BrE"}
@@ -1,6 +1,7 @@
1
1
  import { ConditionBuilder } from 'node-condition-builder';
2
2
  import { camelcaseObject } from '../../naming.js';
3
3
  import { buildTenantCondition, buildTenantJoin } from '../../tenant.js';
4
+ import { httpError } from '../../errors.js';
4
5
  import { primaryAsString } from '../../../types.js';
5
6
  export async function getEngine(params) {
6
7
  const { db, tableConf, id, tenant } = params;
@@ -23,10 +24,7 @@ export async function getEngine(params) {
23
24
  limit: '1',
24
25
  joins: joins.length > 0 ? joins : undefined,
25
26
  });
26
- if (rows.length === 0) {
27
- const err = new Error(`Record not found: ${id}`);
28
- err.statusCode = 404;
29
- throw err;
30
- }
31
- return { main: camelcaseObject(rows[0]) };
27
+ if (rows.length === 0)
28
+ throw httpError(404, `Record not found: ${id}`);
29
+ return { main: camelcaseObject(rows[0], tableConf.Schema) };
32
30
  }
@@ -1 +1 @@
1
- {"version":3,"file":"insert.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/insert.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,YAAY,EACZ,YAAY,EAGb,MAAM,mBAAmB,CAAC;AAE3B,wBAAsB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAyE9E"}
1
+ {"version":3,"file":"insert.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/insert.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,YAAY,EACZ,YAAY,EAEb,MAAM,mBAAmB,CAAC;AAE3B,wBAAsB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CA0D9E"}