@dbml/connector 3.13.9-alpha.0 → 3.14.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.
- package/__tests__/connectors/bigquery/out-files/schema.json +565 -0
- package/__tests__/connectors/mssql/out-files/schema.json +187 -5
- package/__tests__/connectors/mysql/out-files/schema.json +1557 -0
- package/__tests__/connectors/postgres/out-files/schema.json +1774 -0
- package/dist/connectors/bigqueryConnector.js +41 -22
- package/dist/connectors/types.d.ts +2 -2
- package/dist/utils/credential-loader.js +23 -18
- package/package.json +2 -2
- package/src/connectors/bigqueryConnector.ts +71 -44
- package/src/connectors/types.ts +2 -2
- package/src/utils/credential-loader.ts +29 -23
|
@@ -4,16 +4,32 @@ exports.fetchSchemaJson = fetchSchemaJson;
|
|
|
4
4
|
const bigquery_1 = require("@google-cloud/bigquery");
|
|
5
5
|
const credential_loader_1 = require("../utils/credential-loader");
|
|
6
6
|
const helpers_1 = require("../utils/helpers");
|
|
7
|
+
// BigQuery client authentication strategy:
|
|
8
|
+
// 1. Start with environment/default authentication:
|
|
9
|
+
// - GOOGLE_APPLICATION_CREDENTIALS env variable (service account key file)
|
|
10
|
+
// - Application Default Credentials (gcloud auth application-default login)
|
|
11
|
+
//
|
|
12
|
+
// 2. Override with explicitly provided properties:
|
|
13
|
+
// - If projectId is provided, override the default project
|
|
14
|
+
// - If credentials (clientEmail + privateKey) provided, override auth method
|
|
15
|
+
// - If datasets specified in config, fetch only those datasets
|
|
7
16
|
async function connectBigQuery(credential) {
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
17
|
+
const config = {};
|
|
18
|
+
// Override project if explicitly provided
|
|
19
|
+
if (credential?.projectId) {
|
|
20
|
+
config.projectId = credential.projectId;
|
|
21
|
+
}
|
|
22
|
+
// Override credentials if both email and key are provided
|
|
23
|
+
if (credential?.credentials?.clientEmail && credential?.credentials?.privateKey) {
|
|
24
|
+
config.credentials = {
|
|
12
25
|
client_email: credential.credentials.clientEmail,
|
|
13
|
-
|
|
14
|
-
|
|
26
|
+
private_key: credential.credentials.privateKey,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
// Create client with merged config
|
|
30
|
+
const client = new bigquery_1.BigQuery(config);
|
|
15
31
|
try {
|
|
16
|
-
const query =
|
|
32
|
+
const query = 'SELECT CURRENT_DATE() as today';
|
|
17
33
|
await client.query(query);
|
|
18
34
|
return client;
|
|
19
35
|
}
|
|
@@ -30,6 +46,7 @@ function getDbDefault(columnDefault, defaultValueType) {
|
|
|
30
46
|
}
|
|
31
47
|
if (defaultValueType === 'string') {
|
|
32
48
|
// bigquery store the double qoutes and quotes at the end and the beginning of the string: "string" or 'string'
|
|
49
|
+
// eslint-disable-next-line no-useless-escape
|
|
33
50
|
return { value: columnDefault.slice(1, -1).replaceAll("'", "\'"), type: defaultValueType };
|
|
34
51
|
}
|
|
35
52
|
return { value: columnDefault, type: defaultValueType };
|
|
@@ -53,17 +70,17 @@ function generateField(row) {
|
|
|
53
70
|
return field;
|
|
54
71
|
}
|
|
55
72
|
async function generateTablesAndFields(client, projectId, datasetId) {
|
|
56
|
-
const
|
|
73
|
+
const tableNameToFetch = `${projectId}.${datasetId}`;
|
|
57
74
|
const query = `
|
|
58
75
|
with
|
|
59
76
|
table_comments as (
|
|
60
77
|
select table_schema, table_name, option_value as description
|
|
61
|
-
from ${
|
|
78
|
+
from ${tableNameToFetch}.INFORMATION_SCHEMA.TABLE_OPTIONS
|
|
62
79
|
where option_name = 'description'
|
|
63
80
|
),
|
|
64
81
|
column_comments as (
|
|
65
82
|
select column_name, table_schema, table_name, description
|
|
66
|
-
from ${
|
|
83
|
+
from ${tableNameToFetch}.INFORMATION_SCHEMA.COLUMN_FIELD_PATHS
|
|
67
84
|
where column_name = field_path
|
|
68
85
|
)
|
|
69
86
|
select
|
|
@@ -87,8 +104,8 @@ async function generateTablesAndFields(client, projectId, datasetId) {
|
|
|
87
104
|
WHEN c.column_default = "NULL" THEN NULL
|
|
88
105
|
ELSE 'expression'
|
|
89
106
|
END AS columnDefaultType
|
|
90
|
-
from ${
|
|
91
|
-
join ${
|
|
107
|
+
from ${tableNameToFetch}.INFORMATION_SCHEMA.TABLES t
|
|
108
|
+
join ${tableNameToFetch}.INFORMATION_SCHEMA.COLUMNS c
|
|
92
109
|
on
|
|
93
110
|
t.table_schema = c.table_schema
|
|
94
111
|
and t.table_name = c.table_name
|
|
@@ -120,15 +137,15 @@ async function generateTablesAndFields(client, projectId, datasetId) {
|
|
|
120
137
|
};
|
|
121
138
|
}
|
|
122
139
|
async function generateIndexes(client, projectId, datasetId) {
|
|
123
|
-
const
|
|
140
|
+
const tableNameToFetch = `${projectId}.${datasetId}`;
|
|
124
141
|
const query = `
|
|
125
142
|
select
|
|
126
143
|
s.index_schema as indexSchema,
|
|
127
144
|
s.table_name as tableName,
|
|
128
145
|
s.index_name as indexName,
|
|
129
146
|
string_agg(sc.index_column_name, ', ') as indexColumnNames
|
|
130
|
-
from ${
|
|
131
|
-
join ${
|
|
147
|
+
from ${tableNameToFetch}.INFORMATION_SCHEMA.SEARCH_INDEXES s
|
|
148
|
+
join ${tableNameToFetch}.INFORMATION_SCHEMA.SEARCH_INDEX_COLUMNS sc
|
|
132
149
|
on
|
|
133
150
|
s.index_schema = sc.index_schema
|
|
134
151
|
and s.table_name = sc.table_name
|
|
@@ -140,7 +157,7 @@ async function generateIndexes(client, projectId, datasetId) {
|
|
|
140
157
|
const [queryResult] = await client.query(query);
|
|
141
158
|
const indexMap = {};
|
|
142
159
|
queryResult.forEach((row) => {
|
|
143
|
-
const { indexSchema, tableName, indexName, indexColumnNames } = row;
|
|
160
|
+
const { indexSchema, tableName, indexName, indexColumnNames, } = row;
|
|
144
161
|
const indexColumns = indexColumnNames.split(', ').map((column) => {
|
|
145
162
|
return {
|
|
146
163
|
type: 'column',
|
|
@@ -163,7 +180,7 @@ async function generateIndexes(client, projectId, datasetId) {
|
|
|
163
180
|
return indexMap;
|
|
164
181
|
}
|
|
165
182
|
async function generateTableConstraints(client, projectId, datasetId) {
|
|
166
|
-
const
|
|
183
|
+
const tableNameToFetch = `${projectId}.${datasetId}`;
|
|
167
184
|
const query = `
|
|
168
185
|
select
|
|
169
186
|
tc.table_schema as tableSchema,
|
|
@@ -171,8 +188,8 @@ async function generateTableConstraints(client, projectId, datasetId) {
|
|
|
171
188
|
tc.constraint_name as constraintName,
|
|
172
189
|
string_agg(kcu.column_name, ', ') as columnNames,
|
|
173
190
|
count(kcu.column_name) as columnCount,
|
|
174
|
-
from ${
|
|
175
|
-
join ${
|
|
191
|
+
from ${tableNameToFetch}.INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
|
|
192
|
+
join ${tableNameToFetch}.INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu
|
|
176
193
|
on
|
|
177
194
|
tc.table_schema = kcu.table_schema
|
|
178
195
|
and tc.table_name = kcu.table_name
|
|
@@ -198,7 +215,7 @@ async function generateTableConstraints(client, projectId, datasetId) {
|
|
|
198
215
|
});
|
|
199
216
|
const compositePrimaryKeyMap = {};
|
|
200
217
|
compositePrimaryKeyList.forEach((compositeKey) => {
|
|
201
|
-
const { tableSchema, tableName, constraintName, columnNames } = compositeKey;
|
|
218
|
+
const { tableSchema, tableName, constraintName, columnNames, } = compositeKey;
|
|
202
219
|
const indexColumns = columnNames.split(', ').map((column) => {
|
|
203
220
|
return {
|
|
204
221
|
type: 'column',
|
|
@@ -260,12 +277,14 @@ async function fetchSchemaJson(keyFilename) {
|
|
|
260
277
|
const querySchemaPromises = datasetIdList.map((dataset) => fetchSchemaJsonByDataset(client, projectId, dataset));
|
|
261
278
|
const databaseSchemaRes = await Promise.all(querySchemaPromises);
|
|
262
279
|
const res = databaseSchemaRes.reduce((acc, dbSchema) => {
|
|
263
|
-
const { tables, fields, indexes, tableConstraints } = dbSchema;
|
|
280
|
+
const { tables, fields, indexes, tableConstraints, } = dbSchema;
|
|
264
281
|
acc.tables = (0, helpers_1.mergeTables)(acc.tables, tables);
|
|
265
282
|
acc.fields = (0, helpers_1.mergeFieldDictionary)(acc.fields, fields);
|
|
266
283
|
acc.indexes = (0, helpers_1.mergeIndexDictionary)(acc.indexes, indexes);
|
|
267
284
|
acc.tableConstraints = (0, helpers_1.mergeTableConstraintDictionary)(acc.tableConstraints, tableConstraints);
|
|
268
285
|
return acc;
|
|
269
|
-
}, {
|
|
286
|
+
}, {
|
|
287
|
+
tables: [], fields: {}, tableConstraints: {}, indexes: {}, refs: [], enums: [],
|
|
288
|
+
});
|
|
270
289
|
return res;
|
|
271
290
|
}
|
|
@@ -19,30 +19,35 @@ function parseBigQueryCredential(credentialString) {
|
|
|
19
19
|
try {
|
|
20
20
|
const credentialJson = JSON.parse(credentialString);
|
|
21
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
22
|
// valid datasets: ['dataset_1', 'dataset_2']
|
|
35
23
|
const parsedDatasets = !Array.isArray(datasets)
|
|
36
24
|
? []
|
|
37
25
|
: datasets.filter((dataset) => typeof dataset === 'string');
|
|
38
|
-
|
|
39
|
-
projectId,
|
|
40
|
-
credentials: {
|
|
41
|
-
clientEmail,
|
|
42
|
-
privateKey,
|
|
43
|
-
},
|
|
26
|
+
const parsedCredentials = {
|
|
44
27
|
datasets: parsedDatasets,
|
|
45
28
|
};
|
|
29
|
+
// Validate and add projectId if provided
|
|
30
|
+
if (projectId !== undefined) {
|
|
31
|
+
if (typeof projectId !== 'string' || !projectId || !projectId.trim()) {
|
|
32
|
+
throw new Error('project_id must be a non-empty string when provided');
|
|
33
|
+
}
|
|
34
|
+
parsedCredentials.projectId = projectId;
|
|
35
|
+
}
|
|
36
|
+
// Validate and add credentials if provided
|
|
37
|
+
// Note: If one credential field is provided, both must be provided
|
|
38
|
+
if (clientEmail !== undefined || privateKey !== undefined) {
|
|
39
|
+
if (!clientEmail || typeof clientEmail !== 'string' || !clientEmail.trim()) {
|
|
40
|
+
throw new Error('client_email must be a non-empty string when credentials are provided');
|
|
41
|
+
}
|
|
42
|
+
if (!privateKey || typeof privateKey !== 'string' || !privateKey.trim()) {
|
|
43
|
+
throw new Error('private_key must be a non-empty string when credentials are provided');
|
|
44
|
+
}
|
|
45
|
+
parsedCredentials.credentials = {
|
|
46
|
+
clientEmail,
|
|
47
|
+
privateKey,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return parsedCredentials;
|
|
46
51
|
}
|
|
47
52
|
catch (error) {
|
|
48
53
|
if (error instanceof SyntaxError) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dbml/connector",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.14.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",
|
|
@@ -44,5 +44,5 @@
|
|
|
44
44
|
"engines": {
|
|
45
45
|
"node": ">=18"
|
|
46
46
|
},
|
|
47
|
-
"gitHead": "
|
|
47
|
+
"gitHead": "a3be772822c217e0b8afa975e90fa54b81d38276"
|
|
48
48
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BigQuery } from '@google-cloud/bigquery';
|
|
1
|
+
import { BigQuery, BigQueryOptions } from '@google-cloud/bigquery';
|
|
2
2
|
import { loadCredentialFromFile, parseBigQueryCredential } from '../utils/credential-loader';
|
|
3
3
|
import {
|
|
4
4
|
getIntersection,
|
|
@@ -21,17 +21,36 @@ import {
|
|
|
21
21
|
TableConstraintsDictionary,
|
|
22
22
|
} from './types';
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
// BigQuery client authentication strategy:
|
|
25
|
+
// 1. Start with environment/default authentication:
|
|
26
|
+
// - GOOGLE_APPLICATION_CREDENTIALS env variable (service account key file)
|
|
27
|
+
// - Application Default Credentials (gcloud auth application-default login)
|
|
28
|
+
//
|
|
29
|
+
// 2. Override with explicitly provided properties:
|
|
30
|
+
// - If projectId is provided, override the default project
|
|
31
|
+
// - If credentials (clientEmail + privateKey) provided, override auth method
|
|
32
|
+
// - If datasets specified in config, fetch only those datasets
|
|
33
|
+
async function connectBigQuery (credential: BigQueryCredentials): Promise<BigQuery> {
|
|
34
|
+
const config: BigQueryOptions = {};
|
|
35
|
+
|
|
36
|
+
// Override project if explicitly provided
|
|
37
|
+
if (credential?.projectId) {
|
|
38
|
+
config.projectId = credential.projectId;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Override credentials if both email and key are provided
|
|
42
|
+
if (credential?.credentials?.clientEmail && credential?.credentials?.privateKey) {
|
|
43
|
+
config.credentials = {
|
|
29
44
|
client_email: credential.credentials.clientEmail,
|
|
30
|
-
|
|
31
|
-
|
|
45
|
+
private_key: credential.credentials.privateKey,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Create client with merged config
|
|
50
|
+
const client = new BigQuery(config);
|
|
32
51
|
|
|
33
52
|
try {
|
|
34
|
-
const query =
|
|
53
|
+
const query = 'SELECT CURRENT_DATE() as today';
|
|
35
54
|
await client.query(query);
|
|
36
55
|
return client;
|
|
37
56
|
} catch (error) {
|
|
@@ -43,9 +62,9 @@ async function connectBigQuery(credential: BigQueryCredentials): Promise<BigQuer
|
|
|
43
62
|
}
|
|
44
63
|
}
|
|
45
64
|
|
|
46
|
-
function getDbDefault(
|
|
65
|
+
function getDbDefault (
|
|
47
66
|
columnDefault: string,
|
|
48
|
-
defaultValueType: DefaultType | null
|
|
67
|
+
defaultValueType: DefaultType | null,
|
|
49
68
|
): DefaultInfo | null {
|
|
50
69
|
if (defaultValueType === null) {
|
|
51
70
|
return null;
|
|
@@ -53,13 +72,14 @@ function getDbDefault(
|
|
|
53
72
|
|
|
54
73
|
if (defaultValueType === 'string') {
|
|
55
74
|
// bigquery store the double qoutes and quotes at the end and the beginning of the string: "string" or 'string'
|
|
56
|
-
|
|
75
|
+
// eslint-disable-next-line no-useless-escape
|
|
76
|
+
return { value: columnDefault.slice(1, -1).replaceAll("'", "\'"), type: defaultValueType };
|
|
57
77
|
}
|
|
58
78
|
|
|
59
79
|
return { value: columnDefault, type: defaultValueType };
|
|
60
80
|
}
|
|
61
81
|
|
|
62
|
-
function generateField(row: any): Field {
|
|
82
|
+
function generateField (row: any): Field {
|
|
63
83
|
const {
|
|
64
84
|
columnName,
|
|
65
85
|
isNullable,
|
|
@@ -88,26 +108,26 @@ function generateField(row: any): Field {
|
|
|
88
108
|
return field;
|
|
89
109
|
}
|
|
90
110
|
|
|
91
|
-
async function generateTablesAndFields(
|
|
111
|
+
async function generateTablesAndFields (
|
|
92
112
|
client: BigQuery,
|
|
93
113
|
projectId: string,
|
|
94
|
-
datasetId: string
|
|
114
|
+
datasetId: string,
|
|
95
115
|
): Promise<{
|
|
96
116
|
tables: Table[];
|
|
97
117
|
fieldMap: FieldsDictionary;
|
|
98
118
|
}> {
|
|
99
|
-
const
|
|
119
|
+
const tableNameToFetch = `${projectId}.${datasetId}`;
|
|
100
120
|
|
|
101
121
|
const query = `
|
|
102
122
|
with
|
|
103
123
|
table_comments as (
|
|
104
124
|
select table_schema, table_name, option_value as description
|
|
105
|
-
from ${
|
|
125
|
+
from ${tableNameToFetch}.INFORMATION_SCHEMA.TABLE_OPTIONS
|
|
106
126
|
where option_name = 'description'
|
|
107
127
|
),
|
|
108
128
|
column_comments as (
|
|
109
129
|
select column_name, table_schema, table_name, description
|
|
110
|
-
from ${
|
|
130
|
+
from ${tableNameToFetch}.INFORMATION_SCHEMA.COLUMN_FIELD_PATHS
|
|
111
131
|
where column_name = field_path
|
|
112
132
|
)
|
|
113
133
|
select
|
|
@@ -131,8 +151,8 @@ async function generateTablesAndFields(
|
|
|
131
151
|
WHEN c.column_default = "NULL" THEN NULL
|
|
132
152
|
ELSE 'expression'
|
|
133
153
|
END AS columnDefaultType
|
|
134
|
-
from ${
|
|
135
|
-
join ${
|
|
154
|
+
from ${tableNameToFetch}.INFORMATION_SCHEMA.TABLES t
|
|
155
|
+
join ${tableNameToFetch}.INFORMATION_SCHEMA.COLUMNS c
|
|
136
156
|
on
|
|
137
157
|
t.table_schema = c.table_schema
|
|
138
158
|
and t.table_name = c.table_name
|
|
@@ -172,12 +192,12 @@ async function generateTablesAndFields(
|
|
|
172
192
|
};
|
|
173
193
|
}
|
|
174
194
|
|
|
175
|
-
async function generateIndexes(
|
|
195
|
+
async function generateIndexes (
|
|
176
196
|
client: BigQuery,
|
|
177
197
|
projectId: string,
|
|
178
|
-
datasetId: string
|
|
198
|
+
datasetId: string,
|
|
179
199
|
): Promise<IndexesDictionary> {
|
|
180
|
-
const
|
|
200
|
+
const tableNameToFetch = `${projectId}.${datasetId}`;
|
|
181
201
|
|
|
182
202
|
const query = `
|
|
183
203
|
select
|
|
@@ -185,8 +205,8 @@ async function generateIndexes(
|
|
|
185
205
|
s.table_name as tableName,
|
|
186
206
|
s.index_name as indexName,
|
|
187
207
|
string_agg(sc.index_column_name, ', ') as indexColumnNames
|
|
188
|
-
from ${
|
|
189
|
-
join ${
|
|
208
|
+
from ${tableNameToFetch}.INFORMATION_SCHEMA.SEARCH_INDEXES s
|
|
209
|
+
join ${tableNameToFetch}.INFORMATION_SCHEMA.SEARCH_INDEX_COLUMNS sc
|
|
190
210
|
on
|
|
191
211
|
s.index_schema = sc.index_schema
|
|
192
212
|
and s.table_name = sc.table_name
|
|
@@ -201,7 +221,9 @@ async function generateIndexes(
|
|
|
201
221
|
const indexMap: IndexesDictionary = {};
|
|
202
222
|
|
|
203
223
|
queryResult.forEach((row) => {
|
|
204
|
-
const {
|
|
224
|
+
const {
|
|
225
|
+
indexSchema, tableName, indexName, indexColumnNames,
|
|
226
|
+
} = row;
|
|
205
227
|
|
|
206
228
|
const indexColumns = indexColumnNames.split(', ').map((column: string) => {
|
|
207
229
|
return {
|
|
@@ -227,15 +249,15 @@ async function generateIndexes(
|
|
|
227
249
|
return indexMap;
|
|
228
250
|
}
|
|
229
251
|
|
|
230
|
-
async function generateTableConstraints(
|
|
252
|
+
async function generateTableConstraints (
|
|
231
253
|
client: BigQuery,
|
|
232
254
|
projectId: string,
|
|
233
|
-
datasetId: string
|
|
255
|
+
datasetId: string,
|
|
234
256
|
): Promise<{
|
|
235
257
|
inlinePrimaryKeyMap: TableConstraintsDictionary;
|
|
236
258
|
compositePrimaryKeyMap: IndexesDictionary;
|
|
237
259
|
}> {
|
|
238
|
-
const
|
|
260
|
+
const tableNameToFetch = `${projectId}.${datasetId}`;
|
|
239
261
|
|
|
240
262
|
const query = `
|
|
241
263
|
select
|
|
@@ -244,8 +266,8 @@ async function generateTableConstraints(
|
|
|
244
266
|
tc.constraint_name as constraintName,
|
|
245
267
|
string_agg(kcu.column_name, ', ') as columnNames,
|
|
246
268
|
count(kcu.column_name) as columnCount,
|
|
247
|
-
from ${
|
|
248
|
-
join ${
|
|
269
|
+
from ${tableNameToFetch}.INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
|
|
270
|
+
join ${tableNameToFetch}.INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu
|
|
249
271
|
on
|
|
250
272
|
tc.table_schema = kcu.table_schema
|
|
251
273
|
and tc.table_name = kcu.table_name
|
|
@@ -258,10 +280,10 @@ async function generateTableConstraints(
|
|
|
258
280
|
const [queryResult] = await client.query(query);
|
|
259
281
|
|
|
260
282
|
const inlinePrimaryKeyList = queryResult.filter(
|
|
261
|
-
(row) => row.columnCount && row.columnCount === 1
|
|
283
|
+
(row) => row.columnCount && row.columnCount === 1,
|
|
262
284
|
);
|
|
263
285
|
const compositePrimaryKeyList = queryResult.filter(
|
|
264
|
-
(row) => row.columnCount && row.columnCount > 1
|
|
286
|
+
(row) => row.columnCount && row.columnCount > 1,
|
|
265
287
|
);
|
|
266
288
|
|
|
267
289
|
const inlinePrimaryKeyMap: TableConstraintsDictionary = {};
|
|
@@ -283,7 +305,9 @@ async function generateTableConstraints(
|
|
|
283
305
|
|
|
284
306
|
const compositePrimaryKeyMap: IndexesDictionary = {};
|
|
285
307
|
compositePrimaryKeyList.forEach((compositeKey) => {
|
|
286
|
-
const {
|
|
308
|
+
const {
|
|
309
|
+
tableSchema, tableName, constraintName, columnNames,
|
|
310
|
+
} = compositeKey;
|
|
287
311
|
|
|
288
312
|
const indexColumns = columnNames.split(', ').map((column: string) => {
|
|
289
313
|
return {
|
|
@@ -314,7 +338,7 @@ async function generateTableConstraints(
|
|
|
314
338
|
};
|
|
315
339
|
}
|
|
316
340
|
|
|
317
|
-
async function validateDatasetIdList(client: BigQuery, datasetIdList: string[]): Promise<string[]> {
|
|
341
|
+
async function validateDatasetIdList (client: BigQuery, datasetIdList: string[]): Promise<string[]> {
|
|
318
342
|
const [datasets] = await client.getDatasets();
|
|
319
343
|
|
|
320
344
|
const rawDatasetIdList = datasets.map((dataset) => dataset.id).filter((id) => id) as string[];
|
|
@@ -326,10 +350,10 @@ async function validateDatasetIdList(client: BigQuery, datasetIdList: string[]):
|
|
|
326
350
|
return getIntersection(datasetIdList, rawDatasetIdList);
|
|
327
351
|
}
|
|
328
352
|
|
|
329
|
-
async function fetchSchemaJsonByDataset(
|
|
353
|
+
async function fetchSchemaJsonByDataset (
|
|
330
354
|
client: BigQuery,
|
|
331
355
|
projectId: string,
|
|
332
|
-
datasetId: string
|
|
356
|
+
datasetId: string,
|
|
333
357
|
): Promise<DatabaseSchema> {
|
|
334
358
|
const res = await Promise.all([
|
|
335
359
|
generateTablesAndFields(client, projectId, datasetId),
|
|
@@ -351,7 +375,7 @@ async function fetchSchemaJsonByDataset(
|
|
|
351
375
|
};
|
|
352
376
|
}
|
|
353
377
|
|
|
354
|
-
async function fetchSchemaJson(keyFilename: string): Promise<DatabaseSchema> {
|
|
378
|
+
async function fetchSchemaJson (keyFilename: string): Promise<DatabaseSchema> {
|
|
355
379
|
const rawCredential = await loadCredentialFromFile(keyFilename);
|
|
356
380
|
const credentialJson = parseBigQueryCredential(rawCredential);
|
|
357
381
|
const client = await connectBigQuery(credentialJson);
|
|
@@ -359,14 +383,15 @@ async function fetchSchemaJson(keyFilename: string): Promise<DatabaseSchema> {
|
|
|
359
383
|
const projectId = await client.getProjectId();
|
|
360
384
|
const datasetIdList = await validateDatasetIdList(client, credentialJson.datasets);
|
|
361
385
|
|
|
362
|
-
const querySchemaPromises = datasetIdList.map((dataset) =>
|
|
363
|
-
fetchSchemaJsonByDataset(client, projectId, dataset)
|
|
364
|
-
);
|
|
386
|
+
const querySchemaPromises = datasetIdList.map((dataset) => fetchSchemaJsonByDataset(client, projectId, dataset));
|
|
365
387
|
|
|
366
388
|
const databaseSchemaRes = await Promise.all(querySchemaPromises);
|
|
367
389
|
|
|
368
|
-
const res = databaseSchemaRes.reduce(
|
|
369
|
-
|
|
390
|
+
const res = databaseSchemaRes.reduce(
|
|
391
|
+
(acc, dbSchema) => {
|
|
392
|
+
const {
|
|
393
|
+
tables, fields, indexes, tableConstraints,
|
|
394
|
+
} = dbSchema;
|
|
370
395
|
|
|
371
396
|
acc.tables = mergeTables(acc.tables, tables);
|
|
372
397
|
acc.fields = mergeFieldDictionary(acc.fields, fields);
|
|
@@ -375,7 +400,9 @@ async function fetchSchemaJson(keyFilename: string): Promise<DatabaseSchema> {
|
|
|
375
400
|
|
|
376
401
|
return acc;
|
|
377
402
|
},
|
|
378
|
-
{
|
|
403
|
+
{
|
|
404
|
+
tables: [], fields: {}, tableConstraints: {}, indexes: {}, refs: [], enums: [],
|
|
405
|
+
},
|
|
379
406
|
);
|
|
380
407
|
|
|
381
408
|
return res;
|
package/src/connectors/types.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile } from 'fs/promises';
|
|
2
2
|
import { BigQueryCredentials } from '../connectors/types';
|
|
3
3
|
|
|
4
|
-
export async function loadCredentialFromFile(filename: string): Promise<string> {
|
|
4
|
+
export async function loadCredentialFromFile (filename: string): Promise<string> {
|
|
5
5
|
try {
|
|
6
6
|
const data = await readFile(filename, { encoding: 'utf8' });
|
|
7
7
|
return data;
|
|
@@ -14,7 +14,7 @@ export async function loadCredentialFromFile(filename: string): Promise<string>
|
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
export function parseBigQueryCredential(credentialString: string): BigQueryCredentials {
|
|
17
|
+
export function parseBigQueryCredential (credentialString: string): BigQueryCredentials {
|
|
18
18
|
try {
|
|
19
19
|
const credentialJson = JSON.parse(credentialString);
|
|
20
20
|
|
|
@@ -25,35 +25,41 @@ export function parseBigQueryCredential(credentialString: string): BigQueryCrede
|
|
|
25
25
|
datasets,
|
|
26
26
|
} = credentialJson;
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
// valid datasets: ['dataset_1', 'dataset_2']
|
|
29
|
+
const parsedDatasets = !Array.isArray(datasets)
|
|
30
|
+
? []
|
|
31
|
+
: datasets.filter((dataset) => typeof dataset === 'string');
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
33
|
+
const parsedCredentials: BigQueryCredentials = {
|
|
34
|
+
datasets: parsedDatasets,
|
|
35
|
+
};
|
|
35
36
|
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
// Validate and add projectId if provided
|
|
38
|
+
if (projectId !== undefined) {
|
|
39
|
+
if (typeof projectId !== 'string' || !projectId || !projectId.trim()) {
|
|
40
|
+
throw new Error('project_id must be a non-empty string when provided');
|
|
41
|
+
}
|
|
42
|
+
parsedCredentials.projectId = projectId;
|
|
38
43
|
}
|
|
39
44
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
// Validate and add credentials if provided
|
|
46
|
+
// Note: If one credential field is provided, both must be provided
|
|
47
|
+
if (clientEmail !== undefined || privateKey !== undefined) {
|
|
48
|
+
if (!clientEmail || typeof clientEmail !== 'string' || !clientEmail.trim()) {
|
|
49
|
+
throw new Error('client_email must be a non-empty string when credentials are provided');
|
|
50
|
+
}
|
|
43
51
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
: datasets.filter((dataset) => typeof dataset === 'string');
|
|
52
|
+
if (!privateKey || typeof privateKey !== 'string' || !privateKey.trim()) {
|
|
53
|
+
throw new Error('private_key must be a non-empty string when credentials are provided');
|
|
54
|
+
}
|
|
48
55
|
|
|
49
|
-
|
|
50
|
-
projectId,
|
|
51
|
-
credentials: {
|
|
56
|
+
parsedCredentials.credentials = {
|
|
52
57
|
clientEmail,
|
|
53
58
|
privateKey,
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return parsedCredentials;
|
|
57
63
|
} catch (error) {
|
|
58
64
|
if (error instanceof SyntaxError) {
|
|
59
65
|
throw new Error('Credentials must be in JSON format');
|