@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 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, conf: SQLModelConfig) {
41
- return new SQLModelService(ctx, conf, new MySQLDialect(ctx, conf));
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
- * Auto schema creation
77
+ * Allow storage modification at runtime
78
78
  */
79
- autoCreate?: boolean;
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.1",
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.1",
31
- "@travetto/config": "^7.0.0-rc.1",
32
- "@travetto/context": "^7.0.0-rc.1",
33
- "@travetto/model": "^7.0.0-rc.1",
34
- "@travetto/model-query": "^7.0.0-rc.1",
35
- "@travetto/model-sql": "^7.0.0-rc.1",
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(o: unknown): o is OkPacket | ResultSetHeader {
10
- return o !== null && o !== undefined && typeof o === 'object' && 'constructor' in o && (
11
- o.constructor.name === 'OkPacket' || o.constructor.name === 'ResultSetHeader'
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 JSON.parse(result);
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>(conn: PoolConnection, query: string, values?: unknown[]): Promise<{ count: number, records: T[] }> {
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 conn.prepare(query) : undefined;
67
- const [results,] = await (prepared ? prepared.execute(values) : conn.query(query));
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(v => castTo({ ...v }));
74
+ const records: T[] = [...results].map(value => castTo({ ...value }));
75
75
  return { records, count: records.length };
76
76
  }
77
- } catch (err) {
78
- console.debug('Failed query', { error: err, query });
79
- if (err instanceof Error && err.message.startsWith('Duplicate entry')) {
80
- throw new ExistsError('query', query);
81
- } else {
82
- throw err;
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(conn: PoolConnection): void {
96
- conn.release();
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
- conn: MySQLConnection;
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.conn = new MySQLConnection(context, config);
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.DEFAULT_STRING_LEN = 191; // Mysql limitation with utf8 and keys
34
+ this.DEFAULT_STRING_LENGTH = 191; // Mysql limitation with utf8 and keys
35
35
  } else {
36
- this.DEFAULT_STRING_LEN = 3072 / 4 - 1;
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
  */
@@ -8,7 +8,9 @@ export const service: ServiceDescriptor = {
8
8
  image: `mysql:${version}`,
9
9
  port: 3306,
10
10
  env: {
11
- MYSQL_ROOT_PASSWORD: 'password',
11
+ MYSQL_RANDOM_ROOT_PASSWORD: '1',
12
+ MYSQL_PASSWORD: 'travetto',
13
+ MYSQL_USER: 'travetto',
12
14
  MYSQL_DATABASE: 'app'
13
15
  },
14
16
  };