@travetto/model-mysql 7.0.0-rc.1 → 7.0.0-rc.3
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 +4 -4
- package/package.json +7 -7
- package/src/connection.ts +18 -17
- package/src/dialect.ts +85 -6
- package/support/service.mysql.ts +3 -1
package/README.md
CHANGED
|
@@ -37,8 +37,8 @@ import { MySQLDialect } from '@travetto/model-mysql';
|
|
|
37
37
|
|
|
38
38
|
export class Init {
|
|
39
39
|
@InjectableFactory({ primary: true })
|
|
40
|
-
static getModelService(ctx: AsyncContext,
|
|
41
|
-
return new SQLModelService(ctx,
|
|
40
|
+
static getModelService(ctx: AsyncContext, config: SQLModelConfig) {
|
|
41
|
+
return new SQLModelService(ctx, config, new MySQLDialect(ctx, config));
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
```
|
|
@@ -74,9 +74,9 @@ export class SQLModelConfig<T extends {} = {}> {
|
|
|
74
74
|
*/
|
|
75
75
|
database = 'app';
|
|
76
76
|
/**
|
|
77
|
-
*
|
|
77
|
+
* Allow storage modification at runtime
|
|
78
78
|
*/
|
|
79
|
-
|
|
79
|
+
modifyStorage?: boolean;
|
|
80
80
|
/**
|
|
81
81
|
* Db version
|
|
82
82
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/model-mysql",
|
|
3
|
-
"version": "7.0.0-rc.
|
|
3
|
+
"version": "7.0.0-rc.3",
|
|
4
4
|
"description": "MySQL backing for the travetto model module, with real-time modeling support for SQL schemas.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sql",
|
|
@@ -27,12 +27,12 @@
|
|
|
27
27
|
"directory": "module/model-mysql"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@travetto/cli": "^7.0.0-rc.
|
|
31
|
-
"@travetto/config": "^7.0.0-rc.
|
|
32
|
-
"@travetto/context": "^7.0.0-rc.
|
|
33
|
-
"@travetto/model": "^7.0.0-rc.
|
|
34
|
-
"@travetto/model-query": "^7.0.0-rc.
|
|
35
|
-
"@travetto/model-sql": "^7.0.0-rc.
|
|
30
|
+
"@travetto/cli": "^7.0.0-rc.3",
|
|
31
|
+
"@travetto/config": "^7.0.0-rc.3",
|
|
32
|
+
"@travetto/context": "^7.0.0-rc.3",
|
|
33
|
+
"@travetto/model": "^7.0.0-rc.3",
|
|
34
|
+
"@travetto/model-query": "^7.0.0-rc.3",
|
|
35
|
+
"@travetto/model-sql": "^7.0.0-rc.3",
|
|
36
36
|
"mysql2": "^3.15.3"
|
|
37
37
|
},
|
|
38
38
|
"travetto": {
|
package/src/connection.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { createPool } from 'mysql2';
|
|
2
2
|
import { PoolConnection, Pool, OkPacket, ResultSetHeader } from 'mysql2/promise';
|
|
3
3
|
|
|
4
|
-
import { castTo, ShutdownManager } from '@travetto/runtime';
|
|
4
|
+
import { castTo, JSONUtil, ShutdownManager } from '@travetto/runtime';
|
|
5
5
|
import { AsyncContext } from '@travetto/context';
|
|
6
6
|
import { ExistsError } from '@travetto/model';
|
|
7
7
|
import { Connection, SQLModelConfig } from '@travetto/model-sql';
|
|
8
8
|
|
|
9
|
-
function isSimplePacket(
|
|
10
|
-
return
|
|
11
|
-
|
|
9
|
+
function isSimplePacket(value: unknown): value is OkPacket | ResultSetHeader {
|
|
10
|
+
return value !== null && value !== undefined && typeof value === 'object' && 'constructor' in value && (
|
|
11
|
+
value.constructor.name === 'OkPacket' || value.constructor.name === 'ResultSetHeader'
|
|
12
12
|
);
|
|
13
13
|
}
|
|
14
14
|
|
|
@@ -52,34 +52,35 @@ export class MySQLConnection extends Connection<PoolConnection> {
|
|
|
52
52
|
if (typeof result === 'string' && (field && typeof field === 'object' && 'type' in field) && (field.type === 'JSON' || field.type === 'BLOB')) {
|
|
53
53
|
if (result.charAt(0) === '{' && result.charAt(result.length - 1) === '}') {
|
|
54
54
|
try {
|
|
55
|
-
return
|
|
55
|
+
return JSONUtil.parseSafe(result);
|
|
56
56
|
} catch { }
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
return result;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
async execute<T = unknown>(
|
|
62
|
+
async execute<T = unknown>(pool: PoolConnection, query: string, values?: unknown[]): Promise<{ count: number, records: T[] }> {
|
|
63
63
|
console.debug('Executing query', { query });
|
|
64
64
|
let prepared;
|
|
65
65
|
try {
|
|
66
|
-
prepared = (values?.length ?? 0) > 0 ? await
|
|
67
|
-
const [results,] = await (prepared ? prepared.execute(values) :
|
|
66
|
+
prepared = (values?.length ?? 0) > 0 ? await pool.prepare(query) : undefined;
|
|
67
|
+
const [results,] = await (prepared ? prepared.execute(values) : pool.query(query));
|
|
68
68
|
if (isSimplePacket(results)) {
|
|
69
69
|
return { records: [], count: results.affectedRows };
|
|
70
70
|
} else {
|
|
71
71
|
if (isSimplePacket(results[0])) {
|
|
72
72
|
return { records: [], count: results[0].affectedRows };
|
|
73
73
|
}
|
|
74
|
-
const records: T[] = [...results].map(
|
|
74
|
+
const records: T[] = [...results].map(value => castTo({ ...value }));
|
|
75
75
|
return { records, count: records.length };
|
|
76
76
|
}
|
|
77
|
-
} catch (
|
|
78
|
-
console.debug('Failed query', { error
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
throw
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.debug('Failed query', { error, query });
|
|
79
|
+
const code = error && typeof error === 'object' && 'code' in error ? error.code : undefined;
|
|
80
|
+
switch (code) {
|
|
81
|
+
case 'ER_DUP_ENTRY': throw new ExistsError('query', query);
|
|
82
|
+
case 'ER_DUP_KEYNAME': throw new ExistsError('index', query);
|
|
83
|
+
default: throw error;
|
|
83
84
|
}
|
|
84
85
|
} finally {
|
|
85
86
|
try {
|
|
@@ -92,7 +93,7 @@ export class MySQLConnection extends Connection<PoolConnection> {
|
|
|
92
93
|
return this.#pool.getConnection();
|
|
93
94
|
}
|
|
94
95
|
|
|
95
|
-
release(
|
|
96
|
-
|
|
96
|
+
release(pool: PoolConnection): void {
|
|
97
|
+
pool.release();
|
|
97
98
|
}
|
|
98
99
|
}
|
package/src/dialect.ts
CHANGED
|
@@ -3,8 +3,8 @@ import { Injectable } from '@travetto/di';
|
|
|
3
3
|
import { AsyncContext } from '@travetto/context';
|
|
4
4
|
import { WhereClause } from '@travetto/model-query';
|
|
5
5
|
import { castTo, Class } from '@travetto/runtime';
|
|
6
|
-
import { ModelType } from '@travetto/model';
|
|
7
|
-
import { SQLModelConfig, SQLDialect, VisitStack } from '@travetto/model-sql';
|
|
6
|
+
import { ModelType, type IndexConfig } from '@travetto/model';
|
|
7
|
+
import { SQLModelConfig, SQLDialect, VisitStack, type SQLTableDescription, SQLModelUtil } from '@travetto/model-sql';
|
|
8
8
|
|
|
9
9
|
import { MySQLConnection } from './connection.ts';
|
|
10
10
|
|
|
@@ -14,12 +14,12 @@ import { MySQLConnection } from './connection.ts';
|
|
|
14
14
|
@Injectable()
|
|
15
15
|
export class MySQLDialect extends SQLDialect {
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
connection: MySQLConnection;
|
|
18
18
|
tablePostfix = 'COLLATE=utf8mb4_bin ENGINE=InnoDB';
|
|
19
19
|
|
|
20
20
|
constructor(context: AsyncContext, config: SQLModelConfig) {
|
|
21
21
|
super(config.namespace);
|
|
22
|
-
this.
|
|
22
|
+
this.connection = new MySQLConnection(context, config);
|
|
23
23
|
|
|
24
24
|
// Custom types
|
|
25
25
|
Object.assign(this.COLUMN_TYPES, {
|
|
@@ -31,9 +31,9 @@ export class MySQLDialect extends SQLDialect {
|
|
|
31
31
|
* Set string length limit based on version
|
|
32
32
|
*/
|
|
33
33
|
if (/^5[.][56]/.test(config.version)) {
|
|
34
|
-
this.
|
|
34
|
+
this.DEFAULT_STRING_LENGTH = 191; // Mysql limitation with utf8 and keys
|
|
35
35
|
} else {
|
|
36
|
-
this.
|
|
36
|
+
this.DEFAULT_STRING_LENGTH = 3072 / 4 - 1;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
if (/^5[.].*/.test(config.version)) {
|
|
@@ -61,6 +61,85 @@ export class MySQLDialect extends SQLDialect {
|
|
|
61
61
|
return `SHA2('${value}', '256')`;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Get DROP INDEX sql
|
|
66
|
+
*/
|
|
67
|
+
getDropIndexSQL<T extends ModelType>(cls: Class<T>, idx: IndexConfig<T> | 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_notnull: boolean }>(`
|
|
77
|
+
SELECT
|
|
78
|
+
COLUMN_NAME AS name,
|
|
79
|
+
COLUMN_TYPE AS type,
|
|
80
|
+
IS_NULLABLE <> 'YES' AS is_notnull
|
|
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
|
|
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;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
columns: columns.records.map(col => ({
|
|
127
|
+
...col,
|
|
128
|
+
type: col.type.toUpperCase(),
|
|
129
|
+
is_notnull: !!col.is_notnull
|
|
130
|
+
})),
|
|
131
|
+
foreignKeys: foreignKeys.records,
|
|
132
|
+
indices: indices.records.map(idx => ({
|
|
133
|
+
name: idx.name,
|
|
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
|
+
}))
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
64
143
|
/**
|
|
65
144
|
* Create table, adding in specific engine options
|
|
66
145
|
*/
|
package/support/service.mysql.ts
CHANGED
|
@@ -8,7 +8,9 @@ export const service: ServiceDescriptor = {
|
|
|
8
8
|
image: `mysql:${version}`,
|
|
9
9
|
port: 3306,
|
|
10
10
|
env: {
|
|
11
|
-
|
|
11
|
+
MYSQL_RANDOM_ROOT_PASSWORD: '1',
|
|
12
|
+
MYSQL_PASSWORD: 'travetto',
|
|
13
|
+
MYSQL_USER: 'travetto',
|
|
12
14
|
MYSQL_DATABASE: 'app'
|
|
13
15
|
},
|
|
14
16
|
};
|