@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 +41 -8
- package/dist/antity-pgsql.d.ts +4 -3
- package/dist/antity-pgsql.js +325 -373
- package/package.json +12 -16
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 `
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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`/`
|
|
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
|
-
|
|
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
|
| :---------- | :---- | :---------------------- | :-------------------------------------------------------- |
|
package/dist/antity-pgsql.d.ts
CHANGED
|
@@ -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;
|
package/dist/antity-pgsql.js
CHANGED
|
@@ -24,7 +24,7 @@ SOFTWARE.
|
|
|
24
24
|
https://github.com/DWTechs/Antity-pgsql.js
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
import { isArray, isString
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
356
|
-
rows = this.addConsumer(rows, consumerId, consumerName);
|
|
344
|
+
const hasConsumer = consumerId !== undefined && consumerName !== undefined;
|
|
357
345
|
const propsToUse = [...this._props];
|
|
358
|
-
if (
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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`, `
|
|
403
|
+
quotedPropsToUse.push(`"creatorId"`, `"creatorName"`);
|
|
417
404
|
nbProps += 2;
|
|
418
|
-
cols += `, creatorId,
|
|
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},
|
|
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
|
|
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
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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
|
|
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.
|
|
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
|
|
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": "
|
|
52
|
-
"@types/pg-pool": "2.0.
|
|
53
|
-
"@types/express": "5.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": "
|
|
54
|
+
"typescript": "6.0.3"
|
|
59
55
|
}
|
|
60
56
|
}
|