@travetto/model-mysql 8.0.0-alpha.24 → 8.0.0-alpha.26
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 +30 -29
- package/__index__.ts +3 -2
- package/package.json +7 -7
- package/src/config.ts +50 -0
- package/src/connection.ts +77 -52
- package/src/dialect.ts +176 -157
- package/src/service.ts +28 -0
- package/support/service.mysql.ts +3 -2
package/README.md
CHANGED
|
@@ -13,79 +13,80 @@ npm install @travetto/model-mysql
|
|
|
13
13
|
yarn add @travetto/model-mysql
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
This module provides a [MySQL](https://www.mysql.com/)-based implementation for the [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations.") module.
|
|
16
|
+
This module provides a [MySQL](https://www.mysql.com/)-based implementation for the [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations.") module. This source allows the [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations.") module to read, write and query against [SQL](https://en.wikipedia.org/wiki/SQL) databases. In development mode, the [MysqlModelService](https://github.com/travetto/travetto/tree/main/module/model-mysql/src/service.ts#L12) will also modify the database schema in real time to minimize impact to development.
|
|
17
17
|
|
|
18
|
-
The schema generated will not generally map to existing tables as it is attempting to produce a document store like experience on top of a [SQL](https://en.wikipedia.org/wiki/SQL) database.
|
|
18
|
+
The schema generated will not generally map to existing tables as it is attempting to produce a document store like experience on top of a [SQL](https://en.wikipedia.org/wiki/SQL) database. Every table generated maps to a model, with simple fields mapped as individual columns and complex fields/arrays mapped as native `JSON` columns.
|
|
19
19
|
|
|
20
20
|
Supported features:
|
|
21
|
-
* [
|
|
22
|
-
* [
|
|
23
|
-
* [
|
|
21
|
+
* [Bulk](https://github.com/travetto/travetto/tree/main/module/model/src/types/bulk.ts#L60)
|
|
22
|
+
* [CRUD](https://github.com/travetto/travetto/tree/main/module/model/src/types/crud.ts#L10)
|
|
23
|
+
* [Expiry](https://github.com/travetto/travetto/tree/main/module/model/src/types/expiry.ts#L10)
|
|
24
|
+
* [Indexed](https://github.com/travetto/travetto/tree/main/module/model-indexed/src/types/service.ts#L21)
|
|
24
25
|
* [Query Crud](https://github.com/travetto/travetto/tree/main/module/model-query/src/types/crud.ts#L11)
|
|
25
26
|
* [Facet](https://github.com/travetto/travetto/tree/main/module/model-query/src/types/facet.ts#L14)
|
|
26
|
-
* [Query](https://github.com/travetto/travetto/tree/main/module/model-query/src/types/query.ts#L10)
|
|
27
27
|
* [Suggest](https://github.com/travetto/travetto/tree/main/module/model-query/src/types/suggest.ts#L12)
|
|
28
|
+
* [Query](https://github.com/travetto/travetto/tree/main/module/model-query/src/types/query.ts#L10)
|
|
28
29
|
|
|
29
30
|
Out of the box, by installing the module, everything should be wired up by default.If you need to customize any aspect of the source or config, you can override and register it with the [Dependency Injection](https://github.com/travetto/travetto/tree/main/module/di#readme "Dependency registration/management and injection support.") module.
|
|
30
31
|
|
|
31
32
|
**Code: Wiring up a custom Model Source**
|
|
32
33
|
```typescript
|
|
33
|
-
import type { AsyncContext } from '@travetto/context';
|
|
34
34
|
import { InjectableFactory } from '@travetto/di';
|
|
35
|
-
|
|
36
|
-
import { SQLModelService, type SQLModelConfig } from '@travetto/model-sql';
|
|
37
|
-
import { MySQLDialect } from '@travetto/model-mysql';
|
|
35
|
+
import { type MysqlConnection, MysqlModelService } from '@travetto/model-mysql';
|
|
38
36
|
|
|
39
37
|
export class Init {
|
|
40
38
|
@InjectableFactory({ primary: true })
|
|
41
|
-
static getModelService(
|
|
42
|
-
return new
|
|
39
|
+
static getModelService(connection: MysqlConnection) {
|
|
40
|
+
return new MysqlModelService(connection);
|
|
43
41
|
}
|
|
44
42
|
}
|
|
45
43
|
```
|
|
46
44
|
|
|
47
|
-
where the [
|
|
45
|
+
where the [MysqlModelConfig](https://github.com/travetto/travetto/tree/main/module/model-mysql/src/config.ts#L10) is defined by:
|
|
48
46
|
|
|
49
|
-
**Code: Structure of
|
|
47
|
+
**Code: Structure of MysqlModelConfig**
|
|
50
48
|
```typescript
|
|
51
|
-
@Config('model.
|
|
52
|
-
export class
|
|
49
|
+
@Config('model.mysql')
|
|
50
|
+
export class MysqlModelConfig {
|
|
53
51
|
/**
|
|
54
|
-
*
|
|
52
|
+
* Database host to connect to
|
|
55
53
|
*/
|
|
56
54
|
host = '127.0.0.1';
|
|
55
|
+
|
|
57
56
|
/**
|
|
58
|
-
*
|
|
57
|
+
* Database port to connect to
|
|
59
58
|
*/
|
|
60
59
|
port = 0;
|
|
60
|
+
|
|
61
61
|
/**
|
|
62
|
-
*
|
|
62
|
+
* Database username
|
|
63
63
|
*/
|
|
64
64
|
user = Runtime.production ? '' : 'travetto';
|
|
65
|
+
|
|
65
66
|
/**
|
|
66
|
-
*
|
|
67
|
+
* Database password
|
|
67
68
|
*/
|
|
68
69
|
password = Runtime.production ? '' : 'travetto';
|
|
70
|
+
|
|
69
71
|
/**
|
|
70
|
-
*
|
|
72
|
+
* Namespace/schema prefix for table names
|
|
71
73
|
*/
|
|
72
74
|
namespace = '';
|
|
75
|
+
|
|
73
76
|
/**
|
|
74
77
|
* Database name
|
|
75
78
|
*/
|
|
76
79
|
database = 'app';
|
|
80
|
+
|
|
77
81
|
/**
|
|
78
|
-
* Allow storage
|
|
79
|
-
*/
|
|
80
|
-
modifyStorage?: boolean;
|
|
81
|
-
/**
|
|
82
|
-
* Db version
|
|
82
|
+
* Allow storage modifications (like table auto-creation and schema updates) at runtime
|
|
83
83
|
*/
|
|
84
|
-
|
|
84
|
+
modifyStorage = !Runtime.production;
|
|
85
|
+
|
|
85
86
|
/**
|
|
86
|
-
*
|
|
87
|
+
* Extended client options
|
|
87
88
|
*/
|
|
88
|
-
options
|
|
89
|
+
options?: PoolOptions;
|
|
89
90
|
}
|
|
90
91
|
```
|
|
91
92
|
|
package/__index__.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
export * from './src/
|
|
2
|
-
export * from './src/connection.ts';
|
|
1
|
+
export * from './src/config.ts';
|
|
2
|
+
export * from './src/connection.ts';
|
|
3
|
+
export * from './src/service.ts';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/model-mysql",
|
|
3
|
-
"version": "8.0.0-alpha.
|
|
3
|
+
"version": "8.0.0-alpha.26",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "MySQL backing for the travetto model module, with real-time modeling support for SQL schemas.",
|
|
6
6
|
"keywords": [
|
|
@@ -28,15 +28,15 @@
|
|
|
28
28
|
"directory": "module/model-mysql"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@travetto/config": "^8.0.0-alpha.
|
|
32
|
-
"@travetto/context": "^8.0.0-alpha.
|
|
33
|
-
"@travetto/model": "^8.0.0-alpha.
|
|
34
|
-
"@travetto/model-query": "^8.0.0-alpha.
|
|
35
|
-
"@travetto/model-sql": "^8.0.0-alpha.
|
|
31
|
+
"@travetto/config": "^8.0.0-alpha.22",
|
|
32
|
+
"@travetto/context": "^8.0.0-alpha.20",
|
|
33
|
+
"@travetto/model": "^8.0.0-alpha.23",
|
|
34
|
+
"@travetto/model-query": "^8.0.0-alpha.24",
|
|
35
|
+
"@travetto/model-sql": "^8.0.0-alpha.26",
|
|
36
36
|
"mysql2": "^3.22.6"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
|
-
"@travetto/cli": "^8.0.0-alpha.
|
|
39
|
+
"@travetto/cli": "^8.0.0-alpha.28"
|
|
40
40
|
},
|
|
41
41
|
"peerDependenciesMeta": {
|
|
42
42
|
"@travetto/cli": {
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { PoolOptions } from 'mysql2';
|
|
2
|
+
|
|
3
|
+
import { Config } from '@travetto/config';
|
|
4
|
+
import { Runtime } from '@travetto/runtime';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* MySQL Model Configuration
|
|
8
|
+
*/
|
|
9
|
+
@Config('model.mysql')
|
|
10
|
+
export class MysqlModelConfig {
|
|
11
|
+
/**
|
|
12
|
+
* Database host to connect to
|
|
13
|
+
*/
|
|
14
|
+
host = '127.0.0.1';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Database port to connect to
|
|
18
|
+
*/
|
|
19
|
+
port = 0;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Database username
|
|
23
|
+
*/
|
|
24
|
+
user = Runtime.production ? '' : 'travetto';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Database password
|
|
28
|
+
*/
|
|
29
|
+
password = Runtime.production ? '' : 'travetto';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Namespace/schema prefix for table names
|
|
33
|
+
*/
|
|
34
|
+
namespace = '';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Database name
|
|
38
|
+
*/
|
|
39
|
+
database = 'app';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Allow storage modifications (like table auto-creation and schema updates) at runtime
|
|
43
|
+
*/
|
|
44
|
+
modifyStorage = !Runtime.production;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Extended client options
|
|
48
|
+
*/
|
|
49
|
+
options?: PoolOptions;
|
|
50
|
+
}
|
package/src/connection.ts
CHANGED
|
@@ -1,52 +1,68 @@
|
|
|
1
1
|
import { createPool } from 'mysql2';
|
|
2
|
-
import type {
|
|
2
|
+
import type { OkPacket, Pool, PoolConnection, ResultSetHeader, TypeCastField } from 'mysql2/promise';
|
|
3
3
|
|
|
4
|
-
import { castTo, JSONUtil, ShutdownManager } from '@travetto/runtime';
|
|
5
4
|
import type { AsyncContext } from '@travetto/context';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
5
|
+
import { Injectable } from '@travetto/di';
|
|
6
|
+
import { ExistsError, type ModelType } from '@travetto/model';
|
|
7
|
+
import { SQLConnection, type TableContext } from '@travetto/model-sql';
|
|
8
|
+
import { type Class, castTo, JSONUtil, ShutdownManager } from '@travetto/runtime';
|
|
9
|
+
|
|
10
|
+
import type { MysqlModelConfig } from './config.ts';
|
|
11
|
+
import { MysqlDialect } from './dialect.ts';
|
|
8
12
|
|
|
9
13
|
function isSimplePacket(value: unknown): value is OkPacket | ResultSetHeader {
|
|
10
|
-
return
|
|
11
|
-
value
|
|
14
|
+
return (
|
|
15
|
+
value !== null &&
|
|
16
|
+
value !== undefined &&
|
|
17
|
+
typeof value === 'object' &&
|
|
18
|
+
'constructor' in value &&
|
|
19
|
+
(value.constructor.name === 'OkPacket' || value.constructor.name === 'ResultSetHeader')
|
|
12
20
|
);
|
|
13
21
|
}
|
|
14
22
|
|
|
15
23
|
/**
|
|
16
|
-
* Connection
|
|
24
|
+
* MySQL Connection Manager.
|
|
25
|
+
* Operates on mysql2 promise Pool.
|
|
17
26
|
*/
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
27
|
+
@Injectable()
|
|
28
|
+
export class MysqlConnection extends SQLConnection<PoolConnection> {
|
|
29
|
+
readonly dialect = new MysqlDialect();
|
|
30
|
+
pool: Pool;
|
|
31
|
+
readonly config: MysqlModelConfig;
|
|
22
32
|
|
|
23
|
-
constructor(
|
|
24
|
-
context: AsyncContext,
|
|
25
|
-
config: SQLModelConfig
|
|
26
|
-
) {
|
|
33
|
+
constructor(context: AsyncContext, config: MysqlModelConfig) {
|
|
27
34
|
super(context);
|
|
28
|
-
this
|
|
35
|
+
this.config = config;
|
|
29
36
|
}
|
|
30
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Initializes the mysql2 connection pool
|
|
40
|
+
*/
|
|
31
41
|
async init(): Promise<void> {
|
|
32
|
-
this
|
|
33
|
-
user: this
|
|
34
|
-
password: this
|
|
35
|
-
database: this
|
|
36
|
-
host: this
|
|
37
|
-
port: this
|
|
42
|
+
this.pool = createPool({
|
|
43
|
+
user: this.config.user,
|
|
44
|
+
password: this.config.password,
|
|
45
|
+
database: this.config.database,
|
|
46
|
+
host: this.config.host,
|
|
47
|
+
port: this.config.port,
|
|
38
48
|
supportBigNumbers: true,
|
|
39
49
|
timezone: '+00:00',
|
|
40
50
|
typeCast: this.typeCast.bind(this),
|
|
41
|
-
...
|
|
51
|
+
...this.config.options
|
|
42
52
|
}).promise();
|
|
43
53
|
|
|
44
|
-
|
|
45
|
-
|
|
54
|
+
ShutdownManager.signal.addEventListener('abort', () => this.pool.end());
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
getContext<T extends ModelType>(modelClass: Class<T>): TableContext<T> {
|
|
58
|
+
return {
|
|
59
|
+
...super.getContext(modelClass),
|
|
60
|
+
database: this.config.database
|
|
61
|
+
};
|
|
46
62
|
}
|
|
47
63
|
|
|
48
64
|
/**
|
|
49
|
-
*
|
|
65
|
+
* Typecasting parser for JSON and BLOB mysql columns
|
|
50
66
|
*/
|
|
51
67
|
typeCast(field: TypeCastField, next: () => unknown): unknown {
|
|
52
68
|
const result = next();
|
|
@@ -56,7 +72,7 @@ export class MySQLConnection extends Connection<PoolConnection> {
|
|
|
56
72
|
if (typeof result === 'string' && result.charAt(0) === '{' && result.charAt(result.length - 1) === '}') {
|
|
57
73
|
try {
|
|
58
74
|
return JSONUtil.fromUTF8(result);
|
|
59
|
-
} catch {
|
|
75
|
+
} catch {}
|
|
60
76
|
}
|
|
61
77
|
break;
|
|
62
78
|
}
|
|
@@ -64,41 +80,50 @@ export class MySQLConnection extends Connection<PoolConnection> {
|
|
|
64
80
|
return result;
|
|
65
81
|
}
|
|
66
82
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
83
|
+
/**
|
|
84
|
+
* Acquires a PoolConnection from the pool
|
|
85
|
+
*/
|
|
86
|
+
acquire(): Promise<PoolConnection> {
|
|
87
|
+
return this.pool.getConnection();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Releases a PoolConnection back to the pool
|
|
92
|
+
*/
|
|
93
|
+
release(connection: PoolConnection): void {
|
|
94
|
+
connection.release();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Executes a query on the active client or pool directly
|
|
99
|
+
*/
|
|
100
|
+
async execute<Type = unknown>(query: string, values?: unknown[]): Promise<{ count: number; records: Type[] }> {
|
|
101
|
+
console.debug('Executing MySQL query', { query, values });
|
|
102
|
+
const client = this.active ?? (await this.acquire());
|
|
70
103
|
try {
|
|
71
|
-
|
|
72
|
-
const [results,] = await (prepared ? prepared.execute(values) : pool.query(query));
|
|
104
|
+
const [results] = await client.execute(query, castTo(values ?? []));
|
|
73
105
|
if (isSimplePacket(results)) {
|
|
74
106
|
return { records: [], count: results.affectedRows };
|
|
107
|
+
} else if (isSimplePacket(results[0])) {
|
|
108
|
+
return { records: [], count: results[0].affectedRows };
|
|
75
109
|
} else {
|
|
76
|
-
|
|
77
|
-
return { records: [], count: results[0].affectedRows };
|
|
78
|
-
}
|
|
79
|
-
const records: T[] = [...results].map(value => castTo({ ...value }));
|
|
110
|
+
const records: Type[] = castTo(results);
|
|
80
111
|
return { records, count: records.length };
|
|
81
112
|
}
|
|
82
113
|
} catch (error) {
|
|
83
|
-
console.debug('Failed query', { error, query });
|
|
84
114
|
const code = error && typeof error === 'object' && 'code' in error ? error.code : undefined;
|
|
85
115
|
switch (code) {
|
|
86
|
-
case 'ER_DUP_ENTRY':
|
|
87
|
-
|
|
88
|
-
|
|
116
|
+
case 'ER_DUP_ENTRY':
|
|
117
|
+
throw new ExistsError('query', query);
|
|
118
|
+
case 'ER_DUP_KEYNAME':
|
|
119
|
+
throw new ExistsError('index', query);
|
|
120
|
+
default:
|
|
121
|
+
throw error;
|
|
89
122
|
}
|
|
90
123
|
} finally {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
124
|
+
if (!this.active) {
|
|
125
|
+
this.release(client);
|
|
126
|
+
}
|
|
94
127
|
}
|
|
95
128
|
}
|
|
96
|
-
|
|
97
|
-
acquire(): Promise<PoolConnection> {
|
|
98
|
-
return this.#pool.getConnection();
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
release(pool: PoolConnection): void {
|
|
102
|
-
pool.release();
|
|
103
|
-
}
|
|
104
|
-
}
|
|
129
|
+
}
|
package/src/dialect.ts
CHANGED
|
@@ -1,176 +1,195 @@
|
|
|
1
|
+
import { AbstractANSI99Dialect, type JSONSqlPathMode, type TableContext } from '@travetto/model-sql';
|
|
2
|
+
import { type Class, castTo, JSONUtil } from '@travetto/runtime';
|
|
1
3
|
import type { SchemaFieldConfig } from '@travetto/schema';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
connection: MySQLConnection;
|
|
18
|
-
tablePostfix = 'COLLATE=utf8mb4_bin ENGINE=InnoDB';
|
|
19
|
-
|
|
20
|
-
constructor(context: AsyncContext, config: SQLModelConfig) {
|
|
21
|
-
super(config.namespace);
|
|
22
|
-
this.connection = new MySQLConnection(context, config);
|
|
23
|
-
|
|
24
|
-
// Custom types
|
|
25
|
-
Object.assign(this.COLUMN_TYPES, {
|
|
26
|
-
TIMESTAMP: 'DATETIME(3)',
|
|
27
|
-
JSON: 'TEXT'
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Set string length limit based on version
|
|
32
|
-
*/
|
|
33
|
-
if (/^5[.][56]/.test(config.version)) {
|
|
34
|
-
this.DEFAULT_STRING_LENGTH = 191; // Mysql limitation with utf8 and keys
|
|
35
|
-
} else {
|
|
36
|
-
this.DEFAULT_STRING_LENGTH = 3072 / 4 - 1;
|
|
4
|
+
|
|
5
|
+
export class MysqlDialect extends AbstractANSI99Dialect {
|
|
6
|
+
override returningSupport = false;
|
|
7
|
+
|
|
8
|
+
override escapeIdentifier(name: string): string {
|
|
9
|
+
return `\`${name.replaceAll('`', '``')}\``;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
getComplexColumnType(field: SchemaFieldConfig): string {
|
|
13
|
+
return 'JSON';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
getColumnType(fieldConfiguration: SchemaFieldConfig): string {
|
|
17
|
+
if (fieldConfiguration.type === castTo(BigInt)) {
|
|
18
|
+
return 'BIGINT';
|
|
37
19
|
}
|
|
38
20
|
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
21
|
+
if (fieldConfiguration.type === Number) {
|
|
22
|
+
if (fieldConfiguration.precision) {
|
|
23
|
+
const [digits, decimals] = fieldConfiguration.precision;
|
|
24
|
+
if (decimals) {
|
|
25
|
+
return `DECIMAL(${digits},${decimals})`;
|
|
26
|
+
}
|
|
27
|
+
if (digits < 5) {
|
|
28
|
+
return 'SMALLINT';
|
|
29
|
+
}
|
|
30
|
+
if (digits < 10) {
|
|
31
|
+
return 'INT';
|
|
32
|
+
}
|
|
33
|
+
return 'BIGINT';
|
|
34
|
+
}
|
|
35
|
+
return 'INT';
|
|
54
36
|
}
|
|
37
|
+
|
|
38
|
+
if (fieldConfiguration.type === Date) {
|
|
39
|
+
return 'DATETIME(6)';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (fieldConfiguration.type === Boolean) {
|
|
43
|
+
return 'TINYINT(1)';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (fieldConfiguration.type === String) {
|
|
47
|
+
if (fieldConfiguration.specifiers?.includes('text')) {
|
|
48
|
+
return 'TEXT';
|
|
49
|
+
}
|
|
50
|
+
return `VARCHAR(${fieldConfiguration.maxlength?.limit ?? 767})`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return 'JSON';
|
|
55
54
|
}
|
|
56
55
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
* Get DROP INDEX sql
|
|
66
|
-
*/
|
|
67
|
-
getDropIndexSQL<T extends ModelType>(cls: Class<T>, idx: IndexConfig | string): string {
|
|
68
|
-
const constraint = typeof idx === 'string' ? idx : this.getIndexName(cls, idx);
|
|
69
|
-
return `DROP INDEX ${this.identifier(constraint)} ON ${this.table(SQLModelUtil.classToStack(cls))};`;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async describeTable(table: string): Promise<SQLTableDescription | undefined> {
|
|
73
|
-
const IGNORE_FIELDS = [this.pathField.name, this.parentPathField.name, this.idxField.name].map(field => `'${field}'`);
|
|
74
|
-
const [columns, foreignKeys, indices] = await Promise.all([
|
|
75
|
-
// 1. Columns
|
|
76
|
-
this.executeSQL<{ name: string, type: string, is_not_null: boolean }>(`
|
|
77
|
-
SELECT
|
|
78
|
-
COLUMN_NAME AS name,
|
|
79
|
-
COLUMN_TYPE AS type,
|
|
80
|
-
IS_NULLABLE <> 'YES' AS is_not_null
|
|
81
|
-
FROM information_schema.COLUMNS
|
|
82
|
-
WHERE TABLE_NAME = '${table}'
|
|
83
|
-
AND TABLE_SCHEMA = DATABASE()
|
|
84
|
-
AND COLUMN_NAME NOT IN (${IGNORE_FIELDS.join(',')})
|
|
85
|
-
ORDER BY ORDINAL_POSITION
|
|
86
|
-
`),
|
|
87
|
-
|
|
88
|
-
// 2. Foreign Keys
|
|
89
|
-
this.executeSQL<{ name: string, from_column: string, to_column: string, to_table: string }>(`
|
|
90
|
-
SELECT
|
|
91
|
-
CONSTRAINT_NAME AS name,
|
|
92
|
-
COLUMN_NAME AS from_column,
|
|
93
|
-
REFERENCED_COLUMN_NAME AS to_column,
|
|
94
|
-
REFERENCED_TABLE_NAME AS to_table
|
|
95
|
-
FROM information_schema.KEY_COLUMN_USAGE
|
|
96
|
-
WHERE TABLE_NAME = '${table}'
|
|
97
|
-
AND TABLE_SCHEMA = DATABASE()
|
|
98
|
-
AND REFERENCED_TABLE_NAME IS NOT NULL
|
|
99
|
-
`),
|
|
100
|
-
|
|
101
|
-
// 3. Indices
|
|
102
|
-
this.executeSQL<{ name: string, is_unique: number, columns: string }>(`
|
|
103
|
-
SELECT
|
|
104
|
-
stat.INDEX_NAME AS name,
|
|
105
|
-
stat.NON_UNIQUE = 0 AS is_unique,
|
|
106
|
-
GROUP_CONCAT(CONCAT(stat.COLUMN_NAME, ' ', stat.COLLATION, ' ') ORDER BY stat.SEQ_IN_INDEX) AS columns
|
|
107
|
-
FROM information_schema.STATISTICS stat
|
|
108
|
-
LEFT OUTER JOIN information_schema.TABLE_CONSTRAINTS AS tc
|
|
109
|
-
ON tc.CONSTRAINT_NAME = stat.INDEX_NAME
|
|
110
|
-
AND tc.TABLE_NAME = stat.TABLE_NAME
|
|
111
|
-
AND tc.TABLE_SCHEMA = stat.TABLE_SCHEMA
|
|
112
|
-
WHERE
|
|
113
|
-
stat.TABLE_NAME = '${table}'
|
|
114
|
-
AND stat.TABLE_SCHEMA = DATABASE()
|
|
115
|
-
AND (tc.CONSTRAINT_TYPE IS NULL OR tc.CONSTRAINT_TYPE = 'UNIQUE')
|
|
116
|
-
AND stat.COLUMN_NAME NOT IN (${IGNORE_FIELDS.join(',')})
|
|
117
|
-
GROUP BY stat.INDEX_NAME, stat.NON_UNIQUE
|
|
118
|
-
`)
|
|
119
|
-
]);
|
|
120
|
-
|
|
121
|
-
if (!columns.count) {
|
|
122
|
-
return undefined;
|
|
56
|
+
compileJsonIndexPath(columnName: string, jsonPath: string[], mode: JSONSqlPathMode): string {
|
|
57
|
+
const result = `${columnName}->>'$.${jsonPath.join('.')}'`;
|
|
58
|
+
switch (mode) {
|
|
59
|
+
case 'createIndex':
|
|
60
|
+
return `(CAST(${result} as CHAR(255)) COLLATE utf8mb4_bin)`;
|
|
61
|
+
case 'orderBy':
|
|
62
|
+
case 'read':
|
|
63
|
+
return result;
|
|
123
64
|
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
compileArrayAll(
|
|
68
|
+
sqlPath: string,
|
|
69
|
+
identifier: string,
|
|
70
|
+
value: unknown[],
|
|
71
|
+
field: SchemaFieldConfig,
|
|
72
|
+
topLevel?: boolean
|
|
73
|
+
): { sql: string; formatted: unknown } {
|
|
74
|
+
return { sql: `JSON_CONTAINS(${sqlPath}, ${identifier})`, formatted: JSONUtil.toUTF8(value) };
|
|
75
|
+
}
|
|
124
76
|
|
|
77
|
+
compileArrayEquals(
|
|
78
|
+
sqlPath: string,
|
|
79
|
+
identifier: string,
|
|
80
|
+
values: unknown,
|
|
81
|
+
field: SchemaFieldConfig,
|
|
82
|
+
topLevel?: boolean
|
|
83
|
+
): { sql: string; formatted: unknown } {
|
|
84
|
+
return { sql: `JSON_CONTAINS(${sqlPath}, ${identifier})`, formatted: JSONUtil.toUTF8(values) };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
compileArrayAny(
|
|
88
|
+
sqlPath: string,
|
|
89
|
+
identifier: string,
|
|
90
|
+
values: unknown[],
|
|
91
|
+
field: SchemaFieldConfig,
|
|
92
|
+
topLevel?: boolean
|
|
93
|
+
): { sql: string; formatted: unknown } {
|
|
94
|
+
return { sql: `JSON_OVERLAPS(${sqlPath}, ${identifier})`, formatted: JSONUtil.toUTF8(values) };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
compileArrayExists(sqlPath: string, identifier: string, field: SchemaFieldConfig, topLevel?: boolean): { sql: string } {
|
|
98
|
+
return { sql: `(${sqlPath} IS NOT NULL AND JSON_LENGTH(${sqlPath}) > 0)` };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
compileJsonEquality(sqlPath: string, identifier: string): string {
|
|
102
|
+
return `CAST(${sqlPath} AS JSON) = CAST(${identifier} AS JSON)`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
getRegexOperator(caseInsensitive: boolean): string {
|
|
106
|
+
return caseInsensitive ? 'REGEXP' : 'COLLATE utf8mb4_bin REGEXP';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
formatRegex(source: string, caseInsensitive: boolean): string {
|
|
110
|
+
return source;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
castColumn(sqlPath: string, type: Class): string {
|
|
114
|
+
if (type === Number) {
|
|
115
|
+
return `CAST(${sqlPath} AS DECIMAL)`;
|
|
116
|
+
} else if (type === Boolean) {
|
|
117
|
+
return `CAST(${sqlPath} AS SIGNED)`;
|
|
118
|
+
} else if (type === Date) {
|
|
119
|
+
return `CAST(${sqlPath} AS DATETIME(6))`;
|
|
120
|
+
}
|
|
121
|
+
return sqlPath;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
override getUpsertSQL(
|
|
125
|
+
context: TableContext,
|
|
126
|
+
columns: string[],
|
|
127
|
+
placeholders: string[],
|
|
128
|
+
conflictTarget: string[],
|
|
129
|
+
updates: string[]
|
|
130
|
+
): string {
|
|
131
|
+
const mysqlUpdates = updates.map(val => val.replace(/EXCLUDED\.(.*)/g, 'VALUES($1)'));
|
|
132
|
+
return `
|
|
133
|
+
INSERT INTO
|
|
134
|
+
${this.escapeIdentifier(context.tableName)} (${columns.join(', ')})
|
|
135
|
+
VALUES
|
|
136
|
+
(${placeholders.join(', ')})
|
|
137
|
+
ON DUPLICATE KEY UPDATE ${mysqlUpdates.join(', ')};`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
getTableExistsQuery(context: TableContext): { sql: string; parameters?: unknown[] } {
|
|
141
|
+
return {
|
|
142
|
+
sql: `
|
|
143
|
+
SELECT
|
|
144
|
+
COUNT(*) as total
|
|
145
|
+
FROM information_schema.tables
|
|
146
|
+
WHERE table_schema = ? AND table_name = ?;
|
|
147
|
+
`,
|
|
148
|
+
parameters: [context.database, context.tableName]
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
parseTableExistsResult(records: unknown[]): boolean {
|
|
153
|
+
return Number(castTo<{ total: number }>(records[0])?.total ?? 0) > 0;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
getExistingColumnsQuery(context: TableContext): { sql: string; parameters?: unknown[] } {
|
|
125
157
|
return {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
is_unique: !!idx.is_unique,
|
|
135
|
-
columns: idx.columns
|
|
136
|
-
.split(',')
|
|
137
|
-
.map(column => column.split(' '))
|
|
138
|
-
.map(([name, desc]) => ({ name, desc: desc === 'D' }))
|
|
139
|
-
}))
|
|
158
|
+
sql: `
|
|
159
|
+
SELECT
|
|
160
|
+
COLUMN_NAME as name,
|
|
161
|
+
DATA_TYPE as type
|
|
162
|
+
FROM information_schema.columns
|
|
163
|
+
WHERE table_schema = ? AND table_name = ?;
|
|
164
|
+
`,
|
|
165
|
+
parameters: [context.database, context.tableName]
|
|
140
166
|
};
|
|
141
167
|
}
|
|
142
168
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
*/
|
|
146
|
-
override getCreateTableSQL(stack: VisitStack[]): string {
|
|
147
|
-
return super.getCreateTableSQL(stack).replace(/;$/, ` ${this.tablePostfix};`);
|
|
169
|
+
parseExistingColumns(records: unknown[]): Map<string, string> {
|
|
170
|
+
return new Map(castTo<{ name: string; type: string }[]>(records).map(record => [record.name, record.type.toUpperCase()]));
|
|
148
171
|
}
|
|
149
172
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
173
|
+
getExistingIndexesQuery(context: TableContext): { sql: string; parameters?: unknown[] } {
|
|
174
|
+
return {
|
|
175
|
+
sql: `
|
|
176
|
+
SELECT DISTINCT
|
|
177
|
+
INDEX_NAME as name
|
|
178
|
+
FROM information_schema.statistics
|
|
179
|
+
WHERE
|
|
180
|
+
table_schema = ?
|
|
181
|
+
AND table_name = ?
|
|
182
|
+
AND INDEX_NAME != 'PRIMARY';
|
|
183
|
+
`,
|
|
184
|
+
parameters: [context.database, context.tableName]
|
|
185
|
+
};
|
|
156
186
|
}
|
|
157
187
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
*/
|
|
161
|
-
override getDeleteSQL(stack: VisitStack[], where?: WhereClause<unknown>): string {
|
|
162
|
-
const sql = super.getDeleteSQL(stack, where);
|
|
163
|
-
return sql.replace(/\bDELETE\b/g, `DELETE ${this.rootAlias}`);
|
|
188
|
+
parseExistingIndexes(records: unknown[]): Map<string, string> {
|
|
189
|
+
return new Map(castTo<{ name: string }[]>(records).map(record => [record.name, '']));
|
|
164
190
|
}
|
|
165
191
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
*/
|
|
169
|
-
override getTruncateAllTablesSQL<T extends ModelType>(cls: Class<T>): string[] {
|
|
170
|
-
return [
|
|
171
|
-
'SET FOREIGN_KEY_CHECKS = 0;',
|
|
172
|
-
...super.getTruncateAllTablesSQL(cls),
|
|
173
|
-
'SET FOREIGN_KEY_CHECKS = 1;'
|
|
174
|
-
];
|
|
192
|
+
override getDropIndexSQL(context: TableContext, indexName: string): string {
|
|
193
|
+
return `DROP INDEX ${this.escapeIdentifier(indexName)} ON ${this.escapeIdentifier(context.tableName)};`;
|
|
175
194
|
}
|
|
176
|
-
}
|
|
195
|
+
}
|
package/src/service.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { PoolConnection } from 'mysql2/promise';
|
|
2
|
+
|
|
3
|
+
import { Injectable, PostConstruct } from '@travetto/di';
|
|
4
|
+
import { BaseSQLModelService } from '@travetto/model-sql';
|
|
5
|
+
|
|
6
|
+
import type { MysqlConnection } from './connection.ts';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A MySQL JSON-based document store model service
|
|
10
|
+
*/
|
|
11
|
+
@Injectable()
|
|
12
|
+
export class MysqlModelService extends BaseSQLModelService {
|
|
13
|
+
connection: MysqlConnection;
|
|
14
|
+
|
|
15
|
+
constructor(connection: MysqlConnection) {
|
|
16
|
+
super();
|
|
17
|
+
this.connection = connection;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
get client(): PoolConnection {
|
|
21
|
+
return this.connection.active!;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@PostConstruct()
|
|
25
|
+
override async initialize(): Promise<void> {
|
|
26
|
+
await super.initialize();
|
|
27
|
+
}
|
|
28
|
+
}
|
package/support/service.mysql.ts
CHANGED
|
@@ -7,10 +7,11 @@ export const service: ServiceDescriptor = {
|
|
|
7
7
|
version,
|
|
8
8
|
image: `mysql:${version}`,
|
|
9
9
|
port: 3306,
|
|
10
|
+
args: ['--skip-name-resolve', '--innodb-flush-log-at-trx-commit=0', '--sync-binlog=0'],
|
|
10
11
|
env: {
|
|
11
12
|
MYSQL_RANDOM_ROOT_PASSWORD: '1',
|
|
12
13
|
MYSQL_PASSWORD: 'travetto',
|
|
13
14
|
MYSQL_USER: 'travetto',
|
|
14
15
|
MYSQL_DATABASE: 'app'
|
|
15
|
-
}
|
|
16
|
-
};
|
|
16
|
+
}
|
|
17
|
+
};
|