fastify-auto-sqlapi 0.1.13 → 0.2.0

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 (91) hide show
  1. package/AGENTS_BACKEND.md +32 -5
  2. package/BREAKING_CHANGES.md +80 -5
  3. package/CHANGELOG.md +45 -0
  4. package/README.md +49 -7
  5. package/dist/bin/generate-schema.js +89 -77
  6. package/dist/bin/generate-tables.js +72 -62
  7. package/dist/bin/utils.d.ts.map +1 -1
  8. package/dist/bin/utils.js +47 -33
  9. package/dist/lib/agent/manifest.d.ts +3 -2
  10. package/dist/lib/agent/manifest.d.ts.map +1 -1
  11. package/dist/lib/agent/manifest.js +104 -80
  12. package/dist/lib/cli/mysql-introspect.d.ts +4 -9
  13. package/dist/lib/cli/mysql-introspect.d.ts.map +1 -1
  14. package/dist/lib/cli/mysql-introspect.js +8 -7
  15. package/dist/lib/cli/pg-introspect.d.ts.map +1 -1
  16. package/dist/lib/cli/pg-introspect.js +0 -1
  17. package/dist/lib/cli/schema-codegen.d.ts +3 -0
  18. package/dist/lib/cli/schema-codegen.d.ts.map +1 -1
  19. package/dist/lib/cli/schema-codegen.js +3 -0
  20. package/dist/lib/cli/tables-codegen.d.ts +8 -1
  21. package/dist/lib/cli/tables-codegen.d.ts.map +1 -1
  22. package/dist/lib/cli/tables-codegen.js +100 -118
  23. package/dist/lib/condition-methods.d.ts +14 -4
  24. package/dist/lib/condition-methods.d.ts.map +1 -1
  25. package/dist/lib/condition-methods.js +18 -4
  26. package/dist/lib/db.d.ts +1 -1
  27. package/dist/lib/db.d.ts.map +1 -1
  28. package/dist/lib/db.js +16 -4
  29. package/dist/lib/dialect.d.ts +2 -1
  30. package/dist/lib/dialect.d.ts.map +1 -1
  31. package/dist/lib/engine/bulk/bulk-upsert.d.ts.map +1 -1
  32. package/dist/lib/engine/bulk/bulk-upsert.js +42 -35
  33. package/dist/lib/engine/rest/delete.d.ts.map +1 -1
  34. package/dist/lib/engine/rest/get.d.ts.map +1 -1
  35. package/dist/lib/engine/rest/insert.d.ts.map +1 -1
  36. package/dist/lib/engine/rest/insert.js +1 -1
  37. package/dist/lib/engine/rest/update.d.ts.map +1 -1
  38. package/dist/lib/engine/rest/update.js +25 -18
  39. package/dist/lib/engine/search/aggregations.d.ts +33 -0
  40. package/dist/lib/engine/search/aggregations.d.ts.map +1 -0
  41. package/dist/lib/engine/search/aggregations.js +109 -0
  42. package/dist/lib/engine/search/conditions.d.ts +31 -0
  43. package/dist/lib/engine/search/conditions.d.ts.map +1 -0
  44. package/dist/lib/engine/search/conditions.js +131 -0
  45. package/dist/lib/engine/search/fields.d.ts +49 -0
  46. package/dist/lib/engine/search/fields.d.ts.map +1 -0
  47. package/dist/lib/engine/search/fields.js +142 -0
  48. package/dist/lib/engine/search/join-families.d.ts +35 -0
  49. package/dist/lib/engine/search/join-families.d.ts.map +1 -0
  50. package/dist/lib/engine/search/join-families.js +345 -0
  51. package/dist/lib/engine/search/joins.d.ts +55 -0
  52. package/dist/lib/engine/search/joins.d.ts.map +1 -0
  53. package/dist/lib/engine/search/joins.js +81 -0
  54. package/dist/lib/engine/search/order-by.d.ts +35 -0
  55. package/dist/lib/engine/search/order-by.d.ts.map +1 -0
  56. package/dist/lib/engine/search/order-by.js +104 -0
  57. package/dist/lib/engine/search/search.d.ts.map +1 -1
  58. package/dist/lib/engine/search/search.js +106 -749
  59. package/dist/lib/engine/validate.d.ts +0 -5
  60. package/dist/lib/engine/validate.d.ts.map +1 -1
  61. package/dist/lib/engine/write-helpers.d.ts +25 -17
  62. package/dist/lib/engine/write-helpers.d.ts.map +1 -1
  63. package/dist/lib/engine/write-helpers.js +86 -44
  64. package/dist/lib/read-access.d.ts +37 -1
  65. package/dist/lib/read-access.d.ts.map +1 -1
  66. package/dist/lib/read-access.js +64 -1
  67. package/dist/lib/schema/helpers.d.ts +0 -15
  68. package/dist/lib/schema/helpers.d.ts.map +1 -1
  69. package/dist/lib/schema/helpers.js +7 -4
  70. package/dist/lib/schema/search.d.ts.map +1 -1
  71. package/dist/lib/schema/search.js +120 -121
  72. package/dist/lib/sql-api.d.ts.map +1 -1
  73. package/dist/lib/table-helpers.d.ts +16 -1
  74. package/dist/lib/table-helpers.d.ts.map +1 -1
  75. package/dist/lib/table-helpers.js +55 -1
  76. package/dist/lib/tenant.d.ts +26 -0
  77. package/dist/lib/tenant.d.ts.map +1 -1
  78. package/dist/lib/tenant.js +46 -0
  79. package/dist/routes/auto/route-helpers.d.ts +0 -7
  80. package/dist/routes/auto/route-helpers.d.ts.map +1 -1
  81. package/dist/routes/auto/route-helpers.js +46 -30
  82. package/dist/routes/auto/search.routes.js +0 -1
  83. package/dist/types/join.d.ts +9 -0
  84. package/dist/types/join.d.ts.map +1 -1
  85. package/dist/types/table.d.ts +1 -1
  86. package/docs/GLOSSARY.md +28 -0
  87. package/docs/adr/0010-joins-do-not-run-route-guards.md +100 -0
  88. package/docs/adr/0011-join-fields-allowlist.md +95 -0
  89. package/docs/adr/0012-anti-duplication-tooling.md +59 -0
  90. package/docs/adr/README.md +5 -1
  91. package/package.json +29 -2
