@twin.org/entity-storage-connector-mysql 0.0.1-next.18
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/LICENSE +201 -0
- package/README.md +35 -0
- package/dist/cjs/index.cjs +508 -0
- package/dist/esm/index.mjs +506 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/models/IMySqlEntityStorageConnectorConfig.d.ts +29 -0
- package/dist/types/models/IMySqlEntityStorageConnectorConstructorOptions.d.ts +19 -0
- package/dist/types/mysqlEntityStorageConnector.d.ts +94 -0
- package/docs/changelog.md +5 -0
- package/docs/examples.md +1 -0
- package/docs/reference/classes/MySqlEntityStorageConnector.md +254 -0
- package/docs/reference/index.md +10 -0
- package/docs/reference/interfaces/IMySqlEntityStorageConnectorConfig.md +51 -0
- package/docs/reference/interfaces/IMySqlEntityStorageConnectorConstructorOptions.md +33 -0
- package/locales/en.json +21 -0
- package/package.json +42 -0
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
import { Guards, BaseError, GeneralError, Is, ObjectHelper } from '@twin.org/core';
|
|
2
|
+
import { EntitySchemaFactory, SortDirection, ComparisonOperator, LogicalOperator, EntitySchemaPropertyType } from '@twin.org/entity';
|
|
3
|
+
import { LoggingConnectorFactory } from '@twin.org/logging-models';
|
|
4
|
+
import { createConnection } from 'mysql2/promise';
|
|
5
|
+
|
|
6
|
+
// Copyright 2024 IOTA Stiftung.
|
|
7
|
+
// SPDX-License-Identifier: Apache-2.0.
|
|
8
|
+
/**
|
|
9
|
+
* Class for performing entity storage operations using MySql.
|
|
10
|
+
*/
|
|
11
|
+
class MySqlEntityStorageConnector {
|
|
12
|
+
/**
|
|
13
|
+
* Limit the number of entities when finding.
|
|
14
|
+
* @internal
|
|
15
|
+
*/
|
|
16
|
+
static _PAGE_SIZE = 40;
|
|
17
|
+
/**
|
|
18
|
+
* Runtime name for the class.
|
|
19
|
+
*/
|
|
20
|
+
CLASS_NAME = "MySqlEntityStorageConnector";
|
|
21
|
+
/**
|
|
22
|
+
* The schema for the entity.
|
|
23
|
+
* @internal
|
|
24
|
+
*/
|
|
25
|
+
_entitySchema;
|
|
26
|
+
/**
|
|
27
|
+
* The configuration for the connector.
|
|
28
|
+
* @internal
|
|
29
|
+
*/
|
|
30
|
+
_config;
|
|
31
|
+
/**
|
|
32
|
+
* The configuration for the connector.
|
|
33
|
+
* @internal
|
|
34
|
+
*/
|
|
35
|
+
_connection;
|
|
36
|
+
/**
|
|
37
|
+
* Create a new instance of MySqlEntityStorageConnector.
|
|
38
|
+
* @param options The options for the connector.
|
|
39
|
+
*/
|
|
40
|
+
constructor(options) {
|
|
41
|
+
Guards.object(this.CLASS_NAME, "options", options);
|
|
42
|
+
Guards.stringValue(this.CLASS_NAME, "options.entitySchema", options.entitySchema);
|
|
43
|
+
Guards.object(this.CLASS_NAME, "options.config", options.config);
|
|
44
|
+
Guards.stringValue(this.CLASS_NAME, "options.config.host", options.config.host);
|
|
45
|
+
Guards.stringValue(this.CLASS_NAME, "options.config.user", options.config.user);
|
|
46
|
+
Guards.stringValue(this.CLASS_NAME, "options.config.password", options.config.password);
|
|
47
|
+
Guards.stringValue(this.CLASS_NAME, "options.config.database", options.config.database);
|
|
48
|
+
Guards.stringValue(this.CLASS_NAME, "options.config.table", options.config.table);
|
|
49
|
+
this._entitySchema = EntitySchemaFactory.get(options.entitySchema);
|
|
50
|
+
this._config = options.config;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Initialize the MySql environment.
|
|
54
|
+
* @param nodeLoggingConnectorType Optional type of the logging connector.
|
|
55
|
+
* @returns A promise that resolves to a boolean indicating success.
|
|
56
|
+
*/
|
|
57
|
+
async bootstrap(nodeLoggingConnectorType) {
|
|
58
|
+
const nodeLogging = LoggingConnectorFactory.getIfExists(nodeLoggingConnectorType ?? "node-logging");
|
|
59
|
+
try {
|
|
60
|
+
const dbConnection = await this.createConnection();
|
|
61
|
+
await nodeLogging?.log({
|
|
62
|
+
level: "info",
|
|
63
|
+
source: this.CLASS_NAME,
|
|
64
|
+
ts: Date.now(),
|
|
65
|
+
message: "databaseCreating",
|
|
66
|
+
data: {
|
|
67
|
+
database: this._config.database
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
// Create the database if it does not exist
|
|
71
|
+
await dbConnection.query(`CREATE DATABASE IF NOT EXISTS \`${this._config.database}\``);
|
|
72
|
+
await nodeLogging?.log({
|
|
73
|
+
level: "info",
|
|
74
|
+
source: this.CLASS_NAME,
|
|
75
|
+
ts: Date.now(),
|
|
76
|
+
message: "databaseExists",
|
|
77
|
+
data: {
|
|
78
|
+
database: this._config.database
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
await dbConnection.query(`CREATE TABLE IF NOT EXISTS \`${this._config.database}\`.\`${this._config.table}\` (${this.mapMySqlProperties(this._entitySchema)})`);
|
|
82
|
+
await nodeLogging?.log({
|
|
83
|
+
level: "info",
|
|
84
|
+
source: this.CLASS_NAME,
|
|
85
|
+
ts: Date.now(),
|
|
86
|
+
message: "tableExists",
|
|
87
|
+
data: {
|
|
88
|
+
table: this._config.table
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
// eslint-disable-next-line no-console
|
|
94
|
+
console.log("error", error);
|
|
95
|
+
const errors = error instanceof AggregateError ? error.errors : [error];
|
|
96
|
+
for (const err of errors) {
|
|
97
|
+
await nodeLogging?.log({
|
|
98
|
+
level: "error",
|
|
99
|
+
source: this.CLASS_NAME,
|
|
100
|
+
ts: Date.now(),
|
|
101
|
+
message: "databaseCreateFailed",
|
|
102
|
+
error: BaseError.fromError(err),
|
|
103
|
+
data: {
|
|
104
|
+
database: this._config.database
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Get the schema for the entities.
|
|
114
|
+
* @returns The schema for the entities.
|
|
115
|
+
*/
|
|
116
|
+
getSchema() {
|
|
117
|
+
return this._entitySchema;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Get an entity from MySql.
|
|
121
|
+
* @param id The id of the entity to get, or the index value if secondaryIndex is set.
|
|
122
|
+
* @param secondaryIndex Get the item using a secondary index.
|
|
123
|
+
* @param conditions The optional conditions to match for the entities.
|
|
124
|
+
* @returns The object if it can be found or undefined.
|
|
125
|
+
*/
|
|
126
|
+
async get(id, secondaryIndex, conditions) {
|
|
127
|
+
Guards.stringValue(this.CLASS_NAME, "id", id);
|
|
128
|
+
try {
|
|
129
|
+
const dbConnection = await this.createConnection();
|
|
130
|
+
const whereClauses = [];
|
|
131
|
+
const values = [];
|
|
132
|
+
if (secondaryIndex) {
|
|
133
|
+
whereClauses.push(`\`${String(secondaryIndex)}\` = ?`);
|
|
134
|
+
values.push(id);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
whereClauses.push("`id` = ?");
|
|
138
|
+
values.push(id);
|
|
139
|
+
}
|
|
140
|
+
if (conditions) {
|
|
141
|
+
for (const condition of conditions) {
|
|
142
|
+
whereClauses.push(`\`${String(condition.property)}\` = ?`);
|
|
143
|
+
values.push(condition.value);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const query = `SELECT * FROM \`${this._config.database}\`.\`${this._config.table}\` WHERE ${whereClauses.join(" AND ")} LIMIT 1`;
|
|
147
|
+
const [rows] = await dbConnection.query(query, values);
|
|
148
|
+
if (Array.isArray(rows) && rows.length === 1) {
|
|
149
|
+
return rows[0];
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
throw new GeneralError(this.CLASS_NAME, "getFailed", {
|
|
154
|
+
id
|
|
155
|
+
}, err);
|
|
156
|
+
}
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Set an entity.
|
|
161
|
+
* @param entity The entity to set.
|
|
162
|
+
* @param conditions The optional conditions to match for the entities.
|
|
163
|
+
* @returns The id of the entity.
|
|
164
|
+
*/
|
|
165
|
+
async set(entity, conditions) {
|
|
166
|
+
Guards.object(this.CLASS_NAME, "entity", entity);
|
|
167
|
+
// Validate that the entity matches the schema
|
|
168
|
+
this.entitySqlVerification(entity);
|
|
169
|
+
const id = entity["id"];
|
|
170
|
+
try {
|
|
171
|
+
if (Is.arrayValue(conditions)) {
|
|
172
|
+
const itemData = await this.get(id);
|
|
173
|
+
if (Is.notEmpty(itemData) && !this.verifyConditions(conditions, itemData)) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const columns = Object.keys(entity)
|
|
178
|
+
.map(key => `\`${key}\``)
|
|
179
|
+
.join(", ");
|
|
180
|
+
const values = Object.values(entity);
|
|
181
|
+
const placeholders = values.map(() => "?").join(", ");
|
|
182
|
+
const dbConnection = await this.createConnection();
|
|
183
|
+
await dbConnection.query(`INSERT INTO \`${this._config.database}\`.\`${this._config.table}\` (${columns}) VALUES (${placeholders}) ON DUPLICATE KEY UPDATE ${columns
|
|
184
|
+
.split(", ")
|
|
185
|
+
.map(col => `${col} = VALUES(${col})`)
|
|
186
|
+
.join(", ")};`, values.map(value => (typeof value === "object" ? JSON.stringify(value) : value)));
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
throw new GeneralError(this.CLASS_NAME, "setFailed", {
|
|
190
|
+
id
|
|
191
|
+
}, err);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Remove the entity.
|
|
196
|
+
* @param id The id of the entity to remove.
|
|
197
|
+
* @param conditions The optional conditions to match for the entities.
|
|
198
|
+
* @returns Nothing.
|
|
199
|
+
*/
|
|
200
|
+
async remove(id, conditions) {
|
|
201
|
+
Guards.stringValue(this.CLASS_NAME, "id", id);
|
|
202
|
+
try {
|
|
203
|
+
const dbConnection = await this.createConnection();
|
|
204
|
+
const itemData = await this.get(id);
|
|
205
|
+
if (Is.notEmpty(itemData)) {
|
|
206
|
+
const values = [id];
|
|
207
|
+
let whereClauses = [];
|
|
208
|
+
if (Is.arrayValue(conditions)) {
|
|
209
|
+
whereClauses = conditions.map(condition => {
|
|
210
|
+
values.push(condition.value);
|
|
211
|
+
return `\`${String(condition.property)}\` = ?`;
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
const query = `DELETE FROM \`${this._config.database}\`.\`${this._config.table}\` WHERE \`id\` = ?${whereClauses.length > 0 ? ` AND ${whereClauses.join(" AND ")}` : ""}`;
|
|
215
|
+
await dbConnection.query(query, values);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
throw new GeneralError(this.CLASS_NAME, "removeFailed", {
|
|
220
|
+
id
|
|
221
|
+
}, err);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Find all the entities which match the conditions.
|
|
226
|
+
* @param conditions The conditions to match for the entities.
|
|
227
|
+
* @param sortProperties The optional sort order.
|
|
228
|
+
* @param properties The optional properties to return, defaults to all.
|
|
229
|
+
* @param cursor The cursor to request the next page of entities.
|
|
230
|
+
* @param pageSize The suggested number of entities to return in each chunk, in some scenarios can return a different amount.
|
|
231
|
+
* @returns All the entities for the storage matching the conditions,
|
|
232
|
+
* and a cursor which can be used to request more entities.
|
|
233
|
+
*/
|
|
234
|
+
async query(conditions, sortProperties, properties, cursor, pageSize) {
|
|
235
|
+
const sql = "";
|
|
236
|
+
try {
|
|
237
|
+
const returnSize = pageSize ?? MySqlEntityStorageConnector._PAGE_SIZE;
|
|
238
|
+
let orderByClause = "";
|
|
239
|
+
if (Array.isArray(sortProperties)) {
|
|
240
|
+
const orderClauses = [];
|
|
241
|
+
for (const sortProperty of sortProperties) {
|
|
242
|
+
const direction = sortProperty.sortDirection === SortDirection.Ascending ? "ASC" : "DESC";
|
|
243
|
+
orderClauses.push(`\`${String(sortProperty.property)}\` ${direction}`);
|
|
244
|
+
}
|
|
245
|
+
orderByClause = `ORDER BY ${orderClauses.join(", ")}`;
|
|
246
|
+
}
|
|
247
|
+
const whereClauses = [];
|
|
248
|
+
const values = [];
|
|
249
|
+
if (conditions) {
|
|
250
|
+
this.buildQueryParameters("", conditions, whereClauses, values);
|
|
251
|
+
}
|
|
252
|
+
const query = `SELECT ${properties ? properties.map(p => `\`${String(p)}\``).join(", ") : "*"} FROM \`${this._config.database}\`.\`${this._config.table}\` WHERE ${whereClauses.length > 0 ? whereClauses.join(" AND ") : "1"} ${orderByClause} LIMIT ${returnSize} OFFSET ${cursor ? Number(cursor) : 0}`;
|
|
253
|
+
const dbConnection = await this.createConnection();
|
|
254
|
+
const [rows] = (await dbConnection?.query(query, values)) ?? [];
|
|
255
|
+
return {
|
|
256
|
+
entities: rows,
|
|
257
|
+
cursor: Array.isArray(rows) && rows.length === returnSize
|
|
258
|
+
? String((cursor ? Number(cursor) : 0) + returnSize)
|
|
259
|
+
: undefined
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
catch (err) {
|
|
263
|
+
throw new GeneralError(this.CLASS_NAME, "queryFailed", { sql }, err);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Drop the table.
|
|
268
|
+
* @returns Nothing.
|
|
269
|
+
*/
|
|
270
|
+
async tableDrop() {
|
|
271
|
+
try {
|
|
272
|
+
const dbConnection = await this.createConnection();
|
|
273
|
+
await dbConnection?.query(`DROP TABLE \`${this._config.database}\`.\`${this._config.table}\`;`);
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
// Ignore errors
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Create a new DB connection.
|
|
281
|
+
* @returns The dynamo db connection.
|
|
282
|
+
* @internal
|
|
283
|
+
*/
|
|
284
|
+
async createConnection() {
|
|
285
|
+
if (this._connection) {
|
|
286
|
+
return this._connection;
|
|
287
|
+
}
|
|
288
|
+
const newConnection = await createConnection(this.createConnectionConfig());
|
|
289
|
+
this._connection = newConnection;
|
|
290
|
+
return newConnection;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Create a new DB connection configuration.
|
|
294
|
+
* @returns The dynamo db connection configuration.
|
|
295
|
+
* @internal
|
|
296
|
+
*/
|
|
297
|
+
createConnectionConfig() {
|
|
298
|
+
return {
|
|
299
|
+
host: this._config.host,
|
|
300
|
+
port: this._config.port ?? 3306,
|
|
301
|
+
user: this._config.user,
|
|
302
|
+
password: this._config.password
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Create an SQL condition clause.
|
|
307
|
+
* @param objectPath The path for the nested object.
|
|
308
|
+
* @param condition The conditions to create the query from.
|
|
309
|
+
* @param whereClauses The where clauses to use in the query.
|
|
310
|
+
* @param values The values to use in the query.
|
|
311
|
+
* @internal
|
|
312
|
+
*/
|
|
313
|
+
buildQueryParameters(objectPath, condition, whereClauses, values) {
|
|
314
|
+
if (Is.undefined(condition)) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
if ("conditions" in condition) {
|
|
318
|
+
if (condition.conditions.length === 0) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const joinConditions = condition.conditions.map(c => {
|
|
322
|
+
const subWhereClauses = [];
|
|
323
|
+
const subValues = [];
|
|
324
|
+
this.buildQueryParameters(objectPath, c, subWhereClauses, subValues);
|
|
325
|
+
values.push(...subValues);
|
|
326
|
+
return subWhereClauses.join(" AND ");
|
|
327
|
+
});
|
|
328
|
+
const logicalOperator = this.mapConditionalOperator(condition.logicalOperator);
|
|
329
|
+
const queryClause = joinConditions.filter(j => j.length > 0).join(` ${logicalOperator} `);
|
|
330
|
+
if (queryClause.length > 0) {
|
|
331
|
+
whereClauses.push(`(${queryClause})`);
|
|
332
|
+
}
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
const schemaProp = this._entitySchema.properties?.find(p => p.property === condition.property);
|
|
336
|
+
const comparison = this.mapComparisonOperator(objectPath, condition, schemaProp?.type, values);
|
|
337
|
+
whereClauses.push(comparison);
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Map the framework comparison operators to those in MySQL.
|
|
341
|
+
* @param objectPath The prefix to use for the condition.
|
|
342
|
+
* @param comparator The operator to map.
|
|
343
|
+
* @param type The type of the property.
|
|
344
|
+
* @param values The values to use in the query.
|
|
345
|
+
* @returns The comparison expression.
|
|
346
|
+
* @throws GeneralError if the comparison operator is not supported.
|
|
347
|
+
* @internal
|
|
348
|
+
*/
|
|
349
|
+
mapComparisonOperator(objectPath, comparator, type, values) {
|
|
350
|
+
let prop = objectPath;
|
|
351
|
+
if (prop.length > 0) {
|
|
352
|
+
prop += ".";
|
|
353
|
+
}
|
|
354
|
+
prop += comparator.property;
|
|
355
|
+
// prop = prop.replace(/\./g, "->");
|
|
356
|
+
if (comparator.comparison === ComparisonOperator.In) {
|
|
357
|
+
const inValues = Array.isArray(comparator.value) ? comparator.value : [comparator.value];
|
|
358
|
+
values.push(...inValues.map(val => this.propertyToDbValue(val, type)));
|
|
359
|
+
const placeholders = inValues.map(() => "?").join(", ");
|
|
360
|
+
return `\`${prop}\` IN (${placeholders})`;
|
|
361
|
+
}
|
|
362
|
+
const dbValue = this.propertyToDbValue(comparator.value, type);
|
|
363
|
+
values.push(dbValue);
|
|
364
|
+
if (comparator.property.split(".").length > 1) {
|
|
365
|
+
return `JSON_UNQUOTE(JSON_EXTRACT(\`${comparator.property.split(".")[0]}\`, '$.${comparator.property.split(".").slice(1).join(".")}')) = ?`;
|
|
366
|
+
}
|
|
367
|
+
else if (comparator.comparison === ComparisonOperator.Equals) {
|
|
368
|
+
return `\`${prop}\` = ?`;
|
|
369
|
+
}
|
|
370
|
+
else if (comparator.comparison === ComparisonOperator.NotEquals) {
|
|
371
|
+
return `\`${prop}\` <> ?`;
|
|
372
|
+
}
|
|
373
|
+
else if (comparator.comparison === ComparisonOperator.GreaterThan) {
|
|
374
|
+
return `\`${prop}\` > ?`;
|
|
375
|
+
}
|
|
376
|
+
else if (comparator.comparison === ComparisonOperator.LessThan) {
|
|
377
|
+
return `\`${prop}\` < ?`;
|
|
378
|
+
}
|
|
379
|
+
else if (comparator.comparison === ComparisonOperator.GreaterThanOrEqual) {
|
|
380
|
+
return `\`${prop}\` >= ?`;
|
|
381
|
+
}
|
|
382
|
+
else if (comparator.comparison === ComparisonOperator.LessThanOrEqual) {
|
|
383
|
+
return `\`${prop}\` <= ?`;
|
|
384
|
+
}
|
|
385
|
+
else if (comparator.comparison === ComparisonOperator.Includes) {
|
|
386
|
+
return `JSON_CONTAINS(\`${prop}\`, ?)`;
|
|
387
|
+
}
|
|
388
|
+
throw new GeneralError(this.CLASS_NAME, "comparisonNotSupported", {
|
|
389
|
+
comparison: comparator.comparison
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Format a value to insert into DB.
|
|
394
|
+
* @param value The value to format.
|
|
395
|
+
* @param type The type for the property.
|
|
396
|
+
* @returns The value after conversion.
|
|
397
|
+
* @internal
|
|
398
|
+
*/
|
|
399
|
+
propertyToDbValue(value, type) {
|
|
400
|
+
if (Is.object(value)) {
|
|
401
|
+
return JSON.stringify(value);
|
|
402
|
+
}
|
|
403
|
+
if (type === "string") {
|
|
404
|
+
return String(value);
|
|
405
|
+
}
|
|
406
|
+
else if (type === "number") {
|
|
407
|
+
return Number(value);
|
|
408
|
+
}
|
|
409
|
+
else if (type === "boolean") {
|
|
410
|
+
return Boolean(value);
|
|
411
|
+
}
|
|
412
|
+
return value;
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Map the framework conditional operators to those in MySQL.
|
|
416
|
+
* @param operator The operator to map.
|
|
417
|
+
* @returns The conditional operator.
|
|
418
|
+
* @throws GeneralError if the conditional operator is not supported.
|
|
419
|
+
* @internal
|
|
420
|
+
*/
|
|
421
|
+
mapConditionalOperator(operator) {
|
|
422
|
+
if ((operator ?? LogicalOperator.And) === LogicalOperator.And) {
|
|
423
|
+
return "AND";
|
|
424
|
+
}
|
|
425
|
+
else if (operator === LogicalOperator.Or) {
|
|
426
|
+
return "OR";
|
|
427
|
+
}
|
|
428
|
+
throw new GeneralError(this.CLASS_NAME, "conditionalNotSupported", { operator });
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Verify the conditions for the entity.
|
|
432
|
+
* @param conditions The conditions to verify.
|
|
433
|
+
* @internal
|
|
434
|
+
*/
|
|
435
|
+
verifyConditions(conditions, obj) {
|
|
436
|
+
return conditions.every(condition => ObjectHelper.propertyGet(obj, condition.property) === condition.value);
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Map entity schema properties to SQL properties.
|
|
440
|
+
* @param entitySchema The schema of the entity.
|
|
441
|
+
* @returns The SQL properties as a string.
|
|
442
|
+
* @throws GeneralError if the entity properties do not exist.
|
|
443
|
+
*/
|
|
444
|
+
mapMySqlProperties(entitySchema) {
|
|
445
|
+
const sqlTypeMap = {
|
|
446
|
+
[EntitySchemaPropertyType.String]: "LONGTEXT",
|
|
447
|
+
[EntitySchemaPropertyType.Number]: "FLOAT",
|
|
448
|
+
[EntitySchemaPropertyType.Integer]: "INT",
|
|
449
|
+
[EntitySchemaPropertyType.Object]: "JSON",
|
|
450
|
+
[EntitySchemaPropertyType.Array]: "JSON",
|
|
451
|
+
[EntitySchemaPropertyType.Boolean]: "TINYINT(1)"
|
|
452
|
+
};
|
|
453
|
+
if (!entitySchema.properties) {
|
|
454
|
+
throw new GeneralError(this.CLASS_NAME, "entitySchemaPropertiesUndefined");
|
|
455
|
+
}
|
|
456
|
+
const primaryKeys = [];
|
|
457
|
+
const columnDefinitions = entitySchema.properties
|
|
458
|
+
.map(prop => {
|
|
459
|
+
const sqlType = sqlTypeMap[prop.type] || "TEXT";
|
|
460
|
+
const columnName = String(prop.property);
|
|
461
|
+
const nullable = prop.optional ? " NULL" : " NOT NULL";
|
|
462
|
+
if (prop.isPrimary) {
|
|
463
|
+
if (sqlType === "LONGTEXT" || sqlType === "TEXT") {
|
|
464
|
+
primaryKeys.push(`${columnName}(255)`);
|
|
465
|
+
}
|
|
466
|
+
else {
|
|
467
|
+
primaryKeys.push(columnName);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return `${columnName} ${sqlType}${nullable}`;
|
|
471
|
+
})
|
|
472
|
+
.join(", ");
|
|
473
|
+
const primaryKeyDefinition = primaryKeys.length > 0 ? `, PRIMARY KEY (${primaryKeys.join(", ")})` : "";
|
|
474
|
+
return columnDefinitions + primaryKeyDefinition;
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Validate that the entity matches the schema.
|
|
478
|
+
* @param entity The entity to validate.
|
|
479
|
+
* @throws GeneralError if the entity schema properties are undefined or if the entity does not match the schema.
|
|
480
|
+
*/
|
|
481
|
+
entitySqlVerification(entity) {
|
|
482
|
+
// Validate that the entity matches the schema
|
|
483
|
+
if (!this._entitySchema.properties) {
|
|
484
|
+
throw new GeneralError(this.CLASS_NAME, "entitySchemaPropertiesUndefined");
|
|
485
|
+
}
|
|
486
|
+
for (const prop of this._entitySchema.properties) {
|
|
487
|
+
const value = entity[prop.property];
|
|
488
|
+
if (value === undefined || value === null) {
|
|
489
|
+
if (!prop.optional) {
|
|
490
|
+
throw new GeneralError(this.CLASS_NAME, "invalidEntity", {
|
|
491
|
+
entity,
|
|
492
|
+
entitySchema: this._entitySchema
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
else if (typeof value !== prop.type && (prop.type !== "array" || !Is.array(value))) {
|
|
497
|
+
throw new GeneralError(this.CLASS_NAME, "invalidEntity", {
|
|
498
|
+
entity,
|
|
499
|
+
entitySchema: this._entitySchema
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
export { MySqlEntityStorageConnector };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for the MySql Entity Storage Connector.
|
|
3
|
+
*/
|
|
4
|
+
export interface IMySqlEntityStorageConnectorConfig {
|
|
5
|
+
/**
|
|
6
|
+
* The host for the MySql instance.
|
|
7
|
+
*/
|
|
8
|
+
host: string;
|
|
9
|
+
/**
|
|
10
|
+
* The port for the MySql instance.
|
|
11
|
+
*/
|
|
12
|
+
port?: number;
|
|
13
|
+
/**
|
|
14
|
+
* The user for the MySql instance.
|
|
15
|
+
*/
|
|
16
|
+
user: string;
|
|
17
|
+
/**
|
|
18
|
+
* The password for the MySql instance.
|
|
19
|
+
*/
|
|
20
|
+
password: string;
|
|
21
|
+
/**
|
|
22
|
+
* The name of the database to be used.
|
|
23
|
+
*/
|
|
24
|
+
database: string;
|
|
25
|
+
/**
|
|
26
|
+
* The name of the table to be used.
|
|
27
|
+
*/
|
|
28
|
+
table: string;
|
|
29
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { IMySqlEntityStorageConnectorConfig } from "./IMySqlEntityStorageConnectorConfig";
|
|
2
|
+
/**
|
|
3
|
+
* The options for the MySql entity storage connector constructor.
|
|
4
|
+
*/
|
|
5
|
+
export interface IMySqlEntityStorageConnectorConstructorOptions {
|
|
6
|
+
/**
|
|
7
|
+
* The schema for the entity.
|
|
8
|
+
*/
|
|
9
|
+
entitySchema: string;
|
|
10
|
+
/**
|
|
11
|
+
* The type of logging connector to use.
|
|
12
|
+
* @default logging
|
|
13
|
+
*/
|
|
14
|
+
loggingConnectorType?: string;
|
|
15
|
+
/**
|
|
16
|
+
* The configuration for the connector.
|
|
17
|
+
*/
|
|
18
|
+
config: IMySqlEntityStorageConnectorConfig;
|
|
19
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { type EntityCondition, type IEntitySchema, SortDirection } from "@twin.org/entity";
|
|
2
|
+
import type { IEntityStorageConnector } from "@twin.org/entity-storage-models";
|
|
3
|
+
import type { IMySqlEntityStorageConnectorConstructorOptions } from "./models/IMySqlEntityStorageConnectorConstructorOptions";
|
|
4
|
+
/**
|
|
5
|
+
* Class for performing entity storage operations using MySql.
|
|
6
|
+
*/
|
|
7
|
+
export declare class MySqlEntityStorageConnector<T = unknown> implements IEntityStorageConnector<T> {
|
|
8
|
+
/**
|
|
9
|
+
* Runtime name for the class.
|
|
10
|
+
*/
|
|
11
|
+
readonly CLASS_NAME: string;
|
|
12
|
+
/**
|
|
13
|
+
* Create a new instance of MySqlEntityStorageConnector.
|
|
14
|
+
* @param options The options for the connector.
|
|
15
|
+
*/
|
|
16
|
+
constructor(options: IMySqlEntityStorageConnectorConstructorOptions);
|
|
17
|
+
/**
|
|
18
|
+
* Initialize the MySql environment.
|
|
19
|
+
* @param nodeLoggingConnectorType Optional type of the logging connector.
|
|
20
|
+
* @returns A promise that resolves to a boolean indicating success.
|
|
21
|
+
*/
|
|
22
|
+
bootstrap(nodeLoggingConnectorType?: string): Promise<boolean>;
|
|
23
|
+
/**
|
|
24
|
+
* Get the schema for the entities.
|
|
25
|
+
* @returns The schema for the entities.
|
|
26
|
+
*/
|
|
27
|
+
getSchema(): IEntitySchema;
|
|
28
|
+
/**
|
|
29
|
+
* Get an entity from MySql.
|
|
30
|
+
* @param id The id of the entity to get, or the index value if secondaryIndex is set.
|
|
31
|
+
* @param secondaryIndex Get the item using a secondary index.
|
|
32
|
+
* @param conditions The optional conditions to match for the entities.
|
|
33
|
+
* @returns The object if it can be found or undefined.
|
|
34
|
+
*/
|
|
35
|
+
get(id: string, secondaryIndex?: keyof T, conditions?: {
|
|
36
|
+
property: keyof T;
|
|
37
|
+
value: unknown;
|
|
38
|
+
}[]): Promise<T | undefined>;
|
|
39
|
+
/**
|
|
40
|
+
* Set an entity.
|
|
41
|
+
* @param entity The entity to set.
|
|
42
|
+
* @param conditions The optional conditions to match for the entities.
|
|
43
|
+
* @returns The id of the entity.
|
|
44
|
+
*/
|
|
45
|
+
set(entity: T, conditions?: {
|
|
46
|
+
property: keyof T;
|
|
47
|
+
value: unknown;
|
|
48
|
+
}[]): Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* Remove the entity.
|
|
51
|
+
* @param id The id of the entity to remove.
|
|
52
|
+
* @param conditions The optional conditions to match for the entities.
|
|
53
|
+
* @returns Nothing.
|
|
54
|
+
*/
|
|
55
|
+
remove(id: string, conditions?: {
|
|
56
|
+
property: keyof T;
|
|
57
|
+
value: unknown;
|
|
58
|
+
}[]): Promise<void>;
|
|
59
|
+
/**
|
|
60
|
+
* Find all the entities which match the conditions.
|
|
61
|
+
* @param conditions The conditions to match for the entities.
|
|
62
|
+
* @param sortProperties The optional sort order.
|
|
63
|
+
* @param properties The optional properties to return, defaults to all.
|
|
64
|
+
* @param cursor The cursor to request the next page of entities.
|
|
65
|
+
* @param pageSize The suggested number of entities to return in each chunk, in some scenarios can return a different amount.
|
|
66
|
+
* @returns All the entities for the storage matching the conditions,
|
|
67
|
+
* and a cursor which can be used to request more entities.
|
|
68
|
+
*/
|
|
69
|
+
query(conditions?: EntityCondition<T>, sortProperties?: {
|
|
70
|
+
property: keyof T;
|
|
71
|
+
sortDirection: SortDirection;
|
|
72
|
+
}[], properties?: (keyof T)[], cursor?: string, pageSize?: number): Promise<{
|
|
73
|
+
entities: Partial<T>[];
|
|
74
|
+
cursor?: string;
|
|
75
|
+
}>;
|
|
76
|
+
/**
|
|
77
|
+
* Drop the table.
|
|
78
|
+
* @returns Nothing.
|
|
79
|
+
*/
|
|
80
|
+
tableDrop(): Promise<void>;
|
|
81
|
+
/**
|
|
82
|
+
* Map entity schema properties to SQL properties.
|
|
83
|
+
* @param entitySchema The schema of the entity.
|
|
84
|
+
* @returns The SQL properties as a string.
|
|
85
|
+
* @throws GeneralError if the entity properties do not exist.
|
|
86
|
+
*/
|
|
87
|
+
private mapMySqlProperties;
|
|
88
|
+
/**
|
|
89
|
+
* Validate that the entity matches the schema.
|
|
90
|
+
* @param entity The entity to validate.
|
|
91
|
+
* @throws GeneralError if the entity schema properties are undefined or if the entity does not match the schema.
|
|
92
|
+
*/
|
|
93
|
+
private entitySqlVerification;
|
|
94
|
+
}
|
package/docs/examples.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# @twin.org/entity-storage-connector-mysql - Examples
|