@twin.org/entity-storage-connector-postgresql 0.0.3-next.9 → 0.9.0-next.1
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/dist/es/models/IPostgreSqlEntityStorageConnectorConstructorOptions.js.map +1 -1
- package/dist/es/postgreSqlEntityStorageConnector.js +338 -57
- package/dist/es/postgreSqlEntityStorageConnector.js.map +1 -1
- package/dist/types/models/IPostgreSqlEntityStorageConnectorConstructorOptions.d.ts +0 -1
- package/dist/types/postgreSqlEntityStorageConnector.d.ts +62 -12
- package/docs/changelog.md +603 -57
- package/docs/reference/classes/PostgreSqlEntityStorageConnector.md +276 -14
- package/docs/reference/interfaces/IPostgreSqlEntityStorageConnectorConstructorOptions.md +0 -6
- package/locales/en.json +17 -2
- package/package.json +10 -10
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"IPostgreSqlEntityStorageConnectorConstructorOptions.js","sourceRoot":"","sources":["../../../src/models/IPostgreSqlEntityStorageConnectorConstructorOptions.ts"],"names":[],"mappings":"","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport type { IPostgreSqlEntityStorageConnectorConfig } from \"./IPostgreSqlEntityStorageConnectorConfig.js\";\n\n/**\n * The options for the PostgreSql entity storage connector constructor.\n */\nexport interface IPostgreSqlEntityStorageConnectorConstructorOptions {\n\t/**\n\t * The schema for the entity.\n\t */\n\tentitySchema: string;\n\n\t/**\n\t * The keys to use from the context ids to create partitions.\n\t */\n\tpartitionContextIds?: string[];\n\n\t/**\n\t * The type of logging component to use.\n\t
|
|
1
|
+
{"version":3,"file":"IPostgreSqlEntityStorageConnectorConstructorOptions.js","sourceRoot":"","sources":["../../../src/models/IPostgreSqlEntityStorageConnectorConstructorOptions.ts"],"names":[],"mappings":"","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport type { IPostgreSqlEntityStorageConnectorConfig } from \"./IPostgreSqlEntityStorageConnectorConfig.js\";\n\n/**\n * The options for the PostgreSql entity storage connector constructor.\n */\nexport interface IPostgreSqlEntityStorageConnectorConstructorOptions {\n\t/**\n\t * The schema for the entity.\n\t */\n\tentitySchema: string;\n\n\t/**\n\t * The keys to use from the context ids to create partitions.\n\t */\n\tpartitionContextIds?: string[];\n\n\t/**\n\t * The type of logging component to use.\n\t */\n\tloggingComponentType?: string;\n\n\t/**\n\t * The configuration for the connector.\n\t */\n\tconfig: IPostgreSqlEntityStorageConnectorConfig;\n}\n"]}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// Copyright 2024 IOTA Stiftung.
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0.
|
|
3
3
|
import { ContextIdHelper, ContextIdStore } from "@twin.org/context";
|
|
4
|
-
import { BaseError, Coerce, ComponentFactory, GeneralError, Guards, Is, ObjectHelper } from "@twin.org/core";
|
|
4
|
+
import { BaseError, Coerce, ComponentFactory, GeneralError, Guards, HealthStatus, Is, ObjectHelper, Validation } from "@twin.org/core";
|
|
5
5
|
import { ComparisonOperator, EntitySchemaFactory, EntitySchemaHelper, EntitySchemaPropertyType, LogicalOperator, SortDirection } from "@twin.org/entity";
|
|
6
|
+
import { EntityStorageHelper } from "@twin.org/entity-storage-models";
|
|
6
7
|
import postgres from "postgres";
|
|
7
8
|
/**
|
|
8
9
|
* Class for performing entity storage operations using ql.
|
|
@@ -27,6 +28,11 @@ export class PostgreSqlEntityStorageConnector {
|
|
|
27
28
|
* @internal
|
|
28
29
|
*/
|
|
29
30
|
static _PARTITION_KEY_VALUE = "root";
|
|
31
|
+
/**
|
|
32
|
+
* The name for the schema.
|
|
33
|
+
* @internal
|
|
34
|
+
*/
|
|
35
|
+
_entitySchemaName;
|
|
30
36
|
/**
|
|
31
37
|
* The schema for the entity.
|
|
32
38
|
* @internal
|
|
@@ -65,6 +71,7 @@ export class PostgreSqlEntityStorageConnector {
|
|
|
65
71
|
Guards.stringValue(PostgreSqlEntityStorageConnector.CLASS_NAME, "options.config.password", options.config.password);
|
|
66
72
|
Guards.stringValue(PostgreSqlEntityStorageConnector.CLASS_NAME, "options.config.database", options.config.database);
|
|
67
73
|
Guards.stringValue(PostgreSqlEntityStorageConnector.CLASS_NAME, "options.config.tableName", options.config.tableName);
|
|
74
|
+
this._entitySchemaName = options.entitySchema;
|
|
68
75
|
this._entitySchema = EntitySchemaFactory.get(options.entitySchema);
|
|
69
76
|
this._partitionContextIds = options.partitionContextIds;
|
|
70
77
|
this._primaryKeyProperty = EntitySchemaHelper.getPrimaryKey(this._entitySchema);
|
|
@@ -153,6 +160,35 @@ export class PostgreSqlEntityStorageConnector {
|
|
|
153
160
|
className() {
|
|
154
161
|
return PostgreSqlEntityStorageConnector.CLASS_NAME;
|
|
155
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* Get the health of the component.
|
|
165
|
+
* @returns The health of the component.
|
|
166
|
+
*/
|
|
167
|
+
async health() {
|
|
168
|
+
try {
|
|
169
|
+
const sql = await this.createConnection();
|
|
170
|
+
await sql `SELECT 1 FROM ${sql(this._config.tableName)} LIMIT 0`;
|
|
171
|
+
return [
|
|
172
|
+
{
|
|
173
|
+
source: PostgreSqlEntityStorageConnector.CLASS_NAME,
|
|
174
|
+
status: HealthStatus.Ok,
|
|
175
|
+
description: "healthDescription",
|
|
176
|
+
data: { tableName: this._config.tableName }
|
|
177
|
+
}
|
|
178
|
+
];
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return [
|
|
182
|
+
{
|
|
183
|
+
source: PostgreSqlEntityStorageConnector.CLASS_NAME,
|
|
184
|
+
status: HealthStatus.Error,
|
|
185
|
+
description: "healthDescription",
|
|
186
|
+
message: "connectionFailed",
|
|
187
|
+
data: { tableName: this._config.tableName }
|
|
188
|
+
}
|
|
189
|
+
];
|
|
190
|
+
}
|
|
191
|
+
}
|
|
156
192
|
/**
|
|
157
193
|
* The component needs to be stopped when the node is closed.
|
|
158
194
|
* @returns Nothing.
|
|
@@ -229,9 +265,9 @@ export class PostgreSqlEntityStorageConnector {
|
|
|
229
265
|
}
|
|
230
266
|
}
|
|
231
267
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
268
|
+
return EntityStorageHelper.unPrepareEntity(rows[0], [
|
|
269
|
+
PostgreSqlEntityStorageConnector._PARTITION_KEY
|
|
270
|
+
]);
|
|
235
271
|
}
|
|
236
272
|
}
|
|
237
273
|
catch (err) {
|
|
@@ -251,8 +287,13 @@ export class PostgreSqlEntityStorageConnector {
|
|
|
251
287
|
Guards.object(PostgreSqlEntityStorageConnector.CLASS_NAME, "entity", entity);
|
|
252
288
|
const contextIds = await ContextIdStore.getContextIds();
|
|
253
289
|
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
254
|
-
|
|
255
|
-
|
|
290
|
+
const prepared = EntityStorageHelper.prepareEntity(entity, this._entitySchema, [
|
|
291
|
+
{
|
|
292
|
+
property: PostgreSqlEntityStorageConnector._PARTITION_KEY,
|
|
293
|
+
value: partitionKey ?? PostgreSqlEntityStorageConnector._PARTITION_KEY_VALUE
|
|
294
|
+
}
|
|
295
|
+
], { nullBehavior: "nullify" });
|
|
296
|
+
const id = prepared[this._primaryKeyProperty.property];
|
|
256
297
|
try {
|
|
257
298
|
if (Is.arrayValue(conditions)) {
|
|
258
299
|
const itemData = await this.get(id);
|
|
@@ -260,30 +301,21 @@ export class PostgreSqlEntityStorageConnector {
|
|
|
260
301
|
return;
|
|
261
302
|
}
|
|
262
303
|
}
|
|
263
|
-
const finalEntity = ObjectHelper.clone(entity);
|
|
264
304
|
const props = [...(this._entitySchema.properties ?? [])];
|
|
265
305
|
props.unshift({
|
|
266
306
|
property: PostgreSqlEntityStorageConnector._PARTITION_KEY,
|
|
267
307
|
type: EntitySchemaPropertyType.String
|
|
268
308
|
});
|
|
269
|
-
ObjectHelper.propertySet(finalEntity, PostgreSqlEntityStorageConnector._PARTITION_KEY, partitionKey ?? PostgreSqlEntityStorageConnector._PARTITION_KEY_VALUE);
|
|
270
|
-
ObjectHelper.propertySet(finalEntity, PostgreSqlEntityStorageConnector._PARTITION_KEY, partitionKey ?? PostgreSqlEntityStorageConnector._PARTITION_KEY_VALUE);
|
|
271
309
|
const keys = [];
|
|
272
310
|
const values = [];
|
|
273
311
|
for (const prop of props) {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
values.push(null);
|
|
278
|
-
}
|
|
279
|
-
else {
|
|
280
|
-
values.push(finalEntity[prop.property]);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
312
|
+
keys.push(prop.property);
|
|
313
|
+
const val = prepared[prop.property];
|
|
314
|
+
values.push(val ?? null);
|
|
283
315
|
}
|
|
284
316
|
let sql = `INSERT INTO "${this._config.tableName}"`;
|
|
285
317
|
sql += ` (${keys.map(key => `"${key}"`).join(", ")})`;
|
|
286
|
-
sql += ` VALUES (${values.map((
|
|
318
|
+
sql += ` VALUES (${values.map((value, i) => `$${i + 1}`).join(", ")})`;
|
|
287
319
|
sql += ` ON CONFLICT ("${PostgreSqlEntityStorageConnector._PARTITION_KEY}", "${this._primaryKeyProperty.property}")`;
|
|
288
320
|
sql += ` DO UPDATE SET ${keys.map(key => `"${key}" = EXCLUDED."${key}"`).join(", ")};`;
|
|
289
321
|
const dbConnection = await this.createConnection();
|
|
@@ -295,6 +327,69 @@ export class PostgreSqlEntityStorageConnector {
|
|
|
295
327
|
}, err);
|
|
296
328
|
}
|
|
297
329
|
}
|
|
330
|
+
/**
|
|
331
|
+
* Set multiple entities in a batch.
|
|
332
|
+
* @param entities The entities to set.
|
|
333
|
+
* @returns Nothing.
|
|
334
|
+
*/
|
|
335
|
+
async setBatch(entities) {
|
|
336
|
+
Guards.arrayValue(PostgreSqlEntityStorageConnector.CLASS_NAME, "entities", entities);
|
|
337
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
338
|
+
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
339
|
+
const preparedEntities = entities.map(entity => EntityStorageHelper.prepareEntity(entity, this._entitySchema, [
|
|
340
|
+
{
|
|
341
|
+
property: PostgreSqlEntityStorageConnector._PARTITION_KEY,
|
|
342
|
+
value: partitionKey ?? PostgreSqlEntityStorageConnector._PARTITION_KEY_VALUE
|
|
343
|
+
}
|
|
344
|
+
], { nullBehavior: "nullify" }));
|
|
345
|
+
try {
|
|
346
|
+
const props = [...(this._entitySchema.properties ?? [])];
|
|
347
|
+
props.unshift({
|
|
348
|
+
property: PostgreSqlEntityStorageConnector._PARTITION_KEY,
|
|
349
|
+
type: EntitySchemaPropertyType.String
|
|
350
|
+
});
|
|
351
|
+
const keys = props.map(p => p.property);
|
|
352
|
+
const allValues = [];
|
|
353
|
+
const rowPlaceholders = [];
|
|
354
|
+
for (const prepared of preparedEntities) {
|
|
355
|
+
const rowValues = [];
|
|
356
|
+
for (const prop of props) {
|
|
357
|
+
const val = prepared[prop.property];
|
|
358
|
+
allValues.push(Is.empty(val) ? null : val);
|
|
359
|
+
rowValues.push(`$${allValues.length}`);
|
|
360
|
+
}
|
|
361
|
+
rowPlaceholders.push(`(${rowValues.join(", ")})`);
|
|
362
|
+
}
|
|
363
|
+
let sql = `INSERT INTO "${this._config.tableName}"`;
|
|
364
|
+
sql += ` (${keys.map(key => `"${key}"`).join(", ")})`;
|
|
365
|
+
sql += ` VALUES ${rowPlaceholders.join(", ")}`;
|
|
366
|
+
sql += ` ON CONFLICT ("${PostgreSqlEntityStorageConnector._PARTITION_KEY}", "${this._primaryKeyProperty.property}")`;
|
|
367
|
+
sql += ` DO UPDATE SET ${keys.map(key => `"${key}" = EXCLUDED."${key}"`).join(", ")};`;
|
|
368
|
+
const dbConnection = await this.createConnection();
|
|
369
|
+
await dbConnection.unsafe(sql, allValues);
|
|
370
|
+
}
|
|
371
|
+
catch (err) {
|
|
372
|
+
throw new GeneralError(PostgreSqlEntityStorageConnector.CLASS_NAME, "setBatchFailed", undefined, err);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Empty all the entities.
|
|
377
|
+
* @returns Nothing.
|
|
378
|
+
*/
|
|
379
|
+
async empty() {
|
|
380
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
381
|
+
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
382
|
+
try {
|
|
383
|
+
const sql = `DELETE FROM "${this._config.tableName}" WHERE "${PostgreSqlEntityStorageConnector._PARTITION_KEY}" = $1`;
|
|
384
|
+
const dbConnection = await this.createConnection();
|
|
385
|
+
await dbConnection.unsafe(sql, [
|
|
386
|
+
partitionKey ?? PostgreSqlEntityStorageConnector._PARTITION_KEY_VALUE
|
|
387
|
+
]);
|
|
388
|
+
}
|
|
389
|
+
catch (err) {
|
|
390
|
+
throw new GeneralError(PostgreSqlEntityStorageConnector.CLASS_NAME, "emptyFailed", undefined, err);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
298
393
|
/**
|
|
299
394
|
* Remove the entity.
|
|
300
395
|
* @param id The id of the entity to remove.
|
|
@@ -331,6 +426,136 @@ export class PostgreSqlEntityStorageConnector {
|
|
|
331
426
|
}, err);
|
|
332
427
|
}
|
|
333
428
|
}
|
|
429
|
+
/**
|
|
430
|
+
* Remove multiple entities by their primary key IDs.
|
|
431
|
+
* @param ids The ids of the entities to remove.
|
|
432
|
+
* @returns Nothing.
|
|
433
|
+
*/
|
|
434
|
+
async removeBatch(ids) {
|
|
435
|
+
Guards.arrayValue(PostgreSqlEntityStorageConnector.CLASS_NAME, "ids", ids);
|
|
436
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
437
|
+
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
438
|
+
try {
|
|
439
|
+
const sql = `DELETE FROM "${this._config.tableName}" WHERE "${PostgreSqlEntityStorageConnector._PARTITION_KEY}" = $1 AND "${this._primaryKeyProperty.property}" = ANY($2)`;
|
|
440
|
+
const dbConnection = await this.createConnection();
|
|
441
|
+
await dbConnection.unsafe(sql, [
|
|
442
|
+
partitionKey ?? PostgreSqlEntityStorageConnector._PARTITION_KEY_VALUE,
|
|
443
|
+
ids
|
|
444
|
+
]);
|
|
445
|
+
}
|
|
446
|
+
catch (err) {
|
|
447
|
+
throw new GeneralError(PostgreSqlEntityStorageConnector.CLASS_NAME, "removeBatchFailed", undefined, err);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Teardown the entity storage by dropping the table.
|
|
452
|
+
* @param nodeLoggingComponentType The node logging component type.
|
|
453
|
+
* @returns True if the teardown process was successful.
|
|
454
|
+
*/
|
|
455
|
+
async teardown(nodeLoggingComponentType) {
|
|
456
|
+
const nodeLogging = ComponentFactory.getIfExists(nodeLoggingComponentType);
|
|
457
|
+
await nodeLogging?.log({
|
|
458
|
+
level: "info",
|
|
459
|
+
source: PostgreSqlEntityStorageConnector.CLASS_NAME,
|
|
460
|
+
ts: Date.now(),
|
|
461
|
+
message: "tableDropping",
|
|
462
|
+
data: { tableName: this._config.tableName }
|
|
463
|
+
});
|
|
464
|
+
try {
|
|
465
|
+
const tableExists = await this.tableExists();
|
|
466
|
+
if (tableExists) {
|
|
467
|
+
const dbConnection = await this.createConnection();
|
|
468
|
+
await dbConnection.unsafe(`DROP TABLE "${this._config.tableName}";`);
|
|
469
|
+
await this.waitForTableNotExists();
|
|
470
|
+
}
|
|
471
|
+
await nodeLogging?.log({
|
|
472
|
+
level: "info",
|
|
473
|
+
source: PostgreSqlEntityStorageConnector.CLASS_NAME,
|
|
474
|
+
ts: Date.now(),
|
|
475
|
+
message: "tableDropped",
|
|
476
|
+
data: { tableName: this._config.tableName }
|
|
477
|
+
});
|
|
478
|
+
return true;
|
|
479
|
+
}
|
|
480
|
+
catch (err) {
|
|
481
|
+
await nodeLogging?.log({
|
|
482
|
+
level: "error",
|
|
483
|
+
source: PostgreSqlEntityStorageConnector.CLASS_NAME,
|
|
484
|
+
ts: Date.now(),
|
|
485
|
+
message: "teardownFailed",
|
|
486
|
+
error: BaseError.fromError(err)
|
|
487
|
+
});
|
|
488
|
+
return false;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Get all the distinct partition context ids from the storage.
|
|
493
|
+
* @returns An array of context id objects, one per unique partition.
|
|
494
|
+
*/
|
|
495
|
+
async getPartitionContextIds() {
|
|
496
|
+
if (!Is.arrayValue(this._partitionContextIds)) {
|
|
497
|
+
return [];
|
|
498
|
+
}
|
|
499
|
+
try {
|
|
500
|
+
const dbConnection = await this.createConnection();
|
|
501
|
+
const rows = await dbConnection.unsafe(`SELECT DISTINCT "${PostgreSqlEntityStorageConnector._PARTITION_KEY}" FROM "${this._config.tableName}"`);
|
|
502
|
+
return rows
|
|
503
|
+
.map(row => row[PostgreSqlEntityStorageConnector._PARTITION_KEY])
|
|
504
|
+
.filter((id) => Is.stringValue(id))
|
|
505
|
+
.map(id => ContextIdHelper.shortSplit(this._partitionContextIds ?? [], id));
|
|
506
|
+
}
|
|
507
|
+
catch (err) {
|
|
508
|
+
throw new GeneralError(PostgreSqlEntityStorageConnector.CLASS_NAME, "getPartitionContextIdsFailed", undefined, err);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Create a new target connector for the migration.
|
|
513
|
+
* @param entitySchemaName The entity schema name to use for the target connector.
|
|
514
|
+
* @returns A new connector configured with a migration table name.
|
|
515
|
+
*/
|
|
516
|
+
async createTargetConnector(entitySchemaName) {
|
|
517
|
+
return new PostgreSqlEntityStorageConnector({
|
|
518
|
+
entitySchema: entitySchemaName,
|
|
519
|
+
config: {
|
|
520
|
+
...this._config,
|
|
521
|
+
tableName: `${this._config.tableName}Migration${Date.now()}`
|
|
522
|
+
},
|
|
523
|
+
partitionContextIds: this._partitionContextIds
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Finalize the migration by renaming the migration table to the original table name.
|
|
528
|
+
* @param targetConnector The connector pointing to the migration table.
|
|
529
|
+
* @param options The optional migration options.
|
|
530
|
+
* @param loggingComponentType The node logging component type.
|
|
531
|
+
* @returns A connector pointing to the final (renamed) table.
|
|
532
|
+
*/
|
|
533
|
+
async finalizeMigration(targetConnector, options, loggingComponentType) {
|
|
534
|
+
// Teardown the existing table with the original name to free up the name for the new table
|
|
535
|
+
await this.teardown(loggingComponentType);
|
|
536
|
+
const dbConnection = await targetConnector.createConnection();
|
|
537
|
+
await dbConnection.unsafe(`ALTER TABLE "${targetConnector._config.tableName}" RENAME TO "${this._config.tableName}"`);
|
|
538
|
+
const finalConnector = new PostgreSqlEntityStorageConnector({
|
|
539
|
+
entitySchema: targetConnector._entitySchemaName,
|
|
540
|
+
config: this._config,
|
|
541
|
+
partitionContextIds: this._partitionContextIds
|
|
542
|
+
});
|
|
543
|
+
if (await finalConnector.bootstrap(loggingComponentType)) {
|
|
544
|
+
await targetConnector.stop();
|
|
545
|
+
return finalConnector;
|
|
546
|
+
}
|
|
547
|
+
throw new GeneralError(PostgreSqlEntityStorageConnector.CLASS_NAME, "finalizeMigrationFailedBootstrap", undefined);
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Clean up the migration by tearing down the migration table.
|
|
551
|
+
* @param targetConnector The connector pointing to the migration table.
|
|
552
|
+
* @param options The optional migration options.
|
|
553
|
+
* @param loggingComponentType The node logging component type.
|
|
554
|
+
*/
|
|
555
|
+
async cleanupMigration(targetConnector, options, loggingComponentType) {
|
|
556
|
+
// If something failed the only thing to cleanup is the migration table
|
|
557
|
+
await targetConnector?.teardown?.(loggingComponentType);
|
|
558
|
+
}
|
|
334
559
|
/**
|
|
335
560
|
* Find all the entities which match the conditions.
|
|
336
561
|
* @param conditions The conditions to match for the entities.
|
|
@@ -344,6 +569,13 @@ export class PostgreSqlEntityStorageConnector {
|
|
|
344
569
|
async query(conditions, sortProperties, properties, cursor, limit) {
|
|
345
570
|
const contextIds = await ContextIdStore.getContextIds();
|
|
346
571
|
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
572
|
+
EntityStorageHelper.validateSortProperties(this._entitySchema, sortProperties);
|
|
573
|
+
EntityStorageHelper.validateProperties(this._entitySchema, properties);
|
|
574
|
+
if (!Is.empty(limit)) {
|
|
575
|
+
const validationFailures = [];
|
|
576
|
+
Validation.integer("limit", limit, validationFailures, undefined, { minValue: 1 });
|
|
577
|
+
Validation.asValidationError(PostgreSqlEntityStorageConnector.CLASS_NAME, "query", validationFailures);
|
|
578
|
+
}
|
|
347
579
|
let sql = "";
|
|
348
580
|
try {
|
|
349
581
|
const returnSize = limit ?? PostgreSqlEntityStorageConnector._DEFAULT_LIMIT;
|
|
@@ -356,25 +588,13 @@ export class PostgreSqlEntityStorageConnector {
|
|
|
356
588
|
}
|
|
357
589
|
orderByClause = `ORDER BY ${orderClauses.join(", ")}`;
|
|
358
590
|
}
|
|
359
|
-
const whereClauses =
|
|
360
|
-
const values = [];
|
|
361
|
-
const finalConditions = {
|
|
362
|
-
conditions: [],
|
|
363
|
-
logicalOperator: LogicalOperator.And
|
|
364
|
-
};
|
|
365
|
-
finalConditions.conditions.push({
|
|
366
|
-
property: PostgreSqlEntityStorageConnector._PARTITION_KEY,
|
|
367
|
-
comparison: ComparisonOperator.Equals,
|
|
368
|
-
value: partitionKey ?? PostgreSqlEntityStorageConnector._PARTITION_KEY_VALUE
|
|
369
|
-
});
|
|
370
|
-
if (!Is.empty(conditions)) {
|
|
371
|
-
finalConditions.conditions.push(conditions);
|
|
372
|
-
}
|
|
373
|
-
this.buildQueryParameters("", finalConditions, whereClauses, values, 1);
|
|
591
|
+
const { whereClauses, values } = this.buildWhereClause(conditions, partitionKey);
|
|
374
592
|
const startIndex = Coerce.number(cursor) ?? 0;
|
|
375
593
|
sql = `SELECT ${properties ? properties.map(p => `"${String(p)}"`).join(", ") : "*"} FROM "${this._config.tableName}"`;
|
|
376
|
-
|
|
377
|
-
|
|
594
|
+
if (whereClauses.length > 0) {
|
|
595
|
+
sql += ` WHERE ${whereClauses.join(" AND ")}`;
|
|
596
|
+
}
|
|
597
|
+
sql += ` ${orderByClause} LIMIT ${returnSize + 1} OFFSET ${startIndex}`;
|
|
378
598
|
const dbConnection = await this.createConnection();
|
|
379
599
|
const rows = await dbConnection.unsafe(sql, values);
|
|
380
600
|
if (this._entitySchema.properties) {
|
|
@@ -403,16 +623,17 @@ export class PostgreSqlEntityStorageConnector {
|
|
|
403
623
|
}
|
|
404
624
|
}
|
|
405
625
|
}
|
|
406
|
-
const
|
|
626
|
+
const hasMore = Is.array(rows) && rows.length > returnSize;
|
|
627
|
+
const resultRows = hasMore ? rows.slice(0, returnSize) : rows;
|
|
628
|
+
const entities = resultRows;
|
|
407
629
|
for (let i = 0; i < entities.length; i++) {
|
|
408
|
-
|
|
409
|
-
|
|
630
|
+
entities[i] = EntityStorageHelper.unPrepareEntity(entities[i], [
|
|
631
|
+
PostgreSqlEntityStorageConnector._PARTITION_KEY
|
|
632
|
+
]);
|
|
410
633
|
}
|
|
411
634
|
return {
|
|
412
635
|
entities,
|
|
413
|
-
cursor:
|
|
414
|
-
? Coerce.string(startIndex + returnSize)
|
|
415
|
-
: undefined
|
|
636
|
+
cursor: hasMore ? Coerce.string(startIndex + returnSize) : undefined
|
|
416
637
|
};
|
|
417
638
|
}
|
|
418
639
|
catch (err) {
|
|
@@ -420,21 +641,26 @@ export class PostgreSqlEntityStorageConnector {
|
|
|
420
641
|
}
|
|
421
642
|
}
|
|
422
643
|
/**
|
|
423
|
-
*
|
|
424
|
-
* @
|
|
644
|
+
* Count all the entities which match the conditions.
|
|
645
|
+
* @param conditions The optional conditions to match for the entities.
|
|
646
|
+
* @returns The total count of entities in the storage.
|
|
425
647
|
*/
|
|
426
|
-
async
|
|
648
|
+
async count(conditions) {
|
|
649
|
+
let queryStr;
|
|
427
650
|
try {
|
|
428
|
-
const tableExists = await this.tableExists();
|
|
429
|
-
if (!tableExists) {
|
|
430
|
-
return;
|
|
431
|
-
}
|
|
432
651
|
const dbConnection = await this.createConnection();
|
|
433
|
-
await
|
|
434
|
-
|
|
652
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
653
|
+
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
654
|
+
const { whereClauses, values } = this.buildWhereClause(conditions, partitionKey);
|
|
655
|
+
queryStr = `SELECT COUNT(*) AS count FROM "${this._config.tableName}"`;
|
|
656
|
+
if (whereClauses.length > 0) {
|
|
657
|
+
queryStr += ` WHERE ${whereClauses.join(" AND ")}`;
|
|
658
|
+
}
|
|
659
|
+
const result = await dbConnection.unsafe(queryStr, values);
|
|
660
|
+
return Number(result[0].count);
|
|
435
661
|
}
|
|
436
|
-
catch {
|
|
437
|
-
|
|
662
|
+
catch (err) {
|
|
663
|
+
throw new GeneralError(PostgreSqlEntityStorageConnector.CLASS_NAME, "countFailed", { sql: queryStr }, err);
|
|
438
664
|
}
|
|
439
665
|
}
|
|
440
666
|
/**
|
|
@@ -474,9 +700,8 @@ export class PostgreSqlEntityStorageConnector {
|
|
|
474
700
|
async tableExists() {
|
|
475
701
|
try {
|
|
476
702
|
const dbConnection = await this.createConnection();
|
|
477
|
-
const
|
|
478
|
-
|
|
479
|
-
return tableExistsResult[0].to_regclass !== null;
|
|
703
|
+
const res = await dbConnection.unsafe("SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = $1 LIMIT 1", [this._config.tableName]);
|
|
704
|
+
return res.length > 0;
|
|
480
705
|
}
|
|
481
706
|
catch {
|
|
482
707
|
return false;
|
|
@@ -534,6 +759,31 @@ export class PostgreSqlEntityStorageConnector {
|
|
|
534
759
|
password: this._config.password
|
|
535
760
|
};
|
|
536
761
|
}
|
|
762
|
+
/**
|
|
763
|
+
* Build where clause arrays for a query, combining partition key and optional conditions.
|
|
764
|
+
* @param conditions The optional entity conditions to include.
|
|
765
|
+
* @param partitionKey The partition key value.
|
|
766
|
+
* @returns The where clauses and bound values.
|
|
767
|
+
* @internal
|
|
768
|
+
*/
|
|
769
|
+
buildWhereClause(conditions, partitionKey) {
|
|
770
|
+
const whereClauses = [];
|
|
771
|
+
const values = [];
|
|
772
|
+
const finalConditions = {
|
|
773
|
+
conditions: [],
|
|
774
|
+
logicalOperator: LogicalOperator.And
|
|
775
|
+
};
|
|
776
|
+
finalConditions.conditions.push({
|
|
777
|
+
property: PostgreSqlEntityStorageConnector._PARTITION_KEY,
|
|
778
|
+
comparison: ComparisonOperator.Equals,
|
|
779
|
+
value: partitionKey ?? PostgreSqlEntityStorageConnector._PARTITION_KEY_VALUE
|
|
780
|
+
});
|
|
781
|
+
if (!Is.empty(conditions)) {
|
|
782
|
+
finalConditions.conditions.push(conditions);
|
|
783
|
+
}
|
|
784
|
+
this.buildQueryParameters("", finalConditions, whereClauses, values, 1);
|
|
785
|
+
return { whereClauses, values };
|
|
786
|
+
}
|
|
537
787
|
/**
|
|
538
788
|
* Create an SQL condition clause.
|
|
539
789
|
* @param objectPath The path for the nested object.
|
|
@@ -589,8 +839,13 @@ export class PostgreSqlEntityStorageConnector {
|
|
|
589
839
|
prop += comparator.property;
|
|
590
840
|
if (comparator.comparison === ComparisonOperator.In) {
|
|
591
841
|
const inValues = Is.array(comparator.value) ? comparator.value : [comparator.value];
|
|
842
|
+
if (inValues.length === 0) {
|
|
843
|
+
// PostgreSQL rejects `IN ()` as a syntax error — short-circuit to a condition
|
|
844
|
+
// that is always false so the query returns zero rows cleanly (#141).
|
|
845
|
+
return "1 = 0";
|
|
846
|
+
}
|
|
592
847
|
values.push(...inValues.map(val => this.propertyToDbValue(val, type)));
|
|
593
|
-
const placeholders = inValues.map((
|
|
848
|
+
const placeholders = inValues.map((value, index) => `$${valueIndex + index}`).join(", ");
|
|
594
849
|
return `"${prop}" IN (${placeholders})`;
|
|
595
850
|
}
|
|
596
851
|
// null/undefined must use IS NULL / IS NOT NULL — never a parameterised placeholder.
|
|
@@ -636,6 +891,17 @@ export class PostgreSqlEntityStorageConnector {
|
|
|
636
891
|
}
|
|
637
892
|
return `LOWER(${jsonTextExpr}) ILIKE $${valueIndex}`;
|
|
638
893
|
}
|
|
894
|
+
case ComparisonOperator.NotIncludes: {
|
|
895
|
+
values.pop();
|
|
896
|
+
values.push(`%${String(comparator.value).toLowerCase()}%`);
|
|
897
|
+
if (isArray) {
|
|
898
|
+
const elemPath = nestedParts
|
|
899
|
+
.map((p, i, arr) => (i === arr.length - 1 ? `->>'${p}'` : `->'${p}'`))
|
|
900
|
+
.join("");
|
|
901
|
+
return `NOT EXISTS (SELECT 1 FROM jsonb_array_elements("${rootProp}") elem WHERE LOWER(elem${elemPath}) ILIKE $${valueIndex})`;
|
|
902
|
+
}
|
|
903
|
+
return `LOWER(${jsonTextExpr}) NOT ILIKE $${valueIndex}`;
|
|
904
|
+
}
|
|
639
905
|
case ComparisonOperator.NotEquals:
|
|
640
906
|
return `${jsonTextExpr} <> $${valueIndex}`;
|
|
641
907
|
case ComparisonOperator.GreaterThan:
|
|
@@ -681,6 +947,18 @@ export class PostgreSqlEntityStorageConnector {
|
|
|
681
947
|
type
|
|
682
948
|
});
|
|
683
949
|
}
|
|
950
|
+
case ComparisonOperator.NotIncludes: {
|
|
951
|
+
if (type === EntitySchemaPropertyType.String) {
|
|
952
|
+
return `"${prop}" NOT ILIKE '%' || $${valueIndex} || '%'`;
|
|
953
|
+
}
|
|
954
|
+
if (type === EntitySchemaPropertyType.Array || type === EntitySchemaPropertyType.Object) {
|
|
955
|
+
return `NOT EXISTS (SELECT 1 FROM jsonb_array_elements("${prop}") elem WHERE elem @> $${valueIndex}::jsonb)`;
|
|
956
|
+
}
|
|
957
|
+
throw new GeneralError(PostgreSqlEntityStorageConnector.CLASS_NAME, "comparisonNotSupported", {
|
|
958
|
+
comparison: comparator.comparison,
|
|
959
|
+
type
|
|
960
|
+
});
|
|
961
|
+
}
|
|
684
962
|
default:
|
|
685
963
|
throw new GeneralError(PostgreSqlEntityStorageConnector.CLASS_NAME, "comparisonNotSupported", {
|
|
686
964
|
comparison: comparator.comparison
|
|
@@ -731,6 +1009,8 @@ export class PostgreSqlEntityStorageConnector {
|
|
|
731
1009
|
/**
|
|
732
1010
|
* Verify the conditions for the entity.
|
|
733
1011
|
* @param conditions The conditions to verify.
|
|
1012
|
+
* @param obj The object to verify the conditions against.
|
|
1013
|
+
* @returns True if all conditions are met, false otherwise.
|
|
734
1014
|
* @internal
|
|
735
1015
|
*/
|
|
736
1016
|
verifyConditions(conditions, obj) {
|
|
@@ -741,6 +1021,7 @@ export class PostgreSqlEntityStorageConnector {
|
|
|
741
1021
|
* @param entitySchema The schema of the entity.
|
|
742
1022
|
* @returns The SQL properties as a string.
|
|
743
1023
|
* @throws GeneralError if the entity properties do not exist.
|
|
1024
|
+
* @internal
|
|
744
1025
|
*/
|
|
745
1026
|
mapPostgreSqlProperties(entitySchema) {
|
|
746
1027
|
const sqlTypeMap = {
|