@dwtechs/antity-pgsql 0.12.0 → 0.14.0
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 +129 -1
- package/dist/antity-pgsql.d.ts +28 -1
- package/dist/antity-pgsql.js +162 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -190,6 +190,8 @@ class SQLEntity {
|
|
|
190
190
|
get addOneSubstack(): SubstackTuple;
|
|
191
191
|
get updateArraySubstack(): SubstackTuple;
|
|
192
192
|
get updateOneSubstack(): SubstackTuple;
|
|
193
|
+
get upsertArraySubstack(): SubstackTuple;
|
|
194
|
+
get upsertOneSubstack(): SubstackTuple;
|
|
193
195
|
|
|
194
196
|
query: {
|
|
195
197
|
select: (
|
|
@@ -208,6 +210,13 @@ class SQLEntity {
|
|
|
208
210
|
query: string;
|
|
209
211
|
args: unknown[];
|
|
210
212
|
};
|
|
213
|
+
archive: (
|
|
214
|
+
rows: Record<string, unknown>[],
|
|
215
|
+
consumerId?: number | string,
|
|
216
|
+
consumerName?: string) => {
|
|
217
|
+
query: string;
|
|
218
|
+
args: unknown[];
|
|
219
|
+
};
|
|
211
220
|
insert: (
|
|
212
221
|
rows: Record<string, unknown>[],
|
|
213
222
|
consumerId?: number | string,
|
|
@@ -216,6 +225,15 @@ class SQLEntity {
|
|
|
216
225
|
query: string;
|
|
217
226
|
args: unknown[];
|
|
218
227
|
};
|
|
228
|
+
upsert: (
|
|
229
|
+
rows: Record<string, unknown>[],
|
|
230
|
+
conflictTarget: string | string[],
|
|
231
|
+
consumerId?: number | string,
|
|
232
|
+
consumerName?: string,
|
|
233
|
+
rtn?: string) => {
|
|
234
|
+
query: string;
|
|
235
|
+
args: unknown[];
|
|
236
|
+
};
|
|
219
237
|
delete: (ids: number[]) => {
|
|
220
238
|
query: string;
|
|
221
239
|
args: number[];
|
|
@@ -226,6 +244,7 @@ class SQLEntity {
|
|
|
226
244
|
get: (req: Request, res: Response, next: NextFunction) => void;
|
|
227
245
|
add: (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
228
246
|
update: (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
247
|
+
upsert: (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
229
248
|
archive: (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
230
249
|
delete: (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
231
250
|
deleteArchive: (req: Request, res: Response, next: NextFunction) => void;
|
|
@@ -252,9 +271,11 @@ function execute(
|
|
|
252
271
|
|
|
253
272
|
### Middleware Methods for Express.js
|
|
254
273
|
|
|
255
|
-
get(), add(), update(), archive(), delete(), deleteArchive() and getHistory() methods are made to be used as Express.js middlewares.
|
|
274
|
+
get(), add(), update(), upsert(), archive(), delete(), deleteArchive() and getHistory() methods are made to be used as Express.js middlewares.
|
|
256
275
|
Each method will look for data to work on in the **req.body.rows** parameter.
|
|
257
276
|
|
|
277
|
+
The upsert() method additionally requires **req.body.conflictTarget** to specify which column(s) define uniqueness.
|
|
278
|
+
|
|
258
279
|
### Schema Qualification
|
|
259
280
|
|
|
260
281
|
All SQL queries generated by Antity-pgsql use schema-qualified table names (e.g., `schema.table`). This provides:
|
|
@@ -272,16 +293,123 @@ Substacks are pre-composed middleware chains that combine normalization, validat
|
|
|
272
293
|
- **addOneSubstack**: Combines `normalizeOne`, `validateOne`, and `add`. Use this for POST routes with `req.body` containing a single object.
|
|
273
294
|
- **updateArraySubstack**: Combines `normalizeArray`, `validateArray`, and `update`. Use this for PUT routes with `req.body.rows` containing multiple objects.
|
|
274
295
|
- **updateOneSubstack**: Combines `normalizeOne`, `validateOne`, and `update`. Use this for PUT routes with `req.body` containing a single object.
|
|
296
|
+
- **upsertArraySubstack**: Combines `normalizeArray`, `validateArray`, and `upsert`. Use this for upsert routes with `req.body.rows` containing multiple objects. Requires `req.body.conflictTarget`.
|
|
297
|
+
- **upsertOneSubstack**: Combines `normalizeOne`, `validateOne`, and `upsert`. Use this for upsert routes with `req.body` containing a single object. Requires `req.body.conflictTarget`.
|
|
275
298
|
|
|
276
299
|
Using substacks simplifies your route definitions and ensures consistent data processing.
|
|
277
300
|
|
|
278
301
|
### Query Methods
|
|
279
302
|
|
|
280
303
|
- **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.
|
|
304
|
+
- **query.insert()**: Generates an INSERT query. Accepts an array of objects with properties matching the entity definition. Optionally appends `consumerId` and `consumerName` for history tracking. Supports `RETURNING` clause via the `rtn` parameter.
|
|
305
|
+
- **query.update()**: Generates an UPDATE query using CASE statements. Accepts an array of objects with `id` property. Optionally appends `consumerId` and `consumerName` for history tracking.
|
|
306
|
+
- **query.upsert()**: Generates an INSERT ... ON CONFLICT ... DO UPDATE query. Accepts an array of 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. Optionally appends `consumerId` and `consumerName` for history tracking. Supports `RETURNING` clause via the `rtn` parameter.
|
|
307
|
+
- **query.archive()**: Generates a simplified `UPDATE ... SET archived = true WHERE id IN (...)` query. Accepts an array of objects with `id` property. Optionally appends `consumerId` and `consumerName` for history tracking. Does not require an `archived` field in the rows — it is set directly in the SQL.
|
|
281
308
|
- **delete()**: Deletes rows by their IDs. Expects `req.body.rows` to be an array of objects with `id` property: `[{id: 1}, {id: 2}]`
|
|
282
309
|
- **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.
|
|
283
310
|
- **getHistory()**: Retrieves modification history for rows from the `log.history` table. Expects `req.body.rows` to be an array of objects with `id` property. Returns all historical records for the specified entity IDs.
|
|
284
311
|
|
|
312
|
+
### Upsert (Insert or Update)
|
|
313
|
+
|
|
314
|
+
The upsert functionality uses PostgreSQL's `INSERT ... ON CONFLICT ... DO UPDATE` syntax to insert rows or update them if they already exist based on a unique constraint.
|
|
315
|
+
|
|
316
|
+
#### How It Works
|
|
317
|
+
|
|
318
|
+
1. **Conflict Target**: You specify which column(s) define uniqueness (e.g., `'id'`, `'email'`, or `['name', 'email']`)
|
|
319
|
+
2. **Property Selection**: Properties are automatically included if they have **both** `INSERT` and `UPDATE` in their `operations` array
|
|
320
|
+
3. **On Conflict**: When a conflict occurs, all columns except the conflict target are updated
|
|
321
|
+
|
|
322
|
+
#### Usage Examples
|
|
323
|
+
|
|
324
|
+
**Using the middleware with a single conflict target:**
|
|
325
|
+
|
|
326
|
+
```javascript
|
|
327
|
+
// Route definition
|
|
328
|
+
router.post('/users/upsert', ...entity.upsertArraySubstack);
|
|
329
|
+
|
|
330
|
+
// Request body
|
|
331
|
+
{
|
|
332
|
+
rows: [
|
|
333
|
+
{ id: 1, name: 'John Updated', email: 'john@example.com' },
|
|
334
|
+
{ name: 'Jane New', email: 'jane@example.com' }
|
|
335
|
+
],
|
|
336
|
+
conflictTarget: 'id'
|
|
337
|
+
}
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
**Using email as conflict target:**
|
|
341
|
+
|
|
342
|
+
```javascript
|
|
343
|
+
// If a user with this email exists, update their name; otherwise, insert
|
|
344
|
+
{
|
|
345
|
+
rows: [
|
|
346
|
+
{ name: 'John', email: 'john@example.com', age: 30 }
|
|
347
|
+
],
|
|
348
|
+
conflictTarget: 'email'
|
|
349
|
+
}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
**Using multiple columns as conflict target:**
|
|
353
|
+
|
|
354
|
+
```javascript
|
|
355
|
+
// Unique constraint on combination of name and email
|
|
356
|
+
{
|
|
357
|
+
rows: [
|
|
358
|
+
{ name: 'John', email: 'john@example.com', age: 30 }
|
|
359
|
+
],
|
|
360
|
+
conflictTarget: ['name', 'email']
|
|
361
|
+
}
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
**Using the query generator directly:**
|
|
365
|
+
|
|
366
|
+
```javascript
|
|
367
|
+
const { query, args } = entity.query.upsert(
|
|
368
|
+
[{ id: 1, name: 'John', email: 'john@example.com' }],
|
|
369
|
+
'id',
|
|
370
|
+
1, // consumerId (optional)
|
|
371
|
+
'admin', // consumerName (optional)
|
|
372
|
+
'RETURNING id' // return clause (optional)
|
|
373
|
+
);
|
|
374
|
+
// Generates:
|
|
375
|
+
// INSERT INTO public.users (name, email, "consumerId", "consumerName")
|
|
376
|
+
// VALUES ($1, $2, $3, $4)
|
|
377
|
+
// ON CONFLICT (id) DO UPDATE SET
|
|
378
|
+
// name = EXCLUDED.name,
|
|
379
|
+
// email = EXCLUDED.email,
|
|
380
|
+
// "consumerId" = EXCLUDED."consumerId",
|
|
381
|
+
// "consumerName" = EXCLUDED."consumerName"
|
|
382
|
+
// RETURNING id
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
#### Property Configuration for Upsert
|
|
386
|
+
|
|
387
|
+
Properties are automatically included in upsert if they have both INSERT and UPDATE operations:
|
|
388
|
+
|
|
389
|
+
```javascript
|
|
390
|
+
{
|
|
391
|
+
key: 'name',
|
|
392
|
+
operations: ['SELECT', 'INSERT', 'UPDATE'] // Included in upsert
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
{
|
|
396
|
+
key: 'id',
|
|
397
|
+
operations: ['SELECT', 'UPDATE'] // NOT included (no INSERT)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
{
|
|
401
|
+
key: 'createdAt',
|
|
402
|
+
operations: ['SELECT', 'INSERT'] // NOT included (no UPDATE)
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
#### Important Notes
|
|
407
|
+
|
|
408
|
+
- **Conflict Target Required**: The `conflictTarget` parameter must specify an existing unique constraint or primary key
|
|
409
|
+
- **Mixed Rows**: You can upsert rows with and without IDs in the same request if your conflict target handles it (e.g., using `SERIAL` primary key)
|
|
410
|
+
- **Atomic Operation**: Unlike separate insert/update calls, upsert is a single atomic database operation
|
|
411
|
+
- **Concurrent Safety**: Prevents race conditions when multiple requests try to create the same record
|
|
412
|
+
|
|
285
413
|
### Filters
|
|
286
414
|
|
|
287
415
|
Filters support two formats for maximum flexibility:
|
package/dist/antity-pgsql.d.ts
CHANGED
|
@@ -98,6 +98,8 @@ export declare class SQLEntity extends Entity {
|
|
|
98
98
|
private sel: unknown;
|
|
99
99
|
private ins: unknown;
|
|
100
100
|
private upd: unknown;
|
|
101
|
+
private ups: unknown;
|
|
102
|
+
private arc: unknown;
|
|
101
103
|
|
|
102
104
|
constructor(name: string, properties: Property[], schema?: string);
|
|
103
105
|
|
|
@@ -106,11 +108,15 @@ export declare class SQLEntity extends Entity {
|
|
|
106
108
|
|
|
107
109
|
get schema(): string;
|
|
108
110
|
set schema(schema: string);
|
|
109
|
-
|
|
111
|
+
|
|
112
|
+
get properties(): Property[];
|
|
113
|
+
|
|
110
114
|
get addArraySubstack(): SubstackTuple;
|
|
111
115
|
get addOneSubstack(): SubstackTuple;
|
|
112
116
|
get updateArraySubstack(): SubstackTuple;
|
|
113
117
|
get updateOneSubstack(): SubstackTuple;
|
|
118
|
+
get upsertArraySubstack(): SubstackTuple;
|
|
119
|
+
get upsertOneSubstack(): SubstackTuple;
|
|
114
120
|
|
|
115
121
|
query: {
|
|
116
122
|
select: (
|
|
@@ -133,6 +139,15 @@ export declare class SQLEntity extends Entity {
|
|
|
133
139
|
args: unknown[];
|
|
134
140
|
};
|
|
135
141
|
|
|
142
|
+
archive: (
|
|
143
|
+
rows: Record<string, unknown>[],
|
|
144
|
+
consumerId?: number | string,
|
|
145
|
+
consumerName?: string
|
|
146
|
+
) => {
|
|
147
|
+
query: string;
|
|
148
|
+
args: unknown[];
|
|
149
|
+
};
|
|
150
|
+
|
|
136
151
|
insert: (
|
|
137
152
|
rows: Record<string, unknown>[],
|
|
138
153
|
consumerId?: number | string,
|
|
@@ -143,6 +158,17 @@ export declare class SQLEntity extends Entity {
|
|
|
143
158
|
args: unknown[];
|
|
144
159
|
};
|
|
145
160
|
|
|
161
|
+
upsert: (
|
|
162
|
+
rows: Record<string, unknown>[],
|
|
163
|
+
conflictTarget: string | string[],
|
|
164
|
+
consumerId?: number | string,
|
|
165
|
+
consumerName?: string,
|
|
166
|
+
rtn?: string
|
|
167
|
+
) => {
|
|
168
|
+
query: string;
|
|
169
|
+
args: unknown[];
|
|
170
|
+
};
|
|
171
|
+
|
|
146
172
|
delete: (ids: number[]) => {
|
|
147
173
|
query: string;
|
|
148
174
|
args: number[];
|
|
@@ -156,6 +182,7 @@ export declare class SQLEntity extends Entity {
|
|
|
156
182
|
get(req: Request, res: Response, next: NextFunction): void;
|
|
157
183
|
add(req: Request, res: Response, next: NextFunction): Promise<void>;
|
|
158
184
|
update(req: Request, res: Response, next: NextFunction): Promise<void>;
|
|
185
|
+
upsert(req: Request, res: Response, next: NextFunction): Promise<void>;
|
|
159
186
|
archive(req: Request, res: Response, next: NextFunction): Promise<void>;
|
|
160
187
|
delete(req: Request, res: Response, next: NextFunction): Promise<void>;
|
|
161
188
|
deleteArchive(req: Request, res: Response, next: NextFunction): void;
|
package/dist/antity-pgsql.js
CHANGED
|
@@ -320,7 +320,7 @@ class Insert {
|
|
|
320
320
|
}
|
|
321
321
|
}
|
|
322
322
|
|
|
323
|
-
var __awaiter$
|
|
323
|
+
var __awaiter$3 = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
324
324
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
325
325
|
return new (P || (P = Promise))(function (resolve, reject) {
|
|
326
326
|
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
@@ -362,7 +362,7 @@ class Update {
|
|
|
362
362
|
return { query, args };
|
|
363
363
|
}
|
|
364
364
|
execute(query, args, client) {
|
|
365
|
-
return __awaiter$
|
|
365
|
+
return __awaiter$3(this, void 0, void 0, function* () {
|
|
366
366
|
return execute(query, args, client);
|
|
367
367
|
});
|
|
368
368
|
}
|
|
@@ -372,6 +372,104 @@ class Update {
|
|
|
372
372
|
}
|
|
373
373
|
}
|
|
374
374
|
|
|
375
|
+
class Upsert {
|
|
376
|
+
constructor() {
|
|
377
|
+
this._props = [];
|
|
378
|
+
this._quotedProps = [];
|
|
379
|
+
this._nbProps = 0;
|
|
380
|
+
this._cols = "";
|
|
381
|
+
}
|
|
382
|
+
addProp(prop) {
|
|
383
|
+
this._props.push(prop);
|
|
384
|
+
this._quotedProps.push(quoteIfUppercase(prop));
|
|
385
|
+
this._nbProps++;
|
|
386
|
+
this._cols = this._quotedProps.join(", ");
|
|
387
|
+
}
|
|
388
|
+
query(schema, table, rows, conflictTarget, consumerId, consumerName, rtn = "") {
|
|
389
|
+
if (!conflictTarget ||
|
|
390
|
+
(Array.isArray(conflictTarget) && conflictTarget.length === 0) ||
|
|
391
|
+
(typeof conflictTarget === 'string' && conflictTarget.trim() === '')) {
|
|
392
|
+
throw new Error('conflictTarget must be provided for upsert operation');
|
|
393
|
+
}
|
|
394
|
+
const propsToUse = [...this._props];
|
|
395
|
+
const quotedPropsToUse = [...this._quotedProps];
|
|
396
|
+
let nbProps = this._nbProps;
|
|
397
|
+
let cols = this._cols;
|
|
398
|
+
if (consumerId !== undefined && consumerName !== undefined) {
|
|
399
|
+
propsToUse.push("consumerId", "consumerName");
|
|
400
|
+
quotedPropsToUse.push(`"consumerId"`, `"consumerName"`);
|
|
401
|
+
nbProps += 2;
|
|
402
|
+
cols += `, "consumerId", "consumerName"`;
|
|
403
|
+
}
|
|
404
|
+
const conflictColumns = Array.isArray(conflictTarget)
|
|
405
|
+
? conflictTarget.map(col => quoteIfUppercase(col)).join(", ")
|
|
406
|
+
: quoteIfUppercase(conflictTarget);
|
|
407
|
+
let query = `INSERT INTO ${quoteIfUppercase(schema)}.${quoteIfUppercase(table)} (${cols}) VALUES `;
|
|
408
|
+
const args = [];
|
|
409
|
+
let i = 0;
|
|
410
|
+
for (const row of rows) {
|
|
411
|
+
if (consumerId !== undefined && consumerName !== undefined) {
|
|
412
|
+
row.consumerId = consumerId;
|
|
413
|
+
row.consumerName = consumerName;
|
|
414
|
+
}
|
|
415
|
+
query += `${$i(nbProps, i)}, `;
|
|
416
|
+
for (const prop of propsToUse) {
|
|
417
|
+
args.push(row[prop]);
|
|
418
|
+
}
|
|
419
|
+
i += nbProps;
|
|
420
|
+
}
|
|
421
|
+
query = query.slice(0, -2);
|
|
422
|
+
query += ` ON CONFLICT (${conflictColumns}) DO UPDATE SET `;
|
|
423
|
+
const conflictTargetArray = Array.isArray(conflictTarget) ? conflictTarget : [conflictTarget];
|
|
424
|
+
const updateCols = quotedPropsToUse.filter((_, idx) => {
|
|
425
|
+
const propName = propsToUse[idx];
|
|
426
|
+
return !conflictTargetArray.includes(propName);
|
|
427
|
+
});
|
|
428
|
+
for (const col of updateCols) {
|
|
429
|
+
query += `${col} = EXCLUDED.${col}, `;
|
|
430
|
+
}
|
|
431
|
+
query = query.slice(0, -2);
|
|
432
|
+
if (rtn)
|
|
433
|
+
query += ` ${rtn}`;
|
|
434
|
+
return { query, args };
|
|
435
|
+
}
|
|
436
|
+
rtn(prop) {
|
|
437
|
+
return `RETURNING ${quoteIfUppercase(prop)}`;
|
|
438
|
+
}
|
|
439
|
+
execute(query, args, client) {
|
|
440
|
+
return execute(query, args, client);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
var __awaiter$2 = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
445
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
446
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
447
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
448
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
449
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
450
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
451
|
+
});
|
|
452
|
+
};
|
|
453
|
+
class Archive {
|
|
454
|
+
query(schema, table, rows, consumerId, consumerName) {
|
|
455
|
+
log.debug(`${LOGS_PREFIX}Archive query input rows: ${JSON.stringify(rows, null, 2)}`);
|
|
456
|
+
const l = rows.length;
|
|
457
|
+
const args = rows.map(row => row.id);
|
|
458
|
+
let query = `UPDATE ${quoteIfUppercase(schema)}.${quoteIfUppercase(table)} SET archived = true`;
|
|
459
|
+
if (consumerId !== undefined && consumerName !== undefined) {
|
|
460
|
+
query += `, ${quoteIfUppercase("consumerId")} = $${l + 1}, ${quoteIfUppercase("consumerName")} = $${l + 2}`;
|
|
461
|
+
args.push(consumerId, consumerName);
|
|
462
|
+
}
|
|
463
|
+
query += ` WHERE id IN ${$i(l, 0)}`;
|
|
464
|
+
return { query, args };
|
|
465
|
+
}
|
|
466
|
+
execute(query, args, client) {
|
|
467
|
+
return __awaiter$2(this, void 0, void 0, function* () {
|
|
468
|
+
return execute(query, args, client);
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
375
473
|
var __awaiter$1 = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
376
474
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
377
475
|
return new (P || (P = Promise))(function (resolve, reject) {
|
|
@@ -584,6 +682,8 @@ class SQLEntity extends Entity {
|
|
|
584
682
|
this.sel = new Select();
|
|
585
683
|
this.ins = new Insert();
|
|
586
684
|
this.upd = new Update();
|
|
685
|
+
this.ups = new Upsert();
|
|
686
|
+
this.arc = new Archive();
|
|
587
687
|
this.query = {
|
|
588
688
|
select: (first = 0, rows = null, sortField = null, sortOrder = null, filters = null) => {
|
|
589
689
|
return this.sel.query(this.schema, this.table, first, rows, sortField, sortOrder, filters);
|
|
@@ -594,9 +694,15 @@ class SQLEntity extends Entity {
|
|
|
594
694
|
insert: (rows, consumerId, consumerName, rtn = "") => {
|
|
595
695
|
return this.ins.query(this.schema, this.table, rows, consumerId, consumerName, rtn);
|
|
596
696
|
},
|
|
697
|
+
upsert: (rows, conflictTarget, consumerId, consumerName, rtn = "") => {
|
|
698
|
+
return this.ups.query(this.schema, this.table, rows, conflictTarget, consumerId, consumerName, rtn);
|
|
699
|
+
},
|
|
597
700
|
delete: (ids) => {
|
|
598
701
|
return queryById(this.schema, this.table, ids);
|
|
599
702
|
},
|
|
703
|
+
archive: (rows, consumerId, consumerName) => {
|
|
704
|
+
return this.arc.query(this.schema, this.table, rows, consumerId, consumerName);
|
|
705
|
+
},
|
|
600
706
|
deleteArchive: () => {
|
|
601
707
|
return queryByDate();
|
|
602
708
|
},
|
|
@@ -612,7 +718,7 @@ class SQLEntity extends Entity {
|
|
|
612
718
|
const rows = b.rows || null;
|
|
613
719
|
const sortField = b.sortField || null;
|
|
614
720
|
const sortOrder = b.sortOrder === -1 || b.sortOrder === "DESC" ? "DESC" : "ASC";
|
|
615
|
-
const filters = cleanFilters(b.filters, this.properties
|
|
721
|
+
const filters = cleanFilters(b.filters, this.properties) || null;
|
|
616
722
|
const pagination = b.pagination || false;
|
|
617
723
|
const dbClient = l.dbClient || null;
|
|
618
724
|
log.debug(`get(first='${first}', rows='${rows}',
|
|
@@ -673,17 +779,49 @@ class SQLEntity extends Entity {
|
|
|
673
779
|
l.rows = r;
|
|
674
780
|
next();
|
|
675
781
|
});
|
|
676
|
-
this.
|
|
782
|
+
this.upsert = (req, res, next) => __awaiter(this, void 0, void 0, function* () {
|
|
677
783
|
const l = res.locals;
|
|
678
|
-
|
|
784
|
+
const rows = req.body.rows;
|
|
785
|
+
const conflictTarget = req.body.conflictTarget;
|
|
679
786
|
const dbClient = l.dbClient || null;
|
|
680
787
|
const cId = l.consumerId;
|
|
681
788
|
const cName = l.consumerName;
|
|
682
|
-
|
|
683
|
-
|
|
789
|
+
if (!conflictTarget) {
|
|
790
|
+
return next({ status: 400, msg: "Missing conflictTarget for upsert operation" });
|
|
791
|
+
}
|
|
792
|
+
if (!rows || !Array.isArray(rows) || rows.length === 0) {
|
|
793
|
+
return next({ status: 400, msg: "Missing or empty rows array for upsert operation" });
|
|
794
|
+
}
|
|
795
|
+
log.debug(`${LOGS_PREFIX}upsert(rows=${rows.length}, conflictTarget=${conflictTarget}, consumerId=${cId})`);
|
|
796
|
+
const rtn = this.ups.rtn("id");
|
|
684
797
|
const chunks = chunk(rows);
|
|
685
798
|
for (const c of chunks) {
|
|
686
|
-
const { query, args } = this.
|
|
799
|
+
const { query, args } = this.ups.query(this._schema, this._table, c, conflictTarget, cId, cName, rtn);
|
|
800
|
+
let db;
|
|
801
|
+
try {
|
|
802
|
+
db = yield execute(query, args, dbClient);
|
|
803
|
+
}
|
|
804
|
+
catch (err) {
|
|
805
|
+
return next(err);
|
|
806
|
+
}
|
|
807
|
+
const r = db.rows;
|
|
808
|
+
for (let i = 0; i < c.length; i++) {
|
|
809
|
+
c[i].id = r[i].id;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
l.rows = flatten(chunks);
|
|
813
|
+
next();
|
|
814
|
+
});
|
|
815
|
+
this.archive = (req, res, next) => __awaiter(this, void 0, void 0, function* () {
|
|
816
|
+
const l = res.locals;
|
|
817
|
+
let r = req.body.rows;
|
|
818
|
+
const dbClient = l.dbClient || null;
|
|
819
|
+
const cId = l.consumerId;
|
|
820
|
+
const cName = l.consumerName;
|
|
821
|
+
log.debug(`${LOGS_PREFIX}archive(rows=${r.length}, consumerId=${cId})`);
|
|
822
|
+
const chunks = chunk(r);
|
|
823
|
+
for (const c of chunks) {
|
|
824
|
+
const { query, args } = this.arc.query(this._schema, this._table, c, cId, cName);
|
|
687
825
|
try {
|
|
688
826
|
yield execute(query, args, dbClient);
|
|
689
827
|
}
|
|
@@ -766,6 +904,9 @@ class SQLEntity extends Entity {
|
|
|
766
904
|
throw new Error(`${LOGS_PREFIX}schema must be a string of length > 0`);
|
|
767
905
|
this._schema = schema;
|
|
768
906
|
}
|
|
907
|
+
get properties() {
|
|
908
|
+
return super.properties;
|
|
909
|
+
}
|
|
769
910
|
get addArraySubstack() {
|
|
770
911
|
return [this.normalizeArray, this.validateArray, this.add];
|
|
771
912
|
}
|
|
@@ -778,7 +919,15 @@ class SQLEntity extends Entity {
|
|
|
778
919
|
get updateOneSubstack() {
|
|
779
920
|
return [this.normalizeOne, this.validateOne, this.update];
|
|
780
921
|
}
|
|
922
|
+
get upsertArraySubstack() {
|
|
923
|
+
return [this.normalizeArray, this.validateArray, this.upsert];
|
|
924
|
+
}
|
|
925
|
+
get upsertOneSubstack() {
|
|
926
|
+
return [this.normalizeOne, this.validateOne, this.upsert];
|
|
927
|
+
}
|
|
781
928
|
mapProps(operations, key) {
|
|
929
|
+
let hasInsert = false;
|
|
930
|
+
let hasUpdate = false;
|
|
782
931
|
for (const o of operations) {
|
|
783
932
|
switch (o) {
|
|
784
933
|
case "SELECT":
|
|
@@ -786,12 +935,17 @@ class SQLEntity extends Entity {
|
|
|
786
935
|
break;
|
|
787
936
|
case "UPDATE":
|
|
788
937
|
this.upd.addProp(key);
|
|
938
|
+
hasUpdate = true;
|
|
789
939
|
break;
|
|
790
940
|
case "INSERT":
|
|
791
941
|
this.ins.addProp(key);
|
|
942
|
+
hasInsert = true;
|
|
792
943
|
break;
|
|
793
944
|
}
|
|
794
945
|
}
|
|
946
|
+
if (hasInsert && hasUpdate) {
|
|
947
|
+
this.ups.addProp(key);
|
|
948
|
+
}
|
|
795
949
|
}
|
|
796
950
|
}
|
|
797
951
|
|