@dbml/connector 3.7.4 → 3.8.0

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,292 @@
1
+ "use strict";
2
+ /* eslint-disable camelcase */
3
+ //
4
+ // Description:
5
+ // Snowflake info schema lacks reference direction for relationships.
6
+ // Constraints and uniqueness cannot be auto-generated from Snowflake info schema.
7
+ // Snowflake does not support ENUMs.
8
+ // Snowflake does not support indexes for standard tables.
9
+ //
10
+ var __importDefault = (this && this.__importDefault) || function (mod) {
11
+ return (mod && mod.__esModule) ? mod : { "default": mod };
12
+ };
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.fetchSchemaJson = void 0;
15
+ const snowflake_sdk_1 = __importDefault(require("snowflake-sdk"));
16
+ const parseSchema_1 = require("../utils/parseSchema");
17
+ const parseConnectionString = (connectionString) => {
18
+ const params = {};
19
+ const regex = /([^;=]+)=([^;]*)/g;
20
+ let match;
21
+ while ((match = regex.exec(connectionString)) !== null) {
22
+ params[match[1].trim()] = match[2].trim();
23
+ }
24
+ return params;
25
+ };
26
+ const connect = async (connection) => {
27
+ return new Promise((resolve, reject) => {
28
+ console.log('Attempting to connect...');
29
+ connection.connect((err) => {
30
+ if (err) {
31
+ reject(err);
32
+ }
33
+ else {
34
+ resolve();
35
+ }
36
+ });
37
+ });
38
+ };
39
+ const executeQuery = (connection, sqlText) => {
40
+ return new Promise((resolve, reject) => {
41
+ console.log('Executing query...');
42
+ connection.execute({
43
+ sqlText: sqlText,
44
+ complete: (err, stmt, rows) => {
45
+ if (err) {
46
+ reject(err);
47
+ }
48
+ else {
49
+ console.log('Query executed successfully.');
50
+ if (rows) {
51
+ resolve(rows);
52
+ }
53
+ }
54
+ }
55
+ });
56
+ });
57
+ };
58
+ const isLogLevel = (value) => {
59
+ return ['ERROR', 'WARN', 'INFO', 'DEBUG', 'TRACE'].includes(value);
60
+ };
61
+ const connectToSnowflake = async (config) => {
62
+ const logLevel = isLogLevel(config.LOG_LEVEL) ? config.LOG_LEVEL : 'INFO';
63
+ const isDebugMode = config.IS_DEBUG_MODE === 'true';
64
+ snowflake_sdk_1.default.configure({
65
+ logLevel,
66
+ additionalLogToConsole: isDebugMode,
67
+ });
68
+ // "SERVER=myaccount.snowflakecomputing.com;UID=myusername;PWD=mypassword;DATABASE=mydatabase;WAREHOUSE=mywarehouse;ROLE=myrole";
69
+ const connection = snowflake_sdk_1.default.createConnection({
70
+ account: config.SERVER,
71
+ username: config.UID,
72
+ password: config.PWD,
73
+ database: config.DATABASE,
74
+ warehouse: config.WAREHOUSE,
75
+ sfRetryMaxLoginRetries: 3,
76
+ timeout: 10000,
77
+ });
78
+ try {
79
+ // Connect to Snowflake
80
+ await connect(connection);
81
+ // Execute the query
82
+ await executeQuery(connection, 'SELECT CURRENT_VERSION();');
83
+ return connection;
84
+ }
85
+ catch (err) {
86
+ throw err;
87
+ }
88
+ };
89
+ const convertQueryBoolean = (val) => val === 'YES';
90
+ const getFieldType = (data_type, character_maximum_length, numeric_precision, numeric_scale) => {
91
+ if (character_maximum_length) {
92
+ return `${data_type}(${character_maximum_length})`;
93
+ }
94
+ if (numeric_precision && numeric_scale) {
95
+ return `${data_type}(${numeric_precision},${numeric_scale})`;
96
+ }
97
+ return data_type;
98
+ };
99
+ const getDbdefault = (column_default, default_type) => {
100
+ if (default_type === 'string') {
101
+ return {
102
+ type: default_type,
103
+ value: column_default.slice(1, -1),
104
+ };
105
+ }
106
+ return {
107
+ type: default_type,
108
+ value: column_default,
109
+ };
110
+ };
111
+ const generateField = (row) => {
112
+ const { COLUMN_NAME: column_name, DATA_TYPE: data_type, CHARACTER_MAXIMUM_LENGTH: character_maximum_length, NUMERIC_PRECISION: numeric_precision, NUMERIC_SCALE: numeric_scale, IDENTITY_INCREMENT: identity_increment, IS_NULLABLE: is_nullable, COLUMN_DEFAULT: column_default, DEFAULT_TYPE: default_type, COLUMN_COMMENT: column_comment, } = row;
113
+ const dbdefault = column_default ? getDbdefault(column_default, default_type) : null;
114
+ const fieldType = {
115
+ type_name: getFieldType(data_type, character_maximum_length, numeric_precision, numeric_scale),
116
+ schemaName: null,
117
+ };
118
+ return {
119
+ name: column_name,
120
+ type: fieldType,
121
+ dbdefault,
122
+ not_null: !convertQueryBoolean(is_nullable),
123
+ increment: !!identity_increment,
124
+ note: column_comment ? { value: column_comment } : { value: '' },
125
+ };
126
+ };
127
+ const generateTablesAndFields = async (conn, schemas) => {
128
+ const fields = {};
129
+ const schemaSql = schemas.length > 0 ? `AND c.table_schema IN (${schemas.map((schema) => `'${schema}'`).join(',')})` : '';
130
+ const tablesAndFieldsSql = `
131
+ SELECT
132
+ c.table_schema,
133
+ c.table_name,
134
+ c.column_name,
135
+ c.data_type,
136
+ c.character_maximum_length,
137
+ c.numeric_precision,
138
+ c.numeric_scale,
139
+ c.is_nullable,
140
+ c.column_default,
141
+ CASE
142
+ WHEN c.column_default IS NULL THEN NULL
143
+ WHEN c.column_default LIKE '''%' THEN 'string'
144
+ WHEN LOWER(c.column_default) = 'true' OR LOWER(c.column_default) = 'false' THEN 'boolean'
145
+ WHEN c.column_default REGEXP '^-?[0-9]+(\\.[0-9]+)?$' THEN 'number'
146
+ ELSE 'expression'
147
+ END AS default_type,
148
+ t.comment AS table_comment,
149
+ c.comment AS column_comment,
150
+ c.identity_increment
151
+ FROM
152
+ information_schema.columns c
153
+ JOIN information_schema.tables t
154
+ ON c.table_name = t.table_name
155
+ AND c.table_schema = t.table_schema
156
+ WHERE
157
+ t.table_type = 'BASE TABLE'
158
+ AND t.table_schema NOT IN ('INFORMATION_SCHEMA')
159
+ ${schemaSql}
160
+ ORDER BY
161
+ c.table_schema,
162
+ c.table_name,
163
+ c.ordinal_position;
164
+ `;
165
+ const tablesAndFieldsResult = await executeQuery(conn, tablesAndFieldsSql);
166
+ const tables = tablesAndFieldsResult.reduce((acc, row) => {
167
+ const { TABLE_SCHEMA: table_schema, TABLE_NAME: table_name, TABLE_COMMENT: table_comment } = row;
168
+ const key = `${table_schema}.${table_name}`;
169
+ if (!acc[key]) {
170
+ acc[key] = {
171
+ name: table_name,
172
+ schemaName: table_schema,
173
+ note: table_comment ? { value: table_comment } : { value: '' },
174
+ };
175
+ }
176
+ if (!fields[key])
177
+ fields[key] = [];
178
+ const field = generateField(row);
179
+ fields[key].push(field);
180
+ return acc;
181
+ }, {});
182
+ return {
183
+ tables: Object.values(tables),
184
+ fields,
185
+ };
186
+ };
187
+ const createConstraintKeysMap = (keys, schemas, constraintType) => {
188
+ return keys.reduce((acc, row) => {
189
+ const { schema_name, table_name, column_name, constraint_name } = row;
190
+ const selectedSchema = schemas.length > 0 ? schemas.includes(schema_name) : true;
191
+ if (!selectedSchema) {
192
+ return acc;
193
+ }
194
+ const key = `${schema_name}.${table_name}.${constraint_name}`;
195
+ if (acc[key]) {
196
+ const columnNames = acc[key].columnNames;
197
+ columnNames.push(column_name);
198
+ return acc;
199
+ }
200
+ acc[key] = {
201
+ schemaName: schema_name,
202
+ tableName: table_name,
203
+ constraintName: constraint_name,
204
+ columnNames: [column_name],
205
+ type: '',
206
+ };
207
+ acc[key][constraintType] = true;
208
+ return acc;
209
+ }, {});
210
+ };
211
+ const generateIndexes = async (conn, databaseName, schemas) => {
212
+ const getPrimaryKeysSql = `
213
+ SHOW PRIMARY KEYS IN DATABASE ${databaseName};
214
+ `;
215
+ const getUniqueKeysSql = `
216
+ SHOW UNIQUE KEYS IN DATABASE ${databaseName};
217
+ `;
218
+ const primaryKeys = await executeQuery(conn, getPrimaryKeysSql);
219
+ const uniqueKeys = await executeQuery(conn, getUniqueKeysSql);
220
+ const primaryKeysByConstraint = createConstraintKeysMap(primaryKeys, schemas, 'primary');
221
+ const uniqueKeysByConstraint = createConstraintKeysMap(uniqueKeys, schemas, 'unique');
222
+ const allConstraints = [Object.values(primaryKeysByConstraint), Object.values(uniqueKeysByConstraint)].flat();
223
+ const { indexes, tableConstraints } = allConstraints.reduce((acc, row) => {
224
+ const { schemaName, tableName, constraintName, columnNames, type, primary, unique } = row;
225
+ const key = `${schemaName}.${tableName}`;
226
+ if (columnNames.length < 2) {
227
+ acc.tableConstraints[key] = {
228
+ [columnNames[0]]: {
229
+ pk: primary,
230
+ unique,
231
+ },
232
+ };
233
+ return acc;
234
+ }
235
+ const index = {
236
+ name: constraintName,
237
+ type,
238
+ unique,
239
+ pk: primary,
240
+ columns: columnNames.map((columnName) => ({
241
+ type: 'column',
242
+ value: columnName,
243
+ })),
244
+ };
245
+ if (acc.indexes[key]) {
246
+ acc.indexes[key].push(index);
247
+ }
248
+ else {
249
+ acc.indexes[key] = [index];
250
+ }
251
+ return acc;
252
+ }, { indexes: {}, tableConstraints: {} });
253
+ return {
254
+ indexes,
255
+ tableConstraints,
256
+ };
257
+ };
258
+ const fetchSchemaJson = async (connection) => {
259
+ const config = parseConnectionString(connection);
260
+ const conn = await connectToSnowflake(config);
261
+ if (conn instanceof Error) {
262
+ throw conn;
263
+ }
264
+ // Schemas: schema1,schema2,schema3
265
+ const schemas = config.SCHEMAS ? (0, parseSchema_1.parseSchema)(config.SCHEMAS) : [];
266
+ const databaseName = config.DATABASE;
267
+ const tablesAndFieldsRes = generateTablesAndFields(conn, schemas);
268
+ const indexesRes = generateIndexes(conn, databaseName, schemas);
269
+ const res = await Promise.all([
270
+ tablesAndFieldsRes,
271
+ indexesRes,
272
+ ]);
273
+ conn.destroy((err) => {
274
+ if (err) {
275
+ throw err;
276
+ }
277
+ else {
278
+ console.log('Connection destroyed successfully.');
279
+ }
280
+ });
281
+ const { tables, fields } = res[0];
282
+ const { indexes, tableConstraints } = res[1];
283
+ return {
284
+ tables,
285
+ fields,
286
+ refs: [],
287
+ enums: [],
288
+ indexes,
289
+ tableConstraints,
290
+ };
291
+ };
292
+ exports.fetchSchemaJson = fetchSchemaJson;
@@ -16,6 +16,8 @@ interface Field {
16
16
  dbdefault: DefaultInfo | null;
17
17
  not_null: boolean;
18
18
  increment: boolean;
19
+ pk?: boolean;
20
+ unique?: boolean;
19
21
  note: NoteInfo;
20
22
  }
21
23
  interface Table {
@@ -77,4 +79,12 @@ interface DatabaseSchema {
77
79
  refs: Ref[];
78
80
  indexes: IndexesDictionary;
79
81
  }
80
- export { NoteInfo, TypeInfo, DefaultType, DefaultInfo, Field, Table, FieldsDictionary, TableConstraint, TableConstraintsDictionary, RefEndpoint, Ref, EnumValue, Enum, IndexColumn, Index, IndexesDictionary, DatabaseSchema, };
82
+ interface BigQueryCredentials {
83
+ projectId: string;
84
+ credentials: {
85
+ clientEmail: string;
86
+ privateKey: string;
87
+ };
88
+ datasets: string[];
89
+ }
90
+ export { NoteInfo, TypeInfo, DefaultType, DefaultInfo, Field, Table, FieldsDictionary, TableConstraint, TableConstraintsDictionary, RefEndpoint, Ref, EnumValue, Enum, IndexColumn, Index, IndexesDictionary, DatabaseSchema, BigQueryCredentials, };
@@ -0,0 +1,3 @@
1
+ import { BigQueryCredentials } from '../connectors/types';
2
+ export declare function loadCredentialFromFile(filename: string): Promise<string>;
3
+ export declare function parseBigQueryCredential(credentialString: string): BigQueryCredentials;
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.loadCredentialFromFile = loadCredentialFromFile;
4
+ exports.parseBigQueryCredential = parseBigQueryCredential;
5
+ const promises_1 = require("fs/promises");
6
+ async function loadCredentialFromFile(filename) {
7
+ try {
8
+ const data = await (0, promises_1.readFile)(filename, { encoding: 'utf8' });
9
+ return data;
10
+ }
11
+ catch (error) {
12
+ if (error.name === 'TypeError' || error.name === 'Error') {
13
+ throw new Error(`Load credential error: ${error.message}`);
14
+ }
15
+ throw error;
16
+ }
17
+ }
18
+ function parseBigQueryCredential(credentialString) {
19
+ try {
20
+ const credentialJson = JSON.parse(credentialString);
21
+ const { project_id: projectId, client_email: clientEmail, private_key: privateKey, datasets, } = credentialJson;
22
+ if (typeof projectId !== 'string' || !projectId || !projectId.trim()) {
23
+ throw new Error('project_id must be a non-empty string');
24
+ }
25
+ if (typeof clientEmail !== 'string' || !clientEmail || !clientEmail.trim()) {
26
+ throw new Error('client_email must be a non-empty string');
27
+ }
28
+ if (typeof privateKey !== 'string' || !privateKey || !privateKey.trim()) {
29
+ throw new Error('private_key must be a non-empty string');
30
+ }
31
+ if (typeof clientEmail !== 'string' || !clientEmail || !clientEmail.trim()) {
32
+ throw new Error('client_email must be a non-empty string');
33
+ }
34
+ // valid datasets: ['dataset_1', 'dataset_2']
35
+ const parsedDatasets = !Array.isArray(datasets)
36
+ ? []
37
+ : datasets.filter((dataset) => typeof dataset === 'string');
38
+ return {
39
+ projectId,
40
+ credentials: {
41
+ clientEmail,
42
+ privateKey,
43
+ },
44
+ datasets: parsedDatasets,
45
+ };
46
+ }
47
+ catch (error) {
48
+ if (error instanceof SyntaxError) {
49
+ throw new Error('Credentials must be in JSON format');
50
+ }
51
+ throw error;
52
+ }
53
+ }
@@ -0,0 +1,7 @@
1
+ import { FieldsDictionary, IndexesDictionary, Table, TableConstraintsDictionary } from '../connectors/types';
2
+ export declare function getIntersection<T>(firstList: T[], secondList: T[]): T[];
3
+ export declare function getTableSchemaKey(schemaName: string, tableName: string): string;
4
+ export declare function mergeTables(firstTableList: Table[], secondTable: Table[]): Table[];
5
+ export declare function mergeFieldDictionary(firstDict: FieldsDictionary, secondDict: FieldsDictionary): FieldsDictionary;
6
+ export declare function mergeIndexDictionary(firstDict: IndexesDictionary, secondDict: IndexesDictionary): IndexesDictionary;
7
+ export declare function mergeTableConstraintDictionary(firstDict: TableConstraintsDictionary, secondDict: TableConstraintsDictionary): TableConstraintsDictionary;
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getIntersection = getIntersection;
4
+ exports.getTableSchemaKey = getTableSchemaKey;
5
+ exports.mergeTables = mergeTables;
6
+ exports.mergeFieldDictionary = mergeFieldDictionary;
7
+ exports.mergeIndexDictionary = mergeIndexDictionary;
8
+ exports.mergeTableConstraintDictionary = mergeTableConstraintDictionary;
9
+ function getIntersection(firstList, secondList) {
10
+ return firstList.filter((item) => secondList.includes(item));
11
+ }
12
+ function getTableSchemaKey(schemaName, tableName) {
13
+ if (!tableName || !tableName.trim()) {
14
+ throw new Error('Table name must be a non-empty string');
15
+ }
16
+ return schemaName ? `${schemaName}.${tableName}` : `${tableName}`;
17
+ }
18
+ function mergeTables(firstTableList, secondTable) {
19
+ return firstTableList.concat(secondTable);
20
+ }
21
+ function mergeFieldDictionary(firstDict, secondDict) {
22
+ const result = Object.assign({}, firstDict);
23
+ Object.keys(secondDict).forEach((key) => {
24
+ if (!result[key]) {
25
+ result[key] = [];
26
+ }
27
+ result[key] = result[key].concat(secondDict[key]);
28
+ });
29
+ return result;
30
+ }
31
+ function mergeIndexDictionary(firstDict, secondDict) {
32
+ const result = Object.assign({}, firstDict);
33
+ Object.keys(secondDict).forEach((key) => {
34
+ if (!result[key]) {
35
+ result[key] = [];
36
+ }
37
+ result[key] = result[key].concat(secondDict[key]);
38
+ });
39
+ return result;
40
+ }
41
+ function mergeTableConstraintDictionary(firstDict, secondDict) {
42
+ const result = Object.assign({}, firstDict);
43
+ Object.keys(secondDict).forEach((key) => {
44
+ if (!result[key]) {
45
+ result[key] = {};
46
+ }
47
+ result[key] = secondDict[key];
48
+ });
49
+ return result;
50
+ }
@@ -0,0 +1,10 @@
1
+ export declare const parseJdbcSchema: (connectionString: string) => {
2
+ connectionString: string;
3
+ schemas: string[];
4
+ };
5
+ export declare const parseConnectionString: (connectionString: string, connectionStringType: "jdbc" | "odbc") => {
6
+ connectionString: string;
7
+ schemas: string[];
8
+ };
9
+ export declare const buildSchemaQuery: (columnName: string, schemas: string[], prefix?: string) => string;
10
+ export declare const parseSchema: (schemas: string | undefined) => string[];
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseSchema = exports.buildSchemaQuery = exports.parseConnectionString = exports.parseJdbcSchema = void 0;
4
+ // You cannot use the "=" inside database's schema name. It will be treated as a delimiter.
5
+ const SCHEMAS_DELIMITER = 'schemas=';
6
+ const haveSchemas = (str) => {
7
+ return str.toLowerCase().includes(SCHEMAS_DELIMITER);
8
+ };
9
+ const getSchemasPart = (str) => {
10
+ return str ? str.toLowerCase().split(SCHEMAS_DELIMITER)[1] : '';
11
+ };
12
+ const noSchemaResult = (connectionString) => ({
13
+ connectionString,
14
+ schemas: [],
15
+ });
16
+ const parseOdbcSchema = (connectionString) => {
17
+ const connectionParts = connectionString.split(';');
18
+ const schemasPart = connectionParts.find((part) => haveSchemas(part));
19
+ return {
20
+ connectionString: connectionParts.filter((part) => !haveSchemas(part)).join(';'),
21
+ schemas: (0, exports.parseSchema)(getSchemasPart(schemasPart)),
22
+ };
23
+ };
24
+ const parseJdbcSchema = (connectionString) => {
25
+ const connectionParts = connectionString.split('?');
26
+ const connectionArgs = connectionParts[1].split('&');
27
+ const schemasPart = connectionArgs.find((part) => haveSchemas(part));
28
+ return {
29
+ connectionString: connectionParts[0],
30
+ schemas: (0, exports.parseSchema)(getSchemasPart(schemasPart)),
31
+ };
32
+ };
33
+ exports.parseJdbcSchema = parseJdbcSchema;
34
+ const parseConnectionString = (connectionString, connectionStringType) => {
35
+ if (!haveSchemas(connectionString))
36
+ return noSchemaResult(connectionString);
37
+ return connectionStringType === 'jdbc'
38
+ ? (0, exports.parseJdbcSchema)(connectionString)
39
+ : parseOdbcSchema(connectionString);
40
+ };
41
+ exports.parseConnectionString = parseConnectionString;
42
+ const buildSchemaQuery = (columnName, schemas, prefix = 'AND') => {
43
+ if (schemas.length === 0)
44
+ return '';
45
+ return `${prefix} ${columnName} IN (${schemas.map((schema) => `'${schema}'`).join(', ')})`;
46
+ };
47
+ exports.buildSchemaQuery = buildSchemaQuery;
48
+ // Inputs: schemas string -> Output: schema array string[]
49
+ // schema1, schema2 -> ['schema1', 'schema2']
50
+ // schema3,schema4 -> ['schema3", 'schema4']
51
+ // "schema5,gg", "schema6", schema7 -> ['schema5,gg', 'schema6', 'schema7']
52
+ // schema8 -> ['schema8']
53
+ const parseSchema = (schemas) => {
54
+ if (!schemas)
55
+ return [];
56
+ const schemasWithDoubleQuotes = schemas.split(/,(?=(?:[^"]*(?:"[^"]*")*)*$)/).map((s) => s.trim());
57
+ return schemasWithDoubleQuotes.map((s) => s.replace(/"/g, ''));
58
+ };
59
+ exports.parseSchema = parseSchema;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dbml/connector",
3
- "version": "3.7.4",
3
+ "version": "3.8.0",
4
4
  "description": "This package was created to fetch the schema JSON from many kind of databases.",
5
5
  "author": "huy.phung.sw@gmail.com",
6
6
  "license": "MIT",
@@ -31,12 +31,14 @@
31
31
  "test": "jest"
32
32
  },
33
33
  "dependencies": {
34
+ "@google-cloud/bigquery": "^7.9.0",
34
35
  "mssql": "^11.0.1",
35
36
  "mysql2": "^3.11.0",
36
- "pg": "^8.12.0"
37
+ "pg": "^8.12.0",
38
+ "snowflake-sdk": "^1.12.0"
37
39
  },
38
40
  "engines": {
39
41
  "node": ">=18"
40
42
  },
41
- "gitHead": "316fd7ce4039ce9b622c30fbb2d03ab2198bd7eb"
43
+ "gitHead": "f106540bc455695c8a4c91ed63aa8fabc8ce645a"
42
44
  }