@dwtechs/antity-pgsql 0.17.4 → 0.17.6

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)
@@ -152,7 +152,7 @@ type Row = Record<string, string | number | boolean | Date | number[]>;
152
152
 
153
153
  type Comparator =
154
154
  "=" | "<" | ">" | "<=" | ">=" | "<>" |
155
- "IS" | "IS NOT" | "IN" | "NOT IN" | "LIKE" | "NOT LIKE";
155
+ "IS" | "IS NOT" | "IN" | "NOT IN" | "LIKE" | "NOT LIKE" | "&&";
156
156
 
157
157
  type MatchMode =
158
158
  "startsWith" |
@@ -164,6 +164,7 @@ type MatchMode =
164
164
  "between" |
165
165
  "in" |
166
166
  "notIn" |
167
+ "&&" | // array overlap — use with array-typed columns; generates: column && ARRAY[$1,$2]
167
168
  "lt" |
168
169
  "lte" |
169
170
  "gt" |
@@ -333,7 +334,7 @@ Using substacks simplifies your route definitions and ensures consistent data pr
333
334
 
334
335
  ### Query Methods
335
336
 
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.
337
+ - **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
338
  - **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
339
  - **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
340
  - **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.
@@ -60,9 +60,9 @@ export declare class Property extends BaseProperty {
60
60
  }
61
61
 
62
62
  export type LogicalOperator = "AND" | "OR";
63
- export type Comparator = "=" | "<" | ">" | "<=" | ">=" | "<>" | "IS" | "IS NOT" | "IN" | "NOT IN" | "LIKE" | "NOT LIKE";
64
- export type MatchMode = "startsWith" | "endsWith" | "contains" | "notContains" | "equals" | "notEquals" | "between" | "in" | "notIn" | "lt" | "lte" | "gt" | "gte" | "is" | "isNot" | "before" | "after" | "st_contains" | "st_dwithin" | Comparator;
65
- export type MappedType = "string" | "number" | "date";
63
+ export type Comparator = "=" | "<" | ">" | "<=" | ">=" | "<>" | "IS" | "IS NOT" | "IN" | "NOT IN" | "LIKE" | "NOT LIKE" | "&&";
64
+ export type MatchMode = "startsWith" | "endsWith" | "contains" | "notContains" | "equals" | "notEquals" | "between" | "in" | "notIn" | "&&" | "lt" | "lte" | "gt" | "gte" | "is" | "isNot" | "before" | "after" | "st_contains" | "st_dwithin" | Comparator;
65
+ export type MappedType = "string" | "number" | "date" | "array";
66
66
  export type Geometry = {
67
67
  lng: number;
68
68
  lat: number;
@@ -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
 
@@ -112,12 +112,14 @@ function index(index, matchMode) {
112
112
  case "IN":
113
113
  case "NOT IN":
114
114
  return `(${i})`;
115
+ case "&&":
116
+ return `ARRAY[${i}]`;
115
117
  default:
116
118
  return `${i}`;
117
119
  }
118
120
  }
119
121
 
120
- const COMPARATORS = new Set(["=", "<", ">", "<=", ">=", "<>", "IS", "IS NOT", "IN", "NOT IN", "LIKE", "NOT LIKE"]);
122
+ const COMPARATORS = new Set(["=", "<", ">", "<=", ">=", "<>", "IS", "IS NOT", "IN", "NOT IN", "LIKE", "NOT LIKE", "&&"]);
121
123
  function comparator(matchMode) {
122
124
  if (matchMode && COMPARATORS.has(matchMode))
123
125
  return matchMode;
@@ -154,6 +156,8 @@ function comparator(matchMode) {
154
156
  return "<";
155
157
  case "after":
156
158
  return ">";
159
+ case "&&":
160
+ return "&&";
157
161
  default:
158
162
  return null;
159
163
  }
@@ -307,11 +311,11 @@ class Insert {
307
311
  nbProps += 2;
308
312
  cols += `, "creatorId", "creatorName"`;
309
313
  }
310
- let query = `INSERT INTO ${quoteIfUppercase(schema)}.${quoteIfUppercase(table)} (${cols}) VALUES `;
311
314
  const args = [];
315
+ const valueParts = [];
312
316
  let i = 0;
313
317
  for (const row of rows) {
314
- query += `${$i(nbProps, i)}, `;
318
+ valueParts.push($i(nbProps, i));
315
319
  for (const prop of propsToUse) {
316
320
  if (prop === "consumerId")
317
321
  args.push(consumerId);
@@ -322,7 +326,7 @@ class Insert {
322
326
  }
323
327
  i += nbProps;
324
328
  }
325
- query = query.slice(0, -2);
329
+ let query = `INSERT INTO ${quoteIfUppercase(schema)}.${quoteIfUppercase(table)} (${cols}) VALUES ${valueParts.join(", ")}`;
326
330
  if (rtn)
327
331
  query += ` ${rtn}`;
328
332
  return { query, args };
@@ -348,8 +352,8 @@ class Update {
348
352
  log.debug(() => `${LOGS_PREFIX}Update query input rows: ${JSON.stringify(rows, null, 2)}`);
349
353
  const l = rows.length;
350
354
  const args = rows.map(row => row.id);
351
- let query = `UPDATE ${quoteIfUppercase(schema)}.${quoteIfUppercase(table)} SET `;
352
355
  let i = args.length + 1;
356
+ const setClauses = [];
353
357
  for (const p of propsToUse) {
354
358
  const isConsumerId = p === "consumerId";
355
359
  const isConsumerName = p === "consumerName";
@@ -357,9 +361,9 @@ class Update {
357
361
  if (!isConsumerProp && rows[0][p] === undefined)
358
362
  continue;
359
363
  const colName = isConsumerId ? '"updaterId"' : isConsumerName ? '"updaterName"' : quoteIfUppercase(p);
360
- query += `${colName} = CASE `;
364
+ const whenParts = [];
361
365
  for (let j = 0; j < l; j++) {
362
- query += `WHEN id = $${j + 1} THEN $${i++} `;
366
+ whenParts.push(`WHEN id = $${j + 1} THEN $${i++}`);
363
367
  if (isConsumerId)
364
368
  args.push(consumerId);
365
369
  else if (isConsumerName)
@@ -367,9 +371,9 @@ class Update {
367
371
  else
368
372
  args.push(rows[j][p]);
369
373
  }
370
- query += `ELSE ${colName} END, `;
374
+ setClauses.push(`${colName} = CASE ${whenParts.join(" ")} ELSE ${colName} END`);
371
375
  }
372
- query = `${query.slice(0, -2)} WHERE id IN ${$i(l, 0)}`;
376
+ const query = `UPDATE ${quoteIfUppercase(schema)}.${quoteIfUppercase(table)} SET ${setClauses.join(", ")} WHERE id IN ${$i(l, 0)}`;
373
377
  return { query, args };
374
378
  }
375
379
  async execute(query, args, client) {
@@ -421,16 +425,13 @@ class Upsert {
421
425
  i += nbProps;
422
426
  }
423
427
  query = query.slice(0, -2);
424
- query += ` ON CONFLICT (${conflictColumns}) DO UPDATE SET `;
425
428
  const conflictTargetArray = Array.isArray(conflictTarget) ? conflictTarget : [conflictTarget];
426
429
  const updateCols = quotedPropsToUse.filter((_, idx) => {
427
430
  const propName = propsToUse[idx];
428
431
  return !conflictTargetArray.includes(propName);
429
432
  });
430
- for (const col of updateCols) {
431
- query += `${col} = EXCLUDED.${col}, `;
432
- }
433
- query = query.slice(0, -2);
433
+ const updateSetClause = updateCols.map(col => `${col} = EXCLUDED.${col}`).join(", ");
434
+ query += ` ON CONFLICT (${conflictColumns}) DO UPDATE SET ${updateSetClause}`;
434
435
  if (rtn)
435
436
  query += ` ${rtn}`;
436
437
  return { query, args };
@@ -471,14 +472,7 @@ function queryByDate() {
471
472
  return `SELECT hard_delete($1, $2, $3)`;
472
473
  }
473
474
  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;
475
+ return execute(query, [schema, table, date], client);
482
476
  }
483
477
 
484
478
  function type(type) {
@@ -534,6 +528,8 @@ function type(type) {
534
528
  return s;
535
529
  case "object":
536
530
  return s;
531
+ case "array":
532
+ return "array";
537
533
  default:
538
534
  return s;
539
535
  }
@@ -543,6 +539,7 @@ const matchModes = {
543
539
  string: new Set(["startsWith", "contains", "endsWith", "notContains", "equals", "notEquals", "lt", "lte", "gt", "gte", "in", "notIn"]),
544
540
  number: new Set(["equals", "notEquals", "lt", "lte", "gt", "gte", "in", "notIn"]),
545
541
  date: new Set(["is", "isNot", "dateAfter"]),
542
+ array: new Set(["&&"]),
546
543
  };
547
544
  function matchMode(type, matchMode) {
548
545
  return COMPARATORS.has(matchMode) || matchModes[type].has(matchMode);
@@ -565,6 +562,12 @@ function cleanFilters(filters, properties) {
565
562
  const type$1 = type(prop.type);
566
563
  const filterValue = filters[k];
567
564
  const filterArray = isArray(filterValue) ? filterValue : [filterValue];
565
+ if (type$1 === "array") {
566
+ for (const f of filterArray) {
567
+ if (f.matchMode === "in")
568
+ f.matchMode = "&&";
569
+ }
570
+ }
568
571
  const validFilters = filterArray.filter((f) => {
569
572
  const { matchMode: matchMode$1 } = f;
570
573
  if (!matchMode$1 || !matchMode(type$1, matchMode$1)) {
@@ -706,7 +709,8 @@ class SQLEntity extends Entity {
706
709
  }
707
710
  query = {
708
711
  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);
712
+ const validatedSortField = sortField && this.properties.some(p => p.key === sortField) ? sortField : null;
713
+ return this.sel.query(this.schema, this.table, first, rows, validatedSortField, sortOrder, filters);
710
714
  },
711
715
  update: (rows, consumer) => {
712
716
  return this.upd.query(this.schema, this.table, rows, consumer?.id, consumer?.nickname);
@@ -735,7 +739,8 @@ class SQLEntity extends Entity {
735
739
  const b = req.body;
736
740
  const first = b?.first ?? 0;
737
741
  const rows = b.rows || null;
738
- const sortField = b.sortField || null;
742
+ const rawSortField = b.sortField || null;
743
+ const sortField = rawSortField && this.properties.some(p => p.key === rawSortField) ? rawSortField : null;
739
744
  const sortOrder = b.sortOrder === -1 || b.sortOrder === "DESC" ? "DESC" : "ASC";
740
745
  const filters = cleanFilters(b.filters, this.properties) || null;
741
746
  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.6",
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
  },