@@ -1,13 +1,13 @@
1
1
  import { toUnderscore } from '../naming.js';
2
2
  // ─── Parsing ─────────────────────────────────────────────────
3
3
  export function parseSchemaFile(content) {
4
- const tableNameMatch = content.match(/tableName:\s*"([^"]+)"/);
4
+ const tableNameMatch = /tableName:\s*"([^"]+)"/.exec(content);
5
5
  if (!tableNameMatch)
6
6
  return null;
7
- const exportMatch = content.match(/export const (Schema\w+)\s*=\s*Schema/);
7
+ const exportMatch = /export const (Schema\w+)\s*=\s*Schema/.exec(content);
8
8
  if (!exportMatch)
9
9
  return null;
10
- const schemaBlockMatch = content.match(/const _Schema\s*=\s*\{([\s\S]*?)};/);
10
+ const schemaBlockMatch = /const _Schema\s*=\s*\{([\s\S]*?)};/.exec(content);
11
11
  if (!schemaBlockMatch)
12
12
  return null;
13
13
  const fields = [];
@@ -22,7 +22,7 @@ export function parseSchemaFile(content) {
22
22
  return null;
23
23
  // primaryKey: ["code"] — emitted by generate-schema from DB introspection
24
24
  let primary;
25
- const pkMatch = content.match(/primaryKey:\s*\[([^\]]*)\]/);
25
+ const pkMatch = /primaryKey:\s*\[([^\]]*)\]/.exec(content);
26
26
  if (pkMatch) {
27
27
  primary = pkMatch[1]
28
28
  .split(',')
@@ -64,12 +64,18 @@ function pkFirst(pk) {
64
64
  }
65
65
  /** Render the `primary:` value for the defineTable template. */
66
66
  function formatPrimary(pk) {
67
- return Array.isArray(pk) ? `[${pk.map((p) => `'${p}'`).join(', ')}]` : `'${pk}'`;
67
+ if (!Array.isArray(pk))
68
+ return `'${pk}'`;
69
+ const quoted = pk.map((p) => `'${p}'`);
70
+ return `[${quoted.join(', ')}]`;
68
71
  }
69
72
  /** All PK fields as an array, for "non-PK field" lookups in example snippets. */
70
73
  function pkFields(pk) {
71
74
  return Array.isArray(pk) ? pk : [pk];
72
75
  }
76
+ /** Infers FK relations between parsed schemas.
77
+ * @testonly Exported only so unit tests can exercise it directly.
78
+ */
73
79
  export function detectRelations(schemas) {
74
80
  const tableBySnakeName = new Map();
75
81
  for (const schema of schemas) {
@@ -96,36 +102,68 @@ export function detectRelations(schemas) {
96
102
  return relations;
97
103
  }
98
104
  // ─── Single Table File Generation ────────────────────────────
99
- export function generateSingleTableFile(schema, allSchemas) {
100
- const relations = detectRelations(allSchemas);
101
- const relsByParent = new Map();
102
- const relsByChild = new Map();
105
+ /** Relations grouped by the schema they hang off, in both directions. */
106
+ function indexRelations(relations) {
107
+ const byParent = new Map();
108
+ const byChild = new Map();
103
109
  for (const rel of relations) {
104
- const arr = relsByParent.get(rel.parentSchemaName) || [];
105
- arr.push(rel);
106
- relsByParent.set(rel.parentSchemaName, arr);
107
- const childArr = relsByChild.get(rel.childSchemaName) || [];
110
+ const parentArr = byParent.get(rel.parentSchemaName) ?? [];
111
+ parentArr.push(rel);
112
+ byParent.set(rel.parentSchemaName, parentArr);
113
+ const childArr = byChild.get(rel.childSchemaName) ?? [];
108
114
  childArr.push(rel);
109
- relsByChild.set(rel.childSchemaName, childArr);
115
+ byChild.set(rel.childSchemaName, childArr);
110
116
  }
111
- const { pk, autoIncrement } = detectPrimaryKey(schema);
112
- const tableVarName = 'Table' + schema.schemaName.replace(/^Schema/, '');
113
- const parentRels = relsByParent.get(schema.schemaName) || [];
114
- const childRels = relsByChild.get(schema.schemaName) || [];
115
- // Collect related schema names for commented imports
116
- const relatedSchemas = new Set();
117
- for (const rel of parentRels) {
118
- if (rel.childSchemaName !== schema.schemaName)
119
- relatedSchemas.add(rel.childSchemaName);
120
- if (rel.parentSchemaName !== schema.schemaName)
121
- relatedSchemas.add(rel.parentSchemaName);
117
+ return { byParent, byChild };
118
+ }
119
+ /** Schemas on the other end of a relation — emitted as commented-out imports. */
120
+ function collectRelatedSchemas(ownName, ...relationGroups) {
121
+ const related = new Set();
122
+ for (const rels of relationGroups) {
123
+ for (const rel of rels) {
124
+ if (rel.childSchemaName !== ownName)
125
+ related.add(rel.childSchemaName);
126
+ if (rel.parentSchemaName !== ownName)
127
+ related.add(rel.parentSchemaName);
128
+ }
122
129
  }
123
- for (const rel of childRels) {
124
- if (rel.parentSchemaName !== schema.schemaName)
125
- relatedSchemas.add(rel.parentSchemaName);
126
- if (rel.childSchemaName !== schema.schemaName)
127
- relatedSchemas.add(rel.childSchemaName);
130
+ return related;
131
+ }
132
+ /** A plausible `schemaOverrides` example: an email format if there is one, else a minLength. */
133
+ function schemaOverridesLine(schema, pk) {
134
+ const emailField = schema.fields.find((f) => f.toLowerCase().includes('email'));
135
+ if (emailField) {
136
+ return ` // schemaOverrides: { ${emailField}: Type.String({ format: 'email' }) },`;
137
+ }
138
+ const stringField = schema.fields.find((f) => !pkFields(pk).includes(f) && schema.fieldTypes[f]?.includes('String'));
139
+ return stringField
140
+ ? ` // schemaOverrides: { ${stringField}: Type.String({ minLength: 1 }) },`
141
+ : ` // schemaOverrides: {},`;
142
+ }
143
+ /** Indirect scope when the table hangs off a parent, direct scope otherwise. */
144
+ function tenantScopeLine(childRels) {
145
+ const rel = childRels[0];
146
+ if (!rel)
147
+ return ` // tenantScope: { column: 'tenant_col' },`;
148
+ return ` // tenantScope: { column: 'tenant_col', through: { schema: ${rel.parentSchemaName}, localField: '${rel.childField}', foreignField: '${rel.parentField}' } },`;
149
+ }
150
+ function allowedReadJoinsLines(parentRels) {
151
+ if (parentRels.length === 0)
152
+ return [` // allowedReadJoins: [],`];
153
+ const lines = [` // allowedReadJoins: [`];
154
+ for (const rel of parentRels) {
155
+ lines.push(` // buildRelation(${rel.parentSchemaName}, '${rel.parentField}', ${rel.childSchemaName}, '${rel.childField}'),`);
128
156
  }
157
+ lines.push(` // ],`);
158
+ return lines;
159
+ }
160
+ export function generateSingleTableFile(schema, allSchemas) {
161
+ const { byParent, byChild } = indexRelations(detectRelations(allSchemas));
162
+ const { pk, autoIncrement } = detectPrimaryKey(schema);
163
+ const tableVarName = 'Table' + schema.schemaName.replace(/^Schema/, '');
164
+ const parentRels = byParent.get(schema.schemaName) ?? [];
165
+ const childRels = byChild.get(schema.schemaName) ?? [];
166
+ const relatedSchemas = collectRelatedSchemas(schema.schemaName, parentRels, childRels);
129
167
  const lines = [];
130
168
  // Imports
131
169
  lines.push(`import {defineTable, exportTableInfo, Type} from 'fastify-auto-sqlapi';`);
@@ -165,31 +203,10 @@ export function generateSingleTableFile(schema, allSchemas) {
165
203
  }
166
204
  // Hide columns from every read (writes still accept them, e.g. a password hash)
167
205
  lines.push(` // readExclude: [],`);
168
- if (parentRels.length > 0) {
169
- lines.push(` // allowedReadJoins: [`);
170
- for (const rel of parentRels) {
171
- lines.push(` // buildRelation(${rel.parentSchemaName}, '${rel.parentField}', ${rel.childSchemaName}, '${rel.childField}'),`);
172
- }
173
- lines.push(` // ],`);
174
- }
175
- else {
176
- lines.push(` // allowedReadJoins: [],`);
177
- }
178
- lines.push(` // upsertMap: buildUpsertRules(buildUpsertRule(Schema, [${pkFields(pk).map((p) => `'${p}'`).join(', ')}])),`);
179
- // schemaOverrides: suggest email format if email field exists, otherwise minLength on first string field
180
- const emailField = schema.fields.find(f => f.toLowerCase().includes('email'));
181
- if (emailField) {
182
- lines.push(` // schemaOverrides: { ${emailField}: Type.String({ format: 'email' }) },`);
183
- }
184
- else {
185
- const stringField = schema.fields.find(f => !pkFields(pk).includes(f) && schema.fieldTypes[f]?.includes('String'));
186
- if (stringField) {
187
- lines.push(` // schemaOverrides: { ${stringField}: Type.String({ minLength: 1 }) },`);
188
- }
189
- else {
190
- lines.push(` // schemaOverrides: {},`);
191
- }
192
- }
206
+ lines.push(...allowedReadJoinsLines(parentRels));
207
+ const upsertPk = pkFields(pk).map((p) => `'${p}'`).join(', ');
208
+ lines.push(` // upsertMap: buildUpsertRules(buildUpsertRule(Schema, [${upsertPk}])),`);
209
+ lines.push(schemaOverridesLine(schema, pk));
193
210
  // validate: use first non-PK field for example
194
211
  const exampleField = schema.fields.find(f => !pkFields(pk).includes(f)) || schema.fields[0];
195
212
  lines.push(` validate: async (db, req, main, secondaries) => {`);
@@ -199,13 +216,7 @@ export function generateSingleTableFile(schema, allSchemas) {
199
216
  lines.push(` },`);
200
217
  lines.push(` // beforeInsert: async (db, req, record) => {},`);
201
218
  lines.push(` // beforeUpdate: async (db, req, fields) => {},`);
202
- if (childRels.length > 0) {
203
- const rel = childRels[0];
204
- lines.push(` // tenantScope: { column: 'tenant_col', through: { schema: ${rel.parentSchemaName}, localField: '${rel.childField}', foreignField: '${rel.parentField}' } },`);
205
- }
206
- else {
207
- lines.push(` // tenantScope: { column: 'tenant_col' },`);
208
- }
219
+ lines.push(tenantScopeLine(childRels));
209
220
  lines.push(`});`);
210
221
  lines.push(``);
211
222
  return lines.join('\n');
@@ -230,18 +241,33 @@ export function generateDbTablesIndex(schemas) {
230
241
  return lines.join('\n');
231
242
  }
232
243
  // ─── Code Generation (legacy) ───────────────────────────────
244
+ /** One `defineTable({...})` block for the legacy single-file generator. */
245
+ function legacyTableBlock(schema, parentRels, childRels) {
246
+ const { pk, autoIncrement } = detectPrimaryKey(schema);
247
+ const tableVarName = 'Table' + schema.schemaName.replace(/^Schema/, '');
248
+ const upsertPk = pkFields(pk).map((p) => `'${p}'`).join(', ');
249
+ const exampleField = schema.fields.find((f) => !pkFields(pk).includes(f)) || schema.fields[0];
250
+ const lines = [
251
+ ``,
252
+ `// ─── ${schema.tableName} ──────────────────────────────`,
253
+ `// Fields: ${schema.fields.join(', ')}`,
254
+ `const ${tableVarName} = defineTable({`,
255
+ ` primary: ${formatPrimary(pk)},`,
256
+ ` ...exportTableInfo(${schema.schemaName}),`,
257
+ ` defaultOrder: '${pkFirst(pk)}',`,
258
+ autoIncrement
259
+ ? ` excludeFromCreation: ['${pkFirst(pk)}'],`
260
+ : ` // excludeFromCreation: [],`,
261
+ ];
262
+ lines.push(...allowedReadJoinsLines(parentRels));
263
+ lines.push(` // upsertMap: buildUpsertRules(buildUpsertRule(${schema.schemaName}, [${upsertPk}])),`, ` // schemaOverrides: {},`, ` validate: async (db, req, main, secondaries) => {`, ` const errors: ValidationError[] = [];`, ` // if (!main.${exampleField}) errors.push(['${exampleField}', 'required']);`, ` return errors;`, ` },`, ` // beforeInsert: async (db, req, record) => {},`, ` // beforeUpdate: async (db, req, fields) => {},`, tenantScopeLine(childRels), `});`);
264
+ return lines;
265
+ }
266
+ /** Legacy single-file DbTables generator.
267
+ * @testonly Exported only so unit tests can exercise it directly.
268
+ */
233
269
  export function generateTablesFile(schemas) {
234
- const relations = detectRelations(schemas);
235
- const relsByParent = new Map();
236
- const relsByChild = new Map();
237
- for (const rel of relations) {
238
- const arr = relsByParent.get(rel.parentSchemaName) || [];
239
- arr.push(rel);
240
- relsByParent.set(rel.parentSchemaName, arr);
241
- const childArr = relsByChild.get(rel.childSchemaName) || [];
242
- childArr.push(rel);
243
- relsByChild.set(rel.childSchemaName, childArr);
244
- }
270
+ const { byParent, byChild } = indexRelations(detectRelations(schemas));
245
271
  const lines = [];
246
272
  // Imports
247
273
  lines.push(`import {defineTable, exportTableInfo, Type} from 'fastify-auto-sqlapi';`);
@@ -253,51 +279,7 @@ export function generateTablesFile(schemas) {
253
279
  }
254
280
  // Table definitions
255
281
  for (const schema of schemas) {
256
- const { pk, autoIncrement } = detectPrimaryKey(schema);
257
- const tableVarName = 'Table' + schema.schemaName.replace(/^Schema/, '');
258
- const parentRels = relsByParent.get(schema.schemaName) || [];
259
- lines.push(``);
260
- lines.push(`// ─── ${schema.tableName} ──────────────────────────────`);
261
- lines.push(`// Fields: ${schema.fields.join(', ')}`);
262
- lines.push(`const ${tableVarName} = defineTable({`);
263
- lines.push(` primary: ${formatPrimary(pk)},`);
264
- lines.push(` ...exportTableInfo(${schema.schemaName}),`);
265
- lines.push(` defaultOrder: '${pkFirst(pk)}',`);
266
- if (autoIncrement) {
267
- lines.push(` excludeFromCreation: ['${pkFirst(pk)}'],`);
268
- }
269
- else {
270
- lines.push(` // excludeFromCreation: [],`);
271
- }
272
- if (parentRels.length > 0) {
273
- lines.push(` // allowedReadJoins: [`);
274
- for (const rel of parentRels) {
275
- lines.push(` // buildRelation(${rel.parentSchemaName}, '${rel.parentField}', ${rel.childSchemaName}, '${rel.childField}'),`);
276
- }
277
- lines.push(` // ],`);
278
- }
279
- else {
280
- lines.push(` // allowedReadJoins: [],`);
281
- }
282
- lines.push(` // upsertMap: buildUpsertRules(buildUpsertRule(${schema.schemaName}, [${pkFields(pk).map((p) => `'${p}'`).join(', ')}])),`);
283
- lines.push(` // schemaOverrides: {},`);
284
- const exampleFieldLegacy = schema.fields.find(f => !pkFields(pk).includes(f)) || schema.fields[0];
285
- lines.push(` validate: async (db, req, main, secondaries) => {`);
286
- lines.push(` const errors: ValidationError[] = [];`);
287
- lines.push(` // if (!main.${exampleFieldLegacy}) errors.push(['${exampleFieldLegacy}', 'required']);`);
288
- lines.push(` return errors;`);
289
- lines.push(` },`);
290
- lines.push(` // beforeInsert: async (db, req, record) => {},`);
291
- lines.push(` // beforeUpdate: async (db, req, fields) => {},`);
292
- const childRels = relsByChild.get(schema.schemaName) || [];
293
- if (childRels.length > 0) {
294
- const rel = childRels[0];
295
- lines.push(` // tenantScope: { column: 'tenant_col', through: { schema: ${rel.parentSchemaName}, localField: '${rel.childField}', foreignField: '${rel.parentField}' } },`);
296
- }
297
- else {
298
- lines.push(` // tenantScope: { column: 'tenant_col' },`);
299
- }
300
- lines.push(`});`);
282
+ lines.push(...legacyTableBlock(schema, byParent.get(schema.schemaName) ?? [], byChild.get(schema.schemaName) ?? []));
301
283
  }
302
284
  // DbTables export
303
285
  lines.push(``);
@@ -1,11 +1,21 @@
1
- export declare const SINGLE_VALUE_METHODS: readonly ["isEqual", "isNotEqual", "isGreater", "isNotGreater", "isGreaterOrEqual", "isNotGreaterOrEqual", "isLess", "isNotLess", "isLessOrEqual", "isNotLessOrEqual", "isLike", "isNotLike", "isILike", "isNotILike"];
2
- export declare const BETWEEN_METHODS: readonly ["isBetween", "isNotBetween"];
3
- export declare const IN_METHODS: readonly ["isIn", "isNotIn"];
4
- export declare const NULL_METHODS: readonly ["isNull", "isNotNull"];
5
1
  export declare const ALLOWED_METHODS: readonly ["isEqual", "isNotEqual", "isGreater", "isNotGreater", "isGreaterOrEqual", "isNotGreaterOrEqual", "isLess", "isNotLess", "isLessOrEqual", "isNotLessOrEqual", "isLike", "isNotLike", "isILike", "isNotILike", "isBetween", "isNotBetween", "isIn", "isNotIn", "isNull", "isNotNull"];
6
2
  export declare const SINGLE_VALUE_SET: Set<string>;
7
3
  export declare const BETWEEN_SET: Set<string>;
8
4
  export declare const IN_SET: Set<string>;
9
5
  export declare const NULL_SET: Set<string>;
10
6
  export declare const ALLOWED_SET: Set<string>;
7
+ /**
8
+ * Caps on how much work one search request may ask for.
9
+ *
10
+ * Every dotted condition and every 3-part `orderBy` token becomes its own correlated subquery,
11
+ * so an uncapped list turns a single request into arbitrarily many — `maxItemsPerPage` bounds
12
+ * the rows returned, not the work done to find them. They live here, in the data module both
13
+ * the engine and the TypeBox schema builders already share, so the request schema advertises
14
+ * exactly the limit the engine enforces.
15
+ */
16
+ export declare const MAX_CONDITIONS = 100;
17
+ /** Token cap the engine applies to `orderBy`, counted over its comma-separated parts. */
18
+ export declare const MAX_ORDER_BY_PARTS = 20;
19
+ /** Character cap the request schema applies to `orderBy` — generous for MAX_ORDER_BY_PARTS. */
20
+ export declare const MAX_ORDER_BY_LENGTH = 1024;
11
21
  //# sourceMappingURL=condition-methods.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"condition-methods.d.ts","sourceRoot":"","sources":["../../src/lib/condition-methods.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,oBAAoB,wNAKvB,CAAC;AAEX,eAAO,MAAM,eAAe,wCAAyC,CAAC;AACtE,eAAO,MAAM,UAAU,8BAA+B,CAAC;AACvD,eAAO,MAAM,YAAY,kCAAmC,CAAC;AAE7D,eAAO,MAAM,eAAe,+RAElB,CAAC;AAGX,eAAO,MAAM,gBAAgB,aAAwC,CAAC;AACtE,eAAO,MAAM,WAAW,aAAmC,CAAC;AAC5D,eAAO,MAAM,MAAM,aAA8B,CAAC;AAClD,eAAO,MAAM,QAAQ,aAAgC,CAAC;AACtD,eAAO,MAAM,WAAW,aAAmC,CAAC"}
1
+ {"version":3,"file":"condition-methods.d.ts","sourceRoot":"","sources":["../../src/lib/condition-methods.ts"],"names":[],"mappings":"AAcA,eAAO,MAAM,eAAe,+RAElB,CAAC;AAGX,eAAO,MAAM,gBAAgB,aAAwC,CAAC;AACtE,eAAO,MAAM,WAAW,aAAmC,CAAC;AAC5D,eAAO,MAAM,MAAM,aAA8B,CAAC;AAClD,eAAO,MAAM,QAAQ,aAAgC,CAAC;AACtD,eAAO,MAAM,WAAW,aAAmC,CAAC;AAE5D;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,MAAM,CAAC;AAElC,yFAAyF;AACzF,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAErC,+FAA+F;AAC/F,eAAO,MAAM,mBAAmB,OAAO,CAAC"}
@@ -1,14 +1,14 @@
1
1
  // Allowed ConditionBuilder methods for the `conditions` search API.
2
2
  // Single source of truth for both schema validation (Swagger) and runtime whitelist.
3
- export const SINGLE_VALUE_METHODS = [
3
+ const SINGLE_VALUE_METHODS = [
4
4
  'isEqual', 'isNotEqual',
5
5
  'isGreater', 'isNotGreater', 'isGreaterOrEqual', 'isNotGreaterOrEqual',
6
6
  'isLess', 'isNotLess', 'isLessOrEqual', 'isNotLessOrEqual',
7
7
  'isLike', 'isNotLike', 'isILike', 'isNotILike',
8
8
  ];
9
- export const BETWEEN_METHODS = ['isBetween', 'isNotBetween'];
10
- export const IN_METHODS = ['isIn', 'isNotIn'];
11
- export const NULL_METHODS = ['isNull', 'isNotNull'];
9
+ const BETWEEN_METHODS = ['isBetween', 'isNotBetween'];
10
+ const IN_METHODS = ['isIn', 'isNotIn'];
11
+ const NULL_METHODS = ['isNull', 'isNotNull'];
12
12
  export const ALLOWED_METHODS = [
13
13
  ...SINGLE_VALUE_METHODS, ...BETWEEN_METHODS, ...IN_METHODS, ...NULL_METHODS,
14
14
  ];
@@ -18,3 +18,17 @@ export const BETWEEN_SET = new Set(BETWEEN_METHODS);
18
18
  export const IN_SET = new Set(IN_METHODS);
19
19
  export const NULL_SET = new Set(NULL_METHODS);
20
20
  export const ALLOWED_SET = new Set(ALLOWED_METHODS);
21
+ /**
22
+ * Caps on how much work one search request may ask for.
23
+ *
24
+ * Every dotted condition and every 3-part `orderBy` token becomes its own correlated subquery,
25
+ * so an uncapped list turns a single request into arbitrarily many — `maxItemsPerPage` bounds
26
+ * the rows returned, not the work done to find them. They live here, in the data module both
27
+ * the engine and the TypeBox schema builders already share, so the request schema advertises
28
+ * exactly the limit the engine enforces.
29
+ */
30
+ export const MAX_CONDITIONS = 100;
31
+ /** Token cap the engine applies to `orderBy`, counted over its comma-separated parts. */
32
+ export const MAX_ORDER_BY_PARTS = 20;
33
+ /** Character cap the request schema applies to `orderBy` — generous for MAX_ORDER_BY_PARTS. */
34
+ export const MAX_ORDER_BY_LENGTH = 1024;
package/dist/lib/db.d.ts CHANGED
@@ -39,7 +39,7 @@ export declare class QueryClient {
39
39
  bulkInsert(table: string, records: DbRecord[], pkCol: string | string[], chunkSize?: number): Promise<Record<string, unknown>[]>;
40
40
  bulkInsertOrUpdate(table: string, records: DbRecord[], conflictKeys: string[], pkCol: string | string[], chunkSize?: number): Promise<Record<string, unknown>[]>;
41
41
  update(table: string, record: DbRecord, where: DbRecord, extraCondition?: ConditionBuilder): Promise<number>;
42
- delete(table: string, where: DbRecord): Promise<number>;
42
+ delete(table: string, where: DbRecord, extraCondition?: ConditionBuilder): Promise<number>;
43
43
  select<T = Record<string, unknown>>({ tableName, columns, where, values, limit, orderBy, joins, distinct, }: SelectOptions): Promise<T[]>;
44
44
  }
45
45
  export declare function createQueryClient(pool: Queryable, dialectName?: string): QueryClient;
@@ -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;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,iFAAiF;IACjF,IAAI,SAAS,IAAI,UAAU,CAAC,WAAW,CAAC,CAEvC;IAED,mFAAmF;IACnF,IAAI,iBAAiB,IAAI,OAAO,CAE/B;IAED,uEAAuE;IACvE,IAAI,WAAW,IAAI,UAAU,CAAC,aAAa,CAAC,CAE3C;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;IAQxB;;;;;;;;;OASG;IACG,eAAe,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAmHnE,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"}
1
+ {"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/lib/db.ts"],"names":[],"mappings":"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"}
package/dist/lib/db.js CHANGED
@@ -221,16 +221,28 @@ export class QueryClient {
221
221
  extraWhere = ` AND ${extraCondition.build(values.length + 1, this.dialect.ph)}`;
222
222
  values.push(...extraCondition.getValues());
223
223
  }
224
+ const setClause = setF.map((f, i) => `${this.dialect.qi(f)} = ${setP[i]}`).join(', ');
225
+ const whereClause = whereF
226
+ .map((f, i) => `${this.dialect.qi(f)} = ${whereP[i]}`)
227
+ .join(' AND ');
224
228
  const result = await this.query(`UPDATE ${this.dialect.qi(table)}
225
- SET ${setF.map((f, i) => `${this.dialect.qi(f)} = ${setP[i]}`).join(', ')}
226
- WHERE ${whereF.map((f, i) => `${this.dialect.qi(f)} = ${whereP[i]}`).join(' AND ')}${extraWhere}`, values);
229
+ SET ${setClause}
230
+ WHERE ${whereClause}${extraWhere}`, values);
227
231
  return result.affectedRows;
228
232
  }
229
- async delete(table, where) {
233
+ async delete(table, where, extraCondition) {
230
234
  const values = [];
231
235
  const { fields, placeholders } = this.#params(where, values);
236
+ let extraWhere = '';
237
+ if (extraCondition) {
238
+ extraWhere = ` AND ${extraCondition.build(values.length + 1, this.dialect.ph)}`;
239
+ values.push(...extraCondition.getValues());
240
+ }
241
+ const whereClause = fields
242
+ .map((f, i) => `${this.dialect.qi(f)} = ${placeholders[i]}`)
243
+ .join(' AND ');
232
244
  const result = await this.query(`DELETE FROM ${this.dialect.qi(table)}
233
- WHERE ${fields.map((f, i) => `${this.dialect.qi(f)} = ${placeholders[i]}`).join(' AND ')}`, values);
245
+ WHERE ${whereClause}${extraWhere}`, values);
234
246
  return result.affectedRows;
235
247
  }
236
248
  async select({ tableName, columns = '*', where, values, limit = null, orderBy = '', joins = [], distinct = false, }) {
@@ -1,5 +1,5 @@
1
1
  export type DialectName = 'postgres' | 'mysql' | 'mariadb';
2
- export type TruncateUnit = 'year' | 'quarter' | 'month' | 'day' | 'hour';
2
+ type TruncateUnit = 'year' | 'quarter' | 'month' | 'day' | 'hour';
3
3
  export interface SqlDialect {
4
4
  name: DialectName;
5
5
  /** Quote identifier: "id" (postgres) or `id` (mysql/mariadb) */
@@ -31,4 +31,5 @@ export interface SqlDialect {
31
31
  dateTrunc(unit: TruncateUnit, qualifiedCol: string): string;
32
32
  }
33
33
  export declare function getDialect(name: DialectName): SqlDialect;
34
+ export {};
34
35
  //# 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,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"}
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,KAAK,YAAY,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,GAAG,KAAK,GAAG,MAAM,CAAC;AAElE,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"}
@@ -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,EAEjB,MAAM,mBAAmB,CAAC;AAE3B,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAoF5F"}
1
+ {"version":3,"file":"bulk-upsert.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/bulk/bulk-upsert.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAEV,gBAAgB,EAChB,gBAAgB,EAEjB,MAAM,mBAAmB,CAAC;AA4D3B,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CA0C5F"}
@@ -3,8 +3,45 @@ import { processSecondaries, processDeletions, prepareInsertRecord } from '../wr
3
3
  import { enforceTenantOnWrites, assertTenantOwnsConflicts } from '../../tenant.js';
4
4
  import { runBulkValidation } from '../validate.js';
5
5
  import { primaryAsCols } from '../../../types.js';
6
+ /** One bulk statement for every main row, returning the PKs only. */
7
+ function upsertMains(params, preparedMains, pkCol) {
8
+ const { db, tableConf, tenant } = params;
9
+ const schema = tableConf.Schema;
10
+ const upsertKeys = tableConf.upsertMap?.get(schema);
11
+ if (!upsertKeys) {
12
+ return db.bulkInsert(schema.tableName, preparedMains, pkCol);
13
+ }
14
+ const conflictCols = upsertKeys.map((k) => schema.col(k));
15
+ // Tenant isolation: a conflicting upsert must not overwrite rows owned by another tenant.
16
+ return assertTenantOwnsConflicts(db, tenant, schema.tableName, conflictCols, preparedMains)
17
+ .then(() => db.bulkInsertOrUpdate(schema.tableName, preparedMains, conflictCols, pkCol));
18
+ }
19
+ /** Secondaries, deletions and the afterInsert hook for one upserted item. */
20
+ async function finalizeUpsertedItem(params, item, inputMain, pkRow) {
21
+ const { db, tableConf, dbTables, request, tenant } = params;
22
+ // pkRow comes from SQL RETURNING or mysql insertId synthesis — keys are DB column names;
23
+ // camelcaseObject maps them back to schema fields (full composite PK).
24
+ const mainPkCamel = camelcaseObject(pkRow, tableConf.Schema);
25
+ const result = { main: mainPkCamel };
26
+ // FK auto-fill: input camelCase + generated PK
27
+ const mainForFK = { ...inputMain, ...mainPkCamel };
28
+ if (item.secondaries && Object.keys(item.secondaries).length > 0) {
29
+ const sec = await processSecondaries({ db, tableConf, dbTables, mainRecord: mainForFK, tenant }, item.secondaries);
30
+ if (Object.keys(sec).length > 0)
31
+ result.secondaries = sec;
32
+ }
33
+ if (item.deletions && Object.keys(item.deletions).length > 0) {
34
+ const del = await processDeletions({ db, tableConf, dbTables, mainRecord: mainForFK, tenant }, item.deletions);
35
+ if (Object.keys(del).length > 0)
36
+ result.deletions = del;
37
+ }
38
+ if (tableConf.afterInsert) {
39
+ await tableConf.afterInsert(db, request, mainForFK, result.secondaries);
40
+ }
41
+ return result;
42
+ }
6
43
  export async function bulkUpsertEngine(params) {
7
- const { db, tableConf, dbTables, request, items, tenant } = params;
44
+ const { db, tableConf, request, items, tenant } = params;
8
45
  if (!items.length)
9
46
  return [];
10
47
  const schema = tableConf.Schema;
@@ -22,42 +59,12 @@ export async function bulkUpsertEngine(params) {
22
59
  // 3. Tenant: inject (direct) or batch-validate FK (indirect) — enforced after user mutations
23
60
  await enforceTenantOnWrites(db, tenant, preparedMains);
24
61
  // 4. Bulk upsert all mains in one query → returns PK-only
25
- const upsertKeys = tableConf.upsertMap?.get(schema);
26
- let pkRows;
27
- if (upsertKeys) {
28
- const conflictCols = upsertKeys.map((k) => schema.col(k));
29
- // Tenant isolation: a conflicting upsert must not overwrite rows owned by another tenant.
30
- await assertTenantOwnsConflicts(db, tenant, schema.tableName, conflictCols, preparedMains);
31
- pkRows = await db.bulkInsertOrUpdate(schema.tableName, preparedMains, conflictCols, pkCol);
32
- }
33
- else {
34
- pkRows = await db.bulkInsert(schema.tableName, preparedMains, pkCol);
35
- }
36
- // 5. Process secondaries, deletions, afterInsert per item (all camelCase)
62
+ const pkRows = await upsertMains(params, preparedMains, pkCol);
63
+ // 5-6. Secondaries, deletions and afterInsert per item — sequential on purpose, so hooks
64
+ // observe the same order the caller sent, and errors surface on the first bad item.
37
65
  const results = [];
38
66
  for (let i = 0; i < items.length; i++) {
39
- const item = items[i];
40
- // pkRows entries come from SQL RETURNING or mysql insertId synthesis — keys are DB
41
- // column names; camelcaseObject maps them back to schema fields (full composite PK).
42
- const mainPkCamel = camelcaseObject(pkRows[i], schema);
43
- const result = { main: mainPkCamel };
44
- // FK auto-fill: input camelCase + generated PK
45
- const mainForFK = { ...inputMains[i], ...mainPkCamel };
46
- if (item.secondaries && Object.keys(item.secondaries).length > 0) {
47
- const sec = await processSecondaries(db, tableConf, dbTables, mainForFK, item.secondaries);
48
- if (Object.keys(sec).length > 0)
49
- result.secondaries = sec;
50
- }
51
- if (item.deletions && Object.keys(item.deletions).length > 0) {
52
- const del = await processDeletions(db, tableConf, mainForFK, item.deletions);
53
- if (Object.keys(del).length > 0)
54
- result.deletions = del;
55
- }
56
- // 6. afterInsert hook per item (camelCase)
57
- if (tableConf.afterInsert) {
58
- await tableConf.afterInsert(db, request, mainForFK, result.secondaries);
59
- }
60
- results.push(result);
67
+ results.push(await finalizeUpsertedItem(params, items[i], inputMains[i], pkRows[i]));
61
68
  }
62
69
  return results;
63
70
  }
@@ -1 +1 @@
1
- {"version":3,"file":"delete.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/delete.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAY,MAAM,mBAAmB,CAAC;AAE9E,wBAAsB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAuC9E"}
1
+ {"version":3,"file":"delete.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/delete.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEpE,wBAAsB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAuC9E"}
@@ -1 +1 @@
1
- {"version":3,"file":"get.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/get.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAuB,MAAM,mBAAmB,CAAC;AAEnF,wBAAsB,SAAS,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC,CAsCrE"}
1
+ {"version":3,"file":"get.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/get.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAE9D,wBAAsB,SAAS,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC,CAsCrE"}
@@ -1 +1 @@
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,CAiE9E"}
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,CAmE9E"}
@@ -32,7 +32,7 @@ export async function insertEngine(params) {
32
32
  let secondaryResults;
33
33
  if (secondaries && Object.keys(secondaries).length > 0) {
34
34
  const mainForFK = { ...inputRecord, ...mainPkCamel };
35
- secondaryResults = await processSecondaries(tx, tableConf, dbTables, mainForFK, secondaries);
35
+ secondaryResults = await processSecondaries({ db: tx, tableConf, dbTables, mainRecord: mainForFK, tenant }, secondaries);
36
36
  }
37
37
  // 5. afterInsert hook (camelCase — input merged with generated PK)
38
38
  if (tableConf.afterInsert) {
@@ -1 +1 @@
1
- {"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/update.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EACV,YAAY,EACZ,YAAY,EAEb,MAAM,mBAAmB,CAAC;AAE3B,wBAAsB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAwH9E"}
1
+ {"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/update.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EACV,YAAY,EACZ,YAAY,EAEb,MAAM,mBAAmB,CAAC;AAiC3B,wBAAsB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CA2G9E"}