@twin.org/entity-storage-connector-mysql 0.0.3-next.1 → 0.0.3-next.11
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 +310 -81
- package/dist/es/mysqlEntityStorageConnector.js.map +1 -1
- package/dist/types/models/IMySqlEntityStorageConnectorConfig.d.ts +35 -0
- package/dist/types/mysqlEntityStorageConnector.d.ts +38 -8
- package/docs/changelog.md +211 -51
- package/docs/examples.md +105 -1
- package/docs/reference/classes/MySqlEntityStorageConnector.md +156 -26
- package/docs/reference/interfaces/IMySqlEntityStorageConnectorConfig.md +87 -7
- package/docs/reference/interfaces/IMySqlEntityStorageConnectorConstructorOptions.md +6 -6
- package/locales/en.json +15 -2
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Entity Storage Connector MySQL
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This package provides a MySQL backend for relational persistence and SQL-based access patterns. 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,13 @@ Entity Storage connector implementation using MySQL storage.
|
|
|
8
8
|
npm install @twin.org/entity-storage-connector-mysql
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Docker
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
To perform testing of this component it may be necessary to launch a local instance to communicate with.
|
|
14
14
|
|
|
15
|
-
```
|
|
16
|
-
docker
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
Afterwards you can run the tests as follows:
|
|
20
|
-
|
|
21
|
-
```sh
|
|
22
|
-
npm run test
|
|
15
|
+
```shell
|
|
16
|
+
docker pull mysql:latest
|
|
17
|
+
docker run -d --name twin-entity-storage-mysql -e MYSQL_ROOT_PASSWORD=password -p 3400:3306 mysql:latest
|
|
23
18
|
```
|
|
24
19
|
|
|
25
20
|
## Examples
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"IMySqlEntityStorageConnectorConfig.js","sourceRoot":"","sources":["../../../src/models/IMySqlEntityStorageConnectorConfig.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,uCAAuC","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\n\n/**\n * Configuration for the MySql Entity Storage Connector.\n */\nexport interface IMySqlEntityStorageConnectorConfig {\n\t/**\n\t * The host for the MySql instance.\n\t */\n\thost: string;\n\n\t/**\n\t * The port for the MySql instance.\n\t */\n\tport?: number;\n\n\t/**\n\t * The user for the MySql instance.\n\t */\n\tuser: string;\n\n\t/**\n\t * The password for the MySql instance.\n\t */\n\tpassword: string;\n\n\t/**\n\t * The name of the database to be used.\n\t */\n\tdatabase: string;\n\n\t/**\n\t * The name of the table to be used.\n\t */\n\ttableName: string;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"IMySqlEntityStorageConnectorConfig.js","sourceRoot":"","sources":["../../../src/models/IMySqlEntityStorageConnectorConfig.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,uCAAuC","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\n\n/**\n * Configuration for the MySql Entity Storage Connector.\n */\nexport interface IMySqlEntityStorageConnectorConfig {\n\t/**\n\t * The host for the MySql instance.\n\t */\n\thost: string;\n\n\t/**\n\t * The port for the MySql instance.\n\t */\n\tport?: number;\n\n\t/**\n\t * The user for the MySql instance.\n\t */\n\tuser: string;\n\n\t/**\n\t * The password for the MySql instance.\n\t */\n\tpassword: string;\n\n\t/**\n\t * The name of the database to be used.\n\t */\n\tdatabase: string;\n\n\t/**\n\t * The name of the table to be used.\n\t */\n\ttableName: string;\n\n\t/**\n\t * Optional connection pool configuration.\n\t */\n\tpool?: {\n\t\t/**\n\t\t * Maximum number of connections in pool.\n\t\t * @default 10\n\t\t */\n\t\tconnectionLimit?: number;\n\n\t\t/**\n\t\t * Maximum number of idle connections.\n\t\t * @default 10\n\t\t */\n\t\tmaxIdle?: number;\n\n\t\t/**\n\t\t * Time in ms before removing idle connection.\n\t\t * @default 60000 (1 minute)\n\t\t */\n\t\tidleTimeout?: number;\n\n\t\t/**\n\t\t * Enable TCP keep-alive.\n\t\t * @default true\n\t\t */\n\t\tenableKeepAlive?: boolean;\n\n\t\t/**\n\t\t * Wait for available connection when pool is full.\n\t\t * @default true\n\t\t */\n\t\twaitForConnections?: boolean;\n\n\t\t/**\n\t\t * Maximum queued requests (0 = unlimited).\n\t\t * @default 0\n\t\t */\n\t\tqueueLimit?: number;\n\t};\n}\n"]}
|
|
@@ -1,9 +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 { 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 { createPool } from "mysql2/promise";
|
|
7
7
|
/**
|
|
8
8
|
* Class for performing entity storage operations using MySql.
|
|
9
9
|
*/
|
|
@@ -43,10 +43,10 @@ export class MySqlEntityStorageConnector {
|
|
|
43
43
|
*/
|
|
44
44
|
_config;
|
|
45
45
|
/**
|
|
46
|
-
* The
|
|
46
|
+
* The connection pool for MySql.
|
|
47
47
|
* @internal
|
|
48
48
|
*/
|
|
49
|
-
|
|
49
|
+
_pool;
|
|
50
50
|
/**
|
|
51
51
|
* The primary key property.
|
|
52
52
|
* @internal
|
|
@@ -77,6 +77,34 @@ export class MySqlEntityStorageConnector {
|
|
|
77
77
|
className() {
|
|
78
78
|
return MySqlEntityStorageConnector.CLASS_NAME;
|
|
79
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Get the health of the component.
|
|
82
|
+
* @returns The health of the component.
|
|
83
|
+
*/
|
|
84
|
+
async health() {
|
|
85
|
+
try {
|
|
86
|
+
await this.getPool().query(`SELECT 1 FROM \`${this._config.database}\`.\`${this._config.tableName}\` LIMIT 0`);
|
|
87
|
+
return [
|
|
88
|
+
{
|
|
89
|
+
source: MySqlEntityStorageConnector.CLASS_NAME,
|
|
90
|
+
status: HealthStatus.Ok,
|
|
91
|
+
description: "healthDescription",
|
|
92
|
+
data: { database: this._config.database, tableName: this._config.tableName }
|
|
93
|
+
}
|
|
94
|
+
];
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return [
|
|
98
|
+
{
|
|
99
|
+
source: MySqlEntityStorageConnector.CLASS_NAME,
|
|
100
|
+
status: HealthStatus.Error,
|
|
101
|
+
description: "healthDescription",
|
|
102
|
+
message: "connectionFailed",
|
|
103
|
+
data: { database: this._config.database, tableName: this._config.tableName }
|
|
104
|
+
}
|
|
105
|
+
];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
80
108
|
/**
|
|
81
109
|
* Get the schema for the entities.
|
|
82
110
|
* @returns The schema for the entities.
|
|
@@ -92,7 +120,7 @@ export class MySqlEntityStorageConnector {
|
|
|
92
120
|
async bootstrap(nodeLoggingComponentType) {
|
|
93
121
|
const nodeLogging = ComponentFactory.getIfExists(nodeLoggingComponentType);
|
|
94
122
|
try {
|
|
95
|
-
const
|
|
123
|
+
const pool = this.getPool();
|
|
96
124
|
const databaseExists = await this.databaseExists();
|
|
97
125
|
if (!databaseExists) {
|
|
98
126
|
await nodeLogging?.log({
|
|
@@ -104,7 +132,7 @@ export class MySqlEntityStorageConnector {
|
|
|
104
132
|
databaseName: this._config.database
|
|
105
133
|
}
|
|
106
134
|
});
|
|
107
|
-
await
|
|
135
|
+
await pool.query(`CREATE DATABASE IF NOT EXISTS \`${this._config.database}\``);
|
|
108
136
|
await this.waitForDatabaseExists();
|
|
109
137
|
}
|
|
110
138
|
else {
|
|
@@ -129,7 +157,7 @@ export class MySqlEntityStorageConnector {
|
|
|
129
157
|
tableName: this._config.tableName
|
|
130
158
|
}
|
|
131
159
|
});
|
|
132
|
-
await
|
|
160
|
+
await pool.query(`CREATE TABLE IF NOT EXISTS \`${this._config.database}\`.\`${this._config.tableName}\` (${this.mapMySqlProperties()})`);
|
|
133
161
|
await this.waitForTableExists();
|
|
134
162
|
}
|
|
135
163
|
else {
|
|
@@ -159,6 +187,29 @@ export class MySqlEntityStorageConnector {
|
|
|
159
187
|
}
|
|
160
188
|
return true;
|
|
161
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* The component needs to be stopped when the node is closed.
|
|
192
|
+
* @param nodeLoggingComponentType The node logging component type.
|
|
193
|
+
* @returns Nothing.
|
|
194
|
+
*/
|
|
195
|
+
async stop(nodeLoggingComponentType) {
|
|
196
|
+
if (this._pool) {
|
|
197
|
+
const poolConfig = this.createPoolConfig();
|
|
198
|
+
const poolId = `${poolConfig.host}|${poolConfig.port}|${poolConfig.user}`;
|
|
199
|
+
let sharedPools = SharedStore.get("mySqlPools");
|
|
200
|
+
sharedPools ??= {};
|
|
201
|
+
if (sharedPools[poolId]) {
|
|
202
|
+
// Decrease the use counter and close the pool if no longer used
|
|
203
|
+
sharedPools[poolId].useCounter--;
|
|
204
|
+
if (sharedPools[poolId].useCounter <= 0) {
|
|
205
|
+
await this._pool.end();
|
|
206
|
+
delete sharedPools[poolId];
|
|
207
|
+
}
|
|
208
|
+
SharedStore.set("mySqlPools", sharedPools);
|
|
209
|
+
}
|
|
210
|
+
this._pool = undefined;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
162
213
|
/**
|
|
163
214
|
* Get an entity from MySql.
|
|
164
215
|
* @param id The id of the entity to get, or the index value if secondaryIndex is set.
|
|
@@ -171,7 +222,7 @@ export class MySqlEntityStorageConnector {
|
|
|
171
222
|
const contextIds = await ContextIdStore.getContextIds();
|
|
172
223
|
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
173
224
|
try {
|
|
174
|
-
const
|
|
225
|
+
const pool = this.getPool();
|
|
175
226
|
const whereClauses = [];
|
|
176
227
|
const values = [];
|
|
177
228
|
whereClauses.push(`\`${MySqlEntityStorageConnector._PARTITION_KEY}\` = ?`);
|
|
@@ -190,7 +241,7 @@ export class MySqlEntityStorageConnector {
|
|
|
190
241
|
}
|
|
191
242
|
}
|
|
192
243
|
const query = `SELECT * FROM \`${this._config.database}\`.\`${this._config.tableName}\` WHERE ${whereClauses.join(" AND ")} LIMIT 1`;
|
|
193
|
-
const [rows] = await
|
|
244
|
+
const [rows] = await pool.query(query, values);
|
|
194
245
|
if (Is.array(rows) && rows.length === 1) {
|
|
195
246
|
const item = ObjectHelper.removeEmptyProperties(rows[0], { removeNull: true });
|
|
196
247
|
ObjectHelper.propertyDelete(item, MySqlEntityStorageConnector._PARTITION_KEY);
|
|
@@ -248,8 +299,8 @@ export class MySqlEntityStorageConnector {
|
|
|
248
299
|
sql += ` (${keys.map(key => `\`${key}\``).join(", ")})`;
|
|
249
300
|
sql += ` VALUES (${values.map(() => "?").join(", ")})`;
|
|
250
301
|
sql += ` ON DUPLICATE KEY UPDATE ${keys.map(key => `\`${key}\` = VALUES(\`${key}\`)`).join(", ")};`;
|
|
251
|
-
const
|
|
252
|
-
await
|
|
302
|
+
const pool = this.getPool();
|
|
303
|
+
await pool.query(sql, values);
|
|
253
304
|
}
|
|
254
305
|
catch (err) {
|
|
255
306
|
throw new GeneralError(MySqlEntityStorageConnector.CLASS_NAME, "setFailed", {
|
|
@@ -257,6 +308,67 @@ export class MySqlEntityStorageConnector {
|
|
|
257
308
|
}, err);
|
|
258
309
|
}
|
|
259
310
|
}
|
|
311
|
+
/**
|
|
312
|
+
* Set multiple entities in a batch.
|
|
313
|
+
* @param entities The entities to set.
|
|
314
|
+
* @returns Nothing.
|
|
315
|
+
*/
|
|
316
|
+
async setBatch(entities) {
|
|
317
|
+
Guards.arrayValue(MySqlEntityStorageConnector.CLASS_NAME, "entities", entities);
|
|
318
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
319
|
+
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
320
|
+
for (const entity of entities) {
|
|
321
|
+
EntitySchemaHelper.validateEntity(entity, this._entitySchema);
|
|
322
|
+
}
|
|
323
|
+
try {
|
|
324
|
+
const props = [...(this._entitySchema.properties ?? [])];
|
|
325
|
+
props.unshift({
|
|
326
|
+
property: MySqlEntityStorageConnector._PARTITION_KEY,
|
|
327
|
+
type: EntitySchemaPropertyType.String
|
|
328
|
+
});
|
|
329
|
+
const keys = props.map(p => p.property);
|
|
330
|
+
const allValues = [];
|
|
331
|
+
for (const entity of entities) {
|
|
332
|
+
const finalEntity = ObjectHelper.clone(entity);
|
|
333
|
+
ObjectHelper.propertySet(finalEntity, MySqlEntityStorageConnector._PARTITION_KEY, partitionKey ?? MySqlEntityStorageConnector._PARTITION_KEY_VALUE);
|
|
334
|
+
for (const prop of props) {
|
|
335
|
+
const val = finalEntity[prop.property];
|
|
336
|
+
if (prop.type === EntitySchemaPropertyType.Object ||
|
|
337
|
+
prop.type === EntitySchemaPropertyType.Array) {
|
|
338
|
+
allValues.push(Is.empty(val) ? null : JSON.stringify(val));
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
allValues.push(Is.empty(val) ? null : val);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
const rowPlaceholder = `(${keys.map(() => "?").join(", ")})`;
|
|
346
|
+
let sql = `INSERT INTO \`${this._config.database}\`.\`${this._config.tableName}\``;
|
|
347
|
+
sql += ` (${keys.map(key => `\`${key}\``).join(", ")})`;
|
|
348
|
+
sql += ` VALUES ${entities.map(() => rowPlaceholder).join(", ")}`;
|
|
349
|
+
sql += ` ON DUPLICATE KEY UPDATE ${keys.map(key => `\`${key}\` = VALUES(\`${key}\`)`).join(", ")};`;
|
|
350
|
+
const pool = this.getPool();
|
|
351
|
+
await pool.query(sql, allValues);
|
|
352
|
+
}
|
|
353
|
+
catch (err) {
|
|
354
|
+
throw new GeneralError(MySqlEntityStorageConnector.CLASS_NAME, "setBatchFailed", undefined, err);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Empty the entity storage.
|
|
359
|
+
* @returns Nothing.
|
|
360
|
+
*/
|
|
361
|
+
async empty() {
|
|
362
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
363
|
+
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
364
|
+
try {
|
|
365
|
+
const pool = this.getPool();
|
|
366
|
+
await pool.query(`DELETE FROM \`${this._config.database}\`.\`${this._config.tableName}\` WHERE \`${MySqlEntityStorageConnector._PARTITION_KEY}\` = ?`, [partitionKey ?? MySqlEntityStorageConnector._PARTITION_KEY_VALUE]);
|
|
367
|
+
}
|
|
368
|
+
catch (err) {
|
|
369
|
+
throw new GeneralError(MySqlEntityStorageConnector.CLASS_NAME, "emptyFailed", undefined, err);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
260
372
|
/**
|
|
261
373
|
* Remove the entity.
|
|
262
374
|
* @param id The id of the entity to remove.
|
|
@@ -268,7 +380,7 @@ export class MySqlEntityStorageConnector {
|
|
|
268
380
|
const contextIds = await ContextIdStore.getContextIds();
|
|
269
381
|
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
270
382
|
try {
|
|
271
|
-
const
|
|
383
|
+
const pool = this.getPool();
|
|
272
384
|
const itemData = await this.get(id, undefined, conditions);
|
|
273
385
|
if (Is.notEmpty(itemData)) {
|
|
274
386
|
const values = [];
|
|
@@ -284,7 +396,7 @@ export class MySqlEntityStorageConnector {
|
|
|
284
396
|
}));
|
|
285
397
|
}
|
|
286
398
|
const query = `DELETE FROM \`${this._config.database}\`.\`${this._config.tableName}\` WHERE ${whereClauses.join(" AND ")}`;
|
|
287
|
-
await
|
|
399
|
+
await pool.query(query, values);
|
|
288
400
|
}
|
|
289
401
|
}
|
|
290
402
|
catch (err) {
|
|
@@ -293,6 +405,67 @@ export class MySqlEntityStorageConnector {
|
|
|
293
405
|
}, err);
|
|
294
406
|
}
|
|
295
407
|
}
|
|
408
|
+
/**
|
|
409
|
+
* Teardown the entity storage by dropping the table.
|
|
410
|
+
* @param nodeLoggingComponentType The node logging component type.
|
|
411
|
+
* @returns True if the teardown process was successful.
|
|
412
|
+
*/
|
|
413
|
+
async teardown(nodeLoggingComponentType) {
|
|
414
|
+
const nodeLogging = ComponentFactory.getIfExists(nodeLoggingComponentType);
|
|
415
|
+
await nodeLogging?.log({
|
|
416
|
+
level: "info",
|
|
417
|
+
source: MySqlEntityStorageConnector.CLASS_NAME,
|
|
418
|
+
ts: Date.now(),
|
|
419
|
+
message: "tableDropping",
|
|
420
|
+
data: { tableName: this._config.tableName }
|
|
421
|
+
});
|
|
422
|
+
try {
|
|
423
|
+
if (await this.tableExists()) {
|
|
424
|
+
const pool = this.getPool();
|
|
425
|
+
await pool.query(`DROP TABLE \`${this._config.database}\`.\`${this._config.tableName}\`;`);
|
|
426
|
+
await this.waitForTableNotExists();
|
|
427
|
+
}
|
|
428
|
+
await nodeLogging?.log({
|
|
429
|
+
level: "info",
|
|
430
|
+
source: MySqlEntityStorageConnector.CLASS_NAME,
|
|
431
|
+
ts: Date.now(),
|
|
432
|
+
message: "tableDropped",
|
|
433
|
+
data: { tableName: this._config.tableName }
|
|
434
|
+
});
|
|
435
|
+
return true;
|
|
436
|
+
}
|
|
437
|
+
catch (err) {
|
|
438
|
+
await nodeLogging?.log({
|
|
439
|
+
level: "error",
|
|
440
|
+
source: MySqlEntityStorageConnector.CLASS_NAME,
|
|
441
|
+
ts: Date.now(),
|
|
442
|
+
message: "teardownFailed",
|
|
443
|
+
error: BaseError.fromError(err)
|
|
444
|
+
});
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Remove multiple entities by their primary key IDs.
|
|
450
|
+
* @param ids The ids of the entities to remove.
|
|
451
|
+
* @returns Nothing.
|
|
452
|
+
*/
|
|
453
|
+
async removeBatch(ids) {
|
|
454
|
+
Guards.arrayValue(MySqlEntityStorageConnector.CLASS_NAME, "ids", ids);
|
|
455
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
456
|
+
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
457
|
+
try {
|
|
458
|
+
const pool = this.getPool();
|
|
459
|
+
const sql = `DELETE FROM \`${this._config.database}\`.\`${this._config.tableName}\` WHERE \`${MySqlEntityStorageConnector._PARTITION_KEY}\` = ? AND \`${String(this._primaryKeyProperty.property)}\` IN (?)`;
|
|
460
|
+
await pool.query(sql, [
|
|
461
|
+
partitionKey ?? MySqlEntityStorageConnector._PARTITION_KEY_VALUE,
|
|
462
|
+
ids
|
|
463
|
+
]);
|
|
464
|
+
}
|
|
465
|
+
catch (err) {
|
|
466
|
+
throw new GeneralError(MySqlEntityStorageConnector.CLASS_NAME, "removeBatchFailed", undefined, err);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
296
469
|
/**
|
|
297
470
|
* Find all the entities which match the conditions.
|
|
298
471
|
* @param conditions The conditions to match for the entities.
|
|
@@ -337,8 +510,8 @@ export class MySqlEntityStorageConnector {
|
|
|
337
510
|
sql = `SELECT ${properties ? properties.map(p => `\`${String(p)}\``).join(", ") : "*"} FROM \`${this._config.database}\`.\`${this._config.tableName}\``;
|
|
338
511
|
sql += ` WHERE ${whereClauses.join(" AND ")} ${orderByClause}`;
|
|
339
512
|
sql += ` LIMIT ${returnSize} OFFSET ${startIndex}`;
|
|
340
|
-
const
|
|
341
|
-
const [rows] = (await
|
|
513
|
+
const pool = this.getPool();
|
|
514
|
+
const [rows] = (await pool.query(sql, values)) ?? [];
|
|
342
515
|
const entities = rows;
|
|
343
516
|
for (let i = 0; i < entities.length; i++) {
|
|
344
517
|
entities[i] = ObjectHelper.removeEmptyProperties(entities[i], { removeNull: true });
|
|
@@ -356,34 +529,19 @@ export class MySqlEntityStorageConnector {
|
|
|
356
529
|
}
|
|
357
530
|
}
|
|
358
531
|
/**
|
|
359
|
-
*
|
|
360
|
-
* @returns
|
|
532
|
+
* Count all the entities which match the conditions.
|
|
533
|
+
* @returns The total count of entities in the storage.
|
|
361
534
|
*/
|
|
362
|
-
async
|
|
535
|
+
async count() {
|
|
363
536
|
try {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
537
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
538
|
+
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
539
|
+
const pool = this.getPool();
|
|
540
|
+
const [rows] = await pool.query(`SELECT COUNT(*) AS count FROM \`${this._config.database}\`.\`${this._config.tableName}\` WHERE \`${MySqlEntityStorageConnector._PARTITION_KEY}\` = ?`, [partitionKey ?? MySqlEntityStorageConnector._PARTITION_KEY_VALUE]);
|
|
541
|
+
return Number(rows[0].count);
|
|
369
542
|
}
|
|
370
|
-
catch {
|
|
371
|
-
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
/**
|
|
375
|
-
* Empty the table.
|
|
376
|
-
* @returns Nothing.
|
|
377
|
-
*/
|
|
378
|
-
async tableEmpty() {
|
|
379
|
-
try {
|
|
380
|
-
if (await this.tableExists()) {
|
|
381
|
-
const dbConnection = await this.createConnection();
|
|
382
|
-
await dbConnection.query(`TRUNCATE TABLE \`${this._config.database}\`.\`${this._config.tableName}\`;`);
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
catch {
|
|
386
|
-
// Ignore errors
|
|
543
|
+
catch (err) {
|
|
544
|
+
throw new GeneralError(MySqlEntityStorageConnector.CLASS_NAME, "countFailed", undefined, err);
|
|
387
545
|
}
|
|
388
546
|
}
|
|
389
547
|
/**
|
|
@@ -392,8 +550,8 @@ export class MySqlEntityStorageConnector {
|
|
|
392
550
|
*/
|
|
393
551
|
async databaseExists() {
|
|
394
552
|
try {
|
|
395
|
-
const
|
|
396
|
-
const [rows] = await
|
|
553
|
+
const pool = this.getPool();
|
|
554
|
+
const [rows] = await pool.query("SHOW DATABASES LIKE ?;", [this._config.database]);
|
|
397
555
|
return Is.arrayValue(rows);
|
|
398
556
|
}
|
|
399
557
|
catch {
|
|
@@ -421,8 +579,8 @@ export class MySqlEntityStorageConnector {
|
|
|
421
579
|
*/
|
|
422
580
|
async tableExists() {
|
|
423
581
|
try {
|
|
424
|
-
const
|
|
425
|
-
const [rows] = await
|
|
582
|
+
const pool = this.getPool();
|
|
583
|
+
const [rows] = await pool.query("SHOW TABLES FROM ?? LIKE ?", [
|
|
426
584
|
this._config.database,
|
|
427
585
|
this._config.tableName
|
|
428
586
|
]);
|
|
@@ -461,29 +619,49 @@ export class MySqlEntityStorageConnector {
|
|
|
461
619
|
}
|
|
462
620
|
}
|
|
463
621
|
/**
|
|
464
|
-
*
|
|
465
|
-
* @returns The MySql connection.
|
|
622
|
+
* Get or create the connection pool.
|
|
623
|
+
* @returns The MySql connection pool.
|
|
466
624
|
* @internal
|
|
467
625
|
*/
|
|
468
|
-
|
|
469
|
-
if (this.
|
|
470
|
-
|
|
626
|
+
getPool() {
|
|
627
|
+
if (!this._pool) {
|
|
628
|
+
const poolConfig = this.createPoolConfig();
|
|
629
|
+
const poolId = `${poolConfig.host}|${poolConfig.port}|${poolConfig.user}`;
|
|
630
|
+
let sharedPools = SharedStore.get("mySqlPools");
|
|
631
|
+
sharedPools ??= {};
|
|
632
|
+
// If there is no pool for the id, create it
|
|
633
|
+
if (!sharedPools[poolId]) {
|
|
634
|
+
sharedPools[poolId] = {
|
|
635
|
+
pool: createPool(poolConfig),
|
|
636
|
+
useCounter: 0
|
|
637
|
+
};
|
|
638
|
+
SharedStore.set("mySqlPools", sharedPools);
|
|
639
|
+
}
|
|
640
|
+
// Increase the use counter and return the pool
|
|
641
|
+
sharedPools[poolId].useCounter++;
|
|
642
|
+
this._pool = sharedPools[poolId].pool;
|
|
471
643
|
}
|
|
472
|
-
|
|
473
|
-
this._connection = newConnection;
|
|
474
|
-
return newConnection;
|
|
644
|
+
return this._pool;
|
|
475
645
|
}
|
|
476
646
|
/**
|
|
477
|
-
* Create
|
|
478
|
-
* @returns The MySql
|
|
647
|
+
* Create the connection pool configuration.
|
|
648
|
+
* @returns The MySql pool configuration.
|
|
479
649
|
* @internal
|
|
480
650
|
*/
|
|
481
|
-
|
|
651
|
+
createPoolConfig() {
|
|
652
|
+
const poolConfig = this._config.pool ?? {};
|
|
482
653
|
return {
|
|
483
654
|
host: this._config.host,
|
|
484
655
|
port: this._config.port ?? 3306,
|
|
485
656
|
user: this._config.user,
|
|
486
|
-
password: this._config.password
|
|
657
|
+
password: this._config.password,
|
|
658
|
+
connectionLimit: poolConfig.connectionLimit ?? 10,
|
|
659
|
+
maxIdle: poolConfig.maxIdle ?? 10,
|
|
660
|
+
idleTimeout: poolConfig.idleTimeout ?? 60000,
|
|
661
|
+
enableKeepAlive: poolConfig.enableKeepAlive ?? true,
|
|
662
|
+
keepAliveInitialDelay: 0,
|
|
663
|
+
waitForConnections: poolConfig.waitForConnections ?? true,
|
|
664
|
+
queueLimit: poolConfig.queueLimit ?? 0
|
|
487
665
|
};
|
|
488
666
|
}
|
|
489
667
|
/**
|
|
@@ -536,42 +714,93 @@ export class MySqlEntityStorageConnector {
|
|
|
536
714
|
prop += ".";
|
|
537
715
|
}
|
|
538
716
|
prop += comparator.property;
|
|
539
|
-
// prop = prop.replace(/\./g, "->");
|
|
540
717
|
if (comparator.comparison === ComparisonOperator.In) {
|
|
541
718
|
const inValues = Is.array(comparator.value) ? comparator.value : [comparator.value];
|
|
542
719
|
values.push(...inValues.map(val => this.propertyToDbValue(val, type)));
|
|
543
720
|
const placeholders = inValues.map(() => "?").join(", ");
|
|
544
721
|
return `\`${prop}\` IN (${placeholders})`;
|
|
545
722
|
}
|
|
723
|
+
// null/undefined must use IS NULL / IS NOT NULL — never a parameterised placeholder.
|
|
724
|
+
// Passing undefined through propertyToDbValue() coerces it to NaN for number fields
|
|
725
|
+
// (Number(undefined) === NaN), and null coerces to 0 (Number(null) === 0), both of
|
|
726
|
+
// which produce semantically wrong or invalid SQL.
|
|
727
|
+
if (comparator.value === null || comparator.value === undefined) {
|
|
728
|
+
if (comparator.comparison === ComparisonOperator.Equals ||
|
|
729
|
+
comparator.comparison === ComparisonOperator.NotEquals) {
|
|
730
|
+
const nullCheck = comparator.comparison === ComparisonOperator.Equals ? "IS NULL" : "IS NOT NULL";
|
|
731
|
+
if (comparator.property.split(".").length > 1) {
|
|
732
|
+
const rootProp = comparator.property.split(".")[0];
|
|
733
|
+
const nestedPath = comparator.property.split(".").slice(1).join(".");
|
|
734
|
+
const rootSchema = this._entitySchema.properties?.find(p => p.property === rootProp);
|
|
735
|
+
const isArray = rootSchema?.type === EntitySchemaPropertyType.Array;
|
|
736
|
+
const jsonPath = isArray ? `$[*].${nestedPath}` : `$.${nestedPath}`;
|
|
737
|
+
const jsonExpr = `JSON_UNQUOTE(JSON_EXTRACT(\`${rootProp}\`, '${jsonPath}'))`;
|
|
738
|
+
return `${jsonExpr} ${nullCheck}`;
|
|
739
|
+
}
|
|
740
|
+
return `\`${prop}\` ${nullCheck}`;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
546
743
|
const dbValue = this.propertyToDbValue(comparator.value, type);
|
|
547
744
|
values.push(dbValue);
|
|
548
745
|
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
|
-
|
|
746
|
+
const rootProp = comparator.property.split(".")[0];
|
|
747
|
+
const nestedPath = comparator.property.split(".").slice(1).join(".");
|
|
748
|
+
const rootSchema = this._entitySchema.properties?.find(p => p.property === rootProp);
|
|
749
|
+
const isArray = rootSchema?.type === EntitySchemaPropertyType.Array;
|
|
750
|
+
const jsonPath = isArray ? `$[*].${nestedPath}` : `$.${nestedPath}`;
|
|
751
|
+
const jsonExpr = `JSON_UNQUOTE(JSON_EXTRACT(\`${rootProp}\`, '${jsonPath}'))`;
|
|
752
|
+
switch (comparator.comparison) {
|
|
753
|
+
case ComparisonOperator.Includes: {
|
|
754
|
+
values.pop();
|
|
755
|
+
values.push(`%${String(comparator.value).toLowerCase()}%`);
|
|
756
|
+
return `LOWER(${jsonExpr}) LIKE ?`;
|
|
757
|
+
}
|
|
758
|
+
case ComparisonOperator.NotEquals:
|
|
759
|
+
return `${jsonExpr} <> ?`;
|
|
760
|
+
case ComparisonOperator.GreaterThan:
|
|
761
|
+
return `${jsonExpr} > ?`;
|
|
762
|
+
case ComparisonOperator.LessThan:
|
|
763
|
+
return `${jsonExpr} < ?`;
|
|
764
|
+
case ComparisonOperator.GreaterThanOrEqual:
|
|
765
|
+
return `${jsonExpr} >= ?`;
|
|
766
|
+
case ComparisonOperator.LessThanOrEqual:
|
|
767
|
+
return `${jsonExpr} <= ?`;
|
|
768
|
+
default:
|
|
769
|
+
return `${jsonExpr} = ?`;
|
|
770
|
+
}
|
|
568
771
|
}
|
|
569
|
-
|
|
570
|
-
|
|
772
|
+
switch (comparator.comparison) {
|
|
773
|
+
case ComparisonOperator.Equals:
|
|
774
|
+
if (Is.object(comparator.value) || Is.array(comparator.value)) {
|
|
775
|
+
return `JSON_CONTAINS(\`${prop}\`, ?)`;
|
|
776
|
+
}
|
|
777
|
+
return `\`${prop}\` = ?`;
|
|
778
|
+
case ComparisonOperator.NotEquals:
|
|
779
|
+
if (Is.object(comparator.value) || Is.array(comparator.value)) {
|
|
780
|
+
return `NOT JSON_CONTAINS(\`${prop}\`, ?)`;
|
|
781
|
+
}
|
|
782
|
+
return `\`${prop}\` <> ?`;
|
|
783
|
+
case ComparisonOperator.GreaterThan:
|
|
784
|
+
return `\`${prop}\` > ?`;
|
|
785
|
+
case ComparisonOperator.LessThan:
|
|
786
|
+
return `\`${prop}\` < ?`;
|
|
787
|
+
case ComparisonOperator.GreaterThanOrEqual:
|
|
788
|
+
return `\`${prop}\` >= ?`;
|
|
789
|
+
case ComparisonOperator.LessThanOrEqual:
|
|
790
|
+
return `\`${prop}\` <= ?`;
|
|
791
|
+
case ComparisonOperator.Includes: {
|
|
792
|
+
if (type === EntitySchemaPropertyType.String) {
|
|
793
|
+
values.pop();
|
|
794
|
+
values.push(`%${String(comparator.value).toLowerCase()}%`);
|
|
795
|
+
return `LOWER(\`${prop}\`) LIKE ?`;
|
|
796
|
+
}
|
|
797
|
+
return `JSON_CONTAINS(\`${prop}\`, ?)`;
|
|
798
|
+
}
|
|
799
|
+
default:
|
|
800
|
+
throw new GeneralError(MySqlEntityStorageConnector.CLASS_NAME, "comparisonNotSupported", {
|
|
801
|
+
comparison: comparator.comparison
|
|
802
|
+
});
|
|
571
803
|
}
|
|
572
|
-
throw new GeneralError(MySqlEntityStorageConnector.CLASS_NAME, "comparisonNotSupported", {
|
|
573
|
-
comparison: comparator.comparison
|
|
574
|
-
});
|
|
575
804
|
}
|
|
576
805
|
/**
|
|
577
806
|
* Format a value to insert into DB.
|