@twin.org/entity-storage-connector-scylladb 0.0.3-next.9 → 0.9.0

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
@@ -13,7 +13,7 @@ npm install @twin.org/entity-storage-connector-scylladb
13
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 -d --name twin-entity-storage-scylladb -p 9500:9042 scylladb/scylla:5.4.9
16
+ docker run -d --name twin-entity-storage-scylladb -p 9042:9042 scylladb/scylla:5.4.9
17
17
  ```
18
18
 
19
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,118 +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);
160
- let conditionsList = [];
161
- if (conditions !== undefined) {
162
- if ("conditions" in conditions) {
163
- conditionsList = conditions.conditions;
164
- }
165
- else {
166
- conditionsList = [conditions];
167
- }
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);
168
174
  }
169
- // Validate conditions before entering the try-catch so that
170
- // comparisonNotSupported errors surface directly to the caller.
171
- for (const cond of conditionsList) {
172
- const comparator = cond;
173
- if (String(comparator.property).includes(".")) {
174
- throw new GeneralError(AbstractScyllaDBConnector.CLASS_NAME, "comparisonNotSupported", {
175
- property: comparator.property,
176
- reason: "dot-notation nested property paths are not supported in CQL"
177
- });
178
- }
179
- if ((comparator.comparison === ComparisonOperator.Equals ||
180
- comparator.comparison === ComparisonOperator.NotEquals) &&
181
- (comparator.value === null || comparator.value === undefined)) {
182
- throw new GeneralError(AbstractScyllaDBConnector.CLASS_NAME, "comparisonNotSupported", {
183
- property: comparator.property,
184
- reason: "null/undefined comparisons are not supported in CQL WHERE clauses"
185
- });
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
186
  }
187
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
+ }
188
194
  try {
189
- let returnSize = limit ?? AbstractScyllaDBConnector._DEFAULT_LIMIT;
190
- let sql = `SELECT * FROM "${this._fullTableName}"`;
195
+ const returnSize = limit ?? AbstractScyllaDBConnector.DEFAULT_LIMIT;
196
+ let sql = `SELECT * FROM "${this.safeTableName(this._fullTableName)}"`;
191
197
  if (Is.array(properties)) {
192
198
  const fields = [];
193
199
  for (const property of properties) {
194
200
  fields.push(property.toString());
195
201
  }
196
- const selectFields = fields.join(",");
197
- sql = sql.replace("*", selectFields);
202
+ sql = sql.replace("*", fields.join(","));
198
203
  }
199
- const conds = [];
200
- let conditionQuery = "";
201
- // The params to be used to execute the query
202
- const params = [];
203
- let finalConditionQuery = `"${AbstractScyllaDBConnector.PARTITION_KEY}" = ?`;
204
- params.push(partitionKey ?? AbstractScyllaDBConnector.PARTITION_KEY_VALUE);
205
- for (const cond of conditionsList) {
206
- const condition = cond;
207
- const descriptor = this._entitySchema.properties?.find(p => p.property === condition.property);
208
- if (condition.comparison === ComparisonOperator.Includes ||
209
- condition.comparison === ComparisonOperator.NotIncludes) {
210
- const propValue = `'%${condition.value?.toString()}%'`;
211
- if (condition.comparison === ComparisonOperator.Includes) {
212
- conds.push(`"${condition.property}" LIKE ${propValue}`);
213
- }
214
- else if (condition.comparison === ComparisonOperator.NotIncludes) {
215
- conds.push(`"${condition.property}" NOT LIKE ${propValue}`);
216
- }
217
- }
218
- else if (condition.comparison === ComparisonOperator.In) {
219
- let value = [];
220
- if (!Is.arrayValue(condition.value)) {
221
- value.push(this.propertyToDbValue(condition.value, descriptor));
222
- }
223
- else {
224
- value = condition.value.map(v => this.propertyToDbValue(v, descriptor));
225
- }
226
- params.push(value);
227
- conds.push(`"${condition.property}" IN ?`);
228
- }
229
- else {
230
- const propValue = condition.value;
231
- params.push(propValue);
232
- if (condition.comparison === ComparisonOperator.Equals) {
233
- conds.push(`"${condition.property}" = ?`);
234
- }
235
- else if (condition.comparison === ComparisonOperator.NotEquals) {
236
- conds.push(`"${condition.property}" != ?`);
237
- }
238
- else if (condition.comparison === ComparisonOperator.GreaterThan) {
239
- conds.push(`"${condition.property}" > ?`);
240
- }
241
- else if (condition.comparison === ComparisonOperator.LessThan) {
242
- conds.push(`"${condition.property}" < ?`);
243
- }
244
- else if (condition.comparison === ComparisonOperator.GreaterThanOrEqual) {
245
- conds.push(`"${condition.property}" >= ?`);
246
- }
247
- else if (condition.comparison === ComparisonOperator.LessThanOrEqual) {
248
- conds.push(`"${condition.property}" <= ?`);
249
- }
250
- }
251
- const operator = conditions.logicalOperator ?? LogicalOperator.And;
252
- conditionQuery = `${conds.join(` ${operator} `)}`;
253
- }
254
- if (conditionQuery.length > 0) {
255
- finalConditionQuery += ` AND ${conditionQuery}`;
256
- }
257
- sql += ` WHERE ${finalConditionQuery}`;
258
- connection = await this.openConnection();
259
- // TODO: Only supported one sort property at the moment. This code would need to be revised in a follow-up
204
+ sql += ` WHERE ${whereClause}`;
260
205
  if (Is.array(sortProperties) && sortProperties.length >= 1) {
261
- const sortKey = sortProperties[0].property ?? this._primaryKey.property;
262
- const sortDir = sortProperties[0].sortDirection ??
263
- this._entitySchema.properties?.find(e => e.property === sortKey)?.sortDirection;
264
- let sqlSortDir = "asc";
265
- if (sortDir === SortDirection.Descending) {
266
- sqlSortDir = "desc";
267
- }
268
- sql += ` ORDER BY "${String(sortKey)}" ${sqlSortDir.toUpperCase()}`;
269
- // Disabling paging in order by situations
270
- 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(", ")}`;
271
211
  }
