@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 +1 -1
- package/dist/es/abstractScyllaDBConnector.js +247 -113
- package/dist/es/abstractScyllaDBConnector.js.map +1 -1
- package/dist/es/models/IScyllaDBTableConfig.js.map +1 -1
- package/dist/es/models/IScyllaDBViewConfig.js.map +1 -1
- package/dist/es/scyllaDBTableConnector.js +419 -49
- package/dist/es/scyllaDBTableConnector.js.map +1 -1
- package/dist/es/scyllaDBViewConnector.js +39 -3
- package/dist/es/scyllaDBViewConnector.js.map +1 -1
- package/dist/types/abstractScyllaDBConnector.d.ts +12 -0
- package/dist/types/models/IScyllaDBTableConfig.d.ts +1 -1
- package/dist/types/models/IScyllaDBViewConfig.d.ts +2 -2
- package/dist/types/scyllaDBTableConnector.d.ts +53 -6
- package/dist/types/scyllaDBViewConnector.d.ts +20 -0
- package/docs/changelog.md +616 -57
- package/docs/reference/classes/ScyllaDBTableConnector.md +300 -14
- package/docs/reference/classes/ScyllaDBViewConnector.md +146 -0
- package/docs/reference/interfaces/IScyllaDBTableConfig.md +2 -2
- package/docs/reference/interfaces/IScyllaDBViewConfig.md +5 -5
- package/locales/en.json +22 -5
- package/package.json +10 -10
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
|
|
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,
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
|
|
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
|
-
//
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
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
|
-
|
|
197
|
-
sql = sql.replace("*", selectFields);
|
|
202
|
+
sql = sql.replace("*", fields.join(","));
|
|
198
203
|
}
|
|
199
|
-
|
|
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
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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:
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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
|