@twin.org/entity-storage-connector-mysql 0.0.3-next.1 → 0.0.3-next.10
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 +308 -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 +195 -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,32 @@ 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
|
+
}
|
|
93
|
+
];
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return [
|
|
97
|
+
{
|
|
98
|
+
source: MySqlEntityStorageConnector.CLASS_NAME,
|
|
99
|
+
status: HealthStatus.Error,
|
|
100
|
+
description: "healthDescription",
|
|
101
|
+
message: "connectionFailed"
|
|
102
|
+
}
|
|
103
|
+
];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
80
106
|
/**
|
|
81
107
|
* Get the schema for the entities.
|
|
82
108
|
* @returns The schema for the entities.
|
|
@@ -92,7 +118,7 @@ export class MySqlEntityStorageConnector {
|
|
|
92
118
|
async bootstrap(nodeLoggingComponentType) {
|
|
93
119
|
const nodeLogging = ComponentFactory.getIfExists(nodeLoggingComponentType);
|
|
94
120
|
try {
|
|
95
|
-
const
|
|
121
|
+
const pool = this.getPool();
|
|
96
122
|
const databaseExists = await this.databaseExists();
|
|
97
123
|
if (!databaseExists) {
|
|
98
124
|
await nodeLogging?.log({
|
|
@@ -104,7 +130,7 @@ export class MySqlEntityStorageConnector {
|
|
|
104
130
|
databaseName: this._config.database
|
|
105
131
|
}
|
|
106
132
|
});
|
|
107
|
-
await
|
|
133
|
+
await pool.query(`CREATE DATABASE IF NOT EXISTS \`${this._config.database}\``);
|
|
108
134
|
await this.waitForDatabaseExists();
|
|
109
135
|
}
|
|
110
136
|
else {
|
|
@@ -129,7 +155,7 @@ export class MySqlEntityStorageConnector {
|
|
|
129
155
|
tableName: this._config.tableName
|
|
130
156
|
}
|
|
131
157
|
});
|
|
132
|
-
await
|
|
158
|
+
await pool.query(`CREATE TABLE IF NOT EXISTS \`${this._config.database}\`.\`${this._config.tableName}\` (${this.mapMySqlProperties()})`);
|
|
133
159
|
await this.waitForTableExists();
|
|
134
160
|
}
|
|
135
161
|
else {
|
|
@@ -159,6 +185,29 @@ export class MySqlEntityStorageConnector {
|
|
|
159
185
|
}
|
|
160
186
|
return true;
|
|
161
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* The component needs to be stopped when the node is closed.
|
|
190
|
+
* @param nodeLoggingComponentType The node logging component type.
|
|
191
|
+
* @returns Nothing.
|
|
192
|
+
*/
|
|
193
|
+
async stop(nodeLoggingComponentType) {
|
|
194
|
+
if (this._pool) {
|
|
195
|
+
const poolConfig = this.createPoolConfig();
|
|
196
|
+
const poolId = `${poolConfig.host}|${poolConfig.port}|${poolConfig.user}`;
|
|
197
|
+
let sharedPools = SharedStore.get("mySqlPools");
|
|
198
|
+
sharedPools ??= {};
|
|
199
|
+
if (sharedPools[poolId]) {
|
|
200
|
+
// Decrease the use counter and close the pool if no longer used
|
|
201
|
+
sharedPools[poolId].useCounter--;
|
|
202
|
+
if (sharedPools[poolId].useCounter <= 0) {
|
|
203
|
+
await this._pool.end();
|
|
204
|
+
delete sharedPools[poolId];
|
|
205
|
+
}
|
|
206
|
+
SharedStore.set("mySqlPools", sharedPools);
|
|
207
|
+
}
|
|
208
|
+
this._pool = undefined;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
162
211
|
/**
|
|
163
212
|
* Get an entity from MySql.
|
|
164
213
|
* @param id The id of the entity to get, or the index value if secondaryIndex is set.
|
|
@@ -171,7 +220,7 @@ export class MySqlEntityStorageConnector {
|
|
|
171
220
|
const contextIds = await ContextIdStore.getContextIds();
|
|
172
221
|
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
173
222
|
try {
|
|
174
|
-
const
|
|
223
|
+
const pool = this.getPool();
|
|
175
224
|
const whereClauses = [];
|
|
176
225
|
const values = [];
|
|
177
226
|
whereClauses.push(`\`${MySqlEntityStorageConnector._PARTITION_KEY}\` = ?`);
|
|
@@ -190,7 +239,7 @@ export class MySqlEntityStorageConnector {
|
|
|
190
239
|
}
|
|
191
240
|
}
|
|
192
241
|
const query = `SELECT * FROM \`${this._config.database}\`.\`${this._config.tableName}\` WHERE ${whereClauses.join(" AND ")} LIMIT 1`;
|
|
193
|
-
const [rows] = await
|
|
242
|
+
const [rows] = await pool.query(query, values);
|
|
194
243
|
if (Is.array(rows) && rows.length === 1) {
|
|
195
244
|
const item = ObjectHelper.removeEmptyProperties(rows[0], { removeNull: true });
|
|
196
245
|
ObjectHelper.propertyDelete(item, MySqlEntityStorageConnector._PARTITION_KEY);
|
|
@@ -248,8 +297,8 @@ export class MySqlEntityStorageConnector {
|
|
|
248
297
|
sql += ` (${keys.map(key => `\`${key}\``).join(", ")})`;
|
|
249
298
|
sql += ` VALUES (${values.map(() => "?").join(", ")})`;
|
|
250
299
|
sql += ` ON DUPLICATE KEY UPDATE ${keys.map(key => `\`${key}\` = VALUES(\`${key}\`)`).join(", ")};`;
|
|
251
|
-
const
|
|
252
|
-
await
|
|
300
|
+
const pool = this.getPool();
|
|
301
|
+
await pool.query(sql, values);
|
|
253
302
|
}
|
|
254
303
|
catch (err) {
|
|
255
304
|
throw new GeneralError(MySqlEntityStorageConnector.CLASS_NAME, "setFailed", {
|
|
@@ -257,6 +306,67 @@ export class MySqlEntityStorageConnector {
|
|
|
257
306
|
}, err);
|
|
258
307
|
}
|
|
259
308
|
}
|
|
309
|
+
/**
|
|
310
|
+
* Set multiple entities in a batch.
|
|
311
|
+
* @param entities The entities to set.
|
|
312
|
+
* @returns Nothing.
|
|
313
|
+
*/
|
|
314
|
+
async setBatch(entities) {
|
|
315
|
+
Guards.arrayValue(MySqlEntityStorageConnector.CLASS_NAME, "entities", entities);
|
|
316
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
317
|
+
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
318
|
+
for (const entity of entities) {
|
|
319
|
+
EntitySchemaHelper.validateEntity(entity, this._entitySchema);
|
|
320
|
+
}
|
|
321
|
+
try {
|
|
322
|
+
const props = [...(this._entitySchema.properties ?? [])];
|
|
323
|
+
props.unshift({
|
|
324
|
+
property: MySqlEntityStorageConnector._PARTITION_KEY,
|
|
325
|
+
type: EntitySchemaPropertyType.String
|
|
326
|
+
});
|
|
327
|
+
const keys = props.map(p => p.property);
|
|
328
|
+
const allValues = [];
|
|
329
|
+
for (const entity of entities) {
|
|
330
|
+
const finalEntity = ObjectHelper.clone(entity);
|
|
331
|
+
ObjectHelper.propertySet(finalEntity, MySqlEntityStorageConnector._PARTITION_KEY, partitionKey ?? MySqlEntityStorageConnector._PARTITION_KEY_VALUE);
|
|
332
|
+
for (const prop of props) {
|
|
333
|
+
const val = finalEntity[prop.property];
|
|
334
|
+
if (prop.type === EntitySchemaPropertyType.Object ||
|
|
335
|
+
prop.type === EntitySchemaPropertyType.Array) {
|
|
336
|
+
allValues.push(Is.empty(val) ? null : JSON.stringify(val));
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
allValues.push(Is.empty(val) ? null : val);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
const rowPlaceholder = `(${keys.map(() => "?").join(", ")})`;
|
|
344
|
+
let sql = `INSERT INTO \`${this._config.database}\`.\`${this._config.tableName}\``;
|
|
345
|
+
sql += ` (${keys.map(key => `\`${key}\``).join(", ")})`;
|
|
346
|
+
sql += ` VALUES ${entities.map(() => rowPlaceholder).join(", ")}`;
|
|
347
|
+
sql += ` ON DUPLICATE KEY UPDATE ${keys.map(key => `\`${key}\` = VALUES(\`${key}\`)`).join(", ")};`;
|
|
348
|
+
const pool = this.getPool();
|
|
349
|
+
await pool.query(sql, allValues);
|
|
350
|
+
}
|
|
351
|
+
catch (err) {
|
|
352
|
+
throw new GeneralError(MySqlEntityStorageConnector.CLASS_NAME, "setBatchFailed", undefined, err);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Empty the entity storage.
|
|
357
|
+
* @returns Nothing.
|
|
358
|
+
*/
|
|
359
|
+
async empty() {
|
|
360
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
361
|
+
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
362
|
+
try {
|
|
363
|
+
const pool = this.getPool();
|
|
364
|
+
await pool.query(`DELETE FROM \`${this._config.database}\`.\`${this._config.tableName}\` WHERE \`${MySqlEntityStorageConnector._PARTITION_KEY}\` = ?`, [partitionKey ?? MySqlEntityStorageConnector._PARTITION_KEY_VALUE]);
|
|
365
|
+
}
|
|
366
|
+
catch (err) {
|
|
367
|
+
throw new GeneralError(MySqlEntityStorageConnector.CLASS_NAME, "emptyFailed", undefined, err);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
260
370
|
/**
|
|
261
371
|
* Remove the entity.
|
|
262
372
|
* @param id The id of the entity to remove.
|
|
@@ -268,7 +378,7 @@ export class MySqlEntityStorageConnector {
|
|
|
268
378
|
const contextIds = await ContextIdStore.getContextIds();
|
|
269
379
|
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
270
380
|
try {
|
|
271
|
-
const
|
|
381
|
+
const pool = this.getPool();
|
|
272
382
|
const itemData = await this.get(id, undefined, conditions);
|
|
273
383
|
if (Is.notEmpty(itemData)) {
|
|
274
384
|
const values = [];
|
|
@@ -284,7 +394,7 @@ export class MySqlEntityStorageConnector {
|
|
|
284
394
|
}));
|
|
285
395
|
}
|
|
286
396
|
const query = `DELETE FROM \`${this._config.database}\`.\`${this._config.tableName}\` WHERE ${whereClauses.join(" AND ")}`;
|
|
287
|
-
await
|
|
397
|
+
await pool.query(query, values);
|
|
288
398
|
}
|
|
289
399
|
}
|
|
290
400
|
catch (err) {
|
|
@@ -293,6 +403,67 @@ export class MySqlEntityStorageConnector {
|
|
|
293
403
|
}, err);
|
|
294
404
|
}
|
|
295
405
|
}
|
|
406
|
+
/**
|
|
407
|
+
* Teardown the entity storage by dropping the table.
|
|
408
|
+
* @param nodeLoggingComponentType The node logging component type.
|
|
409
|
+
* @returns True if the teardown process was successful.
|
|
410
|
+
*/
|
|
411
|
+
async teardown(nodeLoggingComponentType) {
|
|
412
|
+
const nodeLogging = ComponentFactory.getIfExists(nodeLoggingComponentType);
|
|
413
|
+
await nodeLogging?.log({
|
|
414
|
+
level: "info",
|
|
415
|
+
source: MySqlEntityStorageConnector.CLASS_NAME,
|
|
416
|
+
ts: Date.now(),
|
|
417
|
+
message: "tableDropping",
|
|
418
|
+
data: { tableName: this._config.tableName }
|
|
419
|
+
});
|
|
420
|
+
try {
|
|
421
|
+
if (await this.tableExists()) {
|
|
422
|
+
const pool = this.getPool();
|
|
423
|
+
await pool.query(`DROP TABLE \`${this._config.database}\`.\`${this._config.tableName}\`;`);
|
|
424
|
+
await this.waitForTableNotExists();
|
|
425
|
+
}
|
|
426
|
+
await nodeLogging?.log({
|
|
427
|
+
level: "info",
|
|
428
|
+
source: MySqlEntityStorageConnector.CLASS_NAME,
|
|
429
|
+
ts: Date.now(),
|
|
430
|
+
message: "tableDropped",
|
|
431
|
+
data: { tableName: this._config.tableName }
|
|
432
|
+
});
|
|
433
|
+
return true;
|
|
434
|
+
}
|
|
435
|
+
catch (err) {
|
|
436
|
+
await nodeLogging?.log({
|
|
437
|
+
level: "error",
|
|
438
|
+
source: MySqlEntityStorageConnector.CLASS_NAME,
|
|
439
|
+
ts: Date.now(),
|
|
440
|
+
message: "teardownFailed",
|
|
441
|
+
error: BaseError.fromError(err)
|
|
442
|
+
});
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Remove multiple entities by their primary key IDs.
|
|
448
|
+
* @param ids The ids of the entities to remove.
|
|
449
|
+
* @returns Nothing.
|
|
450
|
+
*/
|
|
451
|
+
async removeBatch(ids) {
|
|
452
|
+
Guards.arrayValue(MySqlEntityStorageConnector.CLASS_NAME, "ids", ids);
|
|
453
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
454
|
+
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
455
|
+
try {
|
|
456
|
+
const pool = this.getPool();
|
|
457
|
+
const sql = `DELETE FROM \`${this._config.database}\`.\`${this._config.tableName}\` WHERE \`${MySqlEntityStorageConnector._PARTITION_KEY}\` = ? AND \`${String(this._primaryKeyProperty.property)}\` IN (?)`;
|
|
458
|
+
await pool.query(sql, [
|
|
459
|
+
partitionKey ?? MySqlEntityStorageConnector._PARTITION_KEY_VALUE,
|
|
460
|
+
ids
|
|
461
|
+
]);
|
|
462
|
+
}
|
|
463
|
+
catch (err) {
|
|
464
|
+
throw new GeneralError(MySqlEntityStorageConnector.CLASS_NAME, "removeBatchFailed", undefined, err);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
296
467
|
/**
|
|
297
468
|
* Find all the entities which match the conditions.
|
|
298
469
|
* @param conditions The conditions to match for the entities.
|
|
@@ -337,8 +508,8 @@ export class MySqlEntityStorageConnector {
|
|
|
337
508
|
sql = `SELECT ${properties ? properties.map(p => `\`${String(p)}\``).join(", ") : "*"} FROM \`${this._config.database}\`.\`${this._config.tableName}\``;
|
|
338
509
|
sql += ` WHERE ${whereClauses.join(" AND ")} ${orderByClause}`;
|
|
339
510
|
sql += ` LIMIT ${returnSize} OFFSET ${startIndex}`;
|
|
340
|
-
const
|
|
341
|
-
const [rows] = (await
|
|
511
|
+
const pool = this.getPool();
|
|
512
|
+
const [rows] = (await pool.query(sql, values)) ?? [];
|
|
342
513
|
const entities = rows;
|
|
343
514
|
for (let i = 0; i < entities.length; i++) {
|
|
344
515
|
entities[i] = ObjectHelper.removeEmptyProperties(entities[i], { removeNull: true });
|
|
@@ -356,34 +527,19 @@ export class MySqlEntityStorageConnector {
|
|
|
356
527
|
}
|
|
357
528
|
}
|
|
358
529
|
/**
|
|
359
|
-
*
|
|
360
|
-
* @returns
|
|
530
|
+
* Count all the entities which match the conditions.
|
|
531
|
+
* @returns The total count of entities in the storage.
|
|
361
532
|
*/
|
|
362
|
-
async
|
|
533
|
+
async count() {
|
|
363
534
|
try {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
535
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
536
|
+
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
537
|
+
const pool = this.getPool();
|
|
538
|
+
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]);
|
|
539
|
+
return Number(rows[0].count);
|
|
369
540
|
}
|
|
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
|
|
541
|
+
catch (err) {
|
|
542
|
+
throw new GeneralError(MySqlEntityStorageConnector.CLASS_NAME, "countFailed", undefined, err);
|
|
387
543
|
}
|
|
388
544
|
}
|
|
389
545
|
/**
|
|
@@ -392,8 +548,8 @@ export class MySqlEntityStorageConnector {
|
|
|
392
548
|
*/
|
|
393
549
|
async databaseExists() {
|
|
394
550
|
try {
|
|
395
|
-
const
|
|
396
|
-
const [rows] = await
|
|
551
|
+
const pool = this.getPool();
|
|
552
|
+
const [rows] = await pool.query("SHOW DATABASES LIKE ?;", [this._config.database]);
|
|
397
553
|
return Is.arrayValue(rows);
|
|
398
554
|
}
|
|
399
555
|
catch {
|
|
@@ -421,8 +577,8 @@ export class MySqlEntityStorageConnector {
|
|
|
421
577
|
*/
|
|
422
578
|
async tableExists() {
|
|
423
579
|
try {
|
|
424
|
-
const
|
|
425
|
-
const [rows] = await
|
|
580
|
+
const pool = this.getPool();
|
|
581
|
+
const [rows] = await pool.query("SHOW TABLES FROM ?? LIKE ?", [
|
|
426
582
|
this._config.database,
|
|
427
583
|
this._config.tableName
|
|
428
584
|
]);
|
|
@@ -461,29 +617,49 @@ export class MySqlEntityStorageConnector {
|
|
|
461
617
|
}
|
|
462
618
|
}
|
|
463
619
|
/**
|
|
464
|
-
*
|
|
465
|
-
* @returns The MySql connection.
|
|
620
|
+
* Get or create the connection pool.
|
|
621
|
+
* @returns The MySql connection pool.
|
|
466
622
|
* @internal
|
|
467
623
|
*/
|
|
468
|
-
|
|
469
|
-
if (this.
|
|
470
|
-
|
|
624
|
+
getPool() {
|
|
625
|
+
if (!this._pool) {
|
|
626
|
+
const poolConfig = this.createPoolConfig();
|
|
627
|
+
const poolId = `${poolConfig.host}|${poolConfig.port}|${poolConfig.user}`;
|
|
628
|
+
let sharedPools = SharedStore.get("mySqlPools");
|
|
629
|
+
sharedPools ??= {};
|
|
630
|
+
// If there is no pool for the id, create it
|
|
631
|
+
if (!sharedPools[poolId]) {
|
|
632
|
+
sharedPools[poolId] = {
|
|
633
|
+
pool: createPool(poolConfig),
|
|
634
|
+
useCounter: 0
|
|
635
|
+
};
|
|
636
|
+
SharedStore.set("mySqlPools", sharedPools);
|
|
637
|
+
}
|
|
638
|
+
// Increase the use counter and return the pool
|
|
639
|
+
sharedPools[poolId].useCounter++;
|
|
640
|
+
this._pool = sharedPools[poolId].pool;
|
|
471
641
|
}
|
|
472
|
-
|
|
473
|
-
this._connection = newConnection;
|
|
474
|
-
return newConnection;
|
|
642
|
+
return this._pool;
|
|
475
643
|
}
|
|
476
644
|
/**
|
|
477
|
-
* Create
|
|
478
|
-
* @returns The MySql
|
|
645
|
+
* Create the connection pool configuration.
|
|
646
|
+
* @returns The MySql pool configuration.
|
|
479
647
|
* @internal
|
|
480
648
|
*/
|
|
481
|
-
|
|
649
|
+
createPoolConfig() {
|
|
650
|
+
const poolConfig = this._config.pool ?? {};
|
|
482
651
|
return {
|
|
483
652
|
host: this._config.host,
|
|
484
653
|
port: this._config.port ?? 3306,
|
|
485
654
|
user: this._config.user,
|
|
486
|
-
password: this._config.password
|
|
655
|
+
password: this._config.password,
|
|
656
|
+
connectionLimit: poolConfig.connectionLimit ?? 10,
|
|
657
|
+
maxIdle: poolConfig.maxIdle ?? 10,
|
|
658
|
+
idleTimeout: poolConfig.idleTimeout ?? 60000,
|
|
659
|
+
enableKeepAlive: poolConfig.enableKeepAlive ?? true,
|
|
660
|
+
keepAliveInitialDelay: 0,
|
|
661
|
+
waitForConnections: poolConfig.waitForConnections ?? true,
|
|
662
|
+
queueLimit: poolConfig.queueLimit ?? 0
|
|
487
663
|
};
|
|
488
664
|
}
|
|
489
665
|
/**
|
|
@@ -536,42 +712,93 @@ export class MySqlEntityStorageConnector {
|
|
|
536
712
|
prop += ".";
|
|
537
713
|
}
|
|
538
714
|
prop += comparator.property;
|
|
539
|
-
// prop = prop.replace(/\./g, "->");
|
|
540
715
|
if (comparator.comparison === ComparisonOperator.In) {
|
|
541
716
|
const inValues = Is.array(comparator.value) ? comparator.value : [comparator.value];
|
|
542
717
|
values.push(...inValues.map(val => this.propertyToDbValue(val, type)));
|
|
543
718
|
const placeholders = inValues.map(() => "?").join(", ");
|
|
544
719
|
return `\`${prop}\` IN (${placeholders})`;
|
|
545
720
|
}
|
|
721
|
+
// null/undefined must use IS NULL / IS NOT NULL — never a parameterised placeholder.
|
|
722
|
+
// Passing undefined through propertyToDbValue() coerces it to NaN for number fields
|
|
723
|
+
// (Number(undefined) === NaN), and null coerces to 0 (Number(null) === 0), both of
|
|
724
|
+
// which produce semantically wrong or invalid SQL.
|
|
725
|
+
if (comparator.value === null || comparator.value === undefined) {
|
|
726
|
+
if (comparator.comparison === ComparisonOperator.Equals ||
|
|
727
|
+
comparator.comparison === ComparisonOperator.NotEquals) {
|
|
728
|
+
const nullCheck = comparator.comparison === ComparisonOperator.Equals ? "IS NULL" : "IS NOT NULL";
|
|
729
|
+
if (comparator.property.split(".").length > 1) {
|
|
730
|
+
const rootProp = comparator.property.split(".")[0];
|
|
731
|
+
const nestedPath = comparator.property.split(".").slice(1).join(".");
|
|
732
|
+
const rootSchema = this._entitySchema.properties?.find(p => p.property === rootProp);
|
|
733
|
+
const isArray = rootSchema?.type === EntitySchemaPropertyType.Array;
|
|
734
|
+
const jsonPath = isArray ? `$[*].${nestedPath}` : `$.${nestedPath}`;
|
|
735
|
+
const jsonExpr = `JSON_UNQUOTE(JSON_EXTRACT(\`${rootProp}\`, '${jsonPath}'))`;
|
|
736
|
+
return `${jsonExpr} ${nullCheck}`;
|
|
737
|
+
}
|
|
738
|
+
return `\`${prop}\` ${nullCheck}`;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
546
741
|
const dbValue = this.propertyToDbValue(comparator.value, type);
|
|
547
742
|
values.push(dbValue);
|
|
548
743
|
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
|
-
|
|
744
|
+
const rootProp = comparator.property.split(".")[0];
|
|
745
|
+
const nestedPath = comparator.property.split(".").slice(1).join(".");
|
|
746
|
+
const rootSchema = this._entitySchema.properties?.find(p => p.property === rootProp);
|
|
747
|
+
const isArray = rootSchema?.type === EntitySchemaPropertyType.Array;
|
|
748
|
+
const jsonPath = isArray ? `$[*].${nestedPath}` : `$.${nestedPath}`;
|
|
749
|
+
const jsonExpr = `JSON_UNQUOTE(JSON_EXTRACT(\`${rootProp}\`, '${jsonPath}'))`;
|
|
750
|
+
switch (comparator.comparison) {
|
|
751
|
+
case ComparisonOperator.Includes: {
|
|
752
|
+
values.pop();
|
|
753
|
+
values.push(`%${String(comparator.value).toLowerCase()}%`);
|
|
754
|
+
return `LOWER(${jsonExpr}) LIKE ?`;
|
|
755
|
+
}
|
|
756
|
+
case ComparisonOperator.NotEquals:
|
|
757
|
+
return `${jsonExpr} <> ?`;
|
|
758
|
+
case ComparisonOperator.GreaterThan:
|
|
759
|
+
return `${jsonExpr} > ?`;
|
|
760
|
+
case ComparisonOperator.LessThan:
|
|
761
|
+
return `${jsonExpr} < ?`;
|
|
762
|
+
case ComparisonOperator.GreaterThanOrEqual:
|
|
763
|
+
return `${jsonExpr} >= ?`;
|
|
764
|
+
case ComparisonOperator.LessThanOrEqual:
|
|
765
|
+
return `${jsonExpr} <= ?`;
|
|
766
|
+
default:
|
|
767
|
+
return `${jsonExpr} = ?`;
|
|
768
|
+
}
|
|
568
769
|
}
|
|
569
|
-
|
|
570
|
-
|
|
770
|
+
switch (comparator.comparison) {
|
|
771
|
+
case ComparisonOperator.Equals:
|
|
772
|
+
if (Is.object(comparator.value) || Is.array(comparator.value)) {
|
|
773
|
+
return `JSON_CONTAINS(\`${prop}\`, ?)`;
|
|
774
|
+
}
|
|
775
|
+
return `\`${prop}\` = ?`;
|
|
776
|
+
case ComparisonOperator.NotEquals:
|
|
777
|
+
if (Is.object(comparator.value) || Is.array(comparator.value)) {
|
|
778
|
+
return `NOT JSON_CONTAINS(\`${prop}\`, ?)`;
|
|
779
|
+
}
|
|
780
|
+
return `\`${prop}\` <> ?`;
|
|
781
|
+
case ComparisonOperator.GreaterThan:
|
|
782
|
+
return `\`${prop}\` > ?`;
|
|
783
|
+
case ComparisonOperator.LessThan:
|
|
784
|
+
return `\`${prop}\` < ?`;
|
|
785
|
+
case ComparisonOperator.GreaterThanOrEqual:
|
|
786
|
+
return `\`${prop}\` >= ?`;
|
|
787
|
+
case ComparisonOperator.LessThanOrEqual:
|
|
788
|
+
return `\`${prop}\` <= ?`;
|
|
789
|
+
case ComparisonOperator.Includes: {
|
|
790
|
+
if (type === EntitySchemaPropertyType.String) {
|
|
791
|
+
values.pop();
|
|
792
|
+
values.push(`%${String(comparator.value).toLowerCase()}%`);
|
|
793
|
+
return `LOWER(\`${prop}\`) LIKE ?`;
|
|
794
|
+
}
|
|
795
|
+
return `JSON_CONTAINS(\`${prop}\`, ?)`;
|
|
796
|
+
}
|
|
797
|
+
default:
|
|
798
|
+
throw new GeneralError(MySqlEntityStorageConnector.CLASS_NAME, "comparisonNotSupported", {
|
|
799
|
+
comparison: comparator.comparison
|
|
800
|
+
});
|
|
571
801
|
}
|
|
572
|
-
throw new GeneralError(MySqlEntityStorageConnector.CLASS_NAME, "comparisonNotSupported", {
|
|
573
|
-
comparison: comparator.comparison
|
|
574
|
-
});
|
|
575
802
|
}
|
|
576
803
|
/**
|
|
577
804
|
* Format a value to insert into DB.
|