@twin.org/entity-storage-connector-postgresql 0.0.3-next.1 → 0.0.3-next.10
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 +7 -12
- package/dist/es/postgreSqlEntityStorageConnector.js +326 -54
- package/dist/es/postgreSqlEntityStorageConnector.js.map +1 -1
- package/dist/types/postgreSqlEntityStorageConnector.d.ts +37 -3
- package/docs/changelog.md +188 -44
- package/docs/examples.md +98 -1
- package/docs/reference/classes/PostgreSqlEntityStorageConnector.md +149 -13
- package/docs/reference/interfaces/IPostgreSqlEntityStorageConnectorConfig.md +7 -7
- package/docs/reference/interfaces/IPostgreSqlEntityStorageConnectorConstructorOptions.md +6 -6
- package/locales/en.json +15 -2
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Entity Storage Connector PostgreSQL
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This package provides a PostgreSQL backend for relational persistence, transactions and advanced SQL features. It is designed to work with the wider storage ecosystem so applications can keep behaviour consistent across connectors and environments.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -8,18 +8,13 @@ Entity Storage connector implementation using PostgreSql storage.
|
|
|
8
8
|
npm install @twin.org/entity-storage-connector-postgresql
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Docker
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
To perform testing of this component it may be necessary to launch a local instance to communicate with.
|
|
14
14
|
|
|
15
|
-
```
|
|
16
|
-
docker
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
Afterwards you can run the tests as follows:
|
|
20
|
-
|
|
21
|
-
```sh
|
|
22
|
-
npm run test
|
|
15
|
+
```shell
|
|
16
|
+
docker pull postgres:latest
|
|
17
|
+
docker run -d --name twin-entity-storage-postgresql -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=password -p 5444:5432 postgres:latest
|
|
23
18
|
```
|
|
24
19
|
|
|
25
20
|
## Examples
|
|
@@ -1,7 +1,7 @@
|
|
|
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 } from "@twin.org/core";
|
|
5
5
|
import { ComparisonOperator, EntitySchemaFactory, EntitySchemaHelper, EntitySchemaPropertyType, LogicalOperator, SortDirection } from "@twin.org/entity";
|
|
6
6
|
import postgres from "postgres";
|
|
7
7
|
/**
|
|
@@ -115,7 +115,7 @@ export class PostgreSqlEntityStorageConnector {
|
|
|
115
115
|
tableName: this._config.tableName
|
|
116
116
|
}
|
|
117
117
|
});
|
|
118
|
-
const createTableQuery = `CREATE TABLE ${this._config.tableName} (${this.mapPostgreSqlProperties(this._entitySchema)})`;
|
|
118
|
+
const createTableQuery = `CREATE TABLE "${this._config.tableName}" (${this.mapPostgreSqlProperties(this._entitySchema)})`;
|
|
119
119
|
await dbConnection.unsafe(createTableQuery);
|
|
120
120
|
await this.waitForTableExists();
|
|
121
121
|
}
|
|
@@ -153,6 +153,43 @@ export class PostgreSqlEntityStorageConnector {
|
|
|
153
153
|
className() {
|
|
154
154
|
return PostgreSqlEntityStorageConnector.CLASS_NAME;
|
|
155
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* Get the health of the component.
|
|
158
|
+
* @returns The health of the component.
|
|
159
|
+
*/
|
|
160
|
+
async health() {
|
|
161
|
+
try {
|
|
162
|
+
const sql = await this.createConnection();
|
|
163
|
+
await sql `SELECT 1 FROM ${sql(this._config.tableName)} LIMIT 0`;
|
|
164
|
+
return [
|
|
165
|
+
{
|
|
166
|
+
source: PostgreSqlEntityStorageConnector.CLASS_NAME,
|
|
167
|
+
status: HealthStatus.Ok,
|
|
168
|
+
description: "healthDescription"
|
|
169
|
+
}
|
|
170
|
+
];
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return [
|
|
174
|
+
{
|
|
175
|
+
source: PostgreSqlEntityStorageConnector.CLASS_NAME,
|
|
176
|
+
status: HealthStatus.Error,
|
|
177
|
+
description: "healthDescription",
|
|
178
|
+
message: "connectionFailed"
|
|
179
|
+
}
|
|
180
|
+
];
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* The component needs to be stopped when the node is closed.
|
|
185
|
+
* @returns Nothing.
|
|
186
|
+
*/
|
|
187
|
+
async stop() {
|
|
188
|
+
if (this._connection) {
|
|
189
|
+
await this._connection.end();
|
|
190
|
+
this._connection = undefined;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
156
193
|
/**
|
|
157
194
|
* Get the schema for the entities.
|
|
158
195
|
* @returns The schema for the entities.
|
|
@@ -202,9 +239,17 @@ export class PostgreSqlEntityStorageConnector {
|
|
|
202
239
|
if ((prop.type === EntitySchemaPropertyType.Object ||
|
|
203
240
|
prop.type === EntitySchemaPropertyType.Array) &&
|
|
204
241
|
typeof row[propColumn] === "string") {
|
|
205
|
-
|
|
242
|
+
let value;
|
|
243
|
+
try {
|
|
244
|
+
value = JSON.parse(rows[0][propColumn]);
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
// If JSON.parse fails, keep the value as string
|
|
248
|
+
// This handles cases where plain text was stored in Object/Array fields
|
|
249
|
+
value = rows[0][propColumn];
|
|
250
|
+
}
|
|
206
251
|
delete rows[0][propColumn];
|
|
207
|
-
rows[0][prop.property] =
|
|
252
|
+
rows[0][prop.property] = value;
|
|
208
253
|
}
|
|
209
254
|
if (row[propColumn] === null) {
|
|
210
255
|
rows[0][prop.property] = undefined;
|
|
@@ -249,7 +294,6 @@ export class PostgreSqlEntityStorageConnector {
|
|
|
249
294
|
type: EntitySchemaPropertyType.String
|
|
250
295
|
});
|
|
251
296
|
ObjectHelper.propertySet(finalEntity, PostgreSqlEntityStorageConnector._PARTITION_KEY, partitionKey ?? PostgreSqlEntityStorageConnector._PARTITION_KEY_VALUE);
|
|
252
|
-
ObjectHelper.propertySet(finalEntity, PostgreSqlEntityStorageConnector._PARTITION_KEY, partitionKey ?? PostgreSqlEntityStorageConnector._PARTITION_KEY_VALUE);
|
|
253
297
|
const keys = [];
|
|
254
298
|
const values = [];
|
|
255
299
|
for (const prop of props) {
|
|
@@ -277,6 +321,68 @@ export class PostgreSqlEntityStorageConnector {
|
|
|
277
321
|
}, err);
|
|
278
322
|
}
|
|
279
323
|
}
|
|
324
|
+
/**
|
|
325
|
+
* Set multiple entities in a batch.
|
|
326
|
+
* @param entities The entities to set.
|
|
327
|
+
* @returns Nothing.
|
|
328
|
+
*/
|
|
329
|
+
async setBatch(entities) {
|
|
330
|
+
Guards.arrayValue(PostgreSqlEntityStorageConnector.CLASS_NAME, "entities", entities);
|
|
331
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
332
|
+
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
333
|
+
for (const entity of entities) {
|
|
334
|
+
EntitySchemaHelper.validateEntity(entity, this.getSchema());
|
|
335
|
+
}
|
|
336
|
+
try {
|
|
337
|
+
const props = [...(this._entitySchema.properties ?? [])];
|
|
338
|
+
props.unshift({
|
|
339
|
+
property: PostgreSqlEntityStorageConnector._PARTITION_KEY,
|
|
340
|
+
type: EntitySchemaPropertyType.String
|
|
341
|
+
});
|
|
342
|
+
const keys = props.map(p => p.property);
|
|
343
|
+
const allValues = [];
|
|
344
|
+
const rowPlaceholders = [];
|
|
345
|
+
for (const entity of entities) {
|
|
346
|
+
const finalEntity = ObjectHelper.clone(entity);
|
|
347
|
+
ObjectHelper.propertySet(finalEntity, PostgreSqlEntityStorageConnector._PARTITION_KEY, partitionKey ?? PostgreSqlEntityStorageConnector._PARTITION_KEY_VALUE);
|
|
348
|
+
const rowValues = [];
|
|
349
|
+
for (const prop of props) {
|
|
350
|
+
const val = finalEntity[prop.property];
|
|
351
|
+
allValues.push(Is.empty(val) ? null : val);
|
|
352
|
+
rowValues.push(`$${allValues.length}`);
|
|
353
|
+
}
|
|
354
|
+
rowPlaceholders.push(`(${rowValues.join(", ")})`);
|
|
355
|
+
}
|
|
356
|
+
let sql = `INSERT INTO "${this._config.tableName}"`;
|
|
357
|
+
sql += ` (${keys.map(key => `"${key}"`).join(", ")})`;
|
|
358
|
+
sql += ` VALUES ${rowPlaceholders.join(", ")}`;
|
|
359
|
+
sql += ` ON CONFLICT ("${PostgreSqlEntityStorageConnector._PARTITION_KEY}", "${this._primaryKeyProperty.property}")`;
|
|
360
|
+
sql += ` DO UPDATE SET ${keys.map(key => `"${key}" = EXCLUDED."${key}"`).join(", ")};`;
|
|
361
|
+
const dbConnection = await this.createConnection();
|
|
362
|
+
await dbConnection.unsafe(sql, allValues);
|
|
363
|
+
}
|
|
364
|
+
catch (err) {
|
|
365
|
+
throw new GeneralError(PostgreSqlEntityStorageConnector.CLASS_NAME, "setBatchFailed", undefined, err);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Empty all the entities.
|
|
370
|
+
* @returns Nothing.
|
|
371
|
+
*/
|
|
372
|
+
async empty() {
|
|
373
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
374
|
+
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
375
|
+
try {
|
|
376
|
+
const sql = `DELETE FROM "${this._config.tableName}" WHERE "${PostgreSqlEntityStorageConnector._PARTITION_KEY}" = $1`;
|
|
377
|
+
const dbConnection = await this.createConnection();
|
|
378
|
+
await dbConnection.unsafe(sql, [
|
|
379
|
+
partitionKey ?? PostgreSqlEntityStorageConnector._PARTITION_KEY_VALUE
|
|
380
|
+
]);
|
|
381
|
+
}
|
|
382
|
+
catch (err) {
|
|
383
|
+
throw new GeneralError(PostgreSqlEntityStorageConnector.CLASS_NAME, "emptyFailed", undefined, err);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
280
386
|
/**
|
|
281
387
|
* Remove the entity.
|
|
282
388
|
* @param id The id of the entity to remove.
|
|
@@ -313,6 +419,68 @@ export class PostgreSqlEntityStorageConnector {
|
|
|
313
419
|
}, err);
|
|
314
420
|
}
|
|
315
421
|
}
|
|
422
|
+
/**
|
|
423
|
+
* Remove multiple entities by their primary key IDs.
|
|
424
|
+
* @param ids The ids of the entities to remove.
|
|
425
|
+
* @returns Nothing.
|
|
426
|
+
*/
|
|
427
|
+
async removeBatch(ids) {
|
|
428
|
+
Guards.arrayValue(PostgreSqlEntityStorageConnector.CLASS_NAME, "ids", ids);
|
|
429
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
430
|
+
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
431
|
+
try {
|
|
432
|
+
const sql = `DELETE FROM "${this._config.tableName}" WHERE "${PostgreSqlEntityStorageConnector._PARTITION_KEY}" = $1 AND "${this._primaryKeyProperty.property}" = ANY($2)`;
|
|
433
|
+
const dbConnection = await this.createConnection();
|
|
434
|
+
await dbConnection.unsafe(sql, [
|
|
435
|
+
partitionKey ?? PostgreSqlEntityStorageConnector._PARTITION_KEY_VALUE,
|
|
436
|
+
ids
|
|
437
|
+
]);
|
|
438
|
+
}
|
|
439
|
+
catch (err) {
|
|
440
|
+
throw new GeneralError(PostgreSqlEntityStorageConnector.CLASS_NAME, "removeBatchFailed", undefined, err);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Teardown the entity storage by dropping the table.
|
|
445
|
+
* @param nodeLoggingComponentType The node logging component type.
|
|
446
|
+
* @returns True if the teardown process was successful.
|
|
447
|
+
*/
|
|
448
|
+
async teardown(nodeLoggingComponentType) {
|
|
449
|
+
const nodeLogging = ComponentFactory.getIfExists(nodeLoggingComponentType);
|
|
450
|
+
await nodeLogging?.log({
|
|
451
|
+
level: "info",
|
|
452
|
+
source: PostgreSqlEntityStorageConnector.CLASS_NAME,
|
|
453
|
+
ts: Date.now(),
|
|
454
|
+
message: "tableDropping",
|
|
455
|
+
data: { tableName: this._config.tableName }
|
|
456
|
+
});
|
|
457
|
+
try {
|
|
458
|
+
const tableExists = await this.tableExists();
|
|
459
|
+
if (tableExists) {
|
|
460
|
+
const dbConnection = await this.createConnection();
|
|
461
|
+
await dbConnection.unsafe(`DROP TABLE "${this._config.tableName}";`);
|
|
462
|
+
await this.waitForTableNotExists();
|
|
463
|
+
}
|
|
464
|
+
await nodeLogging?.log({
|
|
465
|
+
level: "info",
|
|
466
|
+
source: PostgreSqlEntityStorageConnector.CLASS_NAME,
|
|
467
|
+
ts: Date.now(),
|
|
468
|
+
message: "tableDropped",
|
|
469
|
+
data: { tableName: this._config.tableName }
|
|
470
|
+
});
|
|
471
|
+
return true;
|
|
472
|
+
}
|
|
473
|
+
catch (err) {
|
|
474
|
+
await nodeLogging?.log({
|
|
475
|
+
level: "error",
|
|
476
|
+
source: PostgreSqlEntityStorageConnector.CLASS_NAME,
|
|
477
|
+
ts: Date.now(),
|
|
478
|
+
message: "teardownFailed",
|
|
479
|
+
error: BaseError.fromError(err)
|
|
480
|
+
});
|
|
481
|
+
return false;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
316
484
|
/**
|
|
317
485
|
* Find all the entities which match the conditions.
|
|
318
486
|
* @param conditions The conditions to match for the entities.
|
|
@@ -367,9 +535,17 @@ export class PostgreSqlEntityStorageConnector {
|
|
|
367
535
|
if ((prop.type === EntitySchemaPropertyType.Object ||
|
|
368
536
|
prop.type === EntitySchemaPropertyType.Array) &&
|
|
369
537
|
Is.string(row[propColumn])) {
|
|
370
|
-
|
|
538
|
+
let value;
|
|
539
|
+
try {
|
|
540
|
+
value = JSON.parse(row[propColumn]);
|
|
541
|
+
}
|
|
542
|
+
catch {
|
|
543
|
+
// If JSON.parse fails, keep the value as string
|
|
544
|
+
// This handles cases where plain text was stored in Object/Array fields
|
|
545
|
+
value = row[propColumn];
|
|
546
|
+
}
|
|
371
547
|
delete row[propColumn];
|
|
372
|
-
row[prop.property] =
|
|
548
|
+
row[prop.property] = value;
|
|
373
549
|
}
|
|
374
550
|
if (row[propColumn] === null) {
|
|
375
551
|
row[prop.property] = undefined;
|
|
@@ -394,21 +570,19 @@ export class PostgreSqlEntityStorageConnector {
|
|
|
394
570
|
}
|
|
395
571
|
}
|
|
396
572
|
/**
|
|
397
|
-
*
|
|
398
|
-
* @returns
|
|
573
|
+
* Count all the entities which match the conditions.
|
|
574
|
+
* @returns The total count of entities in the storage.
|
|
399
575
|
*/
|
|
400
|
-
async
|
|
576
|
+
async count() {
|
|
401
577
|
try {
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
await dbConnection.unsafe(`DROP TABLE ${this._config.tableName};`);
|
|
408
|
-
await this.waitForTableNotExists();
|
|
578
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
579
|
+
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
580
|
+
const sql = await this.createConnection();
|
|
581
|
+
const result = await sql `SELECT COUNT(*) AS count FROM ${sql(this._config.tableName)} WHERE "partitionId" = ${partitionKey ?? PostgreSqlEntityStorageConnector._PARTITION_KEY_VALUE}`;
|
|
582
|
+
return Number(result[0].count);
|
|
409
583
|
}
|
|
410
|
-
catch {
|
|
411
|
-
|
|
584
|
+
catch (err) {
|
|
585
|
+
throw new GeneralError(PostgreSqlEntityStorageConnector.CLASS_NAME, "countFailed", undefined, err);
|
|
412
586
|
}
|
|
413
587
|
}
|
|
414
588
|
/**
|
|
@@ -567,40 +741,99 @@ export class PostgreSqlEntityStorageConnector {
|
|
|
567
741
|
const placeholders = inValues.map((_, index) => `$${valueIndex + index}`).join(", ");
|
|
568
742
|
return `"${prop}" IN (${placeholders})`;
|
|
569
743
|
}
|
|
744
|
+
// null/undefined must use IS NULL / IS NOT NULL — never a parameterised placeholder.
|
|
745
|
+
// Passing undefined through propertyToDbValue() coerces it to NaN for number fields
|
|
746
|
+
// (Number(undefined) === NaN), and null coerces to 0 (Number(null) === 0), both of
|
|
747
|
+
// which produce semantically wrong or invalid SQL.
|
|
748
|
+
if (comparator.value === null || comparator.value === undefined) {
|
|
749
|
+
if (comparator.comparison === ComparisonOperator.Equals ||
|
|
750
|
+
comparator.comparison === ComparisonOperator.NotEquals) {
|
|
751
|
+
const nullCheck = comparator.comparison === ComparisonOperator.Equals ? "IS NULL" : "IS NOT NULL";
|
|
752
|
+
if (comparator.property.split(".").length > 1) {
|
|
753
|
+
const rootProp = comparator.property.split(".")[0];
|
|
754
|
+
const nestedParts = comparator.property.split(".").slice(1);
|
|
755
|
+
const jsonPath = nestedParts
|
|
756
|
+
.map((p, i, arr) => (i === arr.length - 1 ? `->> '${p}'` : `-> '${p}'`))
|
|
757
|
+
.join("");
|
|
758
|
+
const jsonTextExpr = `("${rootProp}"::jsonb ${jsonPath})`;
|
|
759
|
+
return `${jsonTextExpr} ${nullCheck}`;
|
|
760
|
+
}
|
|
761
|
+
return `"${prop}" ${nullCheck}`;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
570
764
|
const dbValue = this.propertyToDbValue(comparator.value, type);
|
|
571
765
|
values.push(dbValue);
|
|
572
766
|
if (comparator.property.split(".").length > 1) {
|
|
573
|
-
const
|
|
574
|
-
|
|
575
|
-
|
|
767
|
+
const rootProp = comparator.property.split(".")[0];
|
|
768
|
+
const nestedParts = comparator.property.split(".").slice(1);
|
|
769
|
+
const rootSchema = this._entitySchema.properties?.find(p => p.property === rootProp);
|
|
770
|
+
const isArray = rootSchema?.type === EntitySchemaPropertyType.Array;
|
|
771
|
+
const jsonPath = nestedParts
|
|
576
772
|
.map((p, i, arr) => (i === arr.length - 1 ? `->> '${p}'` : `-> '${p}'`))
|
|
577
773
|
.join("");
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
774
|
+
const jsonTextExpr = `("${rootProp}"::jsonb ${jsonPath})`;
|
|
775
|
+
switch (comparator.comparison) {
|
|
776
|
+
case ComparisonOperator.Includes: {
|
|
777
|
+
values.pop();
|
|
778
|
+
values.push(`%${String(comparator.value).toLowerCase()}%`);
|
|
779
|
+
if (isArray) {
|
|
780
|
+
const elemPath = nestedParts
|
|
781
|
+
.map((p, i, arr) => (i === arr.length - 1 ? `->>'${p}'` : `->'${p}'`))
|
|
782
|
+
.join("");
|
|
783
|
+
return `EXISTS (SELECT 1 FROM jsonb_array_elements("${rootProp}") elem WHERE LOWER(elem${elemPath}) ILIKE $${valueIndex})`;
|
|
784
|
+
}
|
|
785
|
+
return `LOWER(${jsonTextExpr}) ILIKE $${valueIndex}`;
|
|
786
|
+
}
|
|
787
|
+
case ComparisonOperator.NotEquals:
|
|
788
|
+
return `${jsonTextExpr} <> $${valueIndex}`;
|
|
789
|
+
case ComparisonOperator.GreaterThan:
|
|
790
|
+
return `${jsonTextExpr} > $${valueIndex}`;
|
|
791
|
+
case ComparisonOperator.LessThan:
|
|
792
|
+
return `${jsonTextExpr} < $${valueIndex}`;
|
|
793
|
+
case ComparisonOperator.GreaterThanOrEqual:
|
|
794
|
+
return `${jsonTextExpr} >= $${valueIndex}`;
|
|
795
|
+
case ComparisonOperator.LessThanOrEqual:
|
|
796
|
+
return `${jsonTextExpr} <= $${valueIndex}`;
|
|
797
|
+
default:
|
|
798
|
+
return `${jsonTextExpr} = $${valueIndex}`;
|
|
799
|
+
}
|
|
597
800
|
}
|
|
598
|
-
|
|
599
|
-
|
|
801
|
+
switch (comparator.comparison) {
|
|
802
|
+
case ComparisonOperator.Equals:
|
|
803
|
+
if (Is.object(comparator.value) || Is.array(comparator.value)) {
|
|
804
|
+
return `"${prop}" = $${valueIndex}::jsonb`;
|
|
805
|
+
}
|
|
806
|
+
return `"${prop}" = $${valueIndex}`;
|
|
807
|
+
case ComparisonOperator.NotEquals:
|
|
808
|
+
if (Is.object(comparator.value) || Is.array(comparator.value)) {
|
|
809
|
+
return `"${prop}" != $${valueIndex}::jsonb`;
|
|
810
|
+
}
|
|
811
|
+
return `"${prop}" <> $${valueIndex}`;
|
|
812
|
+
case ComparisonOperator.GreaterThan:
|
|
813
|
+
return `"${prop}" > $${valueIndex}`;
|
|
814
|
+
case ComparisonOperator.LessThan:
|
|
815
|
+
return `"${prop}" < $${valueIndex}`;
|
|
816
|
+
case ComparisonOperator.GreaterThanOrEqual:
|
|
817
|
+
return `"${prop}" >= $${valueIndex}`;
|
|
818
|
+
case ComparisonOperator.LessThanOrEqual:
|
|
819
|
+
return `"${prop}" <= $${valueIndex}`;
|
|
820
|
+
case ComparisonOperator.Includes: {
|
|
821
|
+
if (type === EntitySchemaPropertyType.String) {
|
|
822
|
+
return `"${prop}" ILIKE '%' || $${valueIndex} || '%'`;
|
|
823
|
+
}
|
|
824
|
+
if (type === EntitySchemaPropertyType.Array || type === EntitySchemaPropertyType.Object) {
|
|
825
|
+
return `EXISTS (SELECT 1 FROM jsonb_array_elements("${prop}") elem WHERE elem @> $${valueIndex}::jsonb)`;
|
|
826
|
+
}
|
|
827
|
+
throw new GeneralError(PostgreSqlEntityStorageConnector.CLASS_NAME, "comparisonNotSupported", {
|
|
828
|
+
comparison: comparator.comparison,
|
|
829
|
+
type
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
default:
|
|
833
|
+
throw new GeneralError(PostgreSqlEntityStorageConnector.CLASS_NAME, "comparisonNotSupported", {
|
|
834
|
+
comparison: comparator.comparison
|
|
835
|
+
});
|
|
600
836
|
}
|
|
601
|
-
throw new GeneralError(PostgreSqlEntityStorageConnector.CLASS_NAME, "comparisonNotSupported", {
|
|
602
|
-
comparison: comparator.comparison
|
|
603
|
-
});
|
|
604
837
|
}
|
|
605
838
|
/**
|
|
606
839
|
* Format a value to insert into DB.
|
|
@@ -610,21 +843,19 @@ export class PostgreSqlEntityStorageConnector {
|
|
|
610
843
|
* @internal
|
|
611
844
|
*/
|
|
612
845
|
propertyToDbValue(value, type) {
|
|
613
|
-
if (type ===
|
|
846
|
+
if (type === EntitySchemaPropertyType.String) {
|
|
614
847
|
return String(value);
|
|
615
848
|
}
|
|
616
|
-
else if (type ===
|
|
849
|
+
else if (type === EntitySchemaPropertyType.Number) {
|
|
617
850
|
return Number(value);
|
|
618
851
|
}
|
|
619
|
-
else if (type ===
|
|
852
|
+
else if (type === EntitySchemaPropertyType.Boolean) {
|
|
620
853
|
return Boolean(value);
|
|
621
854
|
}
|
|
622
|
-
else if (type ===
|
|
855
|
+
else if (type === EntitySchemaPropertyType.Object ||
|
|
856
|
+
type === EntitySchemaPropertyType.Array) {
|
|
623
857
|
return value;
|
|
624
858
|
}
|
|
625
|
-
if (Is.object(value)) {
|
|
626
|
-
return JSON.stringify(value);
|
|
627
|
-
}
|
|
628
859
|
return value;
|
|
629
860
|
}
|
|
630
861
|
/**
|
|
@@ -681,7 +912,48 @@ export class PostgreSqlEntityStorageConnector {
|
|
|
681
912
|
});
|
|
682
913
|
const columnDefinitions = props
|
|
683
914
|
.map(prop => {
|
|
684
|
-
|
|
915
|
+
let sqlType = sqlTypeMap[prop.type] || "TEXT";
|
|
916
|
+
if (prop.format) {
|
|
917
|
+
switch (prop.type) {
|
|
918
|
+
case EntitySchemaPropertyType.String:
|
|
919
|
+
switch (prop.format) {
|
|
920
|
+
case "uuid":
|
|
921
|
+
sqlType = "UUID";
|
|
922
|
+
break;
|
|
923
|
+
}
|
|
924
|
+
break;
|
|
925
|
+
case EntitySchemaPropertyType.Number:
|
|
926
|
+
switch (prop.format) {
|
|
927
|
+
case "float":
|
|
928
|
+
sqlType = "REAL";
|
|
929
|
+
break;
|
|
930
|
+
case "double":
|
|
931
|
+
sqlType = "DOUBLE PRECISION";
|
|
932
|
+
break;
|
|
933
|
+
}
|
|
934
|
+
break;
|
|
935
|
+
case EntitySchemaPropertyType.Integer:
|
|
936
|
+
switch (prop.format) {
|
|
937
|
+
case "int8":
|
|
938
|
+
case "uint8":
|
|
939
|
+
sqlType = "SMALLINT";
|
|
940
|
+
break;
|
|
941
|
+
case "int16":
|
|
942
|
+
sqlType = "SMALLINT";
|
|
943
|
+
break;
|
|
944
|
+
case "uint16":
|
|
945
|
+
case "int32":
|
|
946
|
+
sqlType = "INTEGER";
|
|
947
|
+
break;
|
|
948
|
+
case "uint32":
|
|
949
|
+
case "int64":
|
|
950
|
+
case "uint64":
|
|
951
|
+
sqlType = "BIGINT";
|
|
952
|
+
break;
|
|
953
|
+
}
|
|
954
|
+
break;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
685
957
|
const columnName = String(prop.property);
|
|
686
958
|
const nullable = prop.optional ? " NULL" : " NOT NULL";
|
|
687
959
|
if (prop.isPrimary) {
|