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.
- package/AGENTS_BACKEND.md +32 -5
- package/BREAKING_CHANGES.md +80 -5
- package/CHANGELOG.md +45 -0
- package/README.md +49 -7
- package/dist/bin/generate-schema.js +89 -77
- package/dist/bin/generate-tables.js +72 -62
- package/dist/bin/utils.d.ts.map +1 -1
- package/dist/bin/utils.js +47 -33
- package/dist/lib/agent/manifest.d.ts +3 -2
- package/dist/lib/agent/manifest.d.ts.map +1 -1
- package/dist/lib/agent/manifest.js +104 -80
- package/dist/lib/cli/mysql-introspect.d.ts +4 -9
- package/dist/lib/cli/mysql-introspect.d.ts.map +1 -1
- package/dist/lib/cli/mysql-introspect.js +8 -7
- package/dist/lib/cli/pg-introspect.d.ts.map +1 -1
- package/dist/lib/cli/pg-introspect.js +0 -1
- package/dist/lib/cli/schema-codegen.d.ts +3 -0
- package/dist/lib/cli/schema-codegen.d.ts.map +1 -1
- package/dist/lib/cli/schema-codegen.js +3 -0
- package/dist/lib/cli/tables-codegen.d.ts +8 -1
- package/dist/lib/cli/tables-codegen.d.ts.map +1 -1
- package/dist/lib/cli/tables-codegen.js +100 -118
- package/dist/lib/condition-methods.d.ts +14 -4
- package/dist/lib/condition-methods.d.ts.map +1 -1
- package/dist/lib/condition-methods.js +18 -4
- package/dist/lib/db.d.ts +1 -1
- package/dist/lib/db.d.ts.map +1 -1
- package/dist/lib/db.js +16 -4
- package/dist/lib/dialect.d.ts +2 -1
- package/dist/lib/dialect.d.ts.map +1 -1
- package/dist/lib/engine/bulk/bulk-upsert.d.ts.map +1 -1
- package/dist/lib/engine/bulk/bulk-upsert.js +42 -35
- package/dist/lib/engine/rest/delete.d.ts.map +1 -1
- package/dist/lib/engine/rest/get.d.ts.map +1 -1
- package/dist/lib/engine/rest/insert.d.ts.map +1 -1
- package/dist/lib/engine/rest/insert.js +1 -1
- package/dist/lib/engine/rest/update.d.ts.map +1 -1
- package/dist/lib/engine/rest/update.js +25 -18
- package/dist/lib/engine/search/aggregations.d.ts +33 -0
- package/dist/lib/engine/search/aggregations.d.ts.map +1 -0
- package/dist/lib/engine/search/aggregations.js +109 -0
- package/dist/lib/engine/search/conditions.d.ts +31 -0
- package/dist/lib/engine/search/conditions.d.ts.map +1 -0
- package/dist/lib/engine/search/conditions.js +131 -0
- package/dist/lib/engine/search/fields.d.ts +49 -0
- package/dist/lib/engine/search/fields.d.ts.map +1 -0
- package/dist/lib/engine/search/fields.js +142 -0
- package/dist/lib/engine/search/join-families.d.ts +35 -0
- package/dist/lib/engine/search/join-families.d.ts.map +1 -0
- package/dist/lib/engine/search/join-families.js +345 -0
- package/dist/lib/engine/search/joins.d.ts +55 -0
- package/dist/lib/engine/search/joins.d.ts.map +1 -0
- package/dist/lib/engine/search/joins.js +81 -0
- package/dist/lib/engine/search/order-by.d.ts +35 -0
- package/dist/lib/engine/search/order-by.d.ts.map +1 -0
- package/dist/lib/engine/search/order-by.js +104 -0
- package/dist/lib/engine/search/search.d.ts.map +1 -1
- package/dist/lib/engine/search/search.js +106 -749
- package/dist/lib/engine/validate.d.ts +0 -5
- package/dist/lib/engine/validate.d.ts.map +1 -1
- package/dist/lib/engine/write-helpers.d.ts +25 -17
- package/dist/lib/engine/write-helpers.d.ts.map +1 -1
- package/dist/lib/engine/write-helpers.js +86 -44
- package/dist/lib/read-access.d.ts +37 -1
- package/dist/lib/read-access.d.ts.map +1 -1
- package/dist/lib/read-access.js +64 -1
- package/dist/lib/schema/helpers.d.ts +0 -15
- package/dist/lib/schema/helpers.d.ts.map +1 -1
- package/dist/lib/schema/helpers.js +7 -4
- package/dist/lib/schema/search.d.ts.map +1 -1
- package/dist/lib/schema/search.js +120 -121
- package/dist/lib/sql-api.d.ts.map +1 -1
- package/dist/lib/table-helpers.d.ts +16 -1
- package/dist/lib/table-helpers.d.ts.map +1 -1
- package/dist/lib/table-helpers.js +55 -1
- package/dist/lib/tenant.d.ts +26 -0
- package/dist/lib/tenant.d.ts.map +1 -1
- package/dist/lib/tenant.js +46 -0
- package/dist/routes/auto/route-helpers.d.ts +0 -7
- package/dist/routes/auto/route-helpers.d.ts.map +1 -1
- package/dist/routes/auto/route-helpers.js +46 -30
- package/dist/routes/auto/search.routes.js +0 -1
- package/dist/types/join.d.ts +9 -0
- package/dist/types/join.d.ts.map +1 -1
- package/dist/types/table.d.ts +1 -1
- package/docs/GLOSSARY.md +28 -0
- package/docs/adr/0010-joins-do-not-run-route-guards.md +100 -0
- package/docs/adr/0011-join-fields-allowlist.md +95 -0
- package/docs/adr/0012-anti-duplication-tooling.md +59 -0
- package/docs/adr/README.md +5 -1
- 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 =
|
|
4
|
+
const tableNameMatch = /tableName:\s*"([^"]+)"/.exec(content);
|
|
5
5
|
if (!tableNameMatch)
|
|
6
6
|
return null;
|
|
7
|
-
const exportMatch =
|
|
7
|
+
const exportMatch = /export const (Schema\w+)\s*=\s*Schema/.exec(content);
|
|
8
8
|
if (!exportMatch)
|
|
9
9
|
return null;
|
|
10
|
-
const schemaBlockMatch =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
const
|
|
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
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const childArr =
|
|
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
|
-
|
|
115
|
+
byChild.set(rel.childSchemaName, childArr);
|
|
110
116
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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":"
|
|
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
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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;
|
package/dist/lib/db.d.ts.map
CHANGED
|
@@ -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;
|
|
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 ${
|
|
226
|
-
WHERE ${
|
|
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 ${
|
|
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, }) {
|
package/dist/lib/dialect.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type DialectName = 'postgres' | 'mysql' | 'mariadb';
|
|
2
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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":"
|
|
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,
|
|
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,
|
|
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":"
|
|
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"}
|