212
+ connection = await this.openConnection();
272
213
  sql += " ALLOW FILTERING";
273
214
  await this._logging?.log({
274
215
  level: "info",
@@ -282,13 +223,49 @@ export class AbstractScyllaDBConnector {
282
223
  for (const row of result.rows) {
283
224
  entities.push(this.convertRowToObject(this._entitySchema.properties, row));
284
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
+ }
285
236
  return {
286
237
  entities,
287
- cursor: Is.stringValue(result.pageState) ? result.pageState : undefined
238
+ cursor: nextCursor
288
239
  };
289
240
  }
290
241
  catch (error) {
291
- 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);
292
269
  }
293
270
  finally {
294
271
  await this.closeConnection(connection);
@@ -296,12 +273,16 @@ export class AbstractScyllaDBConnector {
296
273
  }
297
274
  /**
298
275
  * Open a new database connection.
299
- * @param config The config for the connection.
300
276
  * @param skipKeySpace Don't include the keyspace in the connection.
301
277
  * @returns The new connection.
302
278
  * @internal
303
279
  */
304
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
+ }
305
286
  const client = new Client({
306
287
  contactPoints: this._config.hosts,
307
288
  localDataCenter: this._config.localDataCenter,
@@ -311,25 +292,48 @@ export class AbstractScyllaDBConnector {
311
292
  }
312
293
  });
313
294
  await client.connect();
295
+ if (!skipKeySpace) {
296
+ this._persistentClient = client;
297
+ }
314
298
  return client;
315
299
  }
316
300
  /**
317
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()`).
318
305
  * @param connection The connection to close.
306
+ * @returns Nothing.
319
307
  * @internal
320
308
  */
321
309
  async closeConnection(connection) {
322
310
  if (!connection) {
323
311
  return;
324
312
  }
313
+ if (connection === this._persistentClient) {
314
+ // Keep the persistent client alive for reuse.
315
+ return;
316
+ }
325
317
  return connection.shutdown();
326
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
+ }
327
330
  /**
328
331
  * Query the database.
329
332
  * @param connection The connection to query.
330
333
  * @param sql The sql statement to execute.
331
334
  * @param params The params to use when executing the query.
332
- * @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.
333
337
  * @returns The rows.
334
338
  * @internal
335
339
  */
@@ -339,7 +343,7 @@ export class AbstractScyllaDBConnector {
339
343
  connection.eachRow(sql, params, {
340
344
  prepare: true,
341
345
  autoPage: false,
342
- fetchSize: limit ?? AbstractScyllaDBConnector._DEFAULT_LIMIT,
346
+ fetchSize: limit ?? AbstractScyllaDBConnector.DEFAULT_LIMIT,
343
347
  pageState
344
348
  }, (n, row) => {
345
349
  rows.push(row);
@@ -357,6 +361,8 @@ export class AbstractScyllaDBConnector {
357
361
  * Execute on the database.
358
362
  * @param connection The connection to execute.
359
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.
360
366
  * @internal
361
367
  */
362
368
  async execute(connection, sql, params) {
@@ -366,6 +372,7 @@ export class AbstractScyllaDBConnector {
366
372
  * Create keyspace if it doesn't exist.
367
373
  * @param connection The connection to perform the query with.
368
374
  * @param keyspaceName The name of the keyspace to create.
375
+ * @returns The result set.
369
376
  * @internal
370
377
  */
371
378
  async createKeyspace(connection, keyspaceName) {
@@ -410,6 +417,7 @@ export class AbstractScyllaDBConnector {
410
417
  * @param value The value to convert to original form.
411
418
  * @param fieldDescriptor The descriptor for the field.
412
419
  * @returns The value as a property for the object.
420
+ * @throws GeneralError if parsing JSON fails.
413
421
  * @internal
414
422
  */
415
423
  dbValueToProperty(value, fieldDescriptor) {
@@ -454,9 +462,9 @@ export class AbstractScyllaDBConnector {
454
462
  return value;
455
463
  }
456
464
  /**
457
- * Format a value for the DB. As the driver takes care of conversion from Javascript
465
+ * Format a value for the DB.
458
466
  * @param value The value to format.
459
- * @param fieldDescriptor The descriptor for the field
467
+ * @param fieldDescriptor The descriptor for the field.
460
468
  * @returns The value after conversion.
461
469
  * @internal
462
470
  */
@@ -493,7 +501,7 @@ export class AbstractScyllaDBConnector {
493
501
  obj[field.property] = this.dbValueToProperty(value, field);
494
502
  }
495
503
  }
496
- return ObjectHelper.removeEmptyProperties(obj, { removeNull: true });
504
+ return EntityStorageHelper.unPrepareEntity(obj, [AbstractScyllaDBConnector.PARTITION_KEY]);
497
505
  }
498
506
  /**
499
507
  * Wrap a string for DB format.
@@ -559,5 +567,131 @@ export class AbstractScyllaDBConnector {
559
567
  }
560
568
  return { sqlCondition: sqlConditions.join(" AND "), conditionValues };
561
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
+ }
562
696
  }
563
697
  //# sourceMappingURL=abstractScyllaDBConnector.js.map