@dwtechs/antity-pgsql 0.17.4 → 0.17.5

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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
  [![License: MIT](https://img.shields.io/npm/l/@dwtechs/antity-pgsql.svg?color=brightgreen)](https://opensource.org/licenses/MIT)
3
3
  [![npm version](https://badge.fury.io/js/%40dwtechs%2Fantity-pgsql.svg)](https://www.npmjs.com/package/@dwtechs/antity-pgsql)
4
4
  [![last version release date](https://img.shields.io/github/release-date/DWTechs/Antity-pgsql.js)](https://www.npmjs.com/package/@dwtechs/antity-pgsql)
5
- ![Jest:coverage](https://img.shields.io/badge/Jest:coverage-87%25-brightgreen.svg)
5
+ ![Jest:coverage](https://img.shields.io/badge/Jest:coverage-89%25-brightgreen.svg)
6
6
 
7
7
  - [Synopsis](#synopsis)
8
8
  - [Support](#support)
@@ -333,7 +333,7 @@ Using substacks simplifies your route definitions and ensures consistent data pr
333
333
 
334
334
  ### Query Methods
335
335
 
336
- - **query.select()**: Generates a SELECT query. When the `rows` parameter is provided (not null), pagination is automatically enabled and the query includes `COUNT(*) OVER () AS total` to return the total number of rows. The total count is extracted from results and returned separately from the row data.
336
+ - **query.select()**: Generates a SELECT query. When the `rows` parameter is provided (not null), pagination is automatically enabled and the query includes `COUNT(*) OVER () AS total` to return the total number of rows. The total count is extracted from results and returned separately from the row data. The `sortField` parameter is validated against the entity's known properties; an unrecognised value is silently dropped.
337
337
  - **query.insert()**: Generates an INSERT query. Accepts an array of `Row` objects with properties matching the entity definition. Consumer fields are appended directly to the query arguments — row objects are **not mutated**. Optionally appends `consumer.id` as `creatorId` and `consumer.nickname` as `creatorName` for audit tracking. Supports `RETURNING` clause via the `rtn` parameter.
338
338
  - **query.update()**: Generates an UPDATE query using CASE statements. Accepts an array of `Row` objects with `id` property. Optionally appends `consumer.id` as `updaterId` and `consumer.nickname` as `updaterName` for audit tracking.
339
339
  - **query.upsert()**: Generates an INSERT ... ON CONFLICT ... DO UPDATE query. (See [Upsert](#upsert-insert-or-update) section below.) Accepts an array of `Row` objects and a `conflictTarget` (single column name or array of column names) that defines uniqueness. If a conflict occurs on the specified column(s), the row is updated; otherwise, it is inserted. Properties are automatically included if they have both INSERT and UPDATE operations. Consumer fields are appended directly to the query arguments — row objects are **not mutated**. Optionally appends `consumer.id` as `creatorId` and `consumer.nickname` as `creatorName` for audit tracking. Supports `RETURNING` clause via the `rtn` parameter.
@@ -100,7 +100,7 @@ const reserved = new Set([
100
100
  ]);
101
101
  function quoteIfUppercase(word) {
102
102
  if (/[A-Z]/.test(word) || reserved.has(word.toLowerCase()))
103
- return `"${word}"`;
103
+ return `"${word.replace(/"/g, '""')}"`;
104
104
  return word;
105
105
  }
106
106
 
@@ -307,11 +307,11 @@ class Insert {
307
307
  nbProps += 2;
308
308
  cols += `, "creatorId", "creatorName"`;
309
309
  }
310
- let query = `INSERT INTO ${quoteIfUppercase(schema)}.${quoteIfUppercase(table)} (${cols}) VALUES `;
311
310
  const args = [];
311
+ const valueParts = [];
312
312
  let i = 0;
313
313
  for (const row of rows) {
314
- query += `${$i(nbProps, i)}, `;
314
+ valueParts.push($i(nbProps, i));
315
315
  for (const prop of propsToUse) {
316
316
  if (prop === "consumerId")
317
317
  args.push(consumerId);
@@ -322,7 +322,7 @@ class Insert {
322
322
  }
323
323
  i += nbProps;
324
324
  }
325
- query = query.slice(0, -2);
325
+ let query = `INSERT INTO ${quoteIfUppercase(schema)}.${quoteIfUppercase(table)} (${cols}) VALUES ${valueParts.join(", ")}`;
326
326
  if (rtn)
327
327
  query += ` ${rtn}`;
328
328
  return { query, args };
@@ -348,8 +348,8 @@ class Update {
348
348
  log.debug(() => `${LOGS_PREFIX}Update query input rows: ${JSON.stringify(rows, null, 2)}`);
349
349
  const l = rows.length;
350
350
  const args = rows.map(row => row.id);
351
- let query = `UPDATE ${quoteIfUppercase(schema)}.${quoteIfUppercase(table)} SET `;
352
351
  let i = args.length + 1;
352
+ const setClauses = [];
353
353
  for (const p of propsToUse) {
354
354
  const isConsumerId = p === "consumerId";
355
355
  const isConsumerName = p === "consumerName";
@@ -357,9 +357,9 @@ class Update {
357
357
  if (!isConsumerProp && rows[0][p] === undefined)
358
358
  continue;
359
359
  const colName = isConsumerId ? '"updaterId"' : isConsumerName ? '"updaterName"' : quoteIfUppercase(p);
360
- query += `${colName} = CASE `;
360
+ const whenParts = [];
361
361
  for (let j = 0; j < l; j++) {
362
- query += `WHEN id = $${j + 1} THEN $${i++} `;
362
+ whenParts.push(`WHEN id = $${j + 1} THEN $${i++}`);
363
363
  if (isConsumerId)
364
364
  args.push(consumerId);
365
365
  else if (isConsumerName)
@@ -367,9 +367,9 @@ class Update {
367
367
  else
368
368
  args.push(rows[j][p]);
369
369
  }
370
- query += `ELSE ${colName} END, `;
370
+ setClauses.push(`${colName} = CASE ${whenParts.join(" ")} ELSE ${colName} END`);
371
371
  }
372
- query = `${query.slice(0, -2)} WHERE id IN ${$i(l, 0)}`;
372
+ const query = `UPDATE ${quoteIfUppercase(schema)}.${quoteIfUppercase(table)} SET ${setClauses.join(", ")} WHERE id IN ${$i(l, 0)}`;
373
373
  return { query, args };
374
374
  }
375
375
  async execute(query, args, client) {
@@ -421,16 +421,13 @@ class Upsert {
421
421
  i += nbProps;
422
422
  }
423
423
  query = query.slice(0, -2);
424
- query += ` ON CONFLICT (${conflictColumns}) DO UPDATE SET `;
425
424
  const conflictTargetArray = Array.isArray(conflictTarget) ? conflictTarget : [conflictTarget];
426
425
  const updateCols = quotedPropsToUse.filter((_, idx) => {
427
426
  const propName = propsToUse[idx];
428
427
  return !conflictTargetArray.includes(propName);
429
428
  });
430
- for (const col of updateCols) {
431
- query += `${col} = EXCLUDED.${col}, `;
432
- }
433
- query = query.slice(0, -2);
429
+ const updateSetClause = updateCols.map(col => `${col} = EXCLUDED.${col}`).join(", ");
430
+ query += ` ON CONFLICT (${conflictColumns}) DO UPDATE SET ${updateSetClause}`;
434
431
  if (rtn)
435
432
  query += ` ${rtn}`;
436
433
  return { query, args };
@@ -471,14 +468,7 @@ function queryByDate() {
471
468
  return `SELECT hard_delete($1, $2, $3)`;
472
469
  }
473
470
  async function executeArchived(schema, table, date, query, client) {
474
- let db;
475
- try {
476
- db = await execute(query, [schema, table, date], client);
477
- }
478
- catch (err) {
479
- throw err;
480
- }
481
- return db;
471
+ return execute(query, [schema, table, date], client);
482
472
  }
483
473
 
484
474
  function type(type) {
@@ -706,7 +696,8 @@ class SQLEntity extends Entity {
706
696
  }
707
697
  query = {
708
698
  select: (first = 0, rows = null, sortField = null, sortOrder = null, filters = null) => {
709
- return this.sel.query(this.schema, this.table, first, rows, sortField, sortOrder, filters);
699
+ const validatedSortField = sortField && this.properties.some(p => p.key === sortField) ? sortField : null;
700
+ return this.sel.query(this.schema, this.table, first, rows, validatedSortField, sortOrder, filters);
710
701
  },
711
702
  update: (rows, consumer) => {
712
703
  return this.upd.query(this.schema, this.table, rows, consumer?.id, consumer?.nickname);
@@ -735,7 +726,8 @@ class SQLEntity extends Entity {
735
726
  const b = req.body;
736
727
  const first = b?.first ?? 0;
737
728
  const rows = b.rows || null;
738
- const sortField = b.sortField || null;
729
+ const rawSortField = b.sortField || null;
730
+ const sortField = rawSortField && this.properties.some(p => p.key === rawSortField) ? rawSortField : null;
739
731
  const sortOrder = b.sortOrder === -1 || b.sortOrder === "DESC" ? "DESC" : "ASC";
740
732
  const filters = cleanFilters(b.filters, this.properties) || null;
741
733
  const pagination = b.pagination || false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dwtechs/antity-pgsql",
3
- "version": "0.17.4",
3
+ "version": "0.17.5",
4
4
  "description": "Open source library to add PostgreSQL support to @dwtechs/Antity entities.",
5
5
  "keywords": [
6
6
  "entities"
@@ -36,7 +36,7 @@
36
36
  "dependencies": {
37
37
  "@dwtechs/checkard": "3.6.0",
38
38
  "@dwtechs/winstan": "0.7.0",
39
- "@dwtechs/antity": "0.17.0",
39
+ "@dwtechs/antity": "0.18.0",
40
40
  "@dwtechs/sparray": "0.2.1",
41
41
  "pg": "8.20.0"
42
42
  },