@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,523 @@
1
+ /* eslint-disable camelcase */
2
+ import sql from 'mssql';
3
+ import {
4
+ Field,
5
+ DefaultType,
6
+ FieldsDictionary,
7
+ Enum,
8
+ Ref,
9
+ RefEndpoint,
10
+ Table,
11
+ IndexesDictionary,
12
+ EnumValue,
13
+ TableConstraintsDictionary,
14
+ DatabaseSchema,
15
+ DefaultInfo,
16
+ } from './types';
17
+
18
+ const MSSQL_DATE_TYPES = [
19
+ 'date',
20
+ 'datetime',
21
+ 'datetime2',
22
+ 'smalldatetime',
23
+ 'datetimeoffset',
24
+ 'time',
25
+ ];
26
+
27
+ const getValidatedClient = async (connection: string): Promise<sql.ConnectionPool> => {
28
+ const pool = await sql.connect(connection);
29
+ try {
30
+ // Establish a connection pool
31
+ // Validate if the connection is successful by making a simple query
32
+ await pool.request().query('SELECT 1');
33
+
34
+ // If successful, return the pool
35
+ return pool;
36
+ } catch (err) {
37
+ // Ensure to close any open pool in case of failure
38
+ if (pool.connected) {
39
+ await pool.close();
40
+ }
41
+ if (err instanceof Error) {
42
+ throw new Error(`SQL connection error: ${err.message}`);
43
+ }
44
+
45
+ throw err; // Rethrow error if you want the calling code to handle it
46
+ }
47
+ };
48
+
49
+ const convertQueryBoolean = (val: string | null): boolean => val === 'YES';
50
+
51
+ const getFieldType = (data_type: string, default_type: DefaultType, character_maximum_length: number, numeric_precision: number, numeric_scale: number): string => {
52
+ if (MSSQL_DATE_TYPES.includes(data_type)) {
53
+ return data_type;
54
+ }
55
+ if (data_type === 'bit') {
56
+ return data_type;
57
+ }
58
+ if (numeric_precision && numeric_scale && default_type === 'number') {
59
+ return `${data_type}(${numeric_precision},${numeric_scale})`;
60
+ }
61
+ if (character_maximum_length && character_maximum_length > 0 && default_type === 'string') {
62
+ return `${data_type}(${character_maximum_length})`;
63
+ }
64
+ return data_type;
65
+ };
66
+
67
+ const getDbdefault = (data_type: string, column_default: string, default_type: DefaultType): DefaultInfo => {
68
+ // The regex below is used to extract the value from the default value
69
+ // \( and \) are used to escape parentheses
70
+ // [^()]+ is used to match any character except parentheses
71
+ // Example: (1) => 1, ('hello') => hello, getdate()-(1) => getdate()-1
72
+ const value = column_default.slice(1, -1).replace(/\(([^()]+)\)/g, '$1');
73
+
74
+ return {
75
+ type: default_type,
76
+ value: default_type === 'string' ? value.slice(1, -1) : value, // Remove the quotes for string values
77
+ };
78
+ };
79
+
80
+ const getEnumValues = (definition: string): EnumValue[] | null => {
81
+ // Use the example below to understand the regex:
82
+ // ([quantity]>(0))
83
+ // ([unit_price]>(0))
84
+ // ([status]='cancelled' OR [status]='delivered' OR [status]='shipped' OR [status]='processing' OR [status]='pending')
85
+ // ([total_amount]>(0))
86
+ // ([price]>(0))
87
+ // ([stock_quantity]>=(0))
88
+ // ([age_start]<=[age_end])
89
+ // ([age_start]<=[age_end])
90
+ // ([gender]='Other' OR [gender]='Female' OR [gender]='Male')
91
+ // ([date_of_birth]<=dateadd(year,(-13),getdate()))
92
+ // ([email] like '%_@_%._%')
93
+ if (!definition) return null;
94
+ const values = definition.match(/\[([^\]]+)\]='([^']+)'/g); // Extracting the enum values when the definition contains ]='
95
+ if (!values) return null;
96
+
97
+ const enumValues = values.map((value) => {
98
+ const enumValue = value.split("]='")[1];
99
+ return { name: enumValue.slice(0, -1) };
100
+ });
101
+ return enumValues;
102
+ };
103
+
104
+ const generateField = (row: Record<string, any>): Field => {
105
+ const {
106
+ column_name,
107
+ data_type,
108
+ character_maximum_length,
109
+ numeric_precision,
110
+ numeric_scale,
111
+ identity_increment,
112
+ is_nullable,
113
+ column_default,
114
+ default_type,
115
+ column_comment,
116
+ } = row;
117
+
118
+ const dbdefault = column_default && default_type !== 'increment' ? getDbdefault(data_type, column_default, default_type) : null;
119
+
120
+ const fieldType = {
121
+ type_name: getFieldType(data_type, default_type, character_maximum_length, numeric_precision, numeric_scale),
122
+ schemaName: null,
123
+ };
124
+
125
+ return {
126
+ name: column_name,
127
+ type: fieldType,
128
+ dbdefault,
129
+ not_null: !convertQueryBoolean(is_nullable),
130
+ increment: !!identity_increment,
131
+ note: column_comment ? { value: column_comment } : { value: '' },
132
+ };
133
+ };
134
+
135
+ const generateTablesFieldsAndEnums = async (client: sql.ConnectionPool): Promise<{
136
+ tables: Table[],
137
+ fields: FieldsDictionary,
138
+ enums: Enum[],
139
+ }> => {
140
+ const fields: FieldsDictionary = {};
141
+ const enums: Enum[] = [];
142
+ const tablesAndFieldsSql = `
143
+ WITH tables_and_fields AS (
144
+ SELECT
145
+ s.name AS table_schema,
146
+ t.name AS table_name,
147
+ c.name AS column_name,
148
+ ty.name AS data_type,
149
+ c.max_length AS character_maximum_length,
150
+ c.precision AS numeric_precision,
151
+ c.scale AS numeric_scale,
152
+ c.is_identity AS identity_increment,
153
+ CASE
154
+ WHEN c.is_nullable = 1 THEN 'YES'
155
+ ELSE 'NO'
156
+ END AS is_nullable,
157
+ CASE
158
+ WHEN c.default_object_id = 0 THEN NULL
159
+ ELSE OBJECT_DEFINITION(c.default_object_id)
160
+ END AS column_default,
161
+ -- Fetching table comments
162
+ p.value AS table_comment,
163
+ ep.value AS column_comment
164
+ FROM
165
+ sys.tables t
166
+ JOIN
167
+ sys.schemas s ON t.schema_id = s.schema_id
168
+ JOIN
169
+ sys.columns c ON t.object_id = c.object_id
170
+ JOIN
171
+ sys.types ty ON c.user_type_id = ty.user_type_id
172
+ LEFT JOIN
173
+ sys.extended_properties p ON p.major_id = t.object_id
174
+ AND p.name = 'MS_Description'
175
+ AND p.minor_id = 0 -- Ensure minor_id is 0 for table comments
176
+ LEFT JOIN
177
+ sys.extended_properties ep ON ep.major_id = c.object_id
178
+ AND ep.minor_id = c.column_id
179
+ AND ep.name = 'MS_Description'
180
+ WHERE
181
+ t.type = 'U' -- User-defined tables
182
+ )
183
+ SELECT
184
+ tf.table_schema,
185
+ tf.table_name,
186
+ tf.column_name,
187
+ tf.data_type,
188
+ tf.character_maximum_length,
189
+ tf.numeric_precision,
190
+ tf.numeric_scale,
191
+ tf.identity_increment,
192
+ tf.is_nullable,
193
+ tf.column_default,
194
+ tf.table_comment,
195
+ tf.column_comment,
196
+ cc.name AS check_constraint_name, -- Adding CHECK constraint name
197
+ cc.definition AS check_constraint_definition, -- Adding CHECK constraint definition
198
+ CASE
199
+ WHEN tf.column_default LIKE '((%))' THEN 'number'
200
+ WHEN tf.column_default LIKE '(''%'')' THEN 'string'
201
+ ELSE 'expression'
202
+ END AS default_type
203
+ FROM
204
+ tables_and_fields AS tf
205
+ LEFT JOIN
206
+ sys.check_constraints cc ON cc.parent_object_id = OBJECT_ID(tf.table_schema + '.' + tf.table_name)
207
+ AND cc.definition LIKE '%' + tf.column_name + '%' -- Ensure the constraint references the column
208
+ ORDER BY
209
+ tf.table_schema,
210
+ tf.table_name,
211
+ tf.column_name;
212
+ `;
213
+
214
+ const tablesAndFieldsResult = await client.query(tablesAndFieldsSql);
215
+ const tables = tablesAndFieldsResult.recordset.reduce((acc, row) => {
216
+ const {
217
+ table_schema,
218
+ table_name,
219
+ table_comment,
220
+ check_constraint_name,
221
+ check_constraint_definition,
222
+ } = row;
223
+ const key = `${table_schema}.${table_name}`;
224
+
225
+ if (!acc[key]) {
226
+ acc[key] = {
227
+ name: table_name,
228
+ schemaName: table_schema,
229
+ note: table_comment ? { value: table_comment } : { value: '' },
230
+ };
231
+ }
232
+
233
+ const enumValues = getEnumValues(check_constraint_definition);
234
+ if (enumValues) {
235
+ enums.push({
236
+ name: check_constraint_name,
237
+ schemaName: table_schema,
238
+ values: enumValues,
239
+ });
240
+ }
241
+
242
+ if (!fields[key]) fields[key] = [];
243
+ const field = generateField(row);
244
+ if (enumValues) {
245
+ field.type = {
246
+ type_name: check_constraint_name,
247
+ schemaName: table_schema,
248
+ };
249
+ }
250
+ fields[key].push(field);
251
+
252
+ return acc;
253
+ }, {});
254
+
255
+ return {
256
+ tables: Object.values(tables),
257
+ fields,
258
+ enums,
259
+ };
260
+ };
261
+
262
+ const generateRefs = async (client: sql.ConnectionPool): Promise<Ref[]> => {
263
+ const refs: Ref[] = [];
264
+
265
+ const refsListSql = `
266
+ SELECT
267
+ s.name AS table_schema,
268
+ t.name AS table_name,
269
+ fk.name AS fk_constraint_name,
270
+ STUFF((
271
+ SELECT ',' + c1.name
272
+ FROM sys.foreign_key_columns AS fkc
273
+ JOIN sys.columns AS c1 ON fkc.parent_object_id = c1.object_id AND fkc.parent_column_id = c1.column_id
274
+ WHERE fkc.constraint_object_id = fk.object_id
275
+ FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 1, '') AS column_names,
276
+ s2.name AS foreign_table_schema,
277
+ t2.name AS foreign_table_name,
278
+ STUFF((
279
+ SELECT ',' + c2.name
280
+ FROM sys.foreign_key_columns AS fkc
281
+ JOIN sys.columns AS c2 ON fkc.referenced_object_id = c2.object_id AND fkc.referenced_column_id = c2.column_id
282
+ WHERE fkc.constraint_object_id = fk.object_id
283
+ FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 1, '') AS foreign_column_names,
284
+ fk.type_desc AS constraint_type,
285
+ fk.delete_referential_action_desc AS on_delete,
286
+ fk.update_referential_action_desc AS on_update
287
+ FROM sys.foreign_keys AS fk
288
+ JOIN sys.tables AS t ON fk.parent_object_id = t.object_id
289
+ JOIN sys.schemas AS s ON t.schema_id = s.schema_id
290
+ JOIN sys.tables AS t2 ON fk.referenced_object_id = t2.object_id
291
+ JOIN sys.schemas AS s2 ON t2.schema_id = s2.schema_id
292
+ WHERE s.name NOT IN ('sys', 'information_schema')
293
+ ORDER BY
294
+ s.name,
295
+ t.name;
296
+ `;
297
+
298
+ const refsQueryResult = await client.query(refsListSql);
299
+ refsQueryResult.recordset.forEach((refRow) => {
300
+ const {
301
+ table_schema,
302
+ fk_constraint_name,
303
+ table_name,
304
+ column_names,
305
+ foreign_table_schema,
306
+ foreign_table_name,
307
+ foreign_column_names,
308
+ on_delete,
309
+ on_update,
310
+ } = refRow;
311
+
312
+ const ep1: RefEndpoint = {
313
+ tableName: table_name,
314
+ schemaName: table_schema,
315
+ fieldNames: column_names.split(','),
316
+ relation: '*',
317
+ };
318
+
319
+ const ep2: RefEndpoint = {
320
+ tableName: foreign_table_name,
321
+ schemaName: foreign_table_schema,
322
+ fieldNames: foreign_column_names.split(','),
323
+ relation: '1',
324
+ };
325
+
326
+ refs.push({
327
+ name: fk_constraint_name,
328
+ endpoints: [ep1, ep2],
329
+ onDelete: on_delete === 'NO_ACTION' ? null : on_delete,
330
+ onUpdate: on_update === 'NO_ACTION' ? null : on_update,
331
+ });
332
+ });
333
+
334
+ return refs;
335
+ };
336
+
337
+ const generateIndexes = async (client: sql.ConnectionPool) => {
338
+ const indexListSql = `
339
+ WITH user_tables AS (
340
+ SELECT
341
+ TABLE_SCHEMA,
342
+ TABLE_NAME
343
+ FROM
344
+ INFORMATION_SCHEMA.TABLES
345
+ WHERE
346
+ TABLE_TYPE = 'BASE TABLE' -- Ensure we are only getting base tables
347
+ AND TABLE_NAME NOT LIKE 'dt%'
348
+ AND TABLE_NAME NOT LIKE 'syscs%'
349
+ AND TABLE_NAME NOT LIKE 'sysss%'
350
+ AND TABLE_NAME NOT LIKE 'sysrs%'
351
+ AND TABLE_NAME NOT LIKE 'sysxlgc%'
352
+ ),
353
+ index_info AS (
354
+ SELECT
355
+ OBJECT_NAME(i.object_id) AS table_name,
356
+ i.name AS index_name,
357
+ i.is_unique,
358
+ CASE
359
+ WHEN i.type = 1 THEN 1
360
+ ELSE 0
361
+ END AS is_primary,
362
+ i.type_desc AS index_type,
363
+ STUFF((
364
+ SELECT
365
+ ', ' + c.name
366
+ FROM
367
+ sys.index_columns ic
368
+ JOIN sys.columns c ON ic.column_id = c.column_id AND ic.object_id = c.object_id
369
+ WHERE
370
+ ic.index_id = i.index_id
371
+ AND ic.object_id = i.object_id
372
+ AND OBJECT_NAME(ic.object_id) IN (SELECT TABLE_NAME FROM user_tables) -- Filter for user tables
373
+ ORDER BY
374
+ ic.key_ordinal
375
+ FOR XML PATH('')
376
+ ), 1, 2, '') AS columns,
377
+ CASE
378
+ WHEN i.type = 1 THEN 'PRIMARY KEY'
379
+ WHEN i.is_unique = 1 THEN 'UNIQUE'
380
+ ELSE NULL
381
+ END AS constraint_type
382
+ FROM
383
+ sys.indexes i
384
+ JOIN sys.tables t ON i.object_id = t.object_id
385
+ WHERE
386
+ t.is_ms_shipped = 0
387
+ AND i.type <> 0
388
+ )
389
+ SELECT
390
+ ut.TABLE_SCHEMA AS table_schema,
391
+ ut.TABLE_NAME AS table_name,
392
+ ii.index_name,
393
+ ii.is_unique,
394
+ ii.is_primary,
395
+ ii.index_type,
396
+ ii.columns,
397
+ ii.constraint_type
398
+ FROM
399
+ user_tables ut
400
+ LEFT JOIN
401
+ index_info ii ON ut.TABLE_NAME = ii.table_name
402
+ WHERE
403
+ ii.columns IS NOT NULL
404
+ ORDER BY
405
+ ut.TABLE_NAME,
406
+ ii.constraint_type,
407
+ ii.index_name;
408
+ `;
409
+ const indexListResult = await client.query(indexListSql);
410
+ const { outOfLineConstraints, inlineConstraints } = indexListResult.recordset.reduce((acc, row) => {
411
+ const { constraint_type, columns } = row;
412
+
413
+ if (columns === 'null' || columns.trim() === '') return acc;
414
+ if (constraint_type === 'PRIMARY KEY' || constraint_type === 'UNIQUE') {
415
+ acc.inlineConstraints.push(row);
416
+ } else {
417
+ acc.outOfLineConstraints.push(row);
418
+ }
419
+ return acc;
420
+ }, { outOfLineConstraints: [], inlineConstraints: [] });
421
+
422
+ const indexes = outOfLineConstraints.reduce((acc: IndexesDictionary, indexRow: Record<string, any>) => {
423
+ const {
424
+ table_schema,
425
+ table_name,
426
+ index_name,
427
+ index_type,
428
+ columns,
429
+ expressions,
430
+ } = indexRow;
431
+ const indexColumns = columns.split(',').map((column: string) => {
432
+ return {
433
+ type: 'column',
434
+ value: column.trim(),
435
+ };
436
+ });
437
+
438
+ const indexExpressions = expressions ? expressions.split(',').map((expression: string) => {
439
+ return {
440
+ type: 'expression',
441
+ value: expression,
442
+ };
443
+ }) : [];
444
+
445
+ const index = {
446
+ name: index_name,
447
+ type: index_type,
448
+ columns: [
449
+ ...indexColumns,
450
+ ...indexExpressions,
451
+ ],
452
+ };
453
+
454
+ const key = `${table_schema}.${table_name}`;
455
+ if (acc[key]) {
456
+ acc[key].push(index);
457
+ } else {
458
+ acc[key] = [index];
459
+ }
460
+
461
+ return acc;
462
+ }, {});
463
+
464
+ const tableConstraints = inlineConstraints.reduce((acc: TableConstraintsDictionary, row: Record<string, any>) => {
465
+ const {
466
+ table_schema,
467
+ table_name,
468
+ columns,
469
+ constraint_type,
470
+ } = row;
471
+ const key = `${table_schema}.${table_name}`;
472
+ if (!acc[key]) acc[key] = {};
473
+
474
+ const columnNames = columns.split(',').map((column: string) => column.trim());
475
+ columnNames.forEach((columnName: string) => {
476
+ if (!acc[key][columnName]) acc[key][columnName] = {};
477
+ if (constraint_type === 'PRIMARY KEY') {
478
+ acc[key][columnName].pk = true;
479
+ }
480
+ if (constraint_type === 'UNIQUE' && !acc[key][columnName].pk) {
481
+ acc[key][columnName].unique = true;
482
+ }
483
+ });
484
+ return acc;
485
+ }, {});
486
+
487
+ return {
488
+ indexes,
489
+ tableConstraints,
490
+ };
491
+ };
492
+
493
+ const fetchSchemaJson = async (connection: string): Promise<DatabaseSchema> => {
494
+ const client = await getValidatedClient(connection);
495
+
496
+ const tablesFieldsAndEnumsRes = generateTablesFieldsAndEnums(client);
497
+ const indexesRes = generateIndexes(client);
498
+ const refsRes = generateRefs(client);
499
+
500
+ const res = await Promise.all([
501
+ tablesFieldsAndEnumsRes,
502
+ indexesRes,
503
+ refsRes,
504
+ ]);
505
+ client.close();
506
+
507
+ const { tables, fields, enums } = res[0];
508
+ const { indexes, tableConstraints } = res[1];
509
+ const refs = res[2];
510
+
511
+ return {
512
+ tables,
513
+ fields,
514
+ enums,
515
+ refs,
516
+ indexes,
517
+ tableConstraints,
518
+ };
519
+ };
520
+
521
+ export {
522
+ fetchSchemaJson,
523
+ };