@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,502 @@
1
+ /* eslint-disable camelcase */
2
+ import { Client } from 'pg';
3
+ import {
4
+ DatabaseSchema,
5
+ DefaultInfo,
6
+ DefaultType,
7
+ Enum,
8
+ Field,
9
+ FieldsDictionary,
10
+ IndexesDictionary,
11
+ Ref,
12
+ RefEndpoint,
13
+ Table,
14
+ TableConstraintsDictionary,
15
+ } from './types';
16
+
17
+ const getValidatedClient = async (connection: string): Promise<Client> => {
18
+ const client = new Client(connection);
19
+ try {
20
+ // Connect to the PostgreSQL server
21
+ await client.connect();
22
+
23
+ // Validate if the connection is successful by making a simple query
24
+ await client.query('SELECT 1');
25
+
26
+ // If successful, return the client
27
+ return client;
28
+ } catch (err) {
29
+ // Ensure to close the client in case of failure
30
+ await client.end();
31
+
32
+ if (err instanceof Error) {
33
+ throw new Error(`PostgreSQL connection error: ${err}`);
34
+ }
35
+
36
+ throw err; // Rethrow error if you want the calling code to handle it
37
+ }
38
+ };
39
+
40
+ const convertQueryBoolean = (val: string | null) => val === 'YES';
41
+
42
+ const getFieldType = (data_type: string, udt_name: string, character_maximum_length: number, numeric_precision: number, numeric_scale: number): string => {
43
+ if (data_type === 'ARRAY') {
44
+ return `${udt_name.slice(1, udt_name.length)}[]`;
45
+ }
46
+ if (character_maximum_length) {
47
+ return `${udt_name}(${character_maximum_length})`;
48
+ }
49
+ if (numeric_precision && numeric_scale) {
50
+ return `${udt_name}(${numeric_precision},${numeric_scale})`;
51
+ }
52
+ return udt_name;
53
+ };
54
+
55
+ const getDbdefault = (data_type: string, column_default: string, default_type: DefaultType): DefaultInfo => {
56
+ if (data_type === 'ARRAY') {
57
+ const values = column_default.slice(6, -1).split(',').map((value) => {
58
+ return value.split('::')[0];
59
+ });
60
+ return {
61
+ type: default_type,
62
+ value: `ARRAY[${values.join(', ')}]`,
63
+ };
64
+ }
65
+ if (default_type === 'string') {
66
+ const defaultValues = column_default.split('::')[0];
67
+ const isJson = data_type === 'json' || data_type === 'jsonb';
68
+ const type = isJson ? 'expression' : 'string';
69
+ return {
70
+ type,
71
+ value: defaultValues.slice(1, -1),
72
+ };
73
+ }
74
+ return {
75
+ type: default_type,
76
+ value: column_default,
77
+ };
78
+ };
79
+
80
+ const generateField = (row: Record<string, any>): Field => {
81
+ const {
82
+ column_name,
83
+ data_type,
84
+ character_maximum_length,
85
+ numeric_precision,
86
+ numeric_scale,
87
+ udt_schema,
88
+ udt_name,
89
+ identity_increment,
90
+ is_nullable,
91
+ column_default,
92
+ default_type,
93
+ column_comment,
94
+ } = row;
95
+
96
+ const dbdefault = column_default && default_type !== 'increment' ? getDbdefault(data_type, column_default, default_type) : null;
97
+
98
+ const fieldType = data_type === 'USER-DEFINED' ? {
99
+ type_name: udt_name,
100
+ schemaName: udt_schema,
101
+ } : {
102
+ type_name: getFieldType(data_type, udt_name, character_maximum_length, numeric_precision, numeric_scale),
103
+ schemaName: null,
104
+ };
105
+
106
+ return {
107
+ name: column_name,
108
+ type: fieldType,
109
+ dbdefault,
110
+ not_null: !convertQueryBoolean(is_nullable),
111
+ increment: !!identity_increment || default_type === 'increment',
112
+ note: column_comment ? { value: column_comment } : { value: '' },
113
+ };
114
+ };
115
+
116
+ const generateTablesAndFields = async (client: Client): Promise<{
117
+ tables: Table[],
118
+ fields: FieldsDictionary,
119
+ }> => {
120
+ const fields: FieldsDictionary = {};
121
+ const tablesAndFieldsSql = `
122
+ WITH comments AS (
123
+ SELECT
124
+ pc.relname AS table_name,
125
+ pn.nspname AS table_schema,
126
+ pa.attname AS column_name,
127
+ pd.description
128
+ FROM
129
+ pg_description pd
130
+ JOIN
131
+ pg_class pc ON pd.objoid = pc.oid
132
+ JOIN
133
+ pg_namespace pn ON pc.relnamespace = pn.oid
134
+ LEFT JOIN
135
+ pg_attribute pa ON pd.objoid = pa.attrelid AND pd.objsubid = pa.attnum
136
+ WHERE
137
+ pc.relkind = 'r'
138
+ AND pn.nspname NOT IN ('pg_catalog', 'information_schema')
139
+ )
140
+ SELECT
141
+ t.table_schema,
142
+ t.table_name,
143
+ c.column_name,
144
+ c.data_type,
145
+ c.character_maximum_length,
146
+ c.numeric_precision,
147
+ c.numeric_scale,
148
+ c.udt_schema,
149
+ c.udt_name,
150
+ c.identity_increment,
151
+ c.is_nullable,
152
+ c.column_default,
153
+ CASE
154
+ WHEN c.column_default IS NULL THEN NULL
155
+ WHEN c.column_default LIKE 'nextval(%' THEN 'increment'
156
+ WHEN c.column_default LIKE '''%' THEN 'string'
157
+ WHEN c.column_default = 'true' OR c.column_default = 'false' THEN 'boolean'
158
+ WHEN c.column_default ~ '^-?[0-9]+(.[0-9]+)?$' THEN 'number'
159
+ ELSE 'expression'
160
+ END AS default_type,
161
+ (SELECT description FROM comments WHERE table_name = t.table_name AND table_schema = t.table_schema AND column_name IS NULL) AS table_comment,
162
+ (SELECT description FROM comments WHERE table_name = t.table_name AND table_schema = t.table_schema AND column_name = c.column_name) AS column_comment
163
+ FROM
164
+ information_schema.columns c
165
+ JOIN
166
+ information_schema.tables t ON c.table_name = t.table_name AND c.table_schema = t.table_schema
167
+ WHERE
168
+ t.table_type = 'BASE TABLE'
169
+ AND t.table_schema NOT IN ('pg_catalog', 'information_schema')
170
+ ORDER BY
171
+ t.table_schema,
172
+ t.table_name,
173
+ c.ordinal_position
174
+ ;
175
+ `;
176
+
177
+ const tablesAndFieldsResult = await client.query(tablesAndFieldsSql);
178
+ const tables = tablesAndFieldsResult.rows.reduce((acc, row) => {
179
+ const { table_schema, table_name, table_comment } = row;
180
+ const key = `${table_schema}.${table_name}`;
181
+
182
+ if (!acc[key]) {
183
+ acc[key] = {
184
+ name: table_name,
185
+ schemaName: table_schema,
186
+ note: table_comment ? { value: table_comment } : { value: '' },
187
+ };
188
+ }
189
+
190
+ if (!fields[key]) fields[key] = [];
191
+ const field = generateField(row);
192
+ fields[key].push(field);
193
+
194
+ return acc;
195
+ }, {});
196
+
197
+ return {
198
+ tables: Object.values(tables),
199
+ fields,
200
+ };
201
+ };
202
+
203
+ const generateRawRefs = async (client: Client): Promise<Ref[]> => {
204
+ const refs: Ref[] = [];
205
+
206
+ const refsListSql = `
207
+ SELECT
208
+ tc.table_schema,
209
+ tc.table_name,
210
+ tc.constraint_name as fk_constraint_name,
211
+ STRING_AGG(DISTINCT kcu.column_name, ',') AS column_names,
212
+ ccu.table_schema AS foreign_table_schema,
213
+ ccu.table_name AS foreign_table_name,
214
+ STRING_AGG(DISTINCT ccu.column_name, ',') AS foreign_column_names,
215
+ tc.constraint_type,
216
+ rc.delete_rule AS on_delete,
217
+ rc.update_rule AS on_update
218
+ FROM information_schema.table_constraints AS tc
219
+ JOIN information_schema.key_column_usage AS kcu
220
+ ON tc.constraint_name = kcu.constraint_name
221
+ AND tc.table_schema = kcu.table_schema
222
+ JOIN information_schema.constraint_column_usage AS ccu
223
+ ON ccu.constraint_name = tc.constraint_name
224
+ JOIN information_schema.referential_constraints AS rc
225
+ ON tc.constraint_name = rc.constraint_name
226
+ AND tc.table_schema = rc.constraint_schema
227
+ WHERE tc.constraint_type = 'FOREIGN KEY'
228
+ AND tc.table_schema NOT IN ('pg_catalog', 'information_schema')
229
+ GROUP BY
230
+ tc.table_schema,
231
+ tc.table_name,
232
+ tc.constraint_name,
233
+ ccu.table_schema,
234
+ ccu.table_name,
235
+ tc.constraint_type,
236
+ rc.delete_rule,
237
+ rc.update_rule
238
+ ORDER BY
239
+ tc.table_schema,
240
+ tc.table_name;
241
+ `;
242
+
243
+ const refsQueryResult = await client.query(refsListSql);
244
+ refsQueryResult.rows.forEach((refRow) => {
245
+ const {
246
+ table_schema,
247
+ fk_constraint_name,
248
+ table_name,
249
+ column_names,
250
+ foreign_table_schema,
251
+ foreign_table_name,
252
+ foreign_column_names,
253
+ on_delete,
254
+ on_update,
255
+ } = refRow;
256
+
257
+ const ep1: RefEndpoint = {
258
+ tableName: table_name,
259
+ schemaName: table_schema,
260
+ fieldNames: column_names.split(','),
261
+ relation: '*',
262
+ };
263
+
264
+ const ep2: RefEndpoint = {
265
+ tableName: foreign_table_name,
266
+ schemaName: foreign_table_schema,
267
+ fieldNames: foreign_column_names.split(','),
268
+ relation: '1',
269
+ };
270
+
271
+ refs.push({
272
+ name: fk_constraint_name,
273
+ endpoints: [ep1, ep2],
274
+ onDelete: on_delete === 'NO ACTION' ? null : on_delete,
275
+ onUpdate: on_update === 'NO ACTION' ? null : on_update,
276
+ });
277
+ });
278
+
279
+ return refs;
280
+ };
281
+
282
+ const generateIndexes = async (client: Client) => {
283
+ // const tableConstraints = {};
284
+ const indexListSql = `
285
+ WITH user_tables AS (
286
+ SELECT
287
+ schemaname AS tableschema,
288
+ tablename
289
+ FROM pg_tables
290
+ WHERE schemaname NOT IN ('pg_catalog', 'information_schema') -- Exclude system schemas
291
+ AND tablename NOT LIKE 'pg_%' -- Exclude PostgreSQL system tables
292
+ AND tablename NOT LIKE 'sql_%' -- Exclude SQL standard tables
293
+ ),
294
+ index_info AS (
295
+ SELECT
296
+ t.relname AS table_name,
297
+ i.relname AS index_name,
298
+ ix.indisunique AS is_unique,
299
+ ix.indisprimary AS is_primary,
300
+ am.amname AS index_type,
301
+ array_to_string(array_agg(a.attname ORDER BY x.n), ', ') AS columns,
302
+ pg_get_expr(ix.indexprs, ix.indrelid) AS expressions,
303
+ CASE
304
+ WHEN ix.indisprimary THEN 'PRIMARY KEY'
305
+ WHEN ix.indisunique THEN 'UNIQUE'
306
+ ELSE NULL
307
+ END AS constraint_type
308
+ FROM
309
+ pg_class t
310
+ JOIN pg_index ix ON t.oid = ix.indrelid
311
+ JOIN pg_class i ON i.oid = ix.indexrelid
312
+ LEFT JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey)
313
+ JOIN pg_am am ON i.relam = am.oid
314
+ LEFT JOIN generate_subscripts(ix.indkey, 1) AS x(n) ON a.attnum = ix.indkey[x.n]
315
+ WHERE
316
+ t.relkind = 'r'
317
+ AND t.relname NOT LIKE 'pg_%'
318
+ AND t.relname NOT LIKE 'sql_%'
319
+ GROUP BY
320
+ t.relname, i.relname, ix.indisunique, ix.indisprimary, am.amname, ix.indexprs, ix.indrelid
321
+ )
322
+ SELECT
323
+ ut.tableschema AS table_schema,
324
+ ut.tablename AS table_name,
325
+ ii.index_name,
326
+ ii.is_unique,
327
+ ii.is_primary,
328
+ ii.index_type,
329
+ ii.columns,
330
+ ii.expressions,
331
+ ii.constraint_type -- Added constraint type
332
+ FROM
333
+ user_tables ut
334
+ LEFT JOIN
335
+ index_info ii ON ut.tablename = ii.table_name
336
+ WHERE ii.columns IS NOT NULL
337
+ ORDER BY
338
+ ut.tablename,
339
+ ii.constraint_type,
340
+ ii.index_name
341
+ ;
342
+ `;
343
+ const indexListResult = await client.query(indexListSql);
344
+ const { outOfLineConstraints, inlineConstraints } = indexListResult.rows.reduce((acc, row) => {
345
+ const { constraint_type, columns } = row;
346
+
347
+ if (columns === 'null' || columns.trim() === '') return acc;
348
+
349
+ const isSingleColumn = columns.split(',').length === 1;
350
+ const isInlineConstraint = isSingleColumn && (constraint_type === 'PRIMARY KEY' || constraint_type === 'UNIQUE');
351
+ if (isInlineConstraint) {
352
+ acc.inlineConstraints.push(row);
353
+ } else {
354
+ acc.outOfLineConstraints.push(row);
355
+ }
356
+ return acc;
357
+ }, { outOfLineConstraints: [], inlineConstraints: [] });
358
+
359
+ const indexes = outOfLineConstraints.reduce((acc: IndexesDictionary, indexRow: Record<string, any>) => {
360
+ const {
361
+ table_schema,
362
+ table_name,
363
+ index_name,
364
+ index_type,
365
+ columns,
366
+ expressions,
367
+ } = indexRow;
368
+ const indexColumns = columns.split(',').map((column: string) => {
369
+ return {
370
+ type: 'column',
371
+ value: column.trim(),
372
+ };
373
+ });
374
+
375
+ const indexExpressions = expressions ? expressions.split(',').map((expression: string) => {
376
+ return {
377
+ type: 'expression',
378
+ value: expression,
379
+ };
380
+ }) : [];
381
+
382
+ const index = {
383
+ name: index_name,
384
+ type: index_type,
385
+ columns: [
386
+ ...indexColumns,
387
+ ...indexExpressions,
388
+ ],
389
+ };
390
+
391
+ const key = `${table_schema}.${table_name}`;
392
+ if (acc[key]) {
393
+ acc[key].push(index);
394
+ } else {
395
+ acc[key] = [index];
396
+ }
397
+
398
+ return acc;
399
+ }, {});
400
+
401
+ const tableConstraints = inlineConstraints.reduce((acc: TableConstraintsDictionary, row: Record<string, any>) => {
402
+ const {
403
+ table_schema,
404
+ table_name,
405
+ columns,
406
+ constraint_type,
407
+ } = row;
408
+ const key = `${table_schema}.${table_name}`;
409
+ if (!acc[key]) acc[key] = {};
410
+
411
+ const columnNames = columns.split(',').map((column: string) => column.trim());
412
+ columnNames.forEach((columnName: string) => {
413
+ if (!acc[key][columnName]) acc[key][columnName] = {};
414
+ if (constraint_type === 'PRIMARY KEY') {
415
+ acc[key][columnName].pk = true;
416
+ }
417
+ if (constraint_type === 'UNIQUE' && !acc[key][columnName].pk) {
418
+ acc[key][columnName].unique = true;
419
+ }
420
+ });
421
+ return acc;
422
+ }, {});
423
+
424
+ return {
425
+ indexes,
426
+ tableConstraints,
427
+ };
428
+ };
429
+
430
+ const generateRawEnums = async (client: Client): Promise<Enum[]> => {
431
+ const enumListSql = `
432
+ SELECT
433
+ n.nspname AS schema_name,
434
+ t.typname AS enum_type,
435
+ e.enumlabel AS enum_value,
436
+ e.enumsortorder AS sort_order
437
+ FROM
438
+ pg_enum e
439
+ JOIN
440
+ pg_type t ON e.enumtypid = t.oid
441
+ JOIN
442
+ pg_namespace n ON t.typnamespace = n.oid
443
+ ORDER BY
444
+ schema_name,
445
+ enum_type,
446
+ sort_order;
447
+ ;
448
+ `;
449
+ const enumListResult = await client.query(enumListSql);
450
+ const enums = enumListResult.rows.reduce((acc, row) => {
451
+ const { schema_name, enum_type, enum_value } = row;
452
+
453
+ if (!acc[enum_type]) {
454
+ acc[enum_type] = {
455
+ name: enum_type,
456
+ schemaName: schema_name,
457
+ values: [],
458
+ };
459
+ }
460
+ acc[enum_type].values.push({
461
+ name: enum_value,
462
+ });
463
+ return acc;
464
+ }, {});
465
+
466
+ return Object.values(enums);
467
+ };
468
+
469
+ const fetchSchemaJson = async (connection: string): Promise<DatabaseSchema> => {
470
+ const client = await getValidatedClient(connection);
471
+
472
+ const tablesAndFieldsRes = generateTablesAndFields(client);
473
+ const indexesRes = generateIndexes(client);
474
+ const refsRes = generateRawRefs(client);
475
+ const enumsRes = generateRawEnums(client);
476
+
477
+ const res = await Promise.all([
478
+ tablesAndFieldsRes,
479
+ indexesRes,
480
+ refsRes,
481
+ enumsRes,
482
+ ]);
483
+ client.end();
484
+
485
+ const { tables, fields } = res[0];
486
+ const { indexes, tableConstraints } = res[1];
487
+ const refs = res[2];
488
+ const enums = res[3];
489
+
490
+ return {
491
+ tables,
492
+ fields,
493
+ refs,
494
+ enums,
495
+ indexes,
496
+ tableConstraints,
497
+ };
498
+ };
499
+
500
+ export {
501
+ fetchSchemaJson,
502
+ };
@@ -0,0 +1,116 @@
1
+ interface NoteInfo {
2
+ value: string;
3
+ }
4
+
5
+ interface TypeInfo {
6
+ type_name: string;
7
+ schemaName: string | null;
8
+ }
9
+
10
+
11
+ type DefaultType = 'string' | 'number' | 'boolean' | 'expression';
12
+
13
+ interface DefaultInfo {
14
+ type: DefaultType,
15
+ value: string;
16
+ }
17
+
18
+ interface Field {
19
+ name: string;
20
+ type: TypeInfo;
21
+ dbdefault: DefaultInfo | null;
22
+ not_null: boolean;
23
+ increment: boolean;
24
+ note: NoteInfo;
25
+ }
26
+
27
+ interface Table {
28
+ name: string;
29
+ schemaName: string;
30
+ note: NoteInfo;
31
+ }
32
+
33
+ interface FieldsDictionary {
34
+ [key: string]: Field[]; // Represents fields within tables, indexed by schemaName.tableName
35
+ }
36
+
37
+ interface TableConstraint {
38
+ [fieldName: string]: {
39
+ pk?: boolean;
40
+ unique?: boolean;
41
+ };
42
+ }
43
+
44
+ interface TableConstraintsDictionary {
45
+ [tableIdentifier: string]: TableConstraint; // Represents constraints within tables, indexed by schemaName.tableName
46
+ }
47
+
48
+ interface RefEndpoint {
49
+ tableName: string; // Parent table or child table
50
+ schemaName: string;
51
+ fieldNames: string[]; // The parent fields or the child fields (foreign key fields)
52
+ relation: '*' | '1'; // The parent endpoint is '*' and the child endpoint is '1'
53
+ }
54
+
55
+ interface Ref {
56
+ name: string;
57
+ endpoints: RefEndpoint[]; // The first endpoint is the parent table and the second endpoint is the child table.
58
+ onDelete: string | null;
59
+ onUpdate: string | null;
60
+ }
61
+
62
+ interface EnumValue {
63
+ name: string;
64
+ }
65
+
66
+ interface Enum {
67
+ name: string;
68
+ schemaName: string;
69
+ values: EnumValue[];
70
+ }
71
+
72
+ interface IndexColumn {
73
+ type: 'column' | 'expression';
74
+ value: string;
75
+ }
76
+
77
+ interface Index {
78
+ name: string;
79
+ type: string;
80
+ unique?: boolean,
81
+ pk?: boolean,
82
+ columns: IndexColumn[];
83
+ }
84
+
85
+ interface IndexesDictionary {
86
+ [tableIdentifier: string]: Index[]; // Represents indexes within tables, indexed by schemaName.tableName
87
+ }
88
+
89
+ interface DatabaseSchema {
90
+ tables: Table[];
91
+ fields: FieldsDictionary;
92
+ enums: Enum[];
93
+ tableConstraints: TableConstraintsDictionary;
94
+ refs: Ref[];
95
+ indexes: IndexesDictionary; // Represents the indexes property
96
+ }
97
+
98
+ export {
99
+ NoteInfo,
100
+ TypeInfo,
101
+ DefaultType,
102
+ DefaultInfo,
103
+ Field,
104
+ Table,
105
+ FieldsDictionary,
106
+ TableConstraint,
107
+ TableConstraintsDictionary,
108
+ RefEndpoint,
109
+ Ref,
110
+ EnumValue,
111
+ Enum,
112
+ IndexColumn,
113
+ Index,
114
+ IndexesDictionary,
115
+ DatabaseSchema,
116
+ };
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ import * as connector from "./connectors/connector";
2
+
3
+ export {
4
+ connector,
5
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2021",
4
+ "lib": ["ES2021"],
5
+ "module": "commonjs",
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "declaration": true, // Generates .d.ts files
13
+ },
14
+ "include": ["./src"],
15
+ "exclude": ["node_modules", "**/*.test.ts"]
16
+ }