@twin.org/entity-storage-connector-mysql 0.0.3-next.2 → 0.0.3-next.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -12
- package/dist/es/models/IMySqlEntityStorageConnectorConfig.js.map +1 -1
- package/dist/es/mysqlEntityStorageConnector.js +509 -119
- package/dist/es/mysqlEntityStorageConnector.js.map +1 -1
- package/dist/types/models/IMySqlEntityStorageConnectorConfig.d.ts +35 -0
- package/dist/types/mysqlEntityStorageConnector.d.ts +74 -8
- package/docs/changelog.md +440 -52
- package/docs/examples.md +105 -1
- package/docs/reference/classes/MySqlEntityStorageConnector.md +317 -26
- package/docs/reference/interfaces/IMySqlEntityStorageConnectorConfig.md +87 -7
- package/docs/reference/interfaces/IMySqlEntityStorageConnectorConstructorOptions.md +6 -6
- package/locales/en.json +17 -2
- package/package.json +6 -6
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// Copyright 2024 IOTA Stiftung.
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0.
|
|
3
3
|
import { ContextIdHelper, ContextIdStore } from "@twin.org/context";
|
|
4
|
-
import { BaseError, Coerce, ComponentFactory, GeneralError, Guards, Is, ObjectHelper } from "@twin.org/core";
|
|
4
|
+
import { BaseError, Coerce, ComponentFactory, GeneralError, Guards, HealthStatus, Is, ObjectHelper, SharedStore } from "@twin.org/core";
|
|
5
5
|
import { ComparisonOperator, EntitySchemaFactory, EntitySchemaHelper, EntitySchemaPropertyType, LogicalOperator, SortDirection } from "@twin.org/entity";
|
|
6
|
-
import {
|
|
6
|
+
import { EntityStorageHelper } from "@twin.org/entity-storage-models";
|
|
7
|
+
import { createPool } from "mysql2/promise";
|
|
7
8
|
/**
|
|
8
9
|
* Class for performing entity storage operations using MySql.
|
|
9
10
|
*/
|
|
@@ -27,6 +28,11 @@ export class MySqlEntityStorageConnector {
|
|
|
27
28
|
* @internal
|
|
28
29
|
*/
|
|
29
30
|
static _PARTITION_KEY_VALUE = "root";
|
|
31
|
+
/**
|
|
32
|
+
* The name for the schema.
|
|
33
|
+
* @internal
|
|
34
|
+
*/
|
|
35
|
+
_entitySchemaName;
|
|
30
36
|
/**
|
|
31
37
|
* The schema for the entity.
|
|
32
38
|
* @internal
|
|
@@ -43,10 +49,10 @@ export class MySqlEntityStorageConnector {
|
|
|
43
49
|
*/
|
|
44
50
|
_config;
|
|
45
51
|
/**
|
|
46
|
-
* The
|
|
52
|
+
* The connection pool for MySql.
|
|
47
53
|
* @internal
|
|
48
54
|
*/
|
|
49
|
-
|
|
55
|
+
_pool;
|
|
50
56
|
/**
|
|
51
57
|
* The primary key property.
|
|
52
58
|
* @internal
|
|
@@ -65,6 +71,7 @@ export class MySqlEntityStorageConnector {
|
|
|
65
71
|
Guards.stringValue(MySqlEntityStorageConnector.CLASS_NAME, "options.config.password", options.config.password);
|
|
66
72
|
Guards.stringValue(MySqlEntityStorageConnector.CLASS_NAME, "options.config.database", options.config.database);
|
|
67
73
|
Guards.stringValue(MySqlEntityStorageConnector.CLASS_NAME, "options.config.tableName", options.config.tableName);
|
|
74
|
+
this._entitySchemaName = options.entitySchema;
|
|
68
75
|
this._entitySchema = EntitySchemaFactory.get(options.entitySchema);
|
|
69
76
|
this._partitionContextIds = options.partitionContextIds;
|
|
70
77
|
this._primaryKeyProperty = EntitySchemaHelper.getPrimaryKey(this._entitySchema);
|
|
@@ -77,6 +84,34 @@ export class MySqlEntityStorageConnector {
|
|
|
77
84
|
className() {
|
|
78
85
|
return MySqlEntityStorageConnector.CLASS_NAME;
|
|
79
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Get the health of the component.
|
|
89
|
+
* @returns The health of the component.
|
|
90
|
+
*/
|
|
91
|
+
async health() {
|
|
92
|
+
try {
|
|
93
|
+
await this.getPool().query(`SELECT 1 FROM \`${this._config.database}\`.\`${this._config.tableName}\` LIMIT 0`);
|
|
94
|
+
return [
|
|
95
|
+
{
|
|
96
|
+
source: MySqlEntityStorageConnector.CLASS_NAME,
|
|
97
|
+
status: HealthStatus.Ok,
|
|
98
|
+
description: "healthDescription",
|
|
99
|
+
data: { database: this._config.database, tableName: this._config.tableName }
|
|
100
|
+
}
|
|
101
|
+
];
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return [
|
|
105
|
+
{
|
|
106
|
+
source: MySqlEntityStorageConnector.CLASS_NAME,
|
|
107
|
+
status: HealthStatus.Error,
|
|
108
|
+
description: "healthDescription",
|
|
109
|
+
message: "connectionFailed",
|
|
110
|
+
data: { database: this._config.database, tableName: this._config.tableName }
|
|
111
|
+
}
|
|
112
|
+
];
|
|
113
|
+
}
|
|
114
|
+
}
|
|
80
115
|
/**
|
|
81
116
|
* Get the schema for the entities.
|
|
82
117
|
* @returns The schema for the entities.
|
|
@@ -92,7 +127,7 @@ export class MySqlEntityStorageConnector {
|
|
|
92
127
|
async bootstrap(nodeLoggingComponentType) {
|
|
93
128
|
const nodeLogging = ComponentFactory.getIfExists(nodeLoggingComponentType);
|
|
94
129
|
try {
|
|
95
|
-
const
|
|
130
|
+
const pool = this.getPool();
|
|
96
131
|
const databaseExists = await this.databaseExists();
|
|
97
132
|
if (!databaseExists) {
|
|
98
133
|
await nodeLogging?.log({
|
|
@@ -104,7 +139,7 @@ export class MySqlEntityStorageConnector {
|
|
|
104
139
|
databaseName: this._config.database
|
|
105
140
|
}
|
|
106
141
|
});
|
|
107
|
-
await
|
|
142
|
+
await pool.query(`CREATE DATABASE IF NOT EXISTS \`${this._config.database}\``);
|
|
108
143
|
await this.waitForDatabaseExists();
|
|
109
144
|
}
|
|
110
145
|
else {
|
|
@@ -129,7 +164,7 @@ export class MySqlEntityStorageConnector {
|
|
|
129
164
|
tableName: this._config.tableName
|
|
130
165
|
}
|
|
131
166
|
});
|
|
132
|
-
await
|
|
167
|
+
await pool.query(`CREATE TABLE IF NOT EXISTS \`${this._config.database}\`.\`${this._config.tableName}\` (${this.mapMySqlProperties()})`);
|
|
133
168
|
await this.waitForTableExists();
|
|
134
169
|
}
|
|
135
170
|
else {
|
|
@@ -159,6 +194,29 @@ export class MySqlEntityStorageConnector {
|
|
|
159
194
|
}
|
|
160
195
|
return true;
|
|
161
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* The component needs to be stopped when the node is closed.
|
|
199
|
+
* @param nodeLoggingComponentType The node logging component type.
|
|
200
|
+
* @returns Nothing.
|
|
201
|
+
*/
|
|
202
|
+
async stop(nodeLoggingComponentType) {
|
|
203
|
+
if (this._pool) {
|
|
204
|
+
const poolConfig = this.createPoolConfig();
|
|
205
|
+
const poolId = `${poolConfig.host}|${poolConfig.port}|${poolConfig.user}`;
|
|
206
|
+
let sharedPools = SharedStore.get("mySqlPools");
|
|
207
|
+
sharedPools ??= {};
|
|
208
|
+
if (sharedPools[poolId]) {
|
|
209
|
+
// Decrease the use counter and close the pool if no longer used
|
|
210
|
+
sharedPools[poolId].useCounter--;
|
|
211
|
+
if (sharedPools[poolId].useCounter <= 0) {
|
|
212
|
+
await this._pool.end();
|
|
213
|
+
delete sharedPools[poolId];
|
|
214
|
+
}
|
|
215
|
+
SharedStore.set("mySqlPools", sharedPools);
|
|
216
|
+
}
|
|
217
|
+
this._pool = undefined;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
162
220
|
/**
|
|
163
221
|
* Get an entity from MySql.
|
|
164
222
|
* @param id The id of the entity to get, or the index value if secondaryIndex is set.
|
|
@@ -171,7 +229,7 @@ export class MySqlEntityStorageConnector {
|
|
|
171
229
|
const contextIds = await ContextIdStore.getContextIds();
|
|
172
230
|
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
173
231
|
try {
|
|
174
|
-
const
|
|
232
|
+
const pool = this.getPool();
|
|
175
233
|
const whereClauses = [];
|
|
176
234
|
const values = [];
|
|
177
235
|
whereClauses.push(`\`${MySqlEntityStorageConnector._PARTITION_KEY}\` = ?`);
|
|
@@ -190,11 +248,12 @@ export class MySqlEntityStorageConnector {
|
|
|
190
248
|
}
|
|
191
249
|
}
|
|
192
250
|
const query = `SELECT * FROM \`${this._config.database}\`.\`${this._config.tableName}\` WHERE ${whereClauses.join(" AND ")} LIMIT 1`;
|
|
193
|
-
const [rows] = await
|
|
251
|
+
const [rows] = await pool.query(query, values);
|
|
194
252
|
if (Is.array(rows) && rows.length === 1) {
|
|
195
|
-
const item =
|
|
196
|
-
|
|
197
|
-
|
|
253
|
+
const item = EntityStorageHelper.unPrepareEntity(rows[0], [
|
|
254
|
+
MySqlEntityStorageConnector._PARTITION_KEY
|
|
255
|
+
]);
|
|
256
|
+
return this.coerceEntityTypes(item);
|
|
198
257
|
}
|
|
199
258
|
}
|
|
200
259
|
catch (err) {
|
|
@@ -214,8 +273,13 @@ export class MySqlEntityStorageConnector {
|
|
|
214
273
|
Guards.object(MySqlEntityStorageConnector.CLASS_NAME, "entity", entity);
|
|
215
274
|
const contextIds = await ContextIdStore.getContextIds();
|
|
216
275
|
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
217
|
-
|
|
218
|
-
|
|
276
|
+
const prepared = EntityStorageHelper.prepareEntity(entity, this._entitySchema, [
|
|
277
|
+
{
|
|
278
|
+
property: MySqlEntityStorageConnector._PARTITION_KEY,
|
|
279
|
+
value: partitionKey ?? MySqlEntityStorageConnector._PARTITION_KEY_VALUE
|
|
280
|
+
}
|
|
281
|
+
], { nullBehavior: "nullify" });
|
|
282
|
+
const id = prepared[this._primaryKeyProperty.property];
|
|
219
283
|
try {
|
|
220
284
|
if (Is.arrayValue(conditions)) {
|
|
221
285
|
const itemData = await this.get(id);
|
|
@@ -223,38 +287,100 @@ export class MySqlEntityStorageConnector {
|
|
|
223
287
|
return;
|
|
224
288
|
}
|
|
225
289
|
}
|
|
226
|
-
const finalEntity = ObjectHelper.clone(entity);
|
|
227
290
|
const props = [...(this._entitySchema.properties ?? [])];
|
|
228
291
|
props.unshift({
|
|
229
292
|
property: MySqlEntityStorageConnector._PARTITION_KEY,
|
|
230
293
|
type: EntitySchemaPropertyType.String
|
|
231
294
|
});
|
|
232
|
-
ObjectHelper.propertySet(finalEntity, MySqlEntityStorageConnector._PARTITION_KEY, partitionKey ?? MySqlEntityStorageConnector._PARTITION_KEY_VALUE);
|
|
233
295
|
const keys = [];
|
|
234
296
|
const values = [];
|
|
235
297
|
for (const prop of props) {
|
|
236
|
-
|
|
237
|
-
|
|
298
|
+
keys.push(prop.property);
|
|
299
|
+
const val = prepared[prop.property];
|
|
300
|
+
if (val === null || val === undefined) {
|
|
301
|
+
values.push(null);
|
|
302
|
+
}
|
|
303
|
+
else if (prop.type === EntitySchemaPropertyType.Object ||
|
|
304
|
+
prop.type === EntitySchemaPropertyType.Array) {
|
|
305
|
+
values.push(JSON.stringify(val));
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
values.push(val);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
let sql = `INSERT INTO \`${this._config.database}\`.\`${this._config.tableName}\``;
|
|
312
|
+
sql += ` (${keys.map(key => `\`${key}\``).join(", ")})`;
|
|
313
|
+
sql += ` VALUES (${values.map(() => "?").join(", ")})`;
|
|
314
|
+
sql += ` ON DUPLICATE KEY UPDATE ${keys.map(key => `\`${key}\` = VALUES(\`${key}\`)`).join(", ")};`;
|
|
315
|
+
const pool = this.getPool();
|
|
316
|
+
await pool.query(sql, values);
|
|
317
|
+
}
|
|
318
|
+
catch (err) {
|
|
319
|
+
throw new GeneralError(MySqlEntityStorageConnector.CLASS_NAME, "setFailed", {
|
|
320
|
+
id
|
|
321
|
+
}, err);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Set multiple entities in a batch.
|
|
326
|
+
* @param entities The entities to set.
|
|
327
|
+
* @returns Nothing.
|
|
328
|
+
*/
|
|
329
|
+
async setBatch(entities) {
|
|
330
|
+
Guards.arrayValue(MySqlEntityStorageConnector.CLASS_NAME, "entities", entities);
|
|
331
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
332
|
+
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
333
|
+
const preparedEntities = entities.map(entity => EntityStorageHelper.prepareEntity(entity, this._entitySchema, [
|
|
334
|
+
{
|
|
335
|
+
property: MySqlEntityStorageConnector._PARTITION_KEY,
|
|
336
|
+
value: partitionKey ?? MySqlEntityStorageConnector._PARTITION_KEY_VALUE
|
|
337
|
+
}
|
|
338
|
+
], { nullBehavior: "nullify" }));
|
|
339
|
+
try {
|
|
340
|
+
const props = [...(this._entitySchema.properties ?? [])];
|
|
341
|
+
props.unshift({
|
|
342
|
+
property: MySqlEntityStorageConnector._PARTITION_KEY,
|
|
343
|
+
type: EntitySchemaPropertyType.String
|
|
344
|
+
});
|
|
345
|
+
const keys = props.map(p => p.property);
|
|
346
|
+
const allValues = [];
|
|
347
|
+
for (const prepared of preparedEntities) {
|
|
348
|
+
for (const prop of props) {
|
|
349
|
+
const val = prepared[prop.property];
|
|
238
350
|
if (prop.type === EntitySchemaPropertyType.Object ||
|
|
239
351
|
prop.type === EntitySchemaPropertyType.Array) {
|
|
240
|
-
|
|
352
|
+
allValues.push(Is.empty(val) ? null : JSON.stringify(val));
|
|
241
353
|
}
|
|
242
354
|
else {
|
|
243
|
-
|
|
355
|
+
allValues.push(Is.empty(val) ? null : val);
|
|
244
356
|
}
|
|
245
357
|
}
|
|
246
358
|
}
|
|
359
|
+
const rowPlaceholder = `(${keys.map(() => "?").join(", ")})`;
|
|
247
360
|
let sql = `INSERT INTO \`${this._config.database}\`.\`${this._config.tableName}\``;
|
|
248
361
|
sql += ` (${keys.map(key => `\`${key}\``).join(", ")})`;
|
|
249
|
-
sql += ` VALUES
|
|
362
|
+
sql += ` VALUES ${entities.map(() => rowPlaceholder).join(", ")}`;
|
|
250
363
|
sql += ` ON DUPLICATE KEY UPDATE ${keys.map(key => `\`${key}\` = VALUES(\`${key}\`)`).join(", ")};`;
|
|
251
|
-
const
|
|
252
|
-
await
|
|
364
|
+
const pool = this.getPool();
|
|
365
|
+
await pool.query(sql, allValues);
|
|
253
366
|
}
|
|
254
367
|
catch (err) {
|
|
255
|
-
throw new GeneralError(MySqlEntityStorageConnector.CLASS_NAME, "
|
|
256
|
-
|
|
257
|
-
|
|
368
|
+
throw new GeneralError(MySqlEntityStorageConnector.CLASS_NAME, "setBatchFailed", undefined, err);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Empty the entity storage.
|
|
373
|
+
* @returns Nothing.
|
|
374
|
+
*/
|
|
375
|
+
async empty() {
|
|
376
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
377
|
+
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
378
|
+
try {
|
|
379
|
+
const pool = this.getPool();
|
|
380
|
+
await pool.query(`DELETE FROM \`${this._config.database}\`.\`${this._config.tableName}\` WHERE \`${MySqlEntityStorageConnector._PARTITION_KEY}\` = ?`, [partitionKey ?? MySqlEntityStorageConnector._PARTITION_KEY_VALUE]);
|
|
381
|
+
}
|
|
382
|
+
catch (err) {
|
|
383
|
+
throw new GeneralError(MySqlEntityStorageConnector.CLASS_NAME, "emptyFailed", undefined, err);
|
|
258
384
|
}
|
|
259
385
|
}
|
|
260
386
|
/**
|
|
@@ -268,7 +394,7 @@ export class MySqlEntityStorageConnector {
|
|
|
268
394
|
const contextIds = await ContextIdStore.getContextIds();
|
|
269
395
|
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
270
396
|
try {
|
|
271
|
-
const
|
|
397
|
+
const pool = this.getPool();
|
|
272
398
|
const itemData = await this.get(id, undefined, conditions);
|
|
273
399
|
if (Is.notEmpty(itemData)) {
|
|
274
400
|
const values = [];
|
|
@@ -284,7 +410,7 @@ export class MySqlEntityStorageConnector {
|
|
|
284
410
|
}));
|
|
285
411
|
}
|
|
286
412
|
const query = `DELETE FROM \`${this._config.database}\`.\`${this._config.tableName}\` WHERE ${whereClauses.join(" AND ")}`;
|
|
287
|
-
await
|
|
413
|
+
await pool.query(query, values);
|
|
288
414
|
}
|
|
289
415
|
}
|
|
290
416
|
catch (err) {
|
|
@@ -293,6 +419,67 @@ export class MySqlEntityStorageConnector {
|
|
|
293
419
|
}, err);
|
|
294
420
|
}
|
|
295
421
|
}
|
|
422
|
+
/**
|
|
423
|
+
* Teardown the entity storage by dropping the table.
|
|
424
|
+
* @param nodeLoggingComponentType The node logging component type.
|
|
425
|
+
* @returns True if the teardown process was successful.
|
|
426
|
+
*/
|
|
427
|
+
async teardown(nodeLoggingComponentType) {
|
|
428
|
+
const nodeLogging = ComponentFactory.getIfExists(nodeLoggingComponentType);
|
|
429
|
+
await nodeLogging?.log({
|
|
430
|
+
level: "info",
|
|
431
|
+
source: MySqlEntityStorageConnector.CLASS_NAME,
|
|
432
|
+
ts: Date.now(),
|
|
433
|
+
message: "tableDropping",
|
|
434
|
+
data: { tableName: this._config.tableName }
|
|
435
|
+
});
|
|
436
|
+
try {
|
|
437
|
+
if (await this.tableExists()) {
|
|
438
|
+
const pool = this.getPool();
|
|
439
|
+
await pool.query(`DROP TABLE \`${this._config.database}\`.\`${this._config.tableName}\`;`);
|
|
440
|
+
await this.waitForTableNotExists();
|
|
441
|
+
}
|
|
442
|
+
await nodeLogging?.log({
|
|
443
|
+
level: "info",
|
|
444
|
+
source: MySqlEntityStorageConnector.CLASS_NAME,
|
|
445
|
+
ts: Date.now(),
|
|
446
|
+
message: "tableDropped",
|
|
447
|
+
data: { tableName: this._config.tableName }
|
|
448
|
+
});
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
451
|
+
catch (err) {
|
|
452
|
+
await nodeLogging?.log({
|
|
453
|
+
level: "error",
|
|
454
|
+
source: MySqlEntityStorageConnector.CLASS_NAME,
|
|
455
|
+
ts: Date.now(),
|
|
456
|
+
message: "teardownFailed",
|
|
457
|
+
error: BaseError.fromError(err)
|
|
458
|
+
});
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Remove multiple entities by their primary key IDs.
|
|
464
|
+
* @param ids The ids of the entities to remove.
|
|
465
|
+
* @returns Nothing.
|
|
466
|
+
*/
|
|
467
|
+
async removeBatch(ids) {
|
|
468
|
+
Guards.arrayValue(MySqlEntityStorageConnector.CLASS_NAME, "ids", ids);
|
|
469
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
470
|
+
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
471
|
+
try {
|
|
472
|
+
const pool = this.getPool();
|
|
473
|
+
const sql = `DELETE FROM \`${this._config.database}\`.\`${this._config.tableName}\` WHERE \`${MySqlEntityStorageConnector._PARTITION_KEY}\` = ? AND \`${String(this._primaryKeyProperty.property)}\` IN (?)`;
|
|
474
|
+
await pool.query(sql, [
|
|
475
|
+
partitionKey ?? MySqlEntityStorageConnector._PARTITION_KEY_VALUE,
|
|
476
|
+
ids
|
|
477
|
+
]);
|
|
478
|
+
}
|
|
479
|
+
catch (err) {
|
|
480
|
+
throw new GeneralError(MySqlEntityStorageConnector.CLASS_NAME, "removeBatchFailed", undefined, err);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
296
483
|
/**
|
|
297
484
|
* Find all the entities which match the conditions.
|
|
298
485
|
* @param conditions The conditions to match for the entities.
|
|
@@ -318,37 +505,27 @@ export class MySqlEntityStorageConnector {
|
|
|
318
505
|
}
|
|
319
506
|
orderByClause = `ORDER BY ${orderClauses.join(", ")}`;
|
|
320
507
|
}
|
|
321
|
-
const whereClauses =
|
|
322
|
-
const values = [];
|
|
323
|
-
const finalConditions = {
|
|
324
|
-
conditions: [],
|
|
325
|
-
logicalOperator: LogicalOperator.And
|
|
326
|
-
};
|
|
327
|
-
finalConditions.conditions.push({
|
|
328
|
-
property: MySqlEntityStorageConnector._PARTITION_KEY,
|
|
329
|
-
comparison: ComparisonOperator.Equals,
|
|
330
|
-
value: partitionKey ?? MySqlEntityStorageConnector._PARTITION_KEY_VALUE
|
|
331
|
-
});
|
|
332
|
-
if (!Is.empty(conditions)) {
|
|
333
|
-
finalConditions.conditions.push(conditions);
|
|
334
|
-
}
|
|
335
|
-
this.buildQueryParameters("", finalConditions, whereClauses, values);
|
|
508
|
+
const { whereClauses, values } = this.buildWhereClause(conditions, partitionKey);
|
|
336
509
|
const startIndex = Coerce.number(cursor) ?? 0;
|
|
337
510
|
sql = `SELECT ${properties ? properties.map(p => `\`${String(p)}\``).join(", ") : "*"} FROM \`${this._config.database}\`.\`${this._config.tableName}\``;
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
const
|
|
511
|
+
if (whereClauses.length > 0) {
|
|
512
|
+
sql += ` WHERE ${whereClauses.join(" AND ")}`;
|
|
513
|
+
}
|
|
514
|
+
sql += ` ${orderByClause} LIMIT ${returnSize + 1} OFFSET ${startIndex}`;
|
|
515
|
+
const pool = this.getPool();
|
|
516
|
+
const [rows] = (await pool.query(sql, values)) ?? [];
|
|
517
|
+
const hasMore = Is.array(rows) && rows.length > returnSize;
|
|
518
|
+
const resultRows = hasMore ? rows.slice(0, returnSize) : rows;
|
|
519
|
+
const entities = resultRows;
|
|
343
520
|
for (let i = 0; i < entities.length; i++) {
|
|
344
|
-
entities[i] =
|
|
345
|
-
|
|
521
|
+
entities[i] = EntityStorageHelper.unPrepareEntity(entities[i], [
|
|
522
|
+
MySqlEntityStorageConnector._PARTITION_KEY
|
|
523
|
+
]);
|
|
524
|
+
entities[i] = this.coerceEntityTypes(entities[i]);
|
|
346
525
|
}
|
|
347
526
|
return {
|
|
348
527
|
entities,
|
|
349
|
-
cursor:
|
|
350
|
-
? Coerce.string(startIndex + returnSize)
|
|
351
|
-
: undefined
|
|
528
|
+
cursor: hasMore ? Coerce.string(startIndex + returnSize) : undefined
|
|
352
529
|
};
|
|
353
530
|
}
|
|
354
531
|
catch (err) {
|
|
@@ -356,50 +533,152 @@ export class MySqlEntityStorageConnector {
|
|
|
356
533
|
}
|
|
357
534
|
}
|
|
358
535
|
/**
|
|
359
|
-
*
|
|
360
|
-
* @
|
|
536
|
+
* Count all the entities which match the conditions.
|
|
537
|
+
* @param conditions The optional conditions to match for the entities.
|
|
538
|
+
* @returns The total count of entities in the storage.
|
|
361
539
|
*/
|
|
362
|
-
async
|
|
540
|
+
async count(conditions) {
|
|
541
|
+
let sql;
|
|
363
542
|
try {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
543
|
+
const pool = this.getPool();
|
|
544
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
545
|
+
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
546
|
+
const { whereClauses, values } = this.buildWhereClause(conditions, partitionKey);
|
|
547
|
+
sql = `SELECT COUNT(*) AS count FROM \`${this._config.database}\`.\`${this._config.tableName}\``;
|
|
548
|
+
if (whereClauses.length > 0) {
|
|
549
|
+
sql += ` WHERE ${whereClauses.join(" AND ")}`;
|
|
368
550
|
}
|
|
551
|
+
const [rows] = await pool.query(sql, values);
|
|
552
|
+
return Number(rows[0].count);
|
|
369
553
|
}
|
|
370
|
-
catch {
|
|
371
|
-
|
|
554
|
+
catch (err) {
|
|
555
|
+
throw new GeneralError(MySqlEntityStorageConnector.CLASS_NAME, "countFailed", { sql }, err);
|
|
372
556
|
}
|
|
373
557
|
}
|
|
374
558
|
/**
|
|
375
|
-
*
|
|
376
|
-
* @returns
|
|
559
|
+
* Get all unique partition context ids present in the table.
|
|
560
|
+
* @returns An array of context id objects, one per unique partition.
|
|
377
561
|
*/
|
|
378
|
-
async
|
|
562
|
+
async getPartitionContextIds() {
|
|
563
|
+
if (!Is.arrayValue(this._partitionContextIds)) {
|
|
564
|
+
return [];
|
|
565
|
+
}
|
|
379
566
|
try {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
567
|
+
const pool = this.getPool();
|
|
568
|
+
const [rows] = await pool.query(`SELECT DISTINCT \`${MySqlEntityStorageConnector._PARTITION_KEY}\` FROM \`${this._config.database}\`.\`${this._config.tableName}\``);
|
|
569
|
+
return rows
|
|
570
|
+
.map(row => row[MySqlEntityStorageConnector._PARTITION_KEY])
|
|
571
|
+
.filter((id) => Is.stringValue(id))
|
|
572
|
+
.map(id => ContextIdHelper.shortSplit(this._partitionContextIds ?? [], id));
|
|
384
573
|
}
|
|
385
|
-
catch {
|
|
386
|
-
|
|
574
|
+
catch (err) {
|
|
575
|
+
throw new GeneralError(MySqlEntityStorageConnector.CLASS_NAME, "getPartitionContextIdsFailed", undefined, err);
|
|
387
576
|
}
|
|
388
577
|
}
|
|
578
|
+
/**
|
|
579
|
+
* Create the target connector for performing the migration using a temporary table.
|
|
580
|
+
* @param newEntitySchema The name of the new entity schema to create the connector for.
|
|
581
|
+
* @returns Connector for performing the migration.
|
|
582
|
+
*/
|
|
583
|
+
async createTargetConnector(newEntitySchema) {
|
|
584
|
+
const migrationTableName = `${this._config.tableName}Migration${Date.now()}`;
|
|
585
|
+
return new MySqlEntityStorageConnector({
|
|
586
|
+
entitySchema: newEntitySchema,
|
|
587
|
+
config: {
|
|
588
|
+
...this._config,
|
|
589
|
+
tableName: migrationTableName
|
|
590
|
+
},
|
|
591
|
+
partitionContextIds: this._partitionContextIds
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Finalize the migration by dropping the source table and renaming the migration table to the original name.
|
|
596
|
+
* @param targetConnector The connector holding the migrated data in a temporary table.
|
|
597
|
+
* @param options The options to control how the migration is finalized.
|
|
598
|
+
* @param loggingComponentType The logging component type to use during finalization.
|
|
599
|
+
* @returns The final connector using the original table name with the new schema.
|
|
600
|
+
*/
|
|
601
|
+
async finalizeMigration(targetConnector, options, loggingComponentType) {
|
|
602
|
+
// Teardown the existing table with the original name to free up the name for the new table
|
|
603
|
+
await this.teardown(loggingComponentType);
|
|
604
|
+
// RENAME TABLE is an atomic metadata-only operation in MySQL — no data copying needed.
|
|
605
|
+
const pool = this.getPool();
|
|
606
|
+
await pool.query(`RENAME TABLE \`${targetConnector._config.database}\`.\`${targetConnector._config.tableName}\` TO \`${this._config.database}\`.\`${this._config.tableName}\``);
|
|
607
|
+
const finalConnector = new MySqlEntityStorageConnector({
|
|
608
|
+
entitySchema: targetConnector._entitySchemaName,
|
|
609
|
+
config: this._config,
|
|
610
|
+
partitionContextIds: this._partitionContextIds
|
|
611
|
+
});
|
|
612
|
+
if (await finalConnector.bootstrap(loggingComponentType)) {
|
|
613
|
+
await targetConnector.stop();
|
|
614
|
+
return finalConnector;
|
|
615
|
+
}
|
|
616
|
+
throw new GeneralError(MySqlEntityStorageConnector.CLASS_NAME, "finalizeMigrationFailedBootstrap", undefined);
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Cleanup a failed or aborted migration by dropping the temporary migration table.
|
|
620
|
+
* @param targetConnector The target connector to cleanup.
|
|
621
|
+
* @param options The options to control how the migration is cleaned up.
|
|
622
|
+
* @param loggingComponentType The optional component type to use for logging.
|
|
623
|
+
* @returns A promise that resolves when the cleanup is complete.
|
|
624
|
+
*/
|
|
625
|
+
async cleanupMigration(targetConnector, options, loggingComponentType) {
|
|
626
|
+
// If something failed the only thing to cleanup is the migration table
|
|
627
|
+
await targetConnector?.teardown?.(loggingComponentType);
|
|
628
|
+
}
|
|
389
629
|
/**
|
|
390
630
|
* Check if the database exists.
|
|
391
631
|
* @returns True if the database exists, false otherwise.
|
|
392
632
|
*/
|
|
393
633
|
async databaseExists() {
|
|
394
634
|
try {
|
|
395
|
-
const
|
|
396
|
-
const [rows] = await
|
|
635
|
+
const pool = this.getPool();
|
|
636
|
+
const [rows] = await pool.query("SHOW DATABASES LIKE ?;", [this._config.database]);
|
|
397
637
|
return Is.arrayValue(rows);
|
|
398
638
|
}
|
|
399
639
|
catch {
|
|
400
640
|
return false;
|
|
401
641
|
}
|
|
402
642
|
}
|
|
643
|
+
/**
|
|
644
|
+
* Close the connection pool and release all connections.
|
|
645
|
+
* Should be called when the connector is no longer needed.
|
|
646
|
+
* @returns Nothing.
|
|
647
|
+
*/
|
|
648
|
+
async close() {
|
|
649
|
+
if (this._pool) {
|
|
650
|
+
const poolConfig = this.createPoolConfig();
|
|
651
|
+
const poolId = `${poolConfig.host}|${poolConfig.port}|${poolConfig.user}`;
|
|
652
|
+
let sharedPools = SharedStore.get("mySqlPools");
|
|
653
|
+
sharedPools ??= {};
|
|
654
|
+
if (sharedPools[poolId]) {
|
|
655
|
+
// Decrease the use counter and close the pool if no longer used
|
|
656
|
+
sharedPools[poolId].useCounter--;
|
|
657
|
+
if (sharedPools[poolId].useCounter <= 0) {
|
|
658
|
+
await this._pool.end();
|
|
659
|
+
delete sharedPools[poolId];
|
|
660
|
+
}
|
|
661
|
+
SharedStore.set("mySqlPools", sharedPools);
|
|
662
|
+
}
|
|
663
|
+
this._pool = undefined;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Coerce MySQL raw row values back to proper TypeScript types based on the entity schema.
|
|
668
|
+
* MySQL returns TINYINT(1) as 0/1 rather than false/true; this method converts those.
|
|
669
|
+
* @param entity The raw entity row from MySQL.
|
|
670
|
+
* @returns The entity with schema-correct types.
|
|
671
|
+
* @internal
|
|
672
|
+
*/
|
|
673
|
+
coerceEntityTypes(entity) {
|
|
674
|
+
for (const prop of this._entitySchema.properties ?? []) {
|
|
675
|
+
const value = entity[prop.property];
|
|
676
|
+
if (prop.type === EntitySchemaPropertyType.Boolean && !Is.empty(value)) {
|
|
677
|
+
ObjectHelper.propertySet(entity, prop.property, Boolean(value));
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
return entity;
|
|
681
|
+
}
|
|
403
682
|
/**
|
|
404
683
|
* Wait for a database to exist.
|
|
405
684
|
* @returns Nothing.
|
|
@@ -421,8 +700,8 @@ export class MySqlEntityStorageConnector {
|
|
|
421
700
|
*/
|
|
422
701
|
async tableExists() {
|
|
423
702
|
try {
|
|
424
|
-
const
|
|
425
|
-
const [rows] = await
|
|
703
|
+
const pool = this.getPool();
|
|
704
|
+
const [rows] = await pool.query("SHOW TABLES FROM ?? LIKE ?", [
|
|
426
705
|
this._config.database,
|
|
427
706
|
this._config.tableName
|
|
428
707
|
]);
|
|
@@ -461,31 +740,76 @@ export class MySqlEntityStorageConnector {
|
|
|
461
740
|
}
|
|
462
741
|
}
|
|
463
742
|
/**
|
|
464
|
-
*
|
|
465
|
-
* @returns The MySql connection.
|
|
743
|
+
* Get or create the connection pool.
|
|
744
|
+
* @returns The MySql connection pool.
|
|
466
745
|
* @internal
|
|
467
746
|
*/
|
|
468
|
-
|
|
469
|
-
if (this.
|
|
470
|
-
|
|
747
|
+
getPool() {
|
|
748
|
+
if (!this._pool) {
|
|
749
|
+
const poolConfig = this.createPoolConfig();
|
|
750
|
+
const poolId = `${poolConfig.host}|${poolConfig.port}|${poolConfig.user}`;
|
|
751
|
+
let sharedPools = SharedStore.get("mySqlPools");
|
|
752
|
+
sharedPools ??= {};
|
|
753
|
+
// If there is no pool for the id, create it
|
|
754
|
+
if (!sharedPools[poolId]) {
|
|
755
|
+
sharedPools[poolId] = {
|
|
756
|
+
pool: createPool(poolConfig),
|
|
757
|
+
useCounter: 0
|
|
758
|
+
};
|
|
759
|
+
SharedStore.set("mySqlPools", sharedPools);
|
|
760
|
+
}
|
|
761
|
+
// Increase the use counter and return the pool
|
|
762
|
+
sharedPools[poolId].useCounter++;
|
|
763
|
+
this._pool = sharedPools[poolId].pool;
|
|
471
764
|
}
|
|
472
|
-
|
|
473
|
-
this._connection = newConnection;
|
|
474
|
-
return newConnection;
|
|
765
|
+
return this._pool;
|
|
475
766
|
}
|
|
476
767
|
/**
|
|
477
|
-
* Create
|
|
478
|
-
* @returns The MySql
|
|
768
|
+
* Create the connection pool configuration.
|
|
769
|
+
* @returns The MySql pool configuration.
|
|
479
770
|
* @internal
|
|
480
771
|
*/
|
|
481
|
-
|
|
772
|
+
createPoolConfig() {
|
|
773
|
+
const poolConfig = this._config.pool ?? {};
|
|
482
774
|
return {
|
|
483
775
|
host: this._config.host,
|
|
484
776
|
port: this._config.port ?? 3306,
|
|
485
777
|
user: this._config.user,
|
|
486
|
-
password: this._config.password
|
|
778
|
+
password: this._config.password,
|
|
779
|
+
connectionLimit: poolConfig.connectionLimit ?? 10,
|
|
780
|
+
maxIdle: poolConfig.maxIdle ?? 10,
|
|
781
|
+
idleTimeout: poolConfig.idleTimeout ?? 60000,
|
|
782
|
+
enableKeepAlive: poolConfig.enableKeepAlive ?? true,
|
|
783
|
+
keepAliveInitialDelay: 0,
|
|
784
|
+
waitForConnections: poolConfig.waitForConnections ?? true,
|
|
785
|
+
queueLimit: poolConfig.queueLimit ?? 0
|
|
487
786
|
};
|
|
488
787
|
}
|
|
788
|
+
/**
|
|
789
|
+
* Build where clause arrays for a query, combining partition key and optional conditions.
|
|
790
|
+
* @param conditions The optional entity conditions to include.
|
|
791
|
+
* @param partitionKey The partition key value.
|
|
792
|
+
* @returns The where clauses and bound values.
|
|
793
|
+
* @internal
|
|
794
|
+
*/
|
|
795
|
+
buildWhereClause(conditions, partitionKey) {
|
|
796
|
+
const whereClauses = [];
|
|
797
|
+
const values = [];
|
|
798
|
+
const finalConditions = {
|
|
799
|
+
conditions: [],
|
|
800
|
+
logicalOperator: LogicalOperator.And
|
|
801
|
+
};
|
|
802
|
+
finalConditions.conditions.push({
|
|
803
|
+
property: MySqlEntityStorageConnector._PARTITION_KEY,
|
|
804
|
+
comparison: ComparisonOperator.Equals,
|
|
805
|
+
value: partitionKey ?? MySqlEntityStorageConnector._PARTITION_KEY_VALUE
|
|
806
|
+
});
|
|
807
|
+
if (!Is.empty(conditions)) {
|
|
808
|
+
finalConditions.conditions.push(conditions);
|
|
809
|
+
}
|
|
810
|
+
this.buildQueryParameters("", finalConditions, whereClauses, values);
|
|
811
|
+
return { whereClauses, values };
|
|
812
|
+
}
|
|
489
813
|
/**
|
|
490
814
|
* Create an SQL condition clause.
|
|
491
815
|
* @param objectPath The path for the nested object.
|
|
@@ -536,42 +860,106 @@ export class MySqlEntityStorageConnector {
|
|
|
536
860
|
prop += ".";
|
|
537
861
|
}
|
|
538
862
|
prop += comparator.property;
|
|
539
|
-
// prop = prop.replace(/\./g, "->");
|
|
540
863
|
if (comparator.comparison === ComparisonOperator.In) {
|
|
541
864
|
const inValues = Is.array(comparator.value) ? comparator.value : [comparator.value];
|
|
865
|
+
if (inValues.length === 0) {
|
|
866
|
+
// MySQL rejects `IN ()` as a syntax error — short-circuit to a condition
|
|
867
|
+
// that is always false so the query returns zero rows cleanly (#141).
|
|
868
|
+
return "1 = 0";
|
|
869
|
+
}
|
|
542
870
|
values.push(...inValues.map(val => this.propertyToDbValue(val, type)));
|
|
543
871
|
const placeholders = inValues.map(() => "?").join(", ");
|
|
544
872
|
return `\`${prop}\` IN (${placeholders})`;
|
|
545
873
|
}
|
|
874
|
+
// null/undefined must use IS NULL / IS NOT NULL — never a parameterised placeholder.
|
|
875
|
+
// Passing undefined through propertyToDbValue() coerces it to NaN for number fields
|
|
876
|
+
// (Number(undefined) === NaN), and null coerces to 0 (Number(null) === 0), both of
|
|
877
|
+
// which produce semantically wrong or invalid SQL.
|
|
878
|
+
if (comparator.value === null || comparator.value === undefined) {
|
|
879
|
+
if (comparator.comparison === ComparisonOperator.Equals ||
|
|
880
|
+
comparator.comparison === ComparisonOperator.NotEquals) {
|
|
881
|
+
const nullCheck = comparator.comparison === ComparisonOperator.Equals ? "IS NULL" : "IS NOT NULL";
|
|
882
|
+
if (comparator.property.split(".").length > 1) {
|
|
883
|
+
const rootProp = comparator.property.split(".")[0];
|
|
884
|
+
const nestedPath = comparator.property.split(".").slice(1).join(".");
|
|
885
|
+
const rootSchema = this._entitySchema.properties?.find(p => p.property === rootProp);
|
|
886
|
+
const isArray = rootSchema?.type === EntitySchemaPropertyType.Array;
|
|
887
|
+
const jsonPath = isArray ? `$[*].${nestedPath}` : `$.${nestedPath}`;
|
|
888
|
+
const jsonExpr = `JSON_UNQUOTE(JSON_EXTRACT(\`${rootProp}\`, '${jsonPath}'))`;
|
|
889
|
+
return `${jsonExpr} ${nullCheck}`;
|
|
890
|
+
}
|
|
891
|
+
return `\`${prop}\` ${nullCheck}`;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
546
894
|
const dbValue = this.propertyToDbValue(comparator.value, type);
|
|
547
895
|
values.push(dbValue);
|
|
548
896
|
if (comparator.property.split(".").length > 1) {
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
897
|
+
const rootProp = comparator.property.split(".")[0];
|
|
898
|
+
const nestedPath = comparator.property.split(".").slice(1).join(".");
|
|
899
|
+
const rootSchema = this._entitySchema.properties?.find(p => p.property === rootProp);
|
|
900
|
+
const isArray = rootSchema?.type === EntitySchemaPropertyType.Array;
|
|
901
|
+
const jsonPath = isArray ? `$[*].${nestedPath}` : `$.${nestedPath}`;
|
|
902
|
+
const jsonExpr = `JSON_UNQUOTE(JSON_EXTRACT(\`${rootProp}\`, '${jsonPath}'))`;
|
|
903
|
+
switch (comparator.comparison) {
|
|
904
|
+
case ComparisonOperator.Includes: {
|
|
905
|
+
values.pop();
|
|
906
|
+
values.push(`%${String(comparator.value).toLowerCase()}%`);
|
|
907
|
+
return `LOWER(${jsonExpr}) LIKE ?`;
|
|
908
|
+
}
|
|
909
|
+
case ComparisonOperator.NotEquals:
|
|
910
|
+
return `${jsonExpr} <> ?`;
|
|
911
|
+
case ComparisonOperator.GreaterThan:
|
|
912
|
+
return `${jsonExpr} > ?`;
|
|
913
|
+
case ComparisonOperator.LessThan:
|
|
914
|
+
return `${jsonExpr} < ?`;
|
|
915
|
+
case ComparisonOperator.GreaterThanOrEqual:
|
|
916
|
+
return `${jsonExpr} >= ?`;
|
|
917
|
+
case ComparisonOperator.LessThanOrEqual:
|
|
918
|
+
return `${jsonExpr} <= ?`;
|
|
919
|
+
default:
|
|
920
|
+
return `${jsonExpr} = ?`;
|
|
921
|
+
}
|
|
568
922
|
}
|
|
569
|
-
|
|
570
|
-
|
|
923
|
+
switch (comparator.comparison) {
|
|
924
|
+
case ComparisonOperator.Equals:
|
|
925
|
+
if (Is.object(comparator.value) || Is.array(comparator.value)) {
|
|
926
|
+
return `JSON_CONTAINS(\`${prop}\`, ?)`;
|
|
927
|
+
}
|
|
928
|
+
return `\`${prop}\` = ?`;
|
|
929
|
+
case ComparisonOperator.NotEquals:
|
|
930
|
+
if (Is.object(comparator.value) || Is.array(comparator.value)) {
|
|
931
|
+
return `NOT JSON_CONTAINS(\`${prop}\`, ?)`;
|
|
932
|
+
}
|
|
933
|
+
return `\`${prop}\` <> ?`;
|
|
934
|
+
case ComparisonOperator.GreaterThan:
|
|
935
|
+
return `\`${prop}\` > ?`;
|
|
936
|
+
case ComparisonOperator.LessThan:
|
|
937
|
+
return `\`${prop}\` < ?`;
|
|
938
|
+
case ComparisonOperator.GreaterThanOrEqual:
|
|
939
|
+
return `\`${prop}\` >= ?`;
|
|
940
|
+
case ComparisonOperator.LessThanOrEqual:
|
|
941
|
+
return `\`${prop}\` <= ?`;
|
|
942
|
+
case ComparisonOperator.Includes: {
|
|
943
|
+
if (type === EntitySchemaPropertyType.String) {
|
|
944
|
+
values.pop();
|
|
945
|
+
values.push(`%${String(comparator.value).toLowerCase()}%`);
|
|
946
|
+
return `LOWER(\`${prop}\`) LIKE ?`;
|
|
947
|
+
}
|
|
948
|
+
return `JSON_CONTAINS(\`${prop}\`, ?)`;
|
|
949
|
+
}
|
|
950
|
+
case ComparisonOperator.NotIncludes: {
|
|
951
|
+
if (type === EntitySchemaPropertyType.String) {
|
|
952
|
+
values.pop();
|
|
953
|
+
values.push(`%${String(comparator.value).toLowerCase()}%`);
|
|
954
|
+
return `LOWER(\`${prop}\`) NOT LIKE ?`;
|
|
955
|
+
}
|
|
956
|
+
return `NOT JSON_CONTAINS(\`${prop}\`, ?)`;
|
|
957
|
+
}
|
|
958
|
+
default:
|
|
959
|
+
throw new GeneralError(MySqlEntityStorageConnector.CLASS_NAME, "comparisonNotSupported", {
|
|
960
|
+
comparison: comparator.comparison
|
|
961
|
+
});
|
|
571
962
|
}
|
|
572
|
-
throw new GeneralError(MySqlEntityStorageConnector.CLASS_NAME, "comparisonNotSupported", {
|
|
573
|
-
comparison: comparator.comparison
|
|
574
|
-
});
|
|
575
963
|
}
|
|
576
964
|
/**
|
|
577
965
|
* Format a value to insert into DB.
|
|
@@ -591,7 +979,7 @@ export class MySqlEntityStorageConnector {
|
|
|
591
979
|
return Number(value);
|
|
592
980
|
}
|
|
593
981
|
else if (type === "boolean") {
|
|
594
|
-
return
|
|
982
|
+
return value ? 1 : 0;
|
|
595
983
|
}
|
|
596
984
|
return value;
|
|
597
985
|
}
|
|
@@ -623,10 +1011,12 @@ export class MySqlEntityStorageConnector {
|
|
|
623
1011
|
}
|
|
624
1012
|
/**
|
|
625
1013
|
* Map entity schema properties to SQL properties.
|
|
1014
|
+
* @param schema The schema to use, defaults to the connector's own schema.
|
|
626
1015
|
* @returns The SQL properties as a string.
|
|
627
1016
|
* @throws GeneralError if the entity properties do not exist.
|
|
628
1017
|
*/
|
|
629
|
-
mapMySqlProperties() {
|
|
1018
|
+
mapMySqlProperties(schema) {
|
|
1019
|
+
const entitySchema = schema ?? this._entitySchema;
|
|
630
1020
|
const sqlTypeMap = {
|
|
631
1021
|
[EntitySchemaPropertyType.String]: "LONGTEXT",
|
|
632
1022
|
[EntitySchemaPropertyType.Number]: "FLOAT",
|
|
@@ -635,11 +1025,11 @@ export class MySqlEntityStorageConnector {
|
|
|
635
1025
|
[EntitySchemaPropertyType.Array]: "JSON",
|
|
636
1026
|
[EntitySchemaPropertyType.Boolean]: "TINYINT(1)"
|
|
637
1027
|
};
|
|
638
|
-
if (!
|
|
1028
|
+
if (!entitySchema.properties) {
|
|
639
1029
|
throw new GeneralError(MySqlEntityStorageConnector.CLASS_NAME, "entitySchemaPropertiesUndefined");
|
|
640
1030
|
}
|
|
641
1031
|
const primaryKeys = [];
|
|
642
|
-
const props = [...
|
|
1032
|
+
const props = [...entitySchema.properties];
|
|
643
1033
|
props.unshift({
|
|
644
1034
|
property: MySqlEntityStorageConnector._PARTITION_KEY,
|
|
645
1035
|
type: EntitySchemaPropertyType.String,
|
|
@@ -650,7 +1040,7 @@ export class MySqlEntityStorageConnector {
|
|
|
650
1040
|
let sqlType = sqlTypeMap[prop.type] || "TEXT";
|
|
651
1041
|
if (prop.format) {
|
|
652
1042
|
switch (prop.type) {
|
|
653
|
-
case
|
|
1043
|
+
case EntitySchemaPropertyType.String:
|
|
654
1044
|
sqlType = "LONGTEXT";
|
|
655
1045
|
switch (prop.format) {
|
|
656
1046
|
case "uuid":
|
|
@@ -662,7 +1052,7 @@ export class MySqlEntityStorageConnector {
|
|
|
662
1052
|
break;
|
|
663
1053
|
}
|
|
664
1054
|
break;
|
|
665
|
-
case
|
|
1055
|
+
case EntitySchemaPropertyType.Number:
|
|
666
1056
|
sqlType = "FLOAT";
|
|
667
1057
|
switch (prop.format) {
|
|
668
1058
|
case "float":
|
|
@@ -673,7 +1063,7 @@ export class MySqlEntityStorageConnector {
|
|
|
673
1063
|
break;
|
|
674
1064
|
}
|
|
675
1065
|
break;
|
|
676
|
-
case
|
|
1066
|
+
case EntitySchemaPropertyType.Integer:
|
|
677
1067
|
sqlType = "INT";
|
|
678
1068
|
switch (prop.format) {
|
|
679
1069
|
case "int8":
|