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/postgresEngine.js CHANGED
@@ -43,18 +43,16 @@ class postgresEngine {
43
43
  * UPDATE with parameterized query
44
44
  */
45
45
  async update(query) {
46
- // Use parameterized query for security
47
- // query.arg now contains {query, params} from _buildSQLEqualToParameterized
48
- if (query.arg && typeof query.arg === 'object' && query.arg.query && query.arg.params) {
49
- const sqlQuery = `UPDATE ${query.tableName} SET ${query.arg.query} WHERE ${query.tableName}.${query.primaryKey} = $${query.arg.params.length + 1}`;
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
- console.log("SQL:", queryString.query);
106
- console.log("Params:", queryString.params || []);
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
- console.log("SQL:", queryString.query);
139
- console.log("Params:", queryString.params);
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
- console.log("SQL:", selectQuery.query);
164
- console.log("Params:", selectQuery.params || []);
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 (strQuery === "") {
251
- if (arg === "null") {
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 (arg === "null") {
264
- strQuery = `${strQuery} AND ${entityRef}.${field} ${func} ${arg}`;
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
- // Check if arg is a parameterized placeholder
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
- andList.push(strQuery);
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 (strQuery === "") {
319
- if (arg === "null") {
320
- strQuery = `WHERE ${entityRef}.${field} ${func} ${arg}`;
321
- } else if (func === "IN") {
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 (arg === "null") {
334
- strQuery = `${strQuery} AND ${entityRef}.${field} ${func} ${arg}`;
335
- } else if (func === "IN") {
336
- strQuery = `${strQuery} AND ${entityRef}.${field} ${func} ${arg}`;
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
- // Check if arg is a parameterized placeholder
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 strQuery;
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
- return Boolean(value);
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
- console.log("SQL:", query);
689
- console.log("Params:", params);
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 {