@dwtechs/antity-pgsql 0.17.1 → 0.17.3

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
@@ -123,6 +123,24 @@ router.get("/:id/history", ..., entity.getHistory);
123
123
 
124
124
  ```
125
125
 
126
+ ### Expected table structure
127
+
128
+ ```sql
129
+ CREATE TABLE IF NOT EXISTS "service" (
130
+ id SERIAL PRIMARY KEY,
131
+ name varchar(20) NOT NULL,
132
+ pattern TEXT,
133
+ archived BOOLEAN DEFAULT FALSE,
134
+ "archivedAt" TIMESTAMP,
135
+ "creatorId" INT,
136
+ "creatorName" TEXT,
137
+ "updaterId" INT,
138
+ "updaterName" TEXT,
139
+ "createdAt" TIMESTAMP DEFAULT NOW(),
140
+ "updatedAt" TIMESTAMP NULL
141
+ );
142
+ ```
143
+
126
144
  ## API Reference
127
145
 
128
146
 
@@ -132,6 +150,10 @@ type Operation = "SELECT" | "INSERT" | "UPDATE";
132
150
 
133
151
  type Row = Record<string, string | number | boolean | Date | number[]>;
134
152
 
153
+ type Comparator =
154
+ "=" | "<" | ">" | "<=" | ">=" | "<>" |
155
+ "IS" | "IS NOT" | "IN" | "NOT IN" | "LIKE" | "NOT LIKE";
156
+
135
157
  type MatchMode =
136
158
  "startsWith" |
137
159
  "endsWith" |
@@ -151,7 +173,8 @@ type MatchMode =
151
173
  "before" |
152
174
  "after" |
153
175
  "st_contains" |
154
- "st_dwithin";
176
+ "st_dwithin" |
177
+ Comparator; // direct SQL comparators are also accepted
155
178
 
156
179
 
157
180
  type Filters = {
@@ -160,7 +183,7 @@ type Filters = {
160
183
 
161
184
  type Filter = {
162
185
  value: string | number | boolean | Date | number[];
163
- matchMode?: MatchMode;
186
+ matchMode?: MatchMode; // semantic mode or direct SQL comparator
164
187
  operator?: string; // 'and' | 'or' - Used when multiple filters apply to the same property
165
188
  }
166
189
 
@@ -311,10 +334,10 @@ Using substacks simplifies your route definitions and ensures consistent data pr
311
334
  ### Query Methods
312
335
 
313
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.
314
- - **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 `name` for audit tracking. Supports `RETURNING` clause via the `rtn` parameter.
315
- - **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 `name` for audit tracking.
316
- - **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 `name` for audit tracking. Supports `RETURNING` clause via the `rtn` parameter.
317
- - **query.archive()**: Generates a simplified `UPDATE ... SET archived = true WHERE id IN (...)` query. Accepts an array of `Row` objects with `id` property. Optionally appends `consumer.id` as `updaterId` and `consumer.nickname` as `name` for audit tracking. Does not require an `archived` field in the rows — it is set directly in the SQL.
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
+ - **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
+ - **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.
340
+ - **query.archive()**: Generates a simplified `UPDATE ... SET archived = true WHERE id IN (...)` query. Accepts an array of `Row` objects with `id` property. Optionally appends `consumer.id` as `updaterId` and `consumer.nickname` as `updaterName` for audit tracking. Does not require an `archived` field in the rows — it is set directly in the SQL.
318
341
  - **sync()**: Atomically synchronises the table with the provided rows inside a single PostgreSQL transaction. Missing rows are inserted, existing rows are updated, and rows absent from the list are deleted. Accepts optional `idField` (default `'id'`) and `filters` to restrict the scope of managed rows. Stores the result in `res.locals.rows` and a summary `{ inserted, updated, deleted }` in `res.locals.sync`.
319
342
  - **delete()**: Deletes rows by their IDs. Expects `req.body.rows` to be an array of objects with `id` property: `[{id: 1}, {id: 2}]`
320
343
  - **deleteArchive()**: Deletes archived rows that were archived before a specific date using a PostgreSQL SECURITY DEFINER function. Expects `req.body.date` to be a Date object.
@@ -376,7 +399,7 @@ res.locals.sync // { inserted: 1, updated: 1, deleted: 1 }
376
399
  - **Atomic**: All insert / update / delete operations are wrapped in a single transaction.
377
400
  - **Filter scope**: When `filters` are provided, only rows matching the filter are considered "managed". Rows outside the filter are never touched.
378
401
  - **Property selection**: Insert uses `INSERT` properties; update uses `UPDATE` properties — same as the standalone `add` and `update` middlewares.
379
- - **Consumer tracking**: `consumer.id` and `consumer.nickname` from `res.locals.consumer` are forwarded to inserts as `creatorId`/`name` and to updates as `updaterId`/`name` for audit tracking.
402
+ - **Consumer tracking**: `consumer.id` and `consumer.nickname` from `res.locals.consumer` are forwarded to inserts as `creatorId`/`creatorName` and to updates as `updaterId`/`updaterName` for audit tracking.
380
403
 
381
404
  ### Upsert (Insert or Update)
382
405
 
@@ -492,6 +515,12 @@ const filters = {
492
515
  age: { value: 30, matchMode: 'equals' },
493
516
  archived: { value: false, matchMode: 'equals' }
494
517
  };
518
+
519
+ // Direct SQL comparators are also accepted
520
+ const filters = {
521
+ age: { value: 30, matchMode: '>=' },
522
+ status: { value: null, matchMode: 'IS NOT' }
523
+ };
495
524
  ```
496
525
 
497
526
  #### Complex Format (Multiple Filters per Property)
@@ -531,7 +560,11 @@ WHERE (name LIKE '%John%' OR name LIKE '%Jane%')
531
560
 
532
561
  ## Match modes
533
562
 
534
- List of possible match modes :
563
+ `matchMode` accepts either a **semantic match mode** (listed below) or a **direct SQL comparator** (`=`, `<`, `>`, `<=`, `>=`, `<>`, `IS`, `IS NOT`, `IN`, `NOT IN`, `LIKE`, `NOT LIKE`).
564
+
565
+ Using a direct comparator bypasses the semantic layer. Note that when using `LIKE` or `NOT LIKE` directly, wildcard characters (`%`) must be included manually in the value.
566
+
567
+ List of possible semantic match modes :
535
568
 
536
569
  | Name | alias | types | Description |
537
570
  | :---------- | :---- | :---------------------- | :-------------------------------------------------------- |
@@ -31,7 +31,7 @@ import type { Request, Response, NextFunction } from 'express';
31
31
  export type Operation = "SELECT" | "INSERT" | "UPDATE";
32
32
  export type Sort = "ASC" | "DESC";
33
33
  export type Filters = {
34
- [key: string]: Filter;
34
+ [key: string]: Filter | Filter[];
35
35
  };
36
36
  export type Filter = {
37
37
  value: string | number | boolean | Date | number[];
@@ -60,8 +60,8 @@ 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" | "LIKE" | "NOT LIKE";
64
- export type MatchMode = "startsWith" | "endsWith" | "contains" | "notContains" | "equals" | "notEquals" | "between" | "in" | "lt" | "lte" | "gt" | "gte" | "is" | "isNot" | "before" | "after" | "st_contains" | "st_dwithin";
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
65
  export type MappedType = "string" | "number" | "date";
66
66
  export type Geometry = {
67
67
  lng: number;
@@ -96,6 +96,7 @@ export type PGResponse = {
96
96
  _types?: unknown;
97
97
  RowCtor?: unknown;
98
98
  rowAsArray?: boolean;
99
+ _prebuiltEmptyResultObject?: Record<string, unknown>;
99
100
  };
100
101
 
101
102
  type ExpressMiddleware = (req: Request, res: Response, next: NextFunction) => void;
@@ -24,7 +24,7 @@ SOFTWARE.
24
24
  https://github.com/DWTechs/Antity-pgsql.js
25
25
  */
26
26
 
27
- import { isArray, isString, isIn } from '@dwtechs/checkard';
27
+ import { isArray, isString } from '@dwtechs/checkard';
28
28
  import { deleteProps, chunk, flatten } from '@dwtechs/sparray';
29
29
  import { log } from '@dwtechs/winstan';
30
30
  import { Entity } from '@dwtechs/antity';
@@ -84,7 +84,7 @@ function deleteIdleProperties(res) {
84
84
  res._prebuiltEmptyResultObject = undefined;
85
85
  }
86
86
 
87
- const reserved = [
87
+ const reserved = new Set([
88
88
  'all', 'analyse', 'analyze', 'and', 'any', 'array', 'as', 'asc', 'asymmetric',
89
89
  'authorization', 'between', 'binary', 'both', 'case', 'cast', 'check', 'collate',
90
90
  'column', 'constraint', 'create', 'cross', 'current_catalog', 'current_date',
@@ -97,9 +97,9 @@ const reserved = [
97
97
  'references', 'returning', 'right', 'select', 'session_user', 'similar', 'some', 'symmetric',
98
98
  'table', 'tablesample', 'then', 'to', 'trailing', 'true', 'union', 'unique', 'user', 'using',
99
99
  'variadic', 'verbose', 'when', 'where', 'window', 'with'
100
- ];
100
+ ]);
101
101
  function quoteIfUppercase(word) {
102
- if (/[A-Z]/.test(word) || reserved.includes(word.toLowerCase()))
102
+ if (/[A-Z]/.test(word) || reserved.has(word.toLowerCase()))
103
103
  return `"${word}"`;
104
104
  return word;
105
105
  }
@@ -109,13 +109,18 @@ function index(index, matchMode) {
109
109
  switch (matchMode) {
110
110
  case "in":
111
111
  case "notIn":
112
+ case "IN":
113
+ case "NOT IN":
112
114
  return `(${i})`;
113
115
  default:
114
116
  return `${i}`;
115
117
  }
116
118
  }
117
119
 
120
+ const COMPARATORS = new Set(["=", "<", ">", "<=", ">=", "<>", "IS", "IS NOT", "IN", "NOT IN", "LIKE", "NOT LIKE"]);
118
121
  function comparator(matchMode) {
122
+ if (matchMode && COMPARATORS.has(matchMode))
123
+ return matchMode;
119
124
  switch (matchMode) {
120
125
  case "startsWith":
121
126
  return "LIKE";
@@ -174,12 +179,11 @@ function shouldSkipValue(value, matchMode) {
174
179
  return true;
175
180
  if (isArray(value, "0"))
176
181
  return true;
177
- if (value === null && matchMode !== 'is' && matchMode !== 'isNot')
182
+ if (value === null && matchMode !== 'is' && matchMode !== 'isNot' && matchMode !== 'IS' && matchMode !== 'IS NOT')
178
183
  return true;
179
184
  return false;
180
185
  }
181
186
  function add(filters) {
182
- var _a, _b;
183
187
  const conditions = [];
184
188
  const args = [];
185
189
  if (filters) {
@@ -203,7 +207,7 @@ function add(filters) {
203
207
  }
204
208
  }
205
209
  if (groupConditions.length > 0) {
206
- const operator = ((_b = (_a = filterArray[0]) === null || _a === void 0 ? void 0 : _a.operator) === null || _b === void 0 ? void 0 : _b.toUpperCase()) || 'AND';
210
+ const operator = filterArray[0]?.operator?.toUpperCase() || 'AND';
207
211
  const combined = groupConditions.length > 1
208
212
  ? `(${groupConditions.join(` ${operator} `)})`
209
213
  : groupConditions[0];
@@ -244,11 +248,9 @@ function limit(rows, first) {
244
248
  }
245
249
 
246
250
  class Select {
247
- constructor() {
248
- this._props = [];
249
- this._cols = "";
250
- this._count = ", COUNT(*) OVER () AS total";
251
- }
251
+ _props = [];
252
+ _cols = "";
253
+ _count = ", COUNT(*) OVER () AS total";
252
254
  addProp(prop) {
253
255
  this._props.push(quoteIfUppercase(prop));
254
256
  this._cols = this._props.join(", ");
@@ -286,12 +288,10 @@ function $i(qty, start) {
286
288
  }
287
289
 
288
290
  class Insert {
289
- constructor() {
290
- this._props = [];
291
- this._quotedProps = [];
292
- this._nbProps = 0;
293
- this._cols = "";
294
- }
291
+ _props = [];
292
+ _quotedProps = [];
293
+ _nbProps = 0;
294
+ _cols = "";
295
295
  addProp(prop) {
296
296
  this._props.push(prop);
297
297
  this._quotedProps.push(quoteIfUppercase(prop));
@@ -305,7 +305,7 @@ class Insert {
305
305
  if (consumerId !== undefined && consumerName !== undefined) {
306
306
  propsToUse.push("consumerId", "consumerName");
307
307
  nbProps += 2;
308
- cols += `, creatorId, name`;
308
+ cols += `, "creatorId", "creatorName"`;
309
309
  }
310
310
  let query = `INSERT INTO ${quoteIfUppercase(schema)}.${quoteIfUppercase(table)} (${cols}) VALUES `;
311
311
  const args = [];
@@ -335,27 +335,15 @@ class Insert {
335
335
  }
336
336
  }
337
337
 
338
- var __awaiter$3 = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
339
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
340
- return new (P || (P = Promise))(function (resolve, reject) {
341
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
342
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
343
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
344
- step((generator = generator.apply(thisArg, _arguments || [])).next());
345
- });
346
- };
347
338
  class Update {
348
- constructor() {
349
- this._props = [];
350
- }
339
+ _props = [];
351
340
  addProp(prop) {
352
341
  this._props.push(prop);
353
342
  }
354
343
  query(schema, table, rows, consumerId, consumerName) {
355
- if (consumerId !== undefined && consumerName !== undefined)
356
- rows = this.addConsumer(rows, consumerId, consumerName);
344
+ const hasConsumer = consumerId !== undefined && consumerName !== undefined;
357
345
  const propsToUse = [...this._props];
358
- if (consumerId !== undefined && consumerName !== undefined)
346
+ if (hasConsumer)
359
347
  propsToUse.push("consumerId", "consumerName");
360
348
  log.debug(() => `${LOGS_PREFIX}Update query input rows: ${JSON.stringify(rows, null, 2)}`);
361
349
  const l = rows.length;
@@ -363,38 +351,37 @@ class Update {
363
351
  let query = `UPDATE ${quoteIfUppercase(schema)}.${quoteIfUppercase(table)} SET `;
364
352
  let i = args.length + 1;
365
353
  for (const p of propsToUse) {
366
- if (rows[0][p] === undefined)
354
+ const isConsumerId = p === "consumerId";
355
+ const isConsumerName = p === "consumerName";
356
+ const isConsumerProp = isConsumerId || isConsumerName;
357
+ if (!isConsumerProp && rows[0][p] === undefined)
367
358
  continue;
368
- const colName = p === "consumerId" ? "updaterId" : p === "consumerName" ? "name" : quoteIfUppercase(p);
359
+ const colName = isConsumerId ? '"updaterId"' : isConsumerName ? '"updaterName"' : quoteIfUppercase(p);
369
360
  query += `${colName} = CASE `;
370
361
  for (let j = 0; j < l; j++) {
371
- const row = rows[j];
372
362
  query += `WHEN id = $${j + 1} THEN $${i++} `;
373
- args.push(row[p]);
363
+ if (isConsumerId)
364
+ args.push(consumerId);
365
+ else if (isConsumerName)
366
+ args.push(consumerName);
367
+ else
368
+ args.push(rows[j][p]);
374
369
  }
375
370
  query += `ELSE ${colName} END, `;
376
371
  }
377
372
  query = `${query.slice(0, -2)} WHERE id IN ${$i(l, 0)}`;
378
373
  return { query, args };
379
374
  }
380
- execute(query, args, client) {
381
- return __awaiter$3(this, void 0, void 0, function* () {
382
- return execute(query, args, client);
383
- });
384
- }
385
- addConsumer(rows, consumerId, consumerName) {
386
- return rows.map((row) => (Object.assign(Object.assign({}, row), { consumerId,
387
- consumerName })));
375
+ async execute(query, args, client) {
376
+ return execute(query, args, client);
388
377
  }
389
378
  }
390
379
 
391
380
  class Upsert {
392
- constructor() {
393
- this._props = [];
394
- this._quotedProps = [];
395
- this._nbProps = 0;
396
- this._cols = "";
397
- }
381
+ _props = [];
382
+ _quotedProps = [];
383
+ _nbProps = 0;
384
+ _cols = "";
398
385
  addProp(prop) {
399
386
  this._props.push(prop);
400
387
  this._quotedProps.push(quoteIfUppercase(prop));
@@ -413,9 +400,9 @@ class Upsert {
413
400
  let cols = this._cols;
414
401
  if (consumerId !== undefined && consumerName !== undefined) {
415
402
  propsToUse.push("consumerId", "consumerName");
416
- quotedPropsToUse.push(`creatorId`, `name`);
403
+ quotedPropsToUse.push(`"creatorId"`, `"creatorName"`);
417
404
  nbProps += 2;
418
- cols += `, creatorId, name`;
405
+ cols += `, "creatorId", "creatorName"`;
419
406
  }
420
407
  const conflictColumns = Array.isArray(conflictTarget)
421
408
  ? conflictTarget.map(col => quoteIfUppercase(col)).join(", ")
@@ -456,15 +443,6 @@ class Upsert {
456
443
  }
457
444
  }
458
445
 
459
- var __awaiter$2 = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
460
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
461
- return new (P || (P = Promise))(function (resolve, reject) {
462
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
463
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
464
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
465
- step((generator = generator.apply(thisArg, _arguments || [])).next());
466
- });
467
- };
468
446
  class Archive {
469
447
  query(schema, table, rows, consumerId, consumerName) {
470
448
  log.debug(() => `${LOGS_PREFIX}Archive query input rows: ${JSON.stringify(rows, null, 2)}`);
@@ -472,28 +450,17 @@ class Archive {
472
450
  const args = rows.map(row => row.id);
473
451
  let query = `UPDATE ${quoteIfUppercase(schema)}.${quoteIfUppercase(table)} SET archived = true`;
474
452
  if (consumerId !== undefined && consumerName !== undefined) {
475
- query += `, updaterId = $${l + 1}, name = $${l + 2}`;
453
+ query += `, "updaterId" = $${l + 1}, "updaterName" = $${l + 2}`;
476
454
  args.push(consumerId, consumerName);
477
455
  }
478
456
  query += ` WHERE id IN ${$i(l, 0)}`;
479
457
  return { query, args };
480
458
  }
481
- execute(query, args, client) {
482
- return __awaiter$2(this, void 0, void 0, function* () {
483
- return execute(query, args, client);
484
- });
459
+ async execute(query, args, client) {
460
+ return execute(query, args, client);
485
461
  }
486
462
  }
487
463
 
488
- var __awaiter$1 = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
489
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
490
- return new (P || (P = Promise))(function (resolve, reject) {
491
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
492
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
493
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
494
- step((generator = generator.apply(thisArg, _arguments || [])).next());
495
- });
496
- };
497
464
  function queryById(schema, table, ids) {
498
465
  return {
499
466
  query: `DELETE FROM ${quoteIfUppercase(schema)}.${quoteIfUppercase(table)} WHERE id = ANY($1)`,
@@ -503,17 +470,15 @@ function queryById(schema, table, ids) {
503
470
  function queryByDate() {
504
471
  return `SELECT hard_delete($1, $2, $3)`;
505
472
  }
506
- function executeArchived(schema, table, date, query, client) {
507
- return __awaiter$1(this, void 0, void 0, function* () {
508
- let db;
509
- try {
510
- db = yield execute(query, [schema, table, date], client);
511
- }
512
- catch (err) {
513
- throw err;
514
- }
515
- return db;
516
- });
473
+ 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;
517
482
  }
518
483
 
519
484
  function type(type) {
@@ -575,12 +540,12 @@ function type(type) {
575
540
  }
576
541
 
577
542
  const matchModes = {
578
- string: ["startsWith", "contains", "endsWith", "notContains", "equals", "notEquals", "lt", "lte", "gt", "gte", "in", "notIn"],
579
- number: ["equals", "notEquals", "lt", "lte", "gt", "gte", "in", "notIn"],
580
- date: ["is", "isNot", "dateAfter"],
543
+ string: new Set(["startsWith", "contains", "endsWith", "notContains", "equals", "notEquals", "lt", "lte", "gt", "gte", "in", "notIn"]),
544
+ number: new Set(["equals", "notEquals", "lt", "lte", "gt", "gte", "in", "notIn"]),
545
+ date: new Set(["is", "isNot", "dateAfter"]),
581
546
  };
582
547
  function matchMode(type, matchMode) {
583
- return isIn(matchModes[type], matchMode);
548
+ return COMPARATORS.has(matchMode) || matchModes[type].has(matchMode);
584
549
  }
585
550
 
586
551
  function cleanFilters(filters, properties) {
@@ -681,290 +646,16 @@ function getCrudMappings(properties) {
681
646
  return mappings;
682
647
  }
683
648
 
684
- var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
685
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
686
- return new (P || (P = Promise))(function (resolve, reject) {
687
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
688
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
689
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
690
- step((generator = generator.apply(thisArg, _arguments || [])).next());
691
- });
692
- };
693
649
  class SQLEntity extends Entity {
650
+ _table;
651
+ _schema;
652
+ sel = new Select();
653
+ ins = new Insert();
654
+ upd = new Update();
655
+ ups = new Upsert();
656
+ arc = new Archive();
694
657
  constructor(name, properties, schema = 'public') {
695
658
  super(name, properties);
696
- this.sel = new Select();
697
- this.ins = new Insert();
698
- this.upd = new Update();
699
- this.ups = new Upsert();
700
- this.arc = new Archive();
701
- this.query = {
702
- select: (first = 0, rows = null, sortField = null, sortOrder = null, filters = null) => {
703
- return this.sel.query(this.schema, this.table, first, rows, sortField, sortOrder, filters);
704
- },
705
- update: (rows, consumer) => {
706
- return this.upd.query(this.schema, this.table, rows, consumer === null || consumer === void 0 ? void 0 : consumer.id, consumer === null || consumer === void 0 ? void 0 : consumer.nickname);
707
- },
708
- insert: (rows, consumer, rtn = "") => {
709
- return this.ins.query(this.schema, this.table, rows, consumer === null || consumer === void 0 ? void 0 : consumer.id, consumer === null || consumer === void 0 ? void 0 : consumer.nickname, rtn);
710
- },
711
- upsert: (rows, conflictTarget, consumer, rtn = "") => {
712
- return this.ups.query(this.schema, this.table, rows, conflictTarget, consumer === null || consumer === void 0 ? void 0 : consumer.id, consumer === null || consumer === void 0 ? void 0 : consumer.nickname, rtn);
713
- },
714
- delete: (ids) => {
715
- return queryById(this.schema, this.table, ids);
716
- },
717
- archive: (rows, consumer) => {
718
- return this.arc.query(this.schema, this.table, rows, consumer === null || consumer === void 0 ? void 0 : consumer.id, consumer === null || consumer === void 0 ? void 0 : consumer.nickname);
719
- },
720
- deleteArchive: () => {
721
- return queryByDate();
722
- },
723
- return: (prop) => {
724
- return this.ins.rtn(prop);
725
- }
726
- };
727
- this.get = (req, res, next) => {
728
- var _a;
729
- const l = res.locals;
730
- const b = req.body;
731
- const first = (_a = b === null || b === void 0 ? void 0 : b.first) !== null && _a !== void 0 ? _a : 0;
732
- const rows = b.rows || null;
733
- const sortField = b.sortField || null;
734
- const sortOrder = b.sortOrder === -1 || b.sortOrder === "DESC" ? "DESC" : "ASC";
735
- const filters = cleanFilters(b.filters, this.properties) || null;
736
- const pagination = b.pagination || false;
737
- const dbClient = l.dbClient || null;
738
- log.debug(() => `get(first='${first}', rows='${rows}', sortOrder='${sortOrder}', sortField='${sortField}', pagination=${pagination}, filters=${JSON.stringify(filters)}`);
739
- const { query, args } = this.sel.query(this._schema, this._table, first, rows, sortField, sortOrder, filters);
740
- this.sel.execute(query, args, dbClient)
741
- .then((r) => {
742
- l.rows = r.rows;
743
- l.total = r.total;
744
- next();
745
- })
746
- .catch((err) => next(err));
747
- };
748
- this.add = (req, res, next) => __awaiter(this, void 0, void 0, function* () {
749
- var _a, _b;
750
- const l = res.locals;
751
- const rows = req.body.rows;
752
- const dbClient = l.dbClient || null;
753
- const cId = (_a = l.consumer) === null || _a === void 0 ? void 0 : _a.id;
754
- const cName = (_b = l.consumer) === null || _b === void 0 ? void 0 : _b.nickname;
755
- log.debug(() => `${LOGS_PREFIX}addMany(rows=${rows.length}, consumerId=${cId})`);
756
- const rtn = this.ins.rtn("id");
757
- const chunks = chunk(rows);
758
- for (const c of chunks) {
759
- const { query, args } = this.ins.query(this._schema, this._table, c, cId, cName, rtn);
760
- let db;
761
- try {
762
- db = yield execute(query, args, dbClient);
763
- }
764
- catch (err) {
765
- return next(err);
766
- }
767
- const r = db.rows;
768
- for (let i = 0; i < c.length; i++) {
769
- c[i].id = r[i].id;
770
- }
771
- }
772
- l.rows = flatten(chunks);
773
- next();
774
- });
775
- this.update = (req, res, next) => __awaiter(this, void 0, void 0, function* () {
776
- var _a, _b;
777
- const l = res.locals;
778
- const r = req.body.rows;
779
- const dbClient = l.dbClient || null;
780
- const cId = (_a = l.consumer) === null || _a === void 0 ? void 0 : _a.id;
781
- const cName = (_b = l.consumer) === null || _b === void 0 ? void 0 : _b.nickname;
782
- log.debug(() => `${LOGS_PREFIX}update(rows=${r.length}, consumerId=${cId})`);
783
- const chunks = chunk(r);
784
- for (const c of chunks) {
785
- const { query, args } = this.upd.query(this._schema, this._table, c, cId, cName);
786
- try {
787
- yield execute(query, args, dbClient);
788
- }
789
- catch (err) {
790
- return next(err);
791
- }
792
- }
793
- l.rows = r;
794
- next();
795
- });
796
- this.upsert = (req, res, next) => __awaiter(this, void 0, void 0, function* () {
797
- var _a, _b;
798
- const l = res.locals;
799
- const rows = req.body.rows;
800
- const conflictTarget = req.body.conflictTarget;
801
- const dbClient = l.dbClient || null;
802
- const cId = (_a = l.consumer) === null || _a === void 0 ? void 0 : _a.id;
803
- const cName = (_b = l.consumer) === null || _b === void 0 ? void 0 : _b.nickname;
804
- if (!conflictTarget) {
805
- return next({ status: 400, msg: "Missing conflictTarget for upsert operation" });
806
- }
807
- if (!rows || !Array.isArray(rows) || rows.length === 0) {
808
- return next({ status: 400, msg: "Missing or empty rows array for upsert operation" });
809
- }
810
- log.debug(() => `${LOGS_PREFIX}upsert(rows=${rows.length}, conflictTarget=${conflictTarget}, consumerId=${cId})`);
811
- const rtn = this.ups.rtn("id");
812
- const chunks = chunk(rows);
813
- for (const c of chunks) {
814
- const { query, args } = this.ups.query(this._schema, this._table, c, conflictTarget, cId, cName, rtn);
815
- let db;
816
- try {
817
- db = yield execute(query, args, dbClient);
818
- }
819
- catch (err) {
820
- return next(err);
821
- }
822
- const r = db.rows;
823
- for (let i = 0; i < c.length; i++) {
824
- c[i].id = r[i].id;
825
- }
826
- }
827
- l.rows = flatten(chunks);
828
- next();
829
- });
830
- this.archive = (req, res, next) => __awaiter(this, void 0, void 0, function* () {
831
- var _a, _b;
832
- const l = res.locals;
833
- const r = req.body.rows;
834
- const dbClient = l.dbClient || null;
835
- const cId = (_a = l.consumer) === null || _a === void 0 ? void 0 : _a.id;
836
- const cName = (_b = l.consumer) === null || _b === void 0 ? void 0 : _b.nickname;
837
- log.debug(() => `${LOGS_PREFIX}archive(rows=${r.length}, consumerId=${cId})`);
838
- const chunks = chunk(r);
839
- for (const c of chunks) {
840
- const { query, args } = this.arc.query(this._schema, this._table, c, cId, cName);
841
- try {
842
- yield execute(query, args, dbClient);
843
- }
844
- catch (err) {
845
- return next(err);
846
- }
847
- }
848
- next();
849
- });
850
- this.delete = (req, res, next) => __awaiter(this, void 0, void 0, function* () {
851
- const rows = req.body.rows;
852
- const dbClient = res.locals.dbClient || null;
853
- const ids = rows.map((row) => row.id);
854
- log.debug(() => `${LOGS_PREFIX}delete ${rows.length} rows : (${ids.join(", ")})`);
855
- const { query, args } = queryById(this._schema, this._table, ids);
856
- try {
857
- yield execute(query, args, dbClient);
858
- }
859
- catch (err) {
860
- return next(err);
861
- }
862
- next();
863
- });
864
- this.deleteArchive = (req, res, next) => {
865
- const date = req.body.date;
866
- const dbClient = res.locals.dbClient || null;
867
- log.debug(() => `${LOGS_PREFIX}deleteArchive(schema=${this._schema}, table=${this._table}, date=${date})`);
868
- executeArchived(this._schema, this._table, date, queryByDate(), dbClient)
869
- .then(() => next())
870
- .catch((err) => next(err));
871
- };
872
- this.getHistory = (req, res, next) => {
873
- const id = req.params.id;
874
- const dbClient = res.locals.dbClient || null;
875
- if (!id) {
876
- next({ status: 400, msg: "Missing id" });
877
- return;
878
- }
879
- log.debug(() => `${LOGS_PREFIX}getHistory(schema=${this._schema}, table=${this._table}, id=${id})`);
880
- const sql = `
881
- SELECT id, tstamp, operation, "consumerId", "consumerName"
882
- FROM log.history
883
- WHERE "schemaName" = $1
884
- AND "tableName" = $2
885
- AND CAST(record->>'id' AS INT) = $3
886
- ORDER BY tstamp ASC
887
- `;
888
- execute(sql, [this._schema, this._table, id], dbClient)
889
- .then((r) => {
890
- const { rowCount, rows } = r;
891
- if (!rowCount) {
892
- return next({ status: 404, msg: "History not found" });
893
- }
894
- res.locals.history = rows;
895
- res.locals.total = rowCount;
896
- next();
897
- })
898
- .catch((err) => next(err));
899
- };
900
- this.sync = (req, res, next) => __awaiter(this, void 0, void 0, function* () {
901
- var _a, _b, _c;
902
- const l = res.locals;
903
- const rows = req.body.rows;
904
- const idField = (_a = req.body.idField) !== null && _a !== void 0 ? _a : 'id';
905
- const cId = (_b = l.consumer) === null || _b === void 0 ? void 0 : _b.id;
906
- const cName = (_c = l.consumer) === null || _c === void 0 ? void 0 : _c.nickname;
907
- if (!rows || !Array.isArray(rows)) {
908
- return next({ status: 400, msg: "Missing or invalid rows array for sync operation" });
909
- }
910
- log.debug(() => `${LOGS_PREFIX}sync(rows=${rows.length}, idField=${idField}, consumerId=${cId})`);
911
- const cleanedFilters = cleanFilters(req.body.filters, this.properties) || null;
912
- const { conditions, args: filterArgs } = add(cleanedFilters);
913
- const whereClause = conditions.length ? ` WHERE ${conditions.join(' AND ')}` : '';
914
- const txClient = l.dbClient || (yield pool.connect());
915
- let toInsert = [];
916
- let toUpdate = [];
917
- let idsToDelete = [];
918
- try {
919
- yield txClient.query('BEGIN');
920
- const selectIdQuery = `SELECT ${quoteIfUppercase(idField)} FROM ${quoteIfUppercase(this._schema)}.${quoteIfUppercase(this._table)}${whereClause}`;
921
- const existingDb = yield execute(selectIdQuery, filterArgs, txClient);
922
- const existingIds = new Set(existingDb.rows.map(r => r[idField]));
923
- const incomingIds = new Set(rows.filter(r => r[idField] != null).map(r => r[idField]));
924
- toInsert = rows.filter(r => r[idField] == null || !existingIds.has(r[idField]));
925
- toUpdate = rows.filter(r => r[idField] != null && existingIds.has(r[idField]));
926
- idsToDelete = [...existingIds].filter(id => !incomingIds.has(id));
927
- if (toInsert.length > 0) {
928
- const rtn = this.ins.rtn(idField);
929
- const chunks = chunk(toInsert);
930
- for (const c of chunks) {
931
- const { query, args } = this.ins.query(this._schema, this._table, c, cId, cName, rtn);
932
- const db = yield execute(query, args, txClient);
933
- const r = db.rows;
934
- for (let i = 0; i < c.length; i++) {
935
- c[i][idField] = r[i][idField];
936
- }
937
- }
938
- }
939
- if (toUpdate.length > 0) {
940
- const chunks = chunk(toUpdate);
941
- for (const c of chunks) {
942
- const { query, args } = this.upd.query(this._schema, this._table, c, cId, cName);
943
- yield execute(query, args, txClient);
944
- }
945
- }
946
- if (idsToDelete.length > 0) {
947
- const deleteArgs = [idsToDelete, ...filterArgs];
948
- const scopedWhere = conditions.length
949
- ? ` AND ${conditions.map(c => c.replace(/\$(\d+)/g, (_, n) => `$${parseInt(n) + 1}`)).join(' AND ')}`
950
- : '';
951
- const deleteQuery = `DELETE FROM ${quoteIfUppercase(this._schema)}.${quoteIfUppercase(this._table)} WHERE ${quoteIfUppercase(idField)} = ANY($1)${scopedWhere}`;
952
- yield execute(deleteQuery, deleteArgs, txClient);
953
- }
954
- yield txClient.query('COMMIT');
955
- }
956
- catch (err) {
957
- yield txClient.query('ROLLBACK');
958
- return next(err);
959
- }
960
- finally {
961
- if (!l.dbClient)
962
- txClient.release();
963
- }
964
- l.rows = rows;
965
- l.sync = { inserted: toInsert.length, updated: toUpdate.length, deleted: idsToDelete.length };
966
- next();
967
- });
968
659
  this._table = name;
969
660
  this._schema = schema;
970
661
  log.info(() => `${LOGS_PREFIX}Creating SQLEntity: "${name}"`);
@@ -1013,6 +704,267 @@ class SQLEntity extends Entity {
1013
704
  get syncArraySubstack() {
1014
705
  return [this.normalizeArray, this.validateArray, this.sync];
1015
706
  }
707
+ query = {
708
+ 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);
710
+ },
711
+ update: (rows, consumer) => {
712
+ return this.upd.query(this.schema, this.table, rows, consumer?.id, consumer?.nickname);
713
+ },
714
+ insert: (rows, consumer, rtn = "") => {
715
+ return this.ins.query(this.schema, this.table, rows, consumer?.id, consumer?.nickname, rtn);
716
+ },
717
+ upsert: (rows, conflictTarget, consumer, rtn = "") => {
718
+ return this.ups.query(this.schema, this.table, rows, conflictTarget, consumer?.id, consumer?.nickname, rtn);
719
+ },
720
+ delete: (ids) => {
721
+ return queryById(this.schema, this.table, ids);
722
+ },
723
+ archive: (rows, consumer) => {
724
+ return this.arc.query(this.schema, this.table, rows, consumer?.id, consumer?.nickname);
725
+ },
726
+ deleteArchive: () => {
727
+ return queryByDate();
728
+ },
729
+ return: (prop) => {
730
+ return this.ins.rtn(prop);
731
+ }
732
+ };
733
+ get = (req, res, next) => {
734
+ const l = res.locals;
735
+ const b = req.body;
736
+ const first = b?.first ?? 0;
737
+ const rows = b.rows || null;
738
+ const sortField = b.sortField || null;
739
+ const sortOrder = b.sortOrder === -1 || b.sortOrder === "DESC" ? "DESC" : "ASC";
740
+ const filters = cleanFilters(b.filters, this.properties) || null;
741
+ const pagination = b.pagination || false;
742
+ const dbClient = l.dbClient || null;
743
+ log.debug(() => `get(first='${first}', rows='${rows}', sortOrder='${sortOrder}', sortField='${sortField}', pagination=${pagination}, filters=${JSON.stringify(filters)}`);
744
+ const { query, args } = this.sel.query(this._schema, this._table, first, rows, sortField, sortOrder, filters);
745
+ this.sel.execute(query, args, dbClient)
746
+ .then((r) => {
747
+ l.rows = r.rows;
748
+ l.total = r.total;
749
+ next();
750
+ })
751
+ .catch((err) => next(err));
752
+ };
753
+ add = async (req, res, next) => {
754
+ const l = res.locals;
755
+ const rows = req.body.rows;
756
+ const dbClient = l.dbClient || null;
757
+ const cId = l.consumer?.id;
758
+ const cName = l.consumer?.nickname;
759
+ log.debug(() => `${LOGS_PREFIX}addMany(rows=${rows.length}, consumerId=${cId})`);
760
+ const rtn = this.ins.rtn("id");
761
+ const chunks = chunk(rows);
762
+ for (const c of chunks) {
763
+ const { query, args } = this.ins.query(this._schema, this._table, c, cId, cName, rtn);
764
+ let db;
765
+ try {
766
+ db = await execute(query, args, dbClient);
767
+ }
768
+ catch (err) {
769
+ return next(err);
770
+ }
771
+ const r = db.rows;
772
+ for (let i = 0; i < c.length; i++) {
773
+ c[i].id = r[i].id;
774
+ }
775
+ }
776
+ l.rows = flatten(chunks);
777
+ next();
778
+ };
779
+ update = async (req, res, next) => {
780
+ const l = res.locals;
781
+ const r = req.body.rows;
782
+ const dbClient = l.dbClient || null;
783
+ const cId = l.consumer?.id;
784
+ const cName = l.consumer?.nickname;
785
+ log.debug(() => `${LOGS_PREFIX}update(rows=${r.length}, consumerId=${cId})`);
786
+ const chunks = chunk(r);
787
+ for (const c of chunks) {
788
+ const { query, args } = this.upd.query(this._schema, this._table, c, cId, cName);
789
+ try {
790
+ await execute(query, args, dbClient);
791
+ }
792
+ catch (err) {
793
+ return next(err);
794
+ }
795
+ }
796
+ l.rows = r;
797
+ next();
798
+ };
799
+ upsert = async (req, res, next) => {
800
+ const l = res.locals;
801
+ const rows = req.body.rows;
802
+ const conflictTarget = req.body.conflictTarget;
803
+ const dbClient = l.dbClient || null;
804
+ const cId = l.consumer?.id;
805
+ const cName = l.consumer?.nickname;
806
+ if (!conflictTarget) {
807
+ return next({ status: 400, msg: "Missing conflictTarget for upsert operation" });
808
+ }
809
+ if (!rows || !Array.isArray(rows) || rows.length === 0) {
810
+ return next({ status: 400, msg: "Missing or empty rows array for upsert operation" });
811
+ }
812
+ log.debug(() => `${LOGS_PREFIX}upsert(rows=${rows.length}, conflictTarget=${conflictTarget}, consumerId=${cId})`);
813
+ const rtn = this.ups.rtn("id");
814
+ const chunks = chunk(rows);
815
+ for (const c of chunks) {
816
+ const { query, args } = this.ups.query(this._schema, this._table, c, conflictTarget, cId, cName, rtn);
817
+ let db;
818
+ try {
819
+ db = await execute(query, args, dbClient);
820
+ }
821
+ catch (err) {
822
+ return next(err);
823
+ }
824
+ const r = db.rows;
825
+ for (let i = 0; i < c.length; i++) {
826
+ c[i].id = r[i].id;
827
+ }
828
+ }
829
+ l.rows = flatten(chunks);
830
+ next();
831
+ };
832
+ archive = async (req, res, next) => {
833
+ const l = res.locals;
834
+ const r = req.body.rows;
835
+ const dbClient = l.dbClient || null;
836
+ const cId = l.consumer?.id;
837
+ const cName = l.consumer?.nickname;
838
+ log.debug(() => `${LOGS_PREFIX}archive(rows=${r.length}, consumerId=${cId})`);
839
+ const chunks = chunk(r);
840
+ for (const c of chunks) {
841
+ const { query, args } = this.arc.query(this._schema, this._table, c, cId, cName);
842
+ try {
843
+ await execute(query, args, dbClient);
844
+ }
845
+ catch (err) {
846
+ return next(err);
847
+ }
848
+ }
849
+ next();
850
+ };
851
+ delete = async (req, res, next) => {
852
+ const rows = req.body.rows;
853
+ const dbClient = res.locals.dbClient || null;
854
+ const ids = rows.map((row) => row.id);
855
+ log.debug(() => `${LOGS_PREFIX}delete ${rows.length} rows : (${ids.join(", ")})`);
856
+ const { query, args } = queryById(this._schema, this._table, ids);
857
+ try {
858
+ await execute(query, args, dbClient);
859
+ }
860
+ catch (err) {
861
+ return next(err);
862
+ }
863
+ next();
864
+ };
865
+ deleteArchive = (req, res, next) => {
866
+ const date = req.body.date;
867
+ const dbClient = res.locals.dbClient || null;
868
+ log.debug(() => `${LOGS_PREFIX}deleteArchive(schema=${this._schema}, table=${this._table}, date=${date})`);
869
+ executeArchived(this._schema, this._table, date, queryByDate(), dbClient)
870
+ .then(() => next())
871
+ .catch((err) => next(err));
872
+ };
873
+ getHistory = (req, res, next) => {
874
+ const id = req.params.id;
875
+ const dbClient = res.locals.dbClient || null;
876
+ if (!id) {
877
+ next({ status: 400, msg: "Missing id" });
878
+ return;
879
+ }
880
+ log.debug(() => `${LOGS_PREFIX}getHistory(schema=${this._schema}, table=${this._table}, id=${id})`);
881
+ const sql = `
882
+ SELECT id, tstamp, operation, "consumerId", "consumerName"
883
+ FROM log.history
884
+ WHERE "schemaName" = $1
885
+ AND "tableName" = $2
886
+ AND CAST(record->>'id' AS INT) = $3
887
+ ORDER BY tstamp ASC
888
+ `;
889
+ execute(sql, [this._schema, this._table, id], dbClient)
890
+ .then((r) => {
891
+ const { rowCount, rows } = r;
892
+ if (!rowCount) {
893
+ return next({ status: 404, msg: "History not found" });
894
+ }
895
+ res.locals.history = rows;
896
+ res.locals.total = rowCount;
897
+ next();
898
+ })
899
+ .catch((err) => next(err));
900
+ };
901
+ sync = async (req, res, next) => {
902
+ const l = res.locals;
903
+ const rows = req.body.rows;
904
+ const idField = req.body.idField ?? 'id';
905
+ const cId = l.consumer?.id;
906
+ const cName = l.consumer?.nickname;
907
+ if (!rows || !Array.isArray(rows)) {
908
+ return next({ status: 400, msg: "Missing or invalid rows array for sync operation" });
909
+ }
910
+ log.debug(() => `${LOGS_PREFIX}sync(rows=${rows.length}, idField=${idField}, consumerId=${cId})`);
911
+ const cleanedFilters = cleanFilters(req.body.filters, this.properties) || null;
912
+ const { conditions, args: filterArgs } = add(cleanedFilters);
913
+ const whereClause = conditions.length ? ` WHERE ${conditions.join(' AND ')}` : '';
914
+ const txClient = l.dbClient || await pool.connect();
915
+ let toInsert = [];
916
+ let toUpdate = [];
917
+ let idsToDelete = [];
918
+ try {
919
+ await txClient.query('BEGIN');
920
+ const selectIdQuery = `SELECT ${quoteIfUppercase(idField)} FROM ${quoteIfUppercase(this._schema)}.${quoteIfUppercase(this._table)}${whereClause}`;
921
+ const existingDb = await execute(selectIdQuery, filterArgs, txClient);
922
+ const existingIds = new Set(existingDb.rows.map(r => r[idField]));
923
+ const incomingIds = new Set(rows.filter(r => r[idField] != null).map(r => r[idField]));
924
+ toInsert = rows.filter(r => r[idField] == null || !existingIds.has(r[idField]));
925
+ toUpdate = rows.filter(r => r[idField] != null && existingIds.has(r[idField]));
926
+ idsToDelete = [...existingIds].filter(id => !incomingIds.has(id));
927
+ if (toInsert.length > 0) {
928
+ const rtn = this.ins.rtn(idField);
929
+ const chunks = chunk(toInsert);
930
+ for (const c of chunks) {
931
+ const { query, args } = this.ins.query(this._schema, this._table, c, cId, cName, rtn);
932
+ const db = await execute(query, args, txClient);
933
+ const r = db.rows;
934
+ for (let i = 0; i < c.length; i++) {
935
+ c[i][idField] = r[i][idField];
936
+ }
937
+ }
938
+ }
939
+ if (toUpdate.length > 0) {
940
+ const chunks = chunk(toUpdate);
941
+ for (const c of chunks) {
942
+ const { query, args } = this.upd.query(this._schema, this._table, c, cId, cName);
943
+ await execute(query, args, txClient);
944
+ }
945
+ }
946
+ if (idsToDelete.length > 0) {
947
+ const deleteArgs = [idsToDelete, ...filterArgs];
948
+ const scopedWhere = conditions.length
949
+ ? ` AND ${conditions.map(c => c.replace(/\$(\d+)/g, (_, n) => `$${parseInt(n) + 1}`)).join(' AND ')}`
950
+ : '';
951
+ const deleteQuery = `DELETE FROM ${quoteIfUppercase(this._schema)}.${quoteIfUppercase(this._table)} WHERE ${quoteIfUppercase(idField)} = ANY($1)${scopedWhere}`;
952
+ await execute(deleteQuery, deleteArgs, txClient);
953
+ }
954
+ await txClient.query('COMMIT');
955
+ }
956
+ catch (err) {
957
+ await txClient.query('ROLLBACK');
958
+ return next(err);
959
+ }
960
+ finally {
961
+ if (!l.dbClient)
962
+ txClient.release();
963
+ }
964
+ l.rows = rows;
965
+ l.sync = { inserted: toInsert.length, updated: toUpdate.length, deleted: idsToDelete.length };
966
+ next();
967
+ };
1016
968
  mapProps(operations, key) {
1017
969
  let hasInsert = false;
1018
970
  let hasUpdate = false;
package/package.json CHANGED
@@ -1,20 +1,19 @@
1
1
  {
2
2
  "name": "@dwtechs/antity-pgsql",
3
- "version": "0.17.1",
3
+ "version": "0.17.3",
4
4
  "description": "Open source library to add PostgreSQL support to @dwtechs/Antity entities.",
5
5
  "keywords": [
6
6
  "entities"
7
7
  ],
8
8
  "homepage": "https://github.com/DWTechs/Antity-pgsql.js",
9
- "main": "dist/antity-pgsql",
10
- "types": "dist/antity-pgsql",
9
+ "main": "dist/antity-pgsql.js",
10
+ "types": "dist/antity-pgsql.d.ts",
11
11
  "repository": {
12
12
  "type": "git",
13
13
  "url": "https://github.com/DWTechs/Antity-pgsql.js"
14
14
  },
15
15
  "bugs": {
16
- "url": "https://github.com/DWTechs/Antity-pgsql.js/issues",
17
- "email": ""
16
+ "url": "https://github.com/DWTechs/Antity-pgsql.js/issues"
18
17
  },
19
18
  "license": "MIT",
20
19
  "author": {
@@ -22,19 +21,18 @@
22
21
  "email": "http://www.lcluber.com/contact",
23
22
  "url": "http://www.lcluber.com"
24
23
  },
25
- "contributors": [],
26
24
  "scripts": {
27
- "start": "",
28
25
  "prebuild": "npm install",
29
26
  "build": "node ./scripts/clear && tsc && npm run rollup && node ./scripts/copy && npm run test",
30
- "rollup:mjs": "rollup --config rollup.config.mjs",
31
- "rollup:cjs": "rollup --config rollup.config.cjs.mjs",
32
- "rollup": "npm run rollup:mjs",
27
+ "rollup": "rollup --config rollup.config.mjs",
33
28
  "test": "jest --coverage"
34
29
  },
35
30
  "files": [
36
31
  "dist/"
37
32
  ],
33
+ "engines": {
34
+ "node": ">= 22"
35
+ },
38
36
  "dependencies": {
39
37
  "@dwtechs/checkard": "3.6.0",
40
38
  "@dwtechs/winstan": "0.7.0",
@@ -45,16 +43,14 @@
45
43
  "devDependencies": {
46
44
  "@babel/core": "7.26.0",
47
45
  "@babel/preset-env": "7.26.0",
48
- "@rollup/plugin-commonjs": "28.0.1",
49
- "@rollup/plugin-node-resolve": "15.3.0",
50
46
  "@types/jest": "29.5.14",
51
- "@types/node": "22.10.1",
52
- "@types/pg-pool": "2.0.6",
53
- "@types/express": "5.0.0",
47
+ "@types/node": "25.6.0",
48
+ "@types/pg-pool": "2.0.7",
49
+ "@types/express": "5.0.6",
54
50
  "babel-jest": "29.7.0",
55
51
  "core-js": "3.33.0",
56
52
  "jest": "29.7.0",
57
53
  "rollup": "4.24.0",
58
- "typescript": "5.6.3"
54
+ "typescript": "6.0.3"
59
55
  }
60
56
  }