masterrecord 0.3.3 → 0.3.4
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/.claude/settings.local.json +5 -1
- package/SQLLiteEngine.js +140 -92
- package/context.js +164 -138
- package/mySQLEngine.js +117 -59
- package/package.json +1 -1
- package/postgresEngine.js +128 -72
package/postgresEngine.js
CHANGED
|
@@ -43,18 +43,16 @@ class postgresEngine {
|
|
|
43
43
|
* UPDATE with parameterized query
|
|
44
44
|
*/
|
|
45
45
|
async update(query) {
|
|
46
|
-
//
|
|
47
|
-
// query.arg
|
|
48
|
-
if (query.arg
|
|
49
|
-
|
|
50
|
-
// Add primaryKeyValue to params array
|
|
51
|
-
const params = [...query.arg.params, query.primaryKeyValue];
|
|
52
|
-
return await this._runWithParams(sqlQuery, params);
|
|
53
|
-
} else {
|
|
54
|
-
// Fallback for legacy support
|
|
55
|
-
const sqlQuery = `UPDATE ${query.tableName} SET ${query.arg} WHERE ${query.tableName}.${query.primaryKey} = $1`;
|
|
56
|
-
return await this._runWithParams(sqlQuery, [query.primaryKeyValue]);
|
|
46
|
+
// Security: ONLY use parameterized queries - no fallback to string concatenation
|
|
47
|
+
// query.arg must contain {query, params} from _buildSQLEqualToParameterized
|
|
48
|
+
if (!query.arg || typeof query.arg !== 'object' || !query.arg.query || !query.arg.params) {
|
|
49
|
+
throw new Error('UPDATE failed: Invalid parameterized query structure. Check entity definition.');
|
|
57
50
|
}
|
|
51
|
+
|
|
52
|
+
const sqlQuery = `UPDATE ${query.tableName} SET ${query.arg.query} WHERE ${query.tableName}.${query.primaryKey} = $${query.arg.params.length + 1}`;
|
|
53
|
+
// Add primaryKeyValue to params array
|
|
54
|
+
const params = [...query.arg.params, query.primaryKeyValue];
|
|
55
|
+
return await this._runWithParams(sqlQuery, params);
|
|
58
56
|
}
|
|
59
57
|
|
|
60
58
|
/**
|
|
@@ -87,6 +85,69 @@ class postgresEngine {
|
|
|
87
85
|
};
|
|
88
86
|
}
|
|
89
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Batch insert using PostgreSQL's multi-value INSERT with RETURNING
|
|
90
|
+
*/
|
|
91
|
+
async bulkInsert(entities) {
|
|
92
|
+
if (!entities || entities.length === 0) return [];
|
|
93
|
+
|
|
94
|
+
// Group by table name
|
|
95
|
+
const byTable = {};
|
|
96
|
+
for (const entity of entities) {
|
|
97
|
+
const tableName = entity.__entity.__name;
|
|
98
|
+
if (!byTable[tableName]) byTable[tableName] = [];
|
|
99
|
+
byTable[tableName].push(entity);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const results = [];
|
|
103
|
+
for (const tableName in byTable) {
|
|
104
|
+
const tableEntities = byTable[tableName];
|
|
105
|
+
const primaryKey = tools.getPrimaryKeyObject(tableEntities[0].__entity);
|
|
106
|
+
|
|
107
|
+
// Build multi-value INSERT
|
|
108
|
+
const first = this._buildSQLInsertObjectParameterized(tableEntities[0], tableEntities[0].__entity);
|
|
109
|
+
const allParams = [...first.params];
|
|
110
|
+
let paramIndex = first.params.length + 1;
|
|
111
|
+
const valueGroups = [`(${first.placeholders})`];
|
|
112
|
+
|
|
113
|
+
for (let i = 1; i < tableEntities.length; i++) {
|
|
114
|
+
const sqlObj = this._buildSQLInsertObjectParameterized(tableEntities[i], tableEntities[i].__entity);
|
|
115
|
+
// Renumber placeholders
|
|
116
|
+
const placeholders = sqlObj.params.map(() => `$${paramIndex++}`).join(', ');
|
|
117
|
+
valueGroups.push(`(${placeholders})`);
|
|
118
|
+
allParams.push(...sqlObj.params);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const query = `INSERT INTO "${first.tableName}" (${first.columns}) VALUES ${valueGroups.join(', ')} RETURNING ${primaryKey}`;
|
|
122
|
+
const result = await this._runWithParams(query, allParams);
|
|
123
|
+
results.push(result.rows);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return results;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Batch update (execute in sequence for PostgreSQL)
|
|
131
|
+
*/
|
|
132
|
+
async bulkUpdate(updateQueries) {
|
|
133
|
+
if (!updateQueries || updateQueries.length === 0) return;
|
|
134
|
+
|
|
135
|
+
for (const query of updateQueries) {
|
|
136
|
+
await this.update(query);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Batch delete using WHERE IN
|
|
142
|
+
*/
|
|
143
|
+
async bulkDelete(tableName, ids) {
|
|
144
|
+
if (!ids || ids.length === 0) return;
|
|
145
|
+
|
|
146
|
+
const placeholders = ids.map((_, i) => `$${i + 1}`).join(', ');
|
|
147
|
+
const query = `DELETE FROM "${tableName}" WHERE id IN (${placeholders})`;
|
|
148
|
+
return await this._runWithParams(query, ids);
|
|
149
|
+
}
|
|
150
|
+
|
|
90
151
|
/**
|
|
91
152
|
* SELECT single record
|
|
92
153
|
*/
|
|
@@ -102,8 +163,10 @@ class postgresEngine {
|
|
|
102
163
|
}
|
|
103
164
|
|
|
104
165
|
if (queryString.query) {
|
|
105
|
-
|
|
106
|
-
|
|
166
|
+
if (process.env.LOG_SQL === 'true' || process.env.NODE_ENV !== 'production') {
|
|
167
|
+
console.debug("[SQL]", queryString.query);
|
|
168
|
+
console.debug("[Params]", queryString.params || []);
|
|
169
|
+
}
|
|
107
170
|
const result = await this._runWithParams(queryString.query, queryString.params || []);
|
|
108
171
|
return result.rows[0] || null;
|
|
109
172
|
}
|
|
@@ -135,8 +198,10 @@ class postgresEngine {
|
|
|
135
198
|
}
|
|
136
199
|
|
|
137
200
|
if (queryString.query) {
|
|
138
|
-
|
|
139
|
-
|
|
201
|
+
if (process.env.LOG_SQL === 'true' || process.env.NODE_ENV !== 'production') {
|
|
202
|
+
console.debug("[SQL]", queryString.query);
|
|
203
|
+
console.debug("[Params]", queryString.params);
|
|
204
|
+
}
|
|
140
205
|
const result = await this._runWithParams(queryString.query, queryString.params);
|
|
141
206
|
return result.rows[0] || null;
|
|
142
207
|
}
|
|
@@ -160,8 +225,10 @@ class postgresEngine {
|
|
|
160
225
|
}
|
|
161
226
|
|
|
162
227
|
if (selectQuery.query) {
|
|
163
|
-
|
|
164
|
-
|
|
228
|
+
if (process.env.LOG_SQL === 'true' || process.env.NODE_ENV !== 'production') {
|
|
229
|
+
console.debug("[SQL]", selectQuery.query);
|
|
230
|
+
console.debug("[Params]", selectQuery.params || []);
|
|
231
|
+
}
|
|
165
232
|
const result = await this._runWithParams(selectQuery.query, selectQuery.params || []);
|
|
166
233
|
return result.rows || [];
|
|
167
234
|
}
|
|
@@ -186,7 +253,7 @@ class postgresEngine {
|
|
|
186
253
|
const entityStr = this.getEntity(entity.__name, query.entityMap);
|
|
187
254
|
const params = query.parameters ? query.parameters.getParams() : [];
|
|
188
255
|
|
|
189
|
-
const sql = `SELECT ${this.buildSelectString(query, entity)} ${this.buildFrom(query, entity)} ${this.buildWhere(query, entity)} ${this.buildAnd(query, entity)} ${this.buildLimit(query)} ${this.buildSkip(query)} ${this.buildOrderBy(query)}`;
|
|
256
|
+
const sql = `SELECT ${this.buildSelectString(query, entity)} ${this.buildFrom(query, entity)} ${this.buildWhere(query, entity)} ${this.buildAnd(query, entity)} ${this.buildLimit(query)} ${this.buildSkip(query)} ${this.buildOrderBy(query, entity)}`;
|
|
190
257
|
|
|
191
258
|
return {
|
|
192
259
|
query: sql,
|
|
@@ -218,7 +285,6 @@ class postgresEngine {
|
|
|
218
285
|
*/
|
|
219
286
|
buildAnd(query, mainQuery) {
|
|
220
287
|
const andEntity = query.and;
|
|
221
|
-
let strQuery = "";
|
|
222
288
|
const $that = this;
|
|
223
289
|
|
|
224
290
|
if (andEntity) {
|
|
@@ -229,6 +295,7 @@ class postgresEngine {
|
|
|
229
295
|
const itemEntity = andEntity[entityPart];
|
|
230
296
|
for (let table in itemEntity[query.parentName]) {
|
|
231
297
|
const item = itemEntity[query.parentName][table];
|
|
298
|
+
const expressions = [];
|
|
232
299
|
for (let exp in item.expressions) {
|
|
233
300
|
let field = tools.capitalizeFirstLetter(item.expressions[exp].field);
|
|
234
301
|
let entityRef = entity;
|
|
@@ -247,33 +314,21 @@ class postgresEngine {
|
|
|
247
314
|
if (func === "!=") func = "IS NOT";
|
|
248
315
|
}
|
|
249
316
|
|
|
250
|
-
if (
|
|
251
|
-
|
|
252
|
-
strQuery = `${entityRef}.${field} ${func} ${arg}`;
|
|
253
|
-
} else {
|
|
254
|
-
// Check if arg is a parameterized placeholder
|
|
255
|
-
const isPlaceholder = (arg === '?' || /^\$\d+$/.test(arg));
|
|
256
|
-
if (isPlaceholder || func === "IN") {
|
|
257
|
-
strQuery = `${entityRef}.${field} ${func} ${arg}`;
|
|
258
|
-
} else {
|
|
259
|
-
strQuery = `${entityRef}.${field} ${func} '${arg}'`;
|
|
260
|
-
}
|
|
261
|
-
}
|
|
317
|
+
if (arg === "null") {
|
|
318
|
+
expressions.push(`${entityRef}.${field} ${func} ${arg}`);
|
|
262
319
|
} else {
|
|
263
|
-
if
|
|
264
|
-
|
|
320
|
+
// Check if arg is a parameterized placeholder
|
|
321
|
+
const isPlaceholder = (arg === '?' || /^\$\d+$/.test(arg));
|
|
322
|
+
if (isPlaceholder || func === "IN") {
|
|
323
|
+
expressions.push(`${entityRef}.${field} ${func} ${arg}`);
|
|
265
324
|
} else {
|
|
266
|
-
|
|
267
|
-
const isPlaceholder = (arg === '?' || /^\$\d+$/.test(arg));
|
|
268
|
-
if (isPlaceholder || func === "IN") {
|
|
269
|
-
strQuery = `${strQuery} AND ${entityRef}.${field} ${func} ${arg}`;
|
|
270
|
-
} else {
|
|
271
|
-
strQuery = `${strQuery} AND ${entityRef}.${field} ${func} '${arg}'`;
|
|
272
|
-
}
|
|
325
|
+
expressions.push(`${entityRef}.${field} ${func} '${arg}'`);
|
|
273
326
|
}
|
|
274
327
|
}
|
|
275
328
|
}
|
|
276
|
-
|
|
329
|
+
if (expressions.length > 0) {
|
|
330
|
+
andList.push(expressions.join(" AND "));
|
|
331
|
+
}
|
|
277
332
|
}
|
|
278
333
|
}
|
|
279
334
|
|
|
@@ -290,12 +345,12 @@ class postgresEngine {
|
|
|
290
345
|
*/
|
|
291
346
|
buildWhere(query, mainQuery) {
|
|
292
347
|
const whereEntity = query.where;
|
|
293
|
-
let strQuery = "";
|
|
294
348
|
const $that = this;
|
|
295
349
|
|
|
296
350
|
if (whereEntity) {
|
|
297
351
|
const entity = this.getEntity(query.parentName, query.entityMap);
|
|
298
352
|
const item = whereEntity[query.parentName].query;
|
|
353
|
+
const conditions = [];
|
|
299
354
|
|
|
300
355
|
for (let exp in item.expressions) {
|
|
301
356
|
let field = tools.capitalizeFirstLetter(item.expressions[exp].field);
|
|
@@ -315,39 +370,27 @@ class postgresEngine {
|
|
|
315
370
|
if (func === "!=") func = "IS NOT";
|
|
316
371
|
}
|
|
317
372
|
|
|
318
|
-
if (
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
}
|
|
322
|
-
strQuery = `WHERE ${entityRef}.${field} ${func} ${arg}`;
|
|
323
|
-
} else {
|
|
324
|
-
// Check if arg is a parameterized placeholder ($1, $2, etc.)
|
|
325
|
-
const isPlaceholder = (arg === '?' || /^\$\d+$/.test(arg));
|
|
326
|
-
if (isPlaceholder) {
|
|
327
|
-
strQuery = `WHERE ${entityRef}.${field} ${func} ${arg}`;
|
|
328
|
-
} else {
|
|
329
|
-
strQuery = `WHERE ${entityRef}.${field} ${func} '${arg}'`;
|
|
330
|
-
}
|
|
331
|
-
}
|
|
373
|
+
if (arg === "null") {
|
|
374
|
+
conditions.push(`${entityRef}.${field} ${func} ${arg}`);
|
|
375
|
+
} else if (func === "IN") {
|
|
376
|
+
conditions.push(`${entityRef}.${field} ${func} ${arg}`);
|
|
332
377
|
} else {
|
|
333
|
-
if
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
378
|
+
// Check if arg is a parameterized placeholder ($1, $2, etc.)
|
|
379
|
+
const isPlaceholder = (arg === '?' || /^\$\d+$/.test(arg));
|
|
380
|
+
if (isPlaceholder) {
|
|
381
|
+
conditions.push(`${entityRef}.${field} ${func} ${arg}`);
|
|
337
382
|
} else {
|
|
338
|
-
|
|
339
|
-
const isPlaceholder = (arg === '?' || /^\$\d+$/.test(arg));
|
|
340
|
-
if (isPlaceholder) {
|
|
341
|
-
strQuery = `${strQuery} AND ${entityRef}.${field} ${func} ${arg}`;
|
|
342
|
-
} else {
|
|
343
|
-
strQuery = `${strQuery} AND ${entityRef}.${field} ${func} '${arg}'`;
|
|
344
|
-
}
|
|
383
|
+
conditions.push(`${entityRef}.${field} ${func} '${arg}'`);
|
|
345
384
|
}
|
|
346
385
|
}
|
|
347
386
|
}
|
|
387
|
+
|
|
388
|
+
if (conditions.length > 0) {
|
|
389
|
+
return `WHERE ${conditions.join(" AND ")}`;
|
|
390
|
+
}
|
|
348
391
|
}
|
|
349
392
|
|
|
350
|
-
return
|
|
393
|
+
return "";
|
|
351
394
|
}
|
|
352
395
|
|
|
353
396
|
buildLimit(query) {
|
|
@@ -364,11 +407,19 @@ class postgresEngine {
|
|
|
364
407
|
return "";
|
|
365
408
|
}
|
|
366
409
|
|
|
367
|
-
buildOrderBy(query) {
|
|
410
|
+
buildOrderBy(query, entity) {
|
|
368
411
|
if (query.orderBy) {
|
|
412
|
+
// Security: Validate field exists in entity
|
|
413
|
+
if (entity && !entity[query.orderBy]) {
|
|
414
|
+
throw new Error(`Invalid ORDER BY field: ${query.orderBy} not found in ${entity.__name || 'entity'}`);
|
|
415
|
+
}
|
|
369
416
|
const entityStr = this.getEntity(query.parentName, query.entityMap);
|
|
370
417
|
return `ORDER BY ${entityStr}.${query.orderBy} ASC`;
|
|
371
418
|
} else if (query.orderByDescending) {
|
|
419
|
+
// Security: Validate field exists in entity
|
|
420
|
+
if (entity && !entity[query.orderByDescending]) {
|
|
421
|
+
throw new Error(`Invalid ORDER BY field: ${query.orderByDescending} not found in ${entity.__name || 'entity'}`);
|
|
422
|
+
}
|
|
372
423
|
const entityStr = this.getEntity(query.parentName, query.entityMap);
|
|
373
424
|
return `ORDER BY ${entityStr}.${query.orderByDescending} DESC`;
|
|
374
425
|
}
|
|
@@ -632,7 +683,10 @@ class postgresEngine {
|
|
|
632
683
|
|
|
633
684
|
case 'boolean':
|
|
634
685
|
case 'bool':
|
|
635
|
-
|
|
686
|
+
if (typeof value === 'boolean') return value;
|
|
687
|
+
if (value === 1 || value === '1' || value === 'true' || value === true) return true;
|
|
688
|
+
if (value === 0 || value === '0' || value === 'false' || value === false) return false;
|
|
689
|
+
throw new Error(`Invalid boolean value: ${value}`);
|
|
636
690
|
|
|
637
691
|
case 'date':
|
|
638
692
|
case 'datetime':
|
|
@@ -685,8 +739,10 @@ class postgresEngine {
|
|
|
685
739
|
*/
|
|
686
740
|
async _runWithParams(query, params = []) {
|
|
687
741
|
try {
|
|
688
|
-
|
|
689
|
-
|
|
742
|
+
if (process.env.LOG_SQL === 'true' || process.env.NODE_ENV !== 'production') {
|
|
743
|
+
console.debug("[SQL]", query);
|
|
744
|
+
console.debug("[Params]", params);
|
|
745
|
+
}
|
|
690
746
|
|
|
691
747
|
const client = await this.pool.connect();
|
|
692
748
|
try {
|