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