@twin.org/entity-storage-connector-scylladb 0.0.3-next.3 → 0.0.3-next.30

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # Entity Storage Connector ScyllaDB
2
2
 
3
- Entity Storage connector implementation using ScyllaDB.
3
+ This package provides a ScyllaDB backend for distributed workloads that need low latency and high throughput. 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,12 @@ Entity Storage connector implementation using ScyllaDB.
8
8
  npm install @twin.org/entity-storage-connector-scylladb
9
9
  ```
10
10
 
11
- ## Testing
11
+ ## Docker
12
12
 
13
- The tests developed are functional tests and need an instance of ScyllaDB up and running. To run ScyllaDB locally:
13
+ To perform testing of this component it may be necessary to launch a local instance to communicate with.
14
14
 
15
15
  ```shell
16
- docker run -p 9500:9042 --name twin-entity-storage-scylladb --hostname scylla -d scylladb/scylla:5.4.9 --smp 1
17
- ```
18
-
19
- Afterwards you can run the tests as follows:
20
-
21
- ```shell
22
- npm run test
16
+ docker run -d --name twin-entity-storage-scylladb -p 9042:9042 scylladb/scylla:5.4.9
23
17
  ```
24
18
 
25
19
  ## Examples
@@ -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 { Coerce, ComponentFactory, GeneralError, Guards, Is, ObjectHelper, StringHelper } from "@twin.org/core";
4
+ import { Coerce, ComponentFactory, GeneralError, Guards, Is, Validation } from "@twin.org/core";
5
5
  import { ComparisonOperator, EntitySchemaFactory, EntitySchemaHelper, LogicalOperator, SortDirection } from "@twin.org/entity";
6
+ import { EntityStorageHelper } from "@twin.org/entity-storage-models";
6
7
  import { types as CassandraTypes, Client } from "cassandra-driver";
7
8
  /**
8
9
  * Store entities using ScyllaDB.
@@ -26,7 +27,7 @@ export class AbstractScyllaDBConnector {
26
27
  * Limit the number of entities when finding.
27
28
  * @internal
28
29
  */
29
- static _DEFAULT_LIMIT = 40;
30
+ static DEFAULT_LIMIT = 40;
30
31
  /**
31
32
  * The name of the database table.
32
33
  * @internal
@@ -57,6 +58,13 @@ export class AbstractScyllaDBConnector {
57
58
  * @internal
58
59
  */
59
60
  _primaryKey;
61
+ /**
62
+ * Cached persistent client (keyspace-scoped). Reused across all operations on this
63
+ * connector instance so the expensive cassandra-driver `connect()` only runs once.
64
+ * Closed by `closePersistentClient()` which callers (e.g. `teardown()`) must invoke.
65
+ * @internal
66
+ */
67
+ _persistentClient;
60
68
  /**
61
69
  * Create a new instance of AbstractScyllaDBConnector.
62
70
  * @param options The options for the connector.
@@ -72,12 +80,12 @@ export class AbstractScyllaDBConnector {
72
80
  Guards.arrayValue(AbstractScyllaDBConnector.CLASS_NAME, "options.config.hosts", options.config.hosts);
73
81
  Guards.stringValue(AbstractScyllaDBConnector.CLASS_NAME, "options.config.localDataCenter", options.config.localDataCenter);
74
82
  Guards.stringValue(AbstractScyllaDBConnector.CLASS_NAME, "options.config.keyspace", options.config.keyspace);
75
- this._logging = ComponentFactory.getIfExists(options.loggingComponentType ?? "logging");
83
+ this._logging = ComponentFactory.getIfExists(options.loggingComponentType);
76
84
  this._entitySchema = EntitySchemaFactory.get(options.entitySchema);
77
85
  this._partitionContextIds = options.partitionContextIds;
78
86
  this._primaryKey = EntitySchemaHelper.getPrimaryKey(this._entitySchema);
79
87
  this._config = options.config;
80
- this._fullTableName = StringHelper.camelCase(Is.stringValue(options.config.tableName) ? options.config.tableName : options.entitySchema);
88
+ this._fullTableName = options.config.tableName;
81
89
  }
82
90
  /**
83
91
  * Returns the class name of the component.
@@ -117,7 +125,7 @@ export class AbstractScyllaDBConnector {
117
125
  value: id
118
126
  });
119
127
  const { sqlCondition, conditionValues } = this.buildConditions(conditions);
120
- let sql = `SELECT * FROM "${this._fullTableName}" WHERE ${sqlCondition}`;
128
+ let sql = `SELECT * FROM "${this.safeTableName(this._fullTableName)}" WHERE ${sqlCondition}`;
121
129
  if (secondaryIndex) {
122
130
  sql += " ALLOW FILTERING";
123
131
  }
@@ -157,100 +165,51 @@ export class AbstractScyllaDBConnector {
157
165
  let connection;
158
166
  const contextIds = await ContextIdStore.getContextIds();
159
167
  const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
168
+ EntityStorageHelper.validateSortProperties(this._entitySchema, sortProperties);
169
+ EntityStorageHelper.validateProperties(this._entitySchema, properties);
170
+ if (!Is.empty(limit)) {
171
+ const validationFailures = [];
172
+ Validation.integer("limit", limit, validationFailures, undefined, { minValue: 1 });
173
+ Validation.asValidationError(AbstractScyllaDBConnector.CLASS_NAME, "query", validationFailures);
174
+ }
175
+ // CQL ORDER BY is only valid on clustering columns (isPrimary). Secondary-index
176
+ // properties cannot be used; throw before reaching the try-catch so the error
177
+ // surfaces directly to the caller without being wrapped as findFailed.
178
+ if (Is.arrayValue(sortProperties)) {
179
+ for (const sortProperty of sortProperties) {
180
+ const propertySchema = this._entitySchema.properties?.find(p => p.property === sortProperty.property);
181
+ if (!propertySchema?.isPrimary) {
182
+ throw new GeneralError(AbstractScyllaDBConnector.CLASS_NAME, "sortOnlyPrimaryKey", {
183
+ property: sortProperty.property
184
+ });
185
+ }
186
+ }
187
+ }
188
+ // Validates and throws for unsupported conditions before entering the try-catch
189
+ // so that comparisonNotSupported errors surface directly to the caller.
190
+ const { whereClause, params, noResults } = this.buildCqlConditions(conditions, partitionKey);
191
+ if (noResults) {
192
+ return { entities: [], cursor: undefined };
193
+ }
160
194
  try {
161
- let returnSize = limit ?? AbstractScyllaDBConnector._DEFAULT_LIMIT;
162
- let sql = `SELECT * FROM "${this._fullTableName}"`;
195
+ const returnSize = limit ?? AbstractScyllaDBConnector.DEFAULT_LIMIT;
196
+ let sql = `SELECT * FROM "${this.safeTableName(this._fullTableName)}"`;
163
197
  if (Is.array(properties)) {
164
198
  const fields = [];
165
199
  for (const property of properties) {
166
200
  fields.push(property.toString());
167
201
  }
168
- const selectFields = fields.join(",");
169
- sql = sql.replace("*", selectFields);
170
- }
171
- const conds = [];
172
- let conditionQuery = "";
173
- // The params to be used to execute the query
174
- const params = [];
175
- let finalConditionQuery = `"${AbstractScyllaDBConnector.PARTITION_KEY}" = ?`;
176
- params.push(partitionKey ?? AbstractScyllaDBConnector.PARTITION_KEY_VALUE);
177
- let theConditions = [];
178
- if (!Is.undefined(conditions)) {
179
- if ("conditions" in conditions) {
180
- theConditions = conditions.conditions;
181
- }
182
- else {
183
- theConditions.push(conditions);
184
- }
185
- }
186
- // TODO: This code needs refactoring to support conditions for sub properties.
187
- for (const cond of theConditions) {
188
- const condition = cond;
189
- const descriptor = this._entitySchema.properties?.find(p => p.property === condition.property);
190
- if (condition.comparison === ComparisonOperator.Includes ||
191
- condition.comparison === ComparisonOperator.NotIncludes) {
192
- const propValue = `'%${condition.value?.toString()}%'`;
193
- if (condition.comparison === ComparisonOperator.Includes) {
194
- conds.push(`"${condition.property}" LIKE ${propValue}`);
195
- }
196
- else if (condition.comparison === ComparisonOperator.NotIncludes) {
197
- conds.push(`"${condition.property}" NOT LIKE ${propValue}`);
198
- }
199
- }
200
- else if (condition.comparison === ComparisonOperator.In) {
201
- let value = [];
202
- if (!Is.arrayValue(condition.value)) {
203
- value.push(this.propertyToDbValue(condition.value, descriptor));
204
- }
205
- else {
206
- value = condition.value.map(v => this.propertyToDbValue(v, descriptor));
207
- }
208
- params.push(value);
209
- conds.push(`"${condition.property}" IN ?`);
210
- }
211
- else {
212
- const propValue = condition.value;
213
- params.push(propValue);
214
- if (condition.comparison === ComparisonOperator.Equals) {
215
- conds.push(`"${condition.property}" = ?`);
216
- }
217
- else if (condition.comparison === ComparisonOperator.NotEquals) {
218
- conds.push(`"${condition.property}" <> ?`);
219
- }
220
- else if (condition.comparison === ComparisonOperator.GreaterThan) {
221
- conds.push(`"${condition.property}" > ?`);
222
- }
223
- else if (condition.comparison === ComparisonOperator.LessThan) {
224
- conds.push(`"${condition.property}" < ?`);
225
- }
226
- else if (condition.comparison === ComparisonOperator.GreaterThanOrEqual) {
227
- conds.push(`"${condition.property}" >= ?`);
228
- }
229
- else if (condition.comparison === ComparisonOperator.LessThanOrEqual) {
230
- conds.push(`"${condition.property}" <= ?`);
231
- }
232
- }
233
- const operator = conditions.logicalOperator ?? LogicalOperator.And;
234
- conditionQuery = `${conds.join(` ${operator} `)}`;
202
+ sql = sql.replace("*", fields.join(","));
235
203
  }
236
- if (conditionQuery.length > 0) {
237
- finalConditionQuery += ` AND (${conditionQuery})`;
238
- }
239
- sql += ` WHERE ${finalConditionQuery}`;
240
- connection = await this.openConnection();
241
- // TODO: Only supported one sort property at the moment. This code would need to be revised in a follow-up
204
+ sql += ` WHERE ${whereClause}`;
242
205
  if (Is.array(sortProperties) && sortProperties.length >= 1) {
243
- const sortKey = sortProperties[0].property ?? this._primaryKey.property;
244
- const sortDir = sortProperties[0].sortDirection ??
245
- this._entitySchema.properties?.find(e => e.property === sortKey)?.sortDirection;
246
- let sqlSortDir = "asc";
247
- if (sortDir === SortDirection.Descending) {
248
- sqlSortDir = "desc";
249
- }
250
- sql += ` ORDER BY "${String(sortKey)}" ${sqlSortDir.toUpperCase()}`;
251
- // Disabling paging in order by situations
252
- returnSize = 0;
206
+ const orderClauses = sortProperties.map(sp => {
207
+ const dir = sp.sortDirection === SortDirection.Descending ? "DESC" : "ASC";
208
+ return `"${String(sp.property)}" ${dir}`;
209
+ });
210
+ sql += ` ORDER BY ${orderClauses.join(", ")}`;
253
211
  }
212
+ connection = await this.openConnection();
254
213
  sql += " ALLOW FILTERING";
255
214
  await this._logging?.log({
256
215
  level: "info",
@@ -264,13 +223,49 @@ export class AbstractScyllaDBConnector {
264
223
  for (const row of result.rows) {
265
224
  entities.push(this.convertRowToObject(this._entitySchema.properties, row));
266
225
  }
226
+ // ScyllaDB may return a pageState even when the current page is the last one
227
+ // (when rows.length == fetchSize). Peek at the next page to verify there are
228
+ // actually more rows before surfacing the cursor to the caller.
229
+ let nextCursor;
230
+ if (returnSize > 0 && result.rows.length >= returnSize && Is.stringValue(result.pageState)) {
231
+ const peek = await this.queryDB(connection, sql, params, result.pageState, 1);
232
+ if (peek.rows.length > 0) {
233
+ nextCursor = result.pageState;
234
+ }
235
+ }
267
236
  return {
268
237
  entities,
269
- cursor: Is.stringValue(result.pageState) ? result.pageState : undefined
238
+ cursor: nextCursor
270
239
  };
271
240
  }
272
241
  catch (error) {
273
- throw new GeneralError(AbstractScyllaDBConnector.CLASS_NAME, "findFailed", { table: this._fullTableName }, error);
242
+ throw new GeneralError(AbstractScyllaDBConnector.CLASS_NAME, "findFailed", { table: this.safeTableName(this._fullTableName) }, error);
243
+ }
244
+ finally {
245
+ await this.closeConnection(connection);
246
+ }
247
+ }
248
+ /**
249
+ * Count all the entities which match the conditions.
250
+ * @param conditions The optional conditions to match for the entities.
251
+ * @returns The total count of entities in the storage.
252
+ */
253
+ async count(conditions) {
254
+ let connection;
255
+ try {
256
+ const contextIds = await ContextIdStore.getContextIds();
257
+ const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
258
+ const { whereClause, params, noResults } = this.buildCqlConditions(conditions, partitionKey);
259
+ if (noResults) {
260
+ return 0;
261
+ }
262
+ const sql = `SELECT COUNT(*) FROM "${this.safeTableName(this._fullTableName)}" WHERE ${whereClause} ALLOW FILTERING`;
263
+ connection = await this.openConnection();
264
+ const result = await this.queryDB(connection, sql, params);
265
+ return Number(result.rows[0]?.get("count") ?? 0);
266
+ }
267
+ catch (err) {
268
+ throw new GeneralError(AbstractScyllaDBConnector.CLASS_NAME, "countFailed", undefined, err);
274
269
  }
275
270
  finally {
276
271
  await this.closeConnection(connection);
@@ -278,12 +273,16 @@ export class AbstractScyllaDBConnector {
278
273
  }
279
274
  /**
280
275
  * Open a new database connection.
281
- * @param config The config for the connection.
282
276
  * @param skipKeySpace Don't include the keyspace in the connection.
283
277
  * @returns The new connection.
284
278
  * @internal
285
279
  */
286
280
  async openConnection(skipKeySpace = false) {
281
+ // Reuse the cached keyspace-scoped client when available (avoids repeated
282
+ // cassandra-driver cluster-discovery on every operation).
283
+ if (!skipKeySpace && this._persistentClient !== undefined) {
284
+ return this._persistentClient;
285
+ }
287
286
  const client = new Client({
288
287
  contactPoints: this._config.hosts,
289
288
  localDataCenter: this._config.localDataCenter,
@@ -293,25 +292,48 @@ export class AbstractScyllaDBConnector {
293
292
  }
294
293
  });
295
294
  await client.connect();
295
+ if (!skipKeySpace) {
296
+ this._persistentClient = client;
297
+ }
296
298
  return client;
297
299
  }
298
300
  /**
299
301
  * Close database connection.
302
+ * When `connection` is the cached persistent client it is kept alive so it
303
+ * can be reused by future operations; call `closePersistentClient()` to
304
+ * explicitly shut it down (e.g. from `teardown()`).
300
305
  * @param connection The connection to close.
306
+ * @returns Nothing.
301
307
  * @internal
302
308
  */
303
309
  async closeConnection(connection) {
304
310
  if (!connection) {
305
311
  return;
306
312
  }
313
+ if (connection === this._persistentClient) {
314
+ // Keep the persistent client alive for reuse.
315
+ return;
316
+ }
307
317
  return connection.shutdown();
308
318
  }
319
+ /**
320
+ * Shut down and clear the persistent client. Call this from `teardown()`
321
+ * implementations to release the underlying TCP connection.
322
+ * @internal
323
+ */
324
+ async closePersistentClient() {
325
+ if (this._persistentClient !== undefined) {
326
+ await this._persistentClient.shutdown();
327
+ this._persistentClient = undefined;
328
+ }
329
+ }
309
330
  /**
310
331
  * Query the database.
311
332
  * @param connection The connection to query.
312
333
  * @param sql The sql statement to execute.
313
334
  * @param params The params to use when executing the query.
314
- * @param state The state to use when it comes to pagination.
335
+ * @param pageState The page state to use when it comes to pagination.
336
+ * @param limit The maximum number of rows to return.
315
337
  * @returns The rows.
316
338
  * @internal
317
339
  */
@@ -321,7 +343,7 @@ export class AbstractScyllaDBConnector {
321
343
  connection.eachRow(sql, params, {
322
344
  prepare: true,
323
345
  autoPage: false,
324
- fetchSize: limit ?? AbstractScyllaDBConnector._DEFAULT_LIMIT,
346
+ fetchSize: limit ?? AbstractScyllaDBConnector.DEFAULT_LIMIT,
325
347
  pageState
326
348
  }, (n, row) => {
327
349
  rows.push(row);
@@ -339,6 +361,8 @@ export class AbstractScyllaDBConnector {
339
361
  * Execute on the database.
340
362
  * @param connection The connection to execute.
341
363
  * @param sql The sql statement to execute.
364
+ * @param params The optional params to use when executing the statement.
365
+ * @returns The result set.
342
366
  * @internal
343
367
  */
344
368
  async execute(connection, sql, params) {
@@ -348,6 +372,7 @@ export class AbstractScyllaDBConnector {
348
372
  * Create keyspace if it doesn't exist.
349
373
  * @param connection The connection to perform the query with.
350
374
  * @param keyspaceName The name of the keyspace to create.
375
+ * @returns The result set.
351
376
  * @internal
352
377
  */
353
378
  async createKeyspace(connection, keyspaceName) {
@@ -392,6 +417,7 @@ export class AbstractScyllaDBConnector {
392
417
  * @param value The value to convert to original form.
393
418
  * @param fieldDescriptor The descriptor for the field.
394
419
  * @returns The value as a property for the object.
420
+ * @throws GeneralError if parsing JSON fails.
395
421
  * @internal
396
422
  */
397
423
  dbValueToProperty(value, fieldDescriptor) {
@@ -436,9 +462,9 @@ export class AbstractScyllaDBConnector {
436
462
  return value;
437
463
  }
438
464
  /**
439
- * Format a value for the DB. As the driver takes care of conversion from Javascript
465
+ * Format a value for the DB.
440
466
  * @param value The value to format.
441
- * @param fieldDescriptor The descriptor for the field
467
+ * @param fieldDescriptor The descriptor for the field.
442
468
  * @returns The value after conversion.
443
469
  * @internal
444
470
  */
@@ -475,7 +501,7 @@ export class AbstractScyllaDBConnector {
475
501
  obj[field.property] = this.dbValueToProperty(value, field);
476
502
  }
477
503
  }
478
- return ObjectHelper.removeEmptyProperties(obj, { removeNull: true });
504
+ return EntityStorageHelper.unPrepareEntity(obj, [AbstractScyllaDBConnector.PARTITION_KEY]);
479
505
  }
480
506
  /**
481
507
  * Wrap a string for DB format.
@@ -541,5 +567,131 @@ export class AbstractScyllaDBConnector {
541
567
  }
542
568
  return { sqlCondition: sqlConditions.join(" AND "), conditionValues };
543
569
  }
570
+ /**
571
+ * Get a safe table name by replacing any non-alphanumeric characters.
572
+ * @param name The name to sanitize.
573
+ * @returns The safe table name.
574
+ */
575
+ safeTableName(name) {
576
+ return name.replace(/[^\dA-Za-z]/g, "");
577
+ }
578
+ /**
579
+ * Parse, validate, and build a CQL WHERE clause from an EntityCondition tree.
580
+ * The partition key equality is always the first clause; user conditions follow.
581
+ * @param conditions The optional conditions to match for the entities.
582
+ * @param partitionKey The partition key value to filter by.
583
+ * @returns The complete WHERE clause (without the WHERE keyword) and bound params.
584
+ * @throws GeneralError if OR conditions, dot-notation paths, null comparisons, NotEquals, or NotIncludes operators are used.
585
+ * @internal
586
+ */
587
+ buildCqlConditions(conditions, partitionKey) {
588
+ let conditionsList = [];
589
+ if (conditions !== undefined) {
590
+ if ("conditions" in conditions) {
591
+ if (conditions.logicalOperator === LogicalOperator.Or) {
592
+ throw new GeneralError(AbstractScyllaDBConnector.CLASS_NAME, "orConditionNotSupported");
593
+ }
594
+ conditionsList = conditions.conditions;
595
+ }
596
+ else {
597
+ conditionsList = [conditions];
598
+ }
599
+ }
600
+ for (const cond of conditionsList) {
601
+ const comparator = cond;
602
+ if (String(comparator.property).includes(".")) {
603
+ throw new GeneralError(AbstractScyllaDBConnector.CLASS_NAME, "comparisonNotSupported", {
604
+ property: comparator.property,
605
+ reason: "dot-notation nested property paths are not supported in CQL"
606
+ });
607
+ }
608
+ if ((comparator.comparison === ComparisonOperator.Equals ||
609
+ comparator.comparison === ComparisonOperator.NotEquals) &&
610
+ (comparator.value === null || comparator.value === undefined)) {
611
+ throw new GeneralError(AbstractScyllaDBConnector.CLASS_NAME, "comparisonNotSupported", {
612
+ property: comparator.property,
613
+ reason: "null/undefined comparisons are not supported in CQL WHERE clauses"
614
+ });
615
+ }
616
+ if (comparator.comparison === ComparisonOperator.NotEquals) {
617
+ throw new GeneralError(AbstractScyllaDBConnector.CLASS_NAME, "notEqualsNotSupported", {
618
+ property: comparator.property
619
+ });
620
+ }
621
+ if (comparator.comparison === ComparisonOperator.NotIncludes) {
622
+ throw new GeneralError(AbstractScyllaDBConnector.CLASS_NAME, "notIncludesNotSupported", {
623
+ property: comparator.property
624
+ });
625
+ }
626
+ }
627
+ const conds = [];
628
+ const params = [partitionKey ?? AbstractScyllaDBConnector.PARTITION_KEY_VALUE];
629
+ for (const cond of conditionsList) {
630
+ const condition = cond;
631
+ const descriptor = this._entitySchema.properties?.find(p => p.property === condition.property);
632
+ if (condition.comparison === ComparisonOperator.Includes ||
633
+ condition.comparison === ComparisonOperator.NotIncludes) {
634
+ const serialized = this.propertyToDbValue(condition.value, descriptor);
635
+ const propValue = `'%${Is.stringValue(serialized) ? serialized : ""}%'`;
636
+ if (condition.comparison === ComparisonOperator.Includes) {
637
+ conds.push(`"${condition.property}" LIKE ${propValue}`);
638
+ }
639
+ else if (condition.comparison === ComparisonOperator.NotIncludes) {
640
+ conds.push(`"${condition.property}" NOT LIKE ${propValue}`);
641
+ }
642
+ }
643
+ else if (condition.comparison === ComparisonOperator.In) {
644
+ // Guard must come first: Is.arrayValue([]) returns false for an empty array,
645
+ // so an empty value would be wrapped as a single element below and bypass
646
+ // the length check. Check Is.array (true for any array) before branching (#141).
647
+ if (Is.array(condition.value) && condition.value.length === 0) {
648
+ return {
649
+ whereClause: "",
650
+ params: [],
651
+ noResults: true
652
+ };
653
+ }
654
+ let value = [];
655
+ if (!Is.arrayValue(condition.value)) {
656
+ value.push(this.propertyToDbValue(condition.value, descriptor));
657
+ }
658
+ else {
659
+ value = condition.value.map(v => this.propertyToDbValue(v, descriptor));
660
+ }
661
+ params.push(value);
662
+ conds.push(`"${condition.property}" IN ?`);
663
+ }
664
+ else {
665
+ const propValue = this.propertyToDbValue(condition.value, descriptor);
666
+ params.push(propValue);
667
+ if (condition.comparison === ComparisonOperator.Equals) {
668
+ conds.push(`"${condition.property}" = ?`);
669
+ }
670
+ else if (condition.comparison === ComparisonOperator.NotEquals) {
671
+ conds.push(`"${condition.property}" != ?`);
672
+ }
673
+ else if (condition.comparison === ComparisonOperator.GreaterThan) {
674
+ conds.push(`"${condition.property}" > ?`);
675
+ }
676
+ else if (condition.comparison === ComparisonOperator.LessThan) {
677
+ conds.push(`"${condition.property}" < ?`);
678
+ }
679
+ else if (condition.comparison === ComparisonOperator.GreaterThanOrEqual) {
680
+ conds.push(`"${condition.property}" >= ?`);
681
+ }
682
+ else if (condition.comparison === ComparisonOperator.LessThanOrEqual) {
683
+ conds.push(`"${condition.property}" <= ?`);
684
+ }
685
+ }
686
+ }
687
+ const operator = "conditions" in (conditions ?? {})
688
+ ? (conditions.logicalOperator ?? LogicalOperator.And)
689
+ : LogicalOperator.And;
690
+ let whereClause = `"${AbstractScyllaDBConnector.PARTITION_KEY}" = ?`;
691
+ if (conds.length > 0) {
692
+ whereClause += ` AND ${conds.join(` ${operator} `)}`;
693
+ }
694
+ return { whereClause, params };
695
+ }
544
696
  }
545
697
  //# sourceMappingURL=abstractScyllaDBConnector.js.map