@twin.org/entity-storage-connector-postgresql 0.0.2-next.9 → 0.0.3-next.2

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.
@@ -0,0 +1,713 @@
1
+ // Copyright 2024 IOTA Stiftung.
2
+ // SPDX-License-Identifier: Apache-2.0.
3
+ import { ContextIdHelper, ContextIdStore } from "@twin.org/context";
4
+ import { BaseError, Coerce, ComponentFactory, GeneralError, Guards, Is, ObjectHelper } from "@twin.org/core";
5
+ import { ComparisonOperator, EntitySchemaFactory, EntitySchemaHelper, EntitySchemaPropertyType, LogicalOperator, SortDirection } from "@twin.org/entity";
6
+ import postgres from "postgres";
7
+ /**
8
+ * Class for performing entity storage operations using ql.
9
+ */
10
+ export class PostgreSqlEntityStorageConnector {
11
+ /**
12
+ * Runtime name for the class.
13
+ */
14
+ static CLASS_NAME = "PostgreSqlEntityStorageConnector";
15
+ /**
16
+ * Limit the number of entities when finding.
17
+ * @internal
18
+ */
19
+ static _DEFAULT_LIMIT = 40;
20
+ /**
21
+ * Partition id field name.
22
+ * @internal
23
+ */
24
+ static _PARTITION_KEY = "partitionId";
25
+ /**
26
+ * Partition id field value.
27
+ * @internal
28
+ */
29
+ static _PARTITION_KEY_VALUE = "root";
30
+ /**
31
+ * The schema for the entity.
32
+ * @internal
33
+ */
34
+ _entitySchema;
35
+ /**
36
+ * The keys to use from the context ids to create partitions.
37
+ * @internal
38
+ */
39
+ _partitionContextIds;
40
+ /**
41
+ * The primary key property.
42
+ * @internal
43
+ */
44
+ _primaryKeyProperty;
45
+ /**
46
+ * The configuration for the connector.
47
+ * @internal
48
+ */
49
+ _config;
50
+ /**
51
+ * The configuration for the connector.
52
+ * @internal
53
+ */
54
+ _connection;
55
+ /**
56
+ * Create a new instance of PostgreSqlEntityStorageConnector.
57
+ * @param options The options for the connector.
58
+ */
59
+ constructor(options) {
60
+ Guards.object(PostgreSqlEntityStorageConnector.CLASS_NAME, "options", options);
61
+ Guards.stringValue(PostgreSqlEntityStorageConnector.CLASS_NAME, "options.entitySchema", options.entitySchema);
62
+ Guards.object(PostgreSqlEntityStorageConnector.CLASS_NAME, "options.config", options.config);
63
+ Guards.stringValue(PostgreSqlEntityStorageConnector.CLASS_NAME, "options.config.host", options.config.host);
64
+ Guards.stringValue(PostgreSqlEntityStorageConnector.CLASS_NAME, "options.config.user", options.config.user);
65
+ Guards.stringValue(PostgreSqlEntityStorageConnector.CLASS_NAME, "options.config.password", options.config.password);
66
+ Guards.stringValue(PostgreSqlEntityStorageConnector.CLASS_NAME, "options.config.database", options.config.database);
67
+ Guards.stringValue(PostgreSqlEntityStorageConnector.CLASS_NAME, "options.config.tableName", options.config.tableName);
68
+ this._entitySchema = EntitySchemaFactory.get(options.entitySchema);
69
+ this._partitionContextIds = options.partitionContextIds;
70
+ this._primaryKeyProperty = EntitySchemaHelper.getPrimaryKey(this._entitySchema);
71
+ this._config = options.config;
72
+ }
73
+ /**
74
+ * Initialize the PostgreSql environment.
75
+ * @param nodeLoggingComponentType Optional type of the logging component.
76
+ * @returns A promise that resolves to a boolean indicating success.
77
+ */
78
+ async bootstrap(nodeLoggingComponentType) {
79
+ const nodeLogging = ComponentFactory.getIfExists(nodeLoggingComponentType);
80
+ try {
81
+ const dbConnection = await this.createConnection();
82
+ const databaseExists = await this.databaseExists();
83
+ if (!databaseExists) {
84
+ await nodeLogging?.log({
85
+ level: "info",
86
+ source: PostgreSqlEntityStorageConnector.CLASS_NAME,
87
+ ts: Date.now(),
88
+ message: "databaseCreating",
89
+ data: {
90
+ databaseName: this._config.database
91
+ }
92
+ });
93
+ await dbConnection.unsafe(`CREATE DATABASE "${this._config.database}";`);
94
+ await this.waitForDatabaseExists();
95
+ }
96
+ else {
97
+ await nodeLogging?.log({
98
+ level: "info",
99
+ source: PostgreSqlEntityStorageConnector.CLASS_NAME,
100
+ ts: Date.now(),
101
+ message: "databaseExists",
102
+ data: {
103
+ databaseName: this._config.database
104
+ }
105
+ });
106
+ }
107
+ const tableExists = await this.tableExists();
108
+ if (!tableExists) {
109
+ await nodeLogging?.log({
110
+ level: "info",
111
+ source: PostgreSqlEntityStorageConnector.CLASS_NAME,
112
+ ts: Date.now(),
113
+ message: "tableCreating",
114
+ data: {
115
+ tableName: this._config.tableName
116
+ }
117
+ });
118
+ const createTableQuery = `CREATE TABLE "${this._config.tableName}" (${this.mapPostgreSqlProperties(this._entitySchema)})`;
119
+ await dbConnection.unsafe(createTableQuery);
120
+ await this.waitForTableExists();
121
+ }
122
+ else {
123
+ await nodeLogging?.log({
124
+ level: "info",
125
+ source: PostgreSqlEntityStorageConnector.CLASS_NAME,
126
+ ts: Date.now(),
127
+ message: "tableExists",
128
+ data: {
129
+ tableName: this._config.tableName
130
+ }
131
+ });
132
+ }
133
+ }
134
+ catch (error) {
135
+ await nodeLogging?.log({
136
+ level: "error",
137
+ source: PostgreSqlEntityStorageConnector.CLASS_NAME,
138
+ ts: Date.now(),
139
+ message: "databaseCreateFailed",
140
+ error: BaseError.fromError(error),
141
+ data: {
142
+ databaseName: this._config.database
143
+ }
144
+ });
145
+ return false;
146
+ }
147
+ return true;
148
+ }
149
+ /**
150
+ * Returns the class name of the component.
151
+ * @returns The class name of the component.
152
+ */
153
+ className() {
154
+ return PostgreSqlEntityStorageConnector.CLASS_NAME;
155
+ }
156
+ /**
157
+ * Get the schema for the entities.
158
+ * @returns The schema for the entities.
159
+ */
160
+ getSchema() {
161
+ return this._entitySchema;
162
+ }
163
+ /**
164
+ * Get an entity from PostgreSql.
165
+ * @param id The id of the entity to get, or the index value if secondaryIndex is set.
166
+ * @param secondaryIndex Get the item using a secondary index.
167
+ * @param conditions The optional conditions to match for the entities.
168
+ * @returns The object if it can be found or undefined.
169
+ */
170
+ async get(id, secondaryIndex, conditions) {
171
+ Guards.stringValue(PostgreSqlEntityStorageConnector.CLASS_NAME, "id", id);
172
+ const contextIds = await ContextIdStore.getContextIds();
173
+ const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
174
+ try {
175
+ const dbConnection = await this.createConnection();
176
+ const whereClauses = [];
177
+ const values = [];
178
+ whereClauses.push(`"${PostgreSqlEntityStorageConnector._PARTITION_KEY}" = $1`);
179
+ values.push(partitionKey ?? PostgreSqlEntityStorageConnector._PARTITION_KEY_VALUE);
180
+ if (secondaryIndex) {
181
+ whereClauses.push(`"${String(secondaryIndex)}" = $2`);
182
+ values.push(id);
183
+ }
184
+ else {
185
+ whereClauses.push(`"${this._primaryKeyProperty.property}" = $2`);
186
+ values.push(id);
187
+ }
188
+ if (Is.arrayValue(conditions)) {
189
+ for (const condition of conditions) {
190
+ whereClauses.push(`"${String(condition.property)}" = $${values.length + 1}`);
191
+ values.push(condition.value);
192
+ }
193
+ }
194
+ const query = `SELECT * FROM "${this._config.tableName}" WHERE ${whereClauses.join(" AND ")} LIMIT 1`;
195
+ const rows = await dbConnection.unsafe(query, values);
196
+ if (Is.array(rows) && rows.length === 1) {
197
+ if (this._entitySchema.properties) {
198
+ for (const prop of this._entitySchema.properties) {
199
+ const row = rows[0];
200
+ let propColumn = prop.property;
201
+ propColumn = propColumn.toLowerCase();
202
+ if ((prop.type === EntitySchemaPropertyType.Object ||
203
+ prop.type === EntitySchemaPropertyType.Array) &&
204
+ typeof row[propColumn] === "string") {
205
+ let value;
206
+ try {
207
+ value = JSON.parse(rows[0][propColumn]);
208
+ }
209
+ catch {
210
+ // If JSON.parse fails, keep the value as string
211
+ // This handles cases where plain text was stored in Object/Array fields
212
+ value = rows[0][propColumn];
213
+ }
214
+ delete rows[0][propColumn];
215
+ rows[0][prop.property] = value;
216
+ }
217
+ if (row[propColumn] === null) {
218
+ rows[0][prop.property] = undefined;
219
+ }
220
+ }
221
+ }
222
+ const entity = ObjectHelper.removeEmptyProperties(rows[0], { removeNull: true });
223
+ ObjectHelper.propertyDelete(entity, PostgreSqlEntityStorageConnector._PARTITION_KEY);
224
+ return entity;
225
+ }
226
+ }
227
+ catch (err) {
228
+ throw new GeneralError(PostgreSqlEntityStorageConnector.CLASS_NAME, "getFailed", {
229
+ id
230
+ }, err);
231
+ }
232
+ return undefined;
233
+ }
234
+ /**
235
+ * Set an entity.
236
+ * @param entity The entity to set.
237
+ * @param conditions The optional conditions to match for the entities.
238
+ * @returns The id of the entity.
239
+ */
240
+ async set(entity, conditions) {
241
+ Guards.object(PostgreSqlEntityStorageConnector.CLASS_NAME, "entity", entity);
242
+ const contextIds = await ContextIdStore.getContextIds();
243
+ const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
244
+ EntitySchemaHelper.validateEntity(entity, this.getSchema());
245
+ const id = entity[this._primaryKeyProperty.property];
246
+ try {
247
+ if (Is.arrayValue(conditions)) {
248
+ const itemData = await this.get(id);
249
+ if (Is.notEmpty(itemData) && !this.verifyConditions(conditions, itemData)) {
250
+ return;
251
+ }
252
+ }
253
+ const finalEntity = ObjectHelper.clone(entity);
254
+ const props = [...(this._entitySchema.properties ?? [])];
255
+ props.unshift({
256
+ property: PostgreSqlEntityStorageConnector._PARTITION_KEY,
257
+ type: EntitySchemaPropertyType.String
258
+ });
259
+ ObjectHelper.propertySet(finalEntity, PostgreSqlEntityStorageConnector._PARTITION_KEY, partitionKey ?? PostgreSqlEntityStorageConnector._PARTITION_KEY_VALUE);
260
+ ObjectHelper.propertySet(finalEntity, PostgreSqlEntityStorageConnector._PARTITION_KEY, partitionKey ?? PostgreSqlEntityStorageConnector._PARTITION_KEY_VALUE);
261
+ const keys = [];
262
+ const values = [];
263
+ for (const prop of props) {
264
+ if (!(Is.empty(finalEntity[prop.property]) && (prop.optional ?? false))) {
265
+ keys.push(prop.property);
266
+ if (finalEntity[prop.property] === undefined) {
267
+ values.push(null);
268
+ }
269
+ else {
270
+ values.push(finalEntity[prop.property]);
271
+ }
272
+ }
273
+ }
274
+ let sql = `INSERT INTO "${this._config.tableName}"`;
275
+ sql += ` (${keys.map(key => `"${key}"`).join(", ")})`;
276
+ sql += ` VALUES (${values.map((_, i) => `$${i + 1}`).join(", ")})`;
277
+ sql += ` ON CONFLICT ("${PostgreSqlEntityStorageConnector._PARTITION_KEY}", "${this._primaryKeyProperty.property}")`;
278
+ sql += ` DO UPDATE SET ${keys.map(key => `"${key}" = EXCLUDED."${key}"`).join(", ")};`;
279
+ const dbConnection = await this.createConnection();
280
+ await dbConnection.unsafe(sql, values);
281
+ }
282
+ catch (err) {
283
+ throw new GeneralError(PostgreSqlEntityStorageConnector.CLASS_NAME, "setFailed", {
284
+ id
285
+ }, err);
286
+ }
287
+ }
288
+ /**
289
+ * Remove the entity.
290
+ * @param id The id of the entity to remove.
291
+ * @param conditions The optional conditions to match for the entities.
292
+ * @returns Nothing.
293
+ */
294
+ async remove(id, conditions) {
295
+ Guards.stringValue(PostgreSqlEntityStorageConnector.CLASS_NAME, "id", id);
296
+ const contextIds = await ContextIdStore.getContextIds();
297
+ const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
298
+ try {
299
+ const dbConnection = await this.createConnection();
300
+ const itemData = await this.get(id);
301
+ if (Is.notEmpty(itemData)) {
302
+ const values = [];
303
+ const whereClauses = [];
304
+ whereClauses.push(`"${this._primaryKeyProperty.property}" = $${values.length + 1}`);
305
+ values.push(id);
306
+ whereClauses.push(`"${PostgreSqlEntityStorageConnector._PARTITION_KEY}" = $${values.length + 1}`);
307
+ values.push(partitionKey ?? PostgreSqlEntityStorageConnector._PARTITION_KEY_VALUE);
308
+ if (Is.arrayValue(conditions)) {
309
+ whereClauses.push(...conditions.map(condition => {
310
+ values.push(condition.value);
311
+ return `"${String(condition.property)}" = $${values.length}`;
312
+ }));
313
+ }
314
+ const query = `DELETE FROM "${this._config.tableName}" WHERE ${whereClauses.join(" AND ")}`;
315
+ await dbConnection.unsafe(query, values);
316
+ }
317
+ }
318
+ catch (err) {
319
+ throw new GeneralError(PostgreSqlEntityStorageConnector.CLASS_NAME, "removeFailed", {
320
+ id
321
+ }, err);
322
+ }
323
+ }
324
+ /**
325
+ * Find all the entities which match the conditions.
326
+ * @param conditions The conditions to match for the entities.
327
+ * @param sortProperties The optional sort order.
328
+ * @param properties The optional properties to return, defaults to all.
329
+ * @param cursor The cursor to request the next chunk of entities.
330
+ * @param limit The suggested number of entities to return in each chunk, in some scenarios can return a different amount.
331
+ * @returns All the entities for the storage matching the conditions,
332
+ * and a cursor which can be used to request more entities.
333
+ */
334
+ async query(conditions, sortProperties, properties, cursor, limit) {
335
+ const contextIds = await ContextIdStore.getContextIds();
336
+ const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
337
+ let sql = "";
338
+ try {
339
+ const returnSize = limit ?? PostgreSqlEntityStorageConnector._DEFAULT_LIMIT;
340
+ let orderByClause = "";
341
+ if (Is.arrayValue(sortProperties)) {
342
+ const orderClauses = [];
343
+ for (const sortProperty of sortProperties) {
344
+ const direction = sortProperty.sortDirection === SortDirection.Ascending ? "ASC" : "DESC";
345
+ orderClauses.push(`"${String(sortProperty.property)}" ${direction}`);
346
+ }
347
+ orderByClause = `ORDER BY ${orderClauses.join(", ")}`;
348
+ }
349
+ const whereClauses = [];
350
+ const values = [];
351
+ const finalConditions = {
352
+ conditions: [],
353
+ logicalOperator: LogicalOperator.And
354
+ };
355
+ finalConditions.conditions.push({
356
+ property: PostgreSqlEntityStorageConnector._PARTITION_KEY,
357
+ comparison: ComparisonOperator.Equals,
358
+ value: partitionKey ?? PostgreSqlEntityStorageConnector._PARTITION_KEY_VALUE
359
+ });
360
+ if (!Is.empty(conditions)) {
361
+ finalConditions.conditions.push(conditions);
362
+ }
363
+ this.buildQueryParameters("", finalConditions, whereClauses, values, 1);
364
+ const startIndex = Coerce.number(cursor) ?? 0;
365
+ sql = `SELECT ${properties ? properties.map(p => `"${String(p)}"`).join(", ") : "*"} FROM "${this._config.tableName}"`;
366
+ sql += ` WHERE ${whereClauses.join(" AND ")}`;
367
+ sql += ` ${orderByClause} LIMIT ${returnSize} OFFSET ${startIndex}`;
368
+ const dbConnection = await this.createConnection();
369
+ const rows = await dbConnection.unsafe(sql, values);
370
+ if (this._entitySchema.properties) {
371
+ for (const row of rows) {
372
+ for (const prop of this._entitySchema.properties) {
373
+ let propColumn = prop.property;
374
+ propColumn = propColumn.toLowerCase();
375
+ if ((prop.type === EntitySchemaPropertyType.Object ||
376
+ prop.type === EntitySchemaPropertyType.Array) &&
377
+ Is.string(row[propColumn])) {
378
+ let value;
379
+ try {
380
+ value = JSON.parse(row[propColumn]);
381
+ }
382
+ catch {
383
+ // If JSON.parse fails, keep the value as string
384
+ // This handles cases where plain text was stored in Object/Array fields
385
+ value = row[propColumn];
386
+ }
387
+ delete row[propColumn];
388
+ row[prop.property] = value;
389
+ }
390
+ if (row[propColumn] === null) {
391
+ row[prop.property] = undefined;
392
+ }
393
+ }
394
+ }
395
+ }
396
+ const entities = rows;
397
+ for (let i = 0; i < entities.length; i++) {
398
+ ObjectHelper.propertyDelete(entities[i], PostgreSqlEntityStorageConnector._PARTITION_KEY);
399
+ entities[i] = ObjectHelper.removeEmptyProperties(entities[i], { removeNull: true });
400
+ }
401
+ return {
402
+ entities,
403
+ cursor: Is.array(rows) && rows.length === returnSize
404
+ ? Coerce.string(startIndex + returnSize)
405
+ : undefined
406
+ };
407
+ }
408
+ catch (err) {
409
+ throw new GeneralError(PostgreSqlEntityStorageConnector.CLASS_NAME, "queryFailed", { sql }, err);
410
+ }
411
+ }
412
+ /**
413
+ * Drop the table.
414
+ * @returns Nothing.
415
+ */
416
+ async tableDrop() {
417
+ try {
418
+ const tableExists = await this.tableExists();
419
+ if (!tableExists) {
420
+ return;
421
+ }
422
+ const dbConnection = await this.createConnection();
423
+ await dbConnection.unsafe(`DROP TABLE "${this._config.tableName}";`);
424
+ await this.waitForTableNotExists();
425
+ }
426
+ catch {
427
+ // Ignore errors
428
+ }
429
+ }
430
+ /**
431
+ * Check if the database exists.
432
+ * @returns True if the database exists, false otherwise.
433
+ * @internal
434
+ */
435
+ async databaseExists() {
436
+ try {
437
+ const dbConnection = await this.createConnection();
438
+ const res = await dbConnection.unsafe(`SELECT datname FROM pg_catalog.pg_database WHERE datname = '${this._config.database}'`);
439
+ return res.length > 0;
440
+ }
441
+ catch {
442
+ return false;
443
+ }
444
+ }
445
+ /**
446
+ * Wait for a database to exist.
447
+ * @returns Nothing.
448
+ * @internal
449
+ */
450
+ async waitForDatabaseExists() {
451
+ for (let attempt = 0; attempt < 20; attempt++) {
452
+ const databaseExists = await this.databaseExists();
453
+ if (databaseExists) {
454
+ break;
455
+ }
456
+ await new Promise(resolve => setTimeout(resolve, 250));
457
+ }
458
+ }
459
+ /**
460
+ * Check if the table exists.
461
+ * @returns True if the table exists, false otherwise.
462
+ * @internal
463
+ */
464
+ async tableExists() {
465
+ try {
466
+ const dbConnection = await this.createConnection();
467
+ const tableExistsQuery = `SELECT to_regclass('${this._config.tableName}')`;
468
+ const tableExistsResult = await dbConnection.unsafe(tableExistsQuery);
469
+ return tableExistsResult[0].to_regclass !== null;
470
+ }
471
+ catch {
472
+ return false;
473
+ }
474
+ }
475
+ /**
476
+ * Wait for a table to exist.
477
+ * @returns Nothing.
478
+ * @internal
479
+ */
480
+ async waitForTableExists() {
481
+ for (let attempt = 0; attempt < 20; attempt++) {
482
+ const tableExists = await this.tableExists();
483
+ if (tableExists) {
484
+ break;
485
+ }
486
+ await new Promise(resolve => setTimeout(resolve, 250));
487
+ }
488
+ }
489
+ /**
490
+ * Wait for a table to not exist.
491
+ * @returns Nothing.
492
+ * @internal
493
+ */
494
+ async waitForTableNotExists() {
495
+ for (let attempt = 0; attempt < 20; attempt++) {
496
+ const tableExists = await this.tableExists();
497
+ if (!tableExists) {
498
+ break;
499
+ }
500
+ await new Promise(resolve => setTimeout(resolve, 250));
501
+ }
502
+ }
503
+ /**
504
+ * Create a new DB connection.
505
+ * @returns The PostgreSql connection.
506
+ * @internal
507
+ */
508
+ async createConnection() {
509
+ if (Is.empty(this._connection)) {
510
+ this._connection = postgres(this.createConnectionConfig());
511
+ }
512
+ return this._connection;
513
+ }
514
+ /**
515
+ * Create a new DB connection configuration.
516
+ * @returns The PostgreSql connection configuration.
517
+ * @internal
518
+ */
519
+ createConnectionConfig() {
520
+ return {
521
+ host: this._config.host,
522
+ port: this._config.port ?? 5432,
523
+ user: this._config.user,
524
+ password: this._config.password
525
+ };
526
+ }
527
+ /**
528
+ * Create an SQL condition clause.
529
+ * @param objectPath The path for the nested object.
530
+ * @param condition The conditions to create the query from.
531
+ * @param whereClauses The where clauses to use in the query.
532
+ * @param values The values to use in the query.
533
+ * @param valueIndex The current value index.
534
+ * @internal
535
+ */
536
+ buildQueryParameters(objectPath, condition, whereClauses, values, valueIndex) {
537
+ if (Is.undefined(condition)) {
538
+ return;
539
+ }
540
+ if ("conditions" in condition) {
541
+ if (condition.conditions.length === 0) {
542
+ return;
543
+ }
544
+ const joinConditions = condition.conditions.map(c => {
545
+ const subWhereClauses = [];
546
+ const subValues = [];
547
+ this.buildQueryParameters(objectPath, c, subWhereClauses, subValues, valueIndex);
548
+ values.push(...subValues);
549
+ valueIndex += subValues.length;
550
+ return subWhereClauses.join(" AND ");
551
+ });
552
+ const logicalOperator = this.mapConditionalOperator(condition.logicalOperator);
553
+ const queryClause = joinConditions.filter(j => j.length > 0).join(` ${logicalOperator} `);
554
+ if (queryClause.length > 0) {
555
+ whereClauses.push(`(${queryClause})`);
556
+ }
557
+ return;
558
+ }
559
+ const schemaProp = this._entitySchema.properties?.find(p => p.property === condition.property);
560
+ const comparison = this.mapComparisonOperator(objectPath, condition, schemaProp?.type, values, valueIndex);
561
+ whereClauses.push(comparison);
562
+ }
563
+ /**
564
+ * Map the framework comparison operators to those in MySQL.
565
+ * @param objectPath The prefix to use for the condition.
566
+ * @param comparator The operator to map.
567
+ * @param type The type of the property.
568
+ * @param values The values to use in the query.
569
+ * @param valueIndex The current value index.
570
+ * @returns The comparison expression.
571
+ * @throws GeneralError if the comparison operator is not supported.
572
+ * @internal
573
+ */
574
+ mapComparisonOperator(objectPath, comparator, type, values, valueIndex) {
575
+ let prop = objectPath;
576
+ if (prop.length > 0) {
577
+ prop += ".";
578
+ }
579
+ prop += comparator.property;
580
+ if (comparator.comparison === ComparisonOperator.In) {
581
+ const inValues = Is.array(comparator.value) ? comparator.value : [comparator.value];
582
+ values.push(...inValues.map(val => this.propertyToDbValue(val, type)));
583
+ const placeholders = inValues.map((_, index) => `$${valueIndex + index}`).join(", ");
584
+ return `"${prop}" IN (${placeholders})`;
585
+ }
586
+ const dbValue = this.propertyToDbValue(comparator.value, type);
587
+ values.push(dbValue);
588
+ if (comparator.property.split(".").length > 1) {
589
+ const jsonPath = comparator.property
590
+ .split(".")
591
+ .slice(1)
592
+ .map((p, i, arr) => (i === arr.length - 1 ? `->> '${p}'` : `-> '${p}'`))
593
+ .join("");
594
+ return `("${comparator.property.split(".")[0]}"::jsonb ${jsonPath}) = $${valueIndex}`;
595
+ }
596
+ else if (comparator.comparison === ComparisonOperator.Equals) {
597
+ return `"${prop}" = $${valueIndex}`;
598
+ }
599
+ else if (comparator.comparison === ComparisonOperator.NotEquals) {
600
+ return `"${prop}" <> $${valueIndex}`;
601
+ }
602
+ else if (comparator.comparison === ComparisonOperator.GreaterThan) {
603
+ return `"${prop}" > $${valueIndex}`;
604
+ }
605
+ else if (comparator.comparison === ComparisonOperator.LessThan) {
606
+ return `"${prop}" < $${valueIndex}`;
607
+ }
608
+ else if (comparator.comparison === ComparisonOperator.GreaterThanOrEqual) {
609
+ return `"${prop}" >= $${valueIndex}`;
610
+ }
611
+ else if (comparator.comparison === ComparisonOperator.LessThanOrEqual) {
612
+ return `"${prop}" <= $${valueIndex}`;
613
+ }
614
+ else if (comparator.comparison === ComparisonOperator.Includes) {
615
+ return `EXISTS (SELECT 1 FROM jsonb_array_elements("${prop}") elem WHERE elem @> $${valueIndex}::jsonb)`;
616
+ }
617
+ throw new GeneralError(PostgreSqlEntityStorageConnector.CLASS_NAME, "comparisonNotSupported", {
618
+ comparison: comparator.comparison
619
+ });
620
+ }
621
+ /**
622
+ * Format a value to insert into DB.
623
+ * @param value The value to format.
624
+ * @param type The type for the property.
625
+ * @returns The value after conversion.
626
+ * @internal
627
+ */
628
+ propertyToDbValue(value, type) {
629
+ if (type === "string") {
630
+ return String(value);
631
+ }
632
+ else if (type === "number") {
633
+ return Number(value);
634
+ }
635
+ else if (type === "boolean") {
636
+ return Boolean(value);
637
+ }
638
+ else if (type === "array") {
639
+ return value;
640
+ }
641
+ if (Is.object(value)) {
642
+ return JSON.stringify(value);
643
+ }
644
+ return value;
645
+ }
646
+ /**
647
+ * Map the framework conditional operators to those in MySQL.
648
+ * @param operator The operator to map.
649
+ * @returns The conditional operator.
650
+ * @throws GeneralError if the conditional operator is not supported.
651
+ * @internal
652
+ */
653
+ mapConditionalOperator(operator) {
654
+ if ((operator ?? LogicalOperator.And) === LogicalOperator.And) {
655
+ return "AND";
656
+ }
657
+ else if (operator === LogicalOperator.Or) {
658
+ return "OR";
659
+ }
660
+ throw new GeneralError(PostgreSqlEntityStorageConnector.CLASS_NAME, "conditionalNotSupported", {
661
+ operator
662
+ });
663
+ }
664
+ /**
665
+ * Verify the conditions for the entity.
666
+ * @param conditions The conditions to verify.
667
+ * @internal
668
+ */
669
+ verifyConditions(conditions, obj) {
670
+ return conditions.every(condition => ObjectHelper.propertyGet(obj, condition.property) === condition.value);
671
+ }
672
+ /**
673
+ * Map entity schema properties to SQL properties.
674
+ * @param entitySchema The schema of the entity.
675
+ * @returns The SQL properties as a string.
676
+ * @throws GeneralError if the entity properties do not exist.
677
+ */
678
+ mapPostgreSqlProperties(entitySchema) {
679
+ const sqlTypeMap = {
680
+ [EntitySchemaPropertyType.String]: "TEXT",
681
+ [EntitySchemaPropertyType.Number]: "REAL",
682
+ [EntitySchemaPropertyType.Integer]: "INTEGER",
683
+ [EntitySchemaPropertyType.Object]: "JSONB",
684
+ [EntitySchemaPropertyType.Array]: "JSONB",
685
+ [EntitySchemaPropertyType.Boolean]: "BOOLEAN"
686
+ };
687
+ if (!entitySchema.properties) {
688
+ throw new GeneralError(PostgreSqlEntityStorageConnector.CLASS_NAME, "entitySchemaPropertiesUndefined");
689
+ }
690
+ const primaryKeys = [];
691
+ const props = [...entitySchema.properties];
692
+ props.unshift({
693
+ property: PostgreSqlEntityStorageConnector._PARTITION_KEY,
694
+ type: EntitySchemaPropertyType.String,
695
+ optional: false,
696
+ isPrimary: true
697
+ });
698
+ const columnDefinitions = props
699
+ .map(prop => {
700
+ const sqlType = sqlTypeMap[prop.type] || "TEXT";
701
+ const columnName = String(prop.property);
702
+ const nullable = prop.optional ? " NULL" : " NOT NULL";
703
+ if (prop.isPrimary) {
704
+ primaryKeys.push(columnName);
705
+ }
706
+ return `"${columnName}" ${sqlType}${nullable}`;
707
+ })
708
+ .join(", ");
709
+ const primaryKeyDefinition = primaryKeys.length > 0 ? `, PRIMARY KEY ("${primaryKeys.join('", "')}")` : "";
710
+ return columnDefinitions + primaryKeyDefinition;
711
+ }
712
+ }
713
+ //# sourceMappingURL=postgreSqlEntityStorageConnector.js.map