@dbml/connector 3.7.2

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.
@@ -0,0 +1,513 @@
1
+ import { createConnection, Connection } from 'mysql2/promise';
2
+ import { flatten } from 'lodash';
3
+ import { DefaultInfo, DatabaseSchema, DefaultType, Field, IndexColumn, FieldsDictionary, Table, Enum, IndexesDictionary, TableConstraintsDictionary, Index, TableConstraint, Ref } from './types';
4
+
5
+ const NUMBER_REGEX = '^-?[0-9]+(.[0-9]+)?$';
6
+
7
+ async function connectMySQL (connection: string): Promise<Connection> {
8
+ const client = await createConnection(connection);
9
+ try {
10
+ await client.connect();
11
+ await client.query('SELECT 1');
12
+
13
+ return client;
14
+ } catch (error) {
15
+ await client.end();
16
+ if (error instanceof Error) {
17
+ throw new Error(`MySQL connection error: ${error.message}`);
18
+ }
19
+
20
+ throw error;
21
+ }
22
+ }
23
+
24
+ function getEnumName (tableName: string, columnName: string): string {
25
+ return `${tableName}_${columnName}_enum`;
26
+ }
27
+
28
+ function getGenerationExpression (extraType: string, generationExpression: string) {
29
+ if (extraType === 'VIRTUAL GENERATED') {
30
+ return `GENERATED ALWAYS AS (${generationExpression}) VIRTUAL`;
31
+ }
32
+
33
+ if (extraType === 'STORED GENERATED') {
34
+ return `GENERATED ALWAYS AS (${generationExpression}) STORED`;
35
+ }
36
+
37
+ // for timestamp data type
38
+ if (extraType.includes('on update CURRENT_TIMESTAMP')) {
39
+ return 'on update CURRENT_TIMESTAMP';
40
+ }
41
+
42
+ // others
43
+ return '';
44
+ }
45
+
46
+ function getDbDefault (columnDefault: string, defaultValueType: DefaultType): DefaultInfo | null {
47
+ if (columnDefault === null) {
48
+ return null;
49
+ }
50
+
51
+ return { value: columnDefault, type: defaultValueType };
52
+ }
53
+
54
+ function getFieldType (tableName: string, columnName: string, columnType: string, columnDataType: string, columnExtra: string, generationExpression: string) {
55
+ if (columnDataType === 'enum') {
56
+ // enum must have static value -> no need to check the generation expression
57
+ return getEnumName(tableName, columnName);
58
+ }
59
+
60
+ const fieldGenerationExpression = getGenerationExpression(columnExtra, generationExpression);
61
+ if (fieldGenerationExpression) {
62
+ return `${columnType} ${fieldGenerationExpression}`;
63
+ }
64
+
65
+ return columnType;
66
+ }
67
+
68
+ function generateField (row: Record<string, any>): Field {
69
+ const {
70
+ tableName,
71
+ columnName,
72
+ columnDefault,
73
+ defaultValueType,
74
+ columnIsNullable,
75
+ columnType,
76
+ columnDataType,
77
+ columnComment,
78
+ columnExtra,
79
+ generationExpression,
80
+ } = row;
81
+
82
+ const fieldType = getFieldType(
83
+ tableName,
84
+ columnName,
85
+ columnType,
86
+ columnDataType,
87
+ columnExtra,
88
+ generationExpression,
89
+ );
90
+
91
+ const fieldDefaultValue = getDbDefault(columnDefault, defaultValueType);
92
+
93
+ const isNullable = columnIsNullable === 'YES';
94
+
95
+ return {
96
+ name: columnName,
97
+ type: { type_name: fieldType, schemaName: null },
98
+ dbdefault: fieldDefaultValue,
99
+ not_null: !isNullable,
100
+ increment: columnExtra === 'auto_increment',
101
+ note: { value: columnComment || '' },
102
+ };
103
+ }
104
+
105
+ // Do not get the index sub part since in DBML, it is impossible to create index on part of column.
106
+ function getIndexColumn (columnName: string, idxExpression: string, idxSubPart: any): IndexColumn | null {
107
+ if (idxExpression) {
108
+ return { value: idxExpression, type: 'expression' };
109
+ }
110
+
111
+ if (idxSubPart) {
112
+ return { value: `${columnName}(${idxSubPart})`, type: 'expression' };
113
+ }
114
+
115
+ if (columnName) {
116
+ return { value: columnName, type: 'column' };
117
+ }
118
+
119
+ return null;
120
+ }
121
+
122
+ async function generateTablesAndFields (client: Connection, schemaName: string): Promise<{
123
+ tableList: Table[];
124
+ fieldMap: FieldsDictionary;
125
+ }> {
126
+ const query = `
127
+ select
128
+ t.table_name as tableName,
129
+ t.table_comment as tableComment,
130
+ c.column_name as columnName,
131
+ c.column_default as columnDefault,
132
+ case
133
+ when c.column_default is null then 'boolean'
134
+ when c.data_type = 'enum' then 'string'
135
+ when c.column_default regexp ? then 'number'
136
+ when c.extra like '%DEFAULT_GENERATED%' then 'expression'
137
+ else 'string'
138
+ end as defaultValueType,
139
+ c.is_nullable as columnIsNullable,
140
+ c.data_type as columnDataType,
141
+ c.column_type as columnType,
142
+ c.extra as columnExtra,
143
+ c.column_comment as columnComment,
144
+ c.generation_expression as generationExpression
145
+ from information_schema.tables t
146
+ join information_schema.columns c
147
+ on t.table_schema = c.table_schema and t.table_name = c.table_name
148
+ where
149
+ t.table_schema = ? and t.table_type = 'BASE TABLE'
150
+ order by
151
+ t.table_name, c.ordinal_position;
152
+ `;
153
+
154
+ const queryResponse = await client.query(query, [NUMBER_REGEX, schemaName]);
155
+ const rows: any = queryResponse[0];
156
+
157
+ const tableMap: Record<string, any> = {};
158
+ const fieldMap: FieldsDictionary = {};
159
+
160
+ rows.forEach((row: Record<string, any>) => {
161
+ const { tableName, tableComment } = row;
162
+ const key = tableName;
163
+
164
+ if (!tableMap[key]) {
165
+ tableMap[key] = {
166
+ name: tableName,
167
+ note: { value: tableComment || '' },
168
+ };
169
+ }
170
+
171
+ if (!fieldMap[key]) {
172
+ fieldMap[key] = [];
173
+ }
174
+
175
+ const field = generateField(row);
176
+ fieldMap[key].push(field);
177
+ });
178
+
179
+ return {
180
+ tableList: Object.values(tableMap),
181
+ fieldMap,
182
+ };
183
+ }
184
+
185
+ async function generateEnums (client: Connection, schemaName: string): Promise<Enum[]> {
186
+ const query = `
187
+ select
188
+ t.table_name as tableName,
189
+ c.column_name as columnName,
190
+ TRIM(LEADING 'enum' FROM c.column_type) AS rawValues
191
+ from information_schema.tables t
192
+ join information_schema.columns c
193
+ on t.table_schema = c.table_schema and t.table_name = c.table_name
194
+ where
195
+ t.table_schema = ? and t.table_type = 'BASE TABLE' and c.data_type = 'enum'
196
+ order by
197
+ t.table_name, c.ordinal_position;
198
+ `;
199
+
200
+ const queryResponse = await client.query(query, [schemaName]);
201
+ const rows: any = queryResponse[0];
202
+
203
+ const enumList = rows.map((row: Record<string, any>) => {
204
+ const { tableName, columnName, rawValues } = row;
205
+
206
+ // i.e. ('value1','value2')
207
+ const valueList = rawValues
208
+ .slice(1, -1)
209
+ .split(',')
210
+ .map((value: string) => ({ name: value.slice(1, -1) }));
211
+
212
+ const enumName = getEnumName(tableName, columnName);
213
+
214
+ return {
215
+ name: enumName,
216
+ values: valueList,
217
+ };
218
+ });
219
+
220
+ return enumList;
221
+ }
222
+
223
+ /**
224
+ * Mysql is automatically create index for primary keys, foreign keys, unique constraint. -> Ignore
225
+ */
226
+ async function generateIndexes (client: Connection, schemaName: string): Promise<IndexesDictionary> {
227
+ const query = `
228
+ with
229
+ pk_fk_uniques as (
230
+ select constraint_name, table_name
231
+ from information_schema.table_constraints
232
+ where table_schema = ?
233
+ )
234
+ select
235
+ st.table_name as tableName,
236
+ case
237
+ when st.non_unique = 0 then true
238
+ else false
239
+ end as isIdxUnique,
240
+ st.index_name as idxName,
241
+ st.column_name as columnName,
242
+ st.sub_part as idxSubPart,
243
+ st.index_type as idxType,
244
+ replace(st.expression, '\`', '') as idxExpression
245
+ from information_schema.statistics st
246
+ where
247
+ st.table_schema = ?
248
+ and st.index_name not in (
249
+ select constraint_name
250
+ from pk_fk_uniques pfu
251
+ where pfu.table_name = st.table_name
252
+ )
253
+ and st.index_type in ('BTREE', 'HASH')
254
+ group by
255
+ st.table_name, st.non_unique, st.index_name, st.column_name, st.sub_part, st.index_type, st.expression
256
+ order by st.index_name;
257
+ `;
258
+
259
+ const queryResponse = await client.query(query, [schemaName, schemaName]);
260
+ const rows: any = queryResponse[0];
261
+
262
+ const tableIndexMap = rows.reduce((acc: Record<string, Record<string, Index>>, row: Record<string, any>) => {
263
+ const {
264
+ tableName, idxName, idxType, isIdxUnique, columnName, idxExpression, idxSubPart,
265
+ } = row;
266
+ const key = tableName;
267
+
268
+ if (!acc[key]) {
269
+ acc[key] = {};
270
+ }
271
+
272
+ if (!acc[key][idxName]) {
273
+ // init first index
274
+ acc[key][idxName] = {
275
+ name: idxName,
276
+ type: idxType,
277
+ columns: [],
278
+ unique: !!isIdxUnique,
279
+ };
280
+ }
281
+
282
+ const currentIndex = acc[key][idxName];
283
+ const column = getIndexColumn(columnName, idxExpression, idxSubPart);
284
+ if (column) {
285
+ currentIndex.columns.push(column);
286
+ }
287
+
288
+ return acc;
289
+ }, {});
290
+
291
+ const indexMap: IndexesDictionary = {};
292
+ Object.keys(tableIndexMap).forEach((tableName) => {
293
+ indexMap[tableName] = flatten(Object.values(tableIndexMap[tableName]));
294
+ });
295
+
296
+ return indexMap;
297
+ }
298
+
299
+ async function generatePrimaryAndUniqueConstraint (client: Connection, schemaName: string) {
300
+ const query = `
301
+ select
302
+ tc.table_name as tableName,
303
+ tc.constraint_name as constraintName,
304
+ group_concat(kcu.column_name order by kcu.ordinal_position separator ',') as columnNames,
305
+ count(kcu.column_name) as columnCount,
306
+ tc.constraint_type as constraintType
307
+ from information_schema.table_constraints tc
308
+ join information_schema.key_column_usage kcu
309
+ on
310
+ tc.constraint_schema = kcu.constraint_schema
311
+ and tc.constraint_name = kcu.constraint_name
312
+ and tc.table_name = kcu.table_name
313
+ where
314
+ (tc.constraint_type = 'PRIMARY KEY' or tc.constraint_type = 'UNIQUE')
315
+ and tc.table_schema = ?
316
+ group by tc.table_name, tc.constraint_name, tc.constraint_type
317
+ order by tc.table_name, tc.constraint_name;
318
+ `;
319
+
320
+ const queryResponse = await client.query(query, [schemaName]);
321
+ const rows: any = queryResponse[0];
322
+
323
+ const inlineConstraintList = rows.filter((constraint: Record<string, any>) => constraint.columnCount === 1);
324
+ const outOfConstraintList = rows.filter((constraint: Record<string, any>) => constraint.columnCount > 1);
325
+
326
+ const compositeTableConstraintMap = outOfConstraintList.reduce((acc: any, row: Record<string, any>) => {
327
+ const {
328
+ tableName, constraintName, columnNames, constraintType,
329
+ } = row;
330
+
331
+ const key = tableName;
332
+ if (!acc[key]) {
333
+ acc[key] = {};
334
+ }
335
+
336
+ if (!acc[key][constraintName]) {
337
+ acc[key][constraintName] = {
338
+ name: constraintName,
339
+ };
340
+ }
341
+
342
+ if (constraintType === 'PRIMARY KEY') {
343
+ acc[key][constraintName].primary = true;
344
+ }
345
+
346
+ if (constraintType === 'UNIQUE') {
347
+ acc[key][constraintName].unique = true;
348
+ }
349
+
350
+ const columnList = columnNames
351
+ .split(',')
352
+ .map((col: string) => ({ type: 'column', value: col }));
353
+ acc[key][constraintName].columns = columnList;
354
+
355
+ return acc;
356
+ }, {});
357
+
358
+ const compositeConstraintMap: any = {};
359
+ Object.keys(compositeTableConstraintMap).forEach((tableName) => {
360
+ compositeConstraintMap[tableName] = flatten(Object.values(compositeTableConstraintMap[tableName]));
361
+ });
362
+
363
+ const constraintMap = inlineConstraintList.reduce((acc: any, row: Record<string, any>) => {
364
+ const { tableName, columnNames, constraintType } = row;
365
+
366
+ const key = tableName;
367
+ if (!acc[key]) {
368
+ acc[key] = {};
369
+ }
370
+
371
+ const columnList = columnNames.split(',');
372
+
373
+ columnList.forEach((columnName: string) => {
374
+ if (!acc[key][columnName]) {
375
+ acc[key][columnName] = {};
376
+ }
377
+
378
+ if (constraintType === 'PRIMARY KEY') {
379
+ acc[key][columnName].pk = true;
380
+ }
381
+
382
+ if (constraintType === 'UNIQUE' && !acc[key][columnName].pk) {
383
+ acc[key][columnName].unique = true;
384
+ }
385
+ });
386
+ return acc;
387
+ }, {});
388
+
389
+ return { compositeConstraintMap, constraintMap };
390
+ }
391
+
392
+ async function generateForeignKeys (client: Connection, schemaName: string): Promise<Ref[]> {
393
+ const query = `
394
+ select
395
+ rc.constraint_name as constraintName,
396
+ rc.table_name as foreignTableName,
397
+ group_concat(kcu.column_name order by kcu.ordinal_position separator ',') as foreignColumnNames,
398
+ kcu.referenced_table_name as refTableName,
399
+ group_concat(kcu.referenced_column_name order by kcu.ordinal_position separator ',') as refColumnNames,
400
+ rc.update_rule as onUpdate,
401
+ rc.delete_rule as onDelete
402
+ from information_schema.referential_constraints rc
403
+ join information_schema.key_column_usage kcu
404
+ on
405
+ rc.constraint_name = kcu.constraint_name
406
+ and rc.table_name = kcu.table_name
407
+ and rc.constraint_schema = kcu.table_schema
408
+ where rc.constraint_schema = ?
409
+ group by
410
+ rc.constraint_name,
411
+ rc.table_name,
412
+ kcu.referenced_table_name,
413
+ rc.update_rule,
414
+ rc.delete_rule
415
+ order by
416
+ rc.table_name;
417
+ `;
418
+
419
+ const queryResponse = await client.query(query, [schemaName]);
420
+ const rows: any = queryResponse[0];
421
+
422
+ const foreignKeyList = rows.map((row: Record<string, any>) => {
423
+ const {
424
+ constraintName,
425
+ onDelete,
426
+ onUpdate,
427
+ foreignTableName,
428
+ foreignColumnNames,
429
+ refTableName,
430
+ refColumnNames,
431
+ } = row;
432
+
433
+ const endpoint1 = {
434
+ tableName: foreignTableName,
435
+ fieldNames: foreignColumnNames.split(','),
436
+ relation: '*',
437
+ };
438
+
439
+ const endpoint2 = {
440
+ tableName: refTableName,
441
+ fieldNames: refColumnNames.split(','),
442
+ relation: '1',
443
+ };
444
+
445
+ return {
446
+ name: constraintName,
447
+ endpoints: [endpoint1, endpoint2],
448
+ onDelete: onDelete === 'NO ACTION' ? null : onDelete,
449
+ onUpdate: onUpdate === 'NO ACTION' ? null : onUpdate,
450
+ };
451
+ });
452
+
453
+ return foreignKeyList;
454
+ }
455
+
456
+ function combineIndexAndCompositeConstraint (userDefinedIndexMap: any, compositeConstraintMap: any) {
457
+ const indexMap = Object.assign(userDefinedIndexMap, {});
458
+
459
+ Object.keys(compositeConstraintMap).forEach((tableName) => {
460
+ const compositeConstraint = compositeConstraintMap[tableName];
461
+ if (!indexMap[tableName]) {
462
+ indexMap[tableName] = compositeConstraint;
463
+ return;
464
+ }
465
+
466
+ indexMap[tableName].push(...compositeConstraint);
467
+ });
468
+
469
+ return indexMap;
470
+ }
471
+
472
+ async function fetchSchemaJson (connection: string): Promise<DatabaseSchema> {
473
+ const client = await connectMySQL(connection);
474
+
475
+ // In MySQL, a schema is equal database
476
+ const { database: schemaName } = client.config;
477
+ if (!schemaName) {
478
+ throw new Error('Cannot get schema name from the connection');
479
+ }
480
+
481
+ const result = await Promise.all([
482
+ generateTablesAndFields(client, schemaName),
483
+ generateEnums(client, schemaName),
484
+ generateIndexes(client, schemaName),
485
+ generatePrimaryAndUniqueConstraint(client, schemaName),
486
+ generateForeignKeys(client, schemaName),
487
+ ]);
488
+ client.end();
489
+
490
+ const [
491
+ { tableList, fieldMap },
492
+ enumList,
493
+ rawIndexMap,
494
+ { constraintMap, compositeConstraintMap },
495
+ foreignKeyList,
496
+ ] = result;
497
+
498
+ // combine normal index and composite key
499
+ const indexMap = combineIndexAndCompositeConstraint(rawIndexMap, compositeConstraintMap);
500
+
501
+ return {
502
+ tables: tableList,
503
+ fields: fieldMap,
504
+ refs: foreignKeyList,
505
+ enums: enumList,
506
+ indexes: indexMap,
507
+ tableConstraints: constraintMap,
508
+ };
509
+ }
510
+
511
+ export {
512
+ fetchSchemaJson,
513
+ };