@malloy-publisher/server 0.0.119 → 0.0.121
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/dist/app/api-doc.yaml +324 -335
- package/dist/app/assets/{HomePage-BxFnfH3M.js → HomePage-z6NLKLPp.js} +1 -1
- package/dist/app/assets/{MainPage-D301Y0mT.js → MainPage-C9McOjLb.js} +2 -2
- package/dist/app/assets/{ModelPage-Df8ivC1J.js → ModelPage-DjlTuT2G.js} +1 -1
- package/dist/app/assets/{PackagePage-CE41SCV_.js → PackagePage-CDh_gnAZ.js} +1 -1
- package/dist/app/assets/ProjectPage-vyvZZWAB.js +1 -0
- package/dist/app/assets/{RouteError-l_WGtNhS.js → RouteError-FbxztVnz.js} +1 -1
- package/dist/app/assets/{WorkbookPage-CY-1oBvt.js → WorkbookPage-DNXFxaeZ.js} +1 -1
- package/dist/app/assets/{index-D5BBaLz8.js → index-BMyI9XZS.js} +1 -1
- package/dist/app/assets/{index-DlZbNvNc.js → index-DHFp2DLx.js} +1 -1
- package/dist/app/assets/{index-DjbXd602.js → index-a6hx_UrL.js} +113 -113
- package/dist/app/assets/{index.umd-DQiSWsWe.js → index.umd-Cv1NyZL8.js} +1 -1
- package/dist/app/index.html +1 -1
- package/dist/server.js +35395 -144722
- package/k6-tests/common.ts +12 -3
- package/package.json +1 -1
- package/src/controller/connection.controller.ts +82 -72
- package/src/controller/query.controller.ts +1 -1
- package/src/server.ts +6 -48
- package/src/service/connection.ts +384 -305
- package/src/service/db_utils.ts +416 -301
- package/src/service/package.spec.ts +8 -97
- package/src/service/package.ts +24 -46
- package/src/service/project.ts +8 -24
- package/src/service/project_store.ts +0 -1
- package/dist/app/assets/ProjectPage-DA66xbmQ.js +0 -1
- package/src/controller/schedule.controller.ts +0 -21
- package/src/service/scheduler.ts +0 -190
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BigQueryConnection } from "@malloydata/db-bigquery";
|
|
2
|
+
import { DuckDBConnection } from "@malloydata/db-duckdb";
|
|
2
3
|
import { MySQLConnection } from "@malloydata/db-mysql";
|
|
3
4
|
import { PostgresConnection } from "@malloydata/db-postgres";
|
|
4
5
|
import { SnowflakeConnection } from "@malloydata/db-snowflake";
|
|
@@ -10,14 +11,10 @@ import fs from "fs/promises";
|
|
|
10
11
|
import path from "path";
|
|
11
12
|
import { v4 as uuidv4 } from "uuid";
|
|
12
13
|
import { components } from "../api";
|
|
13
|
-
import {
|
|
14
|
-
convertConnectionsToApiConnections,
|
|
15
|
-
getConnectionsFromPublisherConfig,
|
|
16
|
-
} from "../config";
|
|
17
14
|
import { TEMP_DIR_PATH } from "../constants";
|
|
18
|
-
import { BadRequestError } from "../errors";
|
|
19
15
|
import { logAxiosError, logger } from "../logger";
|
|
20
16
|
|
|
17
|
+
type AttachedDatabase = components["schemas"]["AttachedDatabase"];
|
|
21
18
|
type ApiConnection = components["schemas"]["Connection"];
|
|
22
19
|
type ApiConnectionAttributes = components["schemas"]["ConnectionAttributes"];
|
|
23
20
|
type ApiConnectionStatus = components["schemas"]["ConnectionStatus"];
|
|
@@ -30,26 +27,9 @@ export type InternalConnection = ApiConnection & {
|
|
|
30
27
|
snowflakeConnection?: components["schemas"]["SnowflakeConnection"];
|
|
31
28
|
trinoConnection?: components["schemas"]["TrinoConnection"];
|
|
32
29
|
mysqlConnection?: components["schemas"]["MysqlConnection"];
|
|
30
|
+
duckdbConnection?: components["schemas"]["DuckdbConnection"];
|
|
33
31
|
};
|
|
34
32
|
|
|
35
|
-
export async function readConnectionConfig(
|
|
36
|
-
_basePath: string,
|
|
37
|
-
projectName?: string,
|
|
38
|
-
serverRootPath?: string,
|
|
39
|
-
): Promise<ApiConnection[]> {
|
|
40
|
-
// If no project name is provided, return empty array for backward compatibility
|
|
41
|
-
if (!projectName || !serverRootPath) {
|
|
42
|
-
return new Array<ApiConnection>();
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Get connections from publisher config
|
|
46
|
-
const connections = getConnectionsFromPublisherConfig(
|
|
47
|
-
serverRootPath,
|
|
48
|
-
projectName,
|
|
49
|
-
);
|
|
50
|
-
return convertConnectionsToApiConnections(connections);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
33
|
function validateAndBuildTrinoConfig(
|
|
54
34
|
trinoConfig: components["schemas"]["TrinoConnection"],
|
|
55
35
|
) {
|
|
@@ -84,28 +64,256 @@ function validateAndBuildTrinoConfig(
|
|
|
84
64
|
}
|
|
85
65
|
}
|
|
86
66
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
67
|
+
async function attachDatabasesToDuckDB(
|
|
68
|
+
duckdbConnection: DuckDBConnection,
|
|
69
|
+
attachedDatabases: AttachedDatabase[],
|
|
70
|
+
): Promise<void> {
|
|
71
|
+
for (const attachedDb of attachedDatabases) {
|
|
72
|
+
try {
|
|
73
|
+
// Check if database is already attached
|
|
74
|
+
try {
|
|
75
|
+
const checkQuery = `SHOW DATABASES`;
|
|
76
|
+
const existingDatabases = await duckdbConnection.runSQL(checkQuery);
|
|
77
|
+
const rows = Array.isArray(existingDatabases)
|
|
78
|
+
? existingDatabases
|
|
79
|
+
: existingDatabases.rows || [];
|
|
80
|
+
|
|
81
|
+
logger.debug(`Existing databases:`, rows);
|
|
82
|
+
|
|
83
|
+
// Check if the database name exists in any column (handle different column names)
|
|
84
|
+
const isAlreadyAttached = rows.some(
|
|
85
|
+
(row: Record<string, unknown>) => {
|
|
86
|
+
return Object.values(row).some(
|
|
87
|
+
(value: unknown) =>
|
|
88
|
+
typeof value === "string" && value === attachedDb.name,
|
|
89
|
+
);
|
|
90
|
+
},
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
if (isAlreadyAttached) {
|
|
94
|
+
logger.info(
|
|
95
|
+
`Database ${attachedDb.name} is already attached, skipping`,
|
|
96
|
+
);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
} catch (error) {
|
|
100
|
+
logger.warn(
|
|
101
|
+
`Failed to check existing databases, proceeding with attachment:`,
|
|
102
|
+
error,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
switch (attachedDb.type) {
|
|
107
|
+
case "bigquery": {
|
|
108
|
+
if (!attachedDb.bigqueryConnection) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
`BigQuery connection configuration is missing for attached database: ${attachedDb.name}`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Install and load the bigquery extension
|
|
115
|
+
await duckdbConnection.runSQL(
|
|
116
|
+
"INSTALL bigquery FROM community;",
|
|
117
|
+
);
|
|
118
|
+
await duckdbConnection.runSQL("LOAD bigquery;");
|
|
119
|
+
|
|
120
|
+
// Build the ATTACH command for BigQuery
|
|
121
|
+
const bigqueryConfig = attachedDb.bigqueryConnection;
|
|
122
|
+
const attachParams = new URLSearchParams();
|
|
123
|
+
|
|
124
|
+
if (!bigqueryConfig.defaultProjectId) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`BigQuery defaultProjectId is required for attached database: ${attachedDb.name}`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
attachParams.set("project", bigqueryConfig.defaultProjectId);
|
|
130
|
+
|
|
131
|
+
// Handle service account key if provided
|
|
132
|
+
if (bigqueryConfig.serviceAccountKeyJson) {
|
|
133
|
+
const serviceAccountKeyPath = path.join(
|
|
134
|
+
TEMP_DIR_PATH,
|
|
135
|
+
`duckdb-${attachedDb.name}-${uuidv4()}-service-account-key.json`,
|
|
136
|
+
);
|
|
137
|
+
await fs.writeFile(
|
|
138
|
+
serviceAccountKeyPath,
|
|
139
|
+
bigqueryConfig.serviceAccountKeyJson as string,
|
|
140
|
+
);
|
|
141
|
+
attachParams.set(
|
|
142
|
+
"service_account_key",
|
|
143
|
+
serviceAccountKeyPath,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const attachCommand = `ATTACH '${attachParams.toString()}' AS ${attachedDb.name} (TYPE bigquery, READ_ONLY);`;
|
|
148
|
+
try {
|
|
149
|
+
await duckdbConnection.runSQL(attachCommand);
|
|
150
|
+
logger.info(
|
|
151
|
+
`Successfully attached BigQuery database: ${attachedDb.name}`,
|
|
152
|
+
);
|
|
153
|
+
} catch (attachError: unknown) {
|
|
154
|
+
if (
|
|
155
|
+
attachError instanceof Error &&
|
|
156
|
+
attachError.message &&
|
|
157
|
+
attachError.message.includes("already exists")
|
|
158
|
+
) {
|
|
159
|
+
logger.info(
|
|
160
|
+
`BigQuery database ${attachedDb.name} is already attached, skipping`,
|
|
161
|
+
);
|
|
162
|
+
} else {
|
|
163
|
+
throw attachError;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
case "snowflake": {
|
|
170
|
+
if (!attachedDb.snowflakeConnection) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
`Snowflake connection configuration is missing for attached database: ${attachedDb.name}`,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Install and load the snowflake extension
|
|
177
|
+
await duckdbConnection.runSQL(
|
|
178
|
+
"INSTALL snowflake FROM community;",
|
|
179
|
+
);
|
|
180
|
+
await duckdbConnection.runSQL("LOAD snowflake;");
|
|
181
|
+
|
|
182
|
+
// Build the ATTACH command for Snowflake
|
|
183
|
+
const snowflakeConfig = attachedDb.snowflakeConnection;
|
|
184
|
+
const attachParams = new URLSearchParams();
|
|
185
|
+
|
|
186
|
+
if (snowflakeConfig.account) {
|
|
187
|
+
attachParams.set("account", snowflakeConfig.account);
|
|
188
|
+
}
|
|
189
|
+
if (snowflakeConfig.username) {
|
|
190
|
+
attachParams.set("username", snowflakeConfig.username);
|
|
191
|
+
}
|
|
192
|
+
if (snowflakeConfig.password) {
|
|
193
|
+
attachParams.set("password", snowflakeConfig.password);
|
|
194
|
+
}
|
|
195
|
+
if (snowflakeConfig.database) {
|
|
196
|
+
attachParams.set("database", snowflakeConfig.database);
|
|
197
|
+
}
|
|
198
|
+
if (snowflakeConfig.warehouse) {
|
|
199
|
+
attachParams.set("warehouse", snowflakeConfig.warehouse);
|
|
200
|
+
}
|
|
201
|
+
if (snowflakeConfig.role) {
|
|
202
|
+
attachParams.set("role", snowflakeConfig.role);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const attachCommand = `ATTACH '${attachParams.toString()}' AS ${attachedDb.name} (TYPE snowflake, READ_ONLY);`;
|
|
206
|
+
try {
|
|
207
|
+
await duckdbConnection.runSQL(attachCommand);
|
|
208
|
+
logger.info(
|
|
209
|
+
`Successfully attached Snowflake database: ${attachedDb.name}`,
|
|
210
|
+
);
|
|
211
|
+
} catch (attachError: unknown) {
|
|
212
|
+
if (
|
|
213
|
+
attachError instanceof Error &&
|
|
214
|
+
attachError.message &&
|
|
215
|
+
attachError.message.includes("already exists")
|
|
216
|
+
) {
|
|
217
|
+
logger.info(
|
|
218
|
+
`Snowflake database ${attachedDb.name} is already attached, skipping`,
|
|
219
|
+
);
|
|
220
|
+
} else {
|
|
221
|
+
throw attachError;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
case "postgres": {
|
|
228
|
+
if (!attachedDb.postgresConnection) {
|
|
229
|
+
throw new Error(
|
|
230
|
+
`PostgreSQL connection configuration is missing for attached database: ${attachedDb.name}`,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Install and load the postgres extension
|
|
235
|
+
await duckdbConnection.runSQL(
|
|
236
|
+
"INSTALL postgres FROM community;",
|
|
237
|
+
);
|
|
238
|
+
await duckdbConnection.runSQL("LOAD postgres;");
|
|
239
|
+
|
|
240
|
+
// Build the ATTACH command for PostgreSQL
|
|
241
|
+
const postgresConfig = attachedDb.postgresConnection;
|
|
242
|
+
let attachString: string;
|
|
243
|
+
|
|
244
|
+
// Use connection string if provided, otherwise build from individual parameters
|
|
245
|
+
if (postgresConfig.connectionString) {
|
|
246
|
+
attachString = postgresConfig.connectionString;
|
|
247
|
+
} else {
|
|
248
|
+
// Build connection string from individual parameters
|
|
249
|
+
const params = new URLSearchParams();
|
|
250
|
+
|
|
251
|
+
if (postgresConfig.host) {
|
|
252
|
+
params.set("host", postgresConfig.host);
|
|
253
|
+
}
|
|
254
|
+
if (postgresConfig.port) {
|
|
255
|
+
params.set("port", postgresConfig.port.toString());
|
|
256
|
+
}
|
|
257
|
+
if (postgresConfig.databaseName) {
|
|
258
|
+
params.set("dbname", postgresConfig.databaseName);
|
|
259
|
+
}
|
|
260
|
+
if (postgresConfig.userName) {
|
|
261
|
+
params.set("user", postgresConfig.userName);
|
|
262
|
+
}
|
|
263
|
+
if (postgresConfig.password) {
|
|
264
|
+
params.set("password", postgresConfig.password);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
attachString = params.toString();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const attachCommand = `ATTACH '${attachString}' AS ${attachedDb.name} (TYPE postgres, READ_ONLY);`;
|
|
271
|
+
try {
|
|
272
|
+
await duckdbConnection.runSQL(attachCommand);
|
|
273
|
+
logger.info(
|
|
274
|
+
`Successfully attached PostgreSQL database: ${attachedDb.name}`,
|
|
275
|
+
);
|
|
276
|
+
} catch (attachError: unknown) {
|
|
277
|
+
if (
|
|
278
|
+
attachError instanceof Error &&
|
|
279
|
+
attachError.message &&
|
|
280
|
+
attachError.message.includes("already exists")
|
|
281
|
+
) {
|
|
282
|
+
logger.info(
|
|
283
|
+
`PostgreSQL database ${attachedDb.name} is already attached, skipping`,
|
|
284
|
+
);
|
|
285
|
+
} else {
|
|
286
|
+
throw attachError;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
default:
|
|
293
|
+
throw new Error(
|
|
294
|
+
`Unsupported attached database type: ${attachedDb.type}`,
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
} catch (error) {
|
|
298
|
+
logger.error(`Failed to attach database ${attachedDb.name}:`, error);
|
|
299
|
+
throw new Error(
|
|
300
|
+
`Failed to attach database ${attachedDb.name}: ${(error as Error).message}`,
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export async function createProjectConnections(
|
|
307
|
+
connections: ApiConnection[] = [],
|
|
92
308
|
): Promise<{
|
|
93
309
|
malloyConnections: Map<string, BaseConnection>;
|
|
94
310
|
apiConnections: InternalConnection[];
|
|
95
311
|
}> {
|
|
96
312
|
const connectionMap = new Map<string, BaseConnection>();
|
|
97
|
-
const connectionConfig = await readConnectionConfig(
|
|
98
|
-
basePath,
|
|
99
|
-
projectName,
|
|
100
|
-
serverRootPath,
|
|
101
|
-
);
|
|
102
|
-
|
|
103
|
-
const allConnections = [...defaultConnections, ...connectionConfig];
|
|
104
|
-
|
|
105
313
|
const processedConnections = new Set<string>();
|
|
106
314
|
const apiConnections: InternalConnection[] = [];
|
|
107
315
|
|
|
108
|
-
for (const connection of
|
|
316
|
+
for (const connection of connections) {
|
|
109
317
|
if (connection.name && processedConnections.has(connection.name)) {
|
|
110
318
|
continue;
|
|
111
319
|
}
|
|
@@ -268,6 +476,12 @@ export async function createConnections(
|
|
|
268
476
|
break;
|
|
269
477
|
}
|
|
270
478
|
|
|
479
|
+
case "duckdb": {
|
|
480
|
+
// DuckDB connections are created at the package level in package.ts
|
|
481
|
+
// to ensure the workingDirectory is set correctly for each connection
|
|
482
|
+
break;
|
|
483
|
+
}
|
|
484
|
+
|
|
271
485
|
default: {
|
|
272
486
|
throw new Error(`Unsupported connection type: ${connection.type}`);
|
|
273
487
|
}
|
|
@@ -283,6 +497,105 @@ export async function createConnections(
|
|
|
283
497
|
};
|
|
284
498
|
}
|
|
285
499
|
|
|
500
|
+
/**
|
|
501
|
+
* DuckDB connections need to be instantiated at the package level to ensure
|
|
502
|
+
* the workingDirectory is set correctly. This allows DuckDB to properly resolve
|
|
503
|
+
* relative paths for database files and attached databases within the project context.
|
|
504
|
+
*/
|
|
505
|
+
export async function createPackageDuckDBConnections(
|
|
506
|
+
connections: ApiConnection[] = [],
|
|
507
|
+
packagePath: string,
|
|
508
|
+
): Promise<{
|
|
509
|
+
malloyConnections: Map<string, BaseConnection>;
|
|
510
|
+
apiConnections: InternalConnection[];
|
|
511
|
+
}> {
|
|
512
|
+
const connectionMap = new Map<string, BaseConnection>();
|
|
513
|
+
|
|
514
|
+
const processedConnections = new Set<string>();
|
|
515
|
+
const apiConnections: InternalConnection[] = [];
|
|
516
|
+
|
|
517
|
+
for (const connection of connections) {
|
|
518
|
+
// Only process DuckDB connections
|
|
519
|
+
if (connection.type !== "duckdb") {
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (connection.name && processedConnections.has(connection.name)) {
|
|
524
|
+
throw new Error(
|
|
525
|
+
`CreatePackageDuckDBConnections only supports one DuckDB connection per name, got ${connection.name}`,
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (!connection.name) {
|
|
530
|
+
throw "Invalid connection configuration. No name.";
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
logger.info(`Adding DuckDB connection ${connection.name}`, {
|
|
534
|
+
connection,
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
processedConnections.add(connection.name);
|
|
538
|
+
|
|
539
|
+
if (!connection.duckdbConnection) {
|
|
540
|
+
throw new Error("DuckDB connection configuration is missing.");
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Create DuckDB connection with project basePath as working directory
|
|
544
|
+
// This ensures relative paths in the project are resolved correctly
|
|
545
|
+
const duckdbConnection = new DuckDBConnection(
|
|
546
|
+
connection.name,
|
|
547
|
+
":memory:",
|
|
548
|
+
packagePath,
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
// Attach databases if configured
|
|
552
|
+
if (
|
|
553
|
+
connection.duckdbConnection.attachedDatabases &&
|
|
554
|
+
Array.isArray(connection.duckdbConnection.attachedDatabases) &&
|
|
555
|
+
connection.duckdbConnection.attachedDatabases.length > 0
|
|
556
|
+
) {
|
|
557
|
+
await attachDatabasesToDuckDB(
|
|
558
|
+
duckdbConnection,
|
|
559
|
+
connection.duckdbConnection.attachedDatabases,
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
connectionMap.set(connection.name, duckdbConnection);
|
|
564
|
+
connection.attributes = getConnectionAttributes(duckdbConnection);
|
|
565
|
+
|
|
566
|
+
// Add the connection to apiConnections (this will be sanitized when returned)
|
|
567
|
+
apiConnections.push(connection);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Create default "duckdb" connection if it doesn't exist
|
|
571
|
+
if (!connectionMap.has("duckdb")) {
|
|
572
|
+
const defaultDuckDBConnection = new DuckDBConnection(
|
|
573
|
+
"duckdb",
|
|
574
|
+
":memory:",
|
|
575
|
+
packagePath,
|
|
576
|
+
);
|
|
577
|
+
connectionMap.set("duckdb", defaultDuckDBConnection);
|
|
578
|
+
|
|
579
|
+
// Create API connection for the default DuckDB connection
|
|
580
|
+
const defaultApiConnection: ApiConnection = {
|
|
581
|
+
name: "duckdb",
|
|
582
|
+
type: "duckdb",
|
|
583
|
+
duckdbConnection: {
|
|
584
|
+
attachedDatabases: [],
|
|
585
|
+
},
|
|
586
|
+
};
|
|
587
|
+
defaultApiConnection.attributes = getConnectionAttributes(
|
|
588
|
+
defaultDuckDBConnection,
|
|
589
|
+
);
|
|
590
|
+
apiConnections.push(defaultApiConnection);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
return {
|
|
594
|
+
malloyConnections: connectionMap,
|
|
595
|
+
apiConnections: apiConnections,
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
|
|
286
599
|
function getConnectionAttributes(
|
|
287
600
|
connection: Connection,
|
|
288
601
|
): ApiConnectionAttributes {
|
|
@@ -303,285 +616,51 @@ function getConnectionAttributes(
|
|
|
303
616
|
export async function testConnectionConfig(
|
|
304
617
|
connectionConfig: ApiConnection,
|
|
305
618
|
): Promise<ApiConnectionStatus> {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
if (!connectionConfig.postgresConnection) {
|
|
311
|
-
throw new Error(
|
|
312
|
-
"Invalid connection configuration. No postgres connection.",
|
|
313
|
-
);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
const postgresConfig = connectionConfig.postgresConnection;
|
|
317
|
-
if (
|
|
318
|
-
!postgresConfig.connectionString &&
|
|
319
|
-
(!postgresConfig.host ||
|
|
320
|
-
!postgresConfig.port ||
|
|
321
|
-
!postgresConfig.userName ||
|
|
322
|
-
!postgresConfig.databaseName)
|
|
323
|
-
) {
|
|
324
|
-
throw new Error(
|
|
325
|
-
"PostgreSQL connection requires: either all of host, port, userName, and databaseName, or connectionString",
|
|
326
|
-
);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
const configReader = async () => {
|
|
330
|
-
return {
|
|
331
|
-
host: postgresConfig.host,
|
|
332
|
-
port: postgresConfig.port,
|
|
333
|
-
username: postgresConfig.userName,
|
|
334
|
-
password: postgresConfig.password,
|
|
335
|
-
databaseName: postgresConfig.databaseName,
|
|
336
|
-
connectionString: postgresConfig.connectionString,
|
|
337
|
-
};
|
|
338
|
-
};
|
|
339
|
-
|
|
340
|
-
const postgresConnection = new PostgresConnection(
|
|
341
|
-
"testConnection",
|
|
342
|
-
() => ({}),
|
|
343
|
-
configReader,
|
|
344
|
-
);
|
|
345
|
-
|
|
346
|
-
try {
|
|
347
|
-
await postgresConnection.test();
|
|
348
|
-
testResult = { status: "ok" };
|
|
349
|
-
} catch (error) {
|
|
350
|
-
if (error instanceof AxiosError) {
|
|
351
|
-
logAxiosError(error);
|
|
352
|
-
} else {
|
|
353
|
-
logger.error(error);
|
|
354
|
-
}
|
|
355
|
-
testResult = {
|
|
356
|
-
status: "failed",
|
|
357
|
-
errorMessage: (error as Error).message,
|
|
358
|
-
};
|
|
359
|
-
}
|
|
360
|
-
break;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
case "snowflake": {
|
|
364
|
-
if (!connectionConfig.snowflakeConnection) {
|
|
365
|
-
throw new Error(
|
|
366
|
-
"Invalid connection configuration. No snowflake connection.",
|
|
367
|
-
);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
const snowflakeConfig = connectionConfig.snowflakeConnection;
|
|
371
|
-
if (
|
|
372
|
-
!snowflakeConfig.account ||
|
|
373
|
-
!snowflakeConfig.username ||
|
|
374
|
-
!snowflakeConfig.password ||
|
|
375
|
-
!snowflakeConfig.warehouse
|
|
376
|
-
) {
|
|
377
|
-
throw new Error(
|
|
378
|
-
"Snowflake connection requires: account, username, password, warehouse, database, and schema",
|
|
379
|
-
);
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
const snowflakeConnectionOptions = {
|
|
383
|
-
connOptions: {
|
|
384
|
-
account: snowflakeConfig.account,
|
|
385
|
-
username: snowflakeConfig.username,
|
|
386
|
-
password: snowflakeConfig.password,
|
|
387
|
-
warehouse: snowflakeConfig.warehouse,
|
|
388
|
-
database: snowflakeConfig.database,
|
|
389
|
-
schema: snowflakeConfig.schema,
|
|
390
|
-
role: snowflakeConfig.role,
|
|
391
|
-
timeout: snowflakeConfig.responseTimeoutMilliseconds,
|
|
392
|
-
},
|
|
393
|
-
};
|
|
394
|
-
const snowflakeConnection = new SnowflakeConnection(
|
|
395
|
-
"testConnection",
|
|
396
|
-
snowflakeConnectionOptions,
|
|
397
|
-
);
|
|
398
|
-
|
|
399
|
-
try {
|
|
400
|
-
await snowflakeConnection.test();
|
|
401
|
-
testResult = { status: "ok" };
|
|
402
|
-
} catch (error) {
|
|
403
|
-
if (error instanceof AxiosError) {
|
|
404
|
-
logAxiosError(error);
|
|
405
|
-
} else {
|
|
406
|
-
logger.error(error);
|
|
407
|
-
}
|
|
408
|
-
testResult = {
|
|
409
|
-
status: "failed",
|
|
410
|
-
errorMessage: (error as Error).message,
|
|
411
|
-
};
|
|
412
|
-
}
|
|
413
|
-
break;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
case "bigquery": {
|
|
417
|
-
if (!connectionConfig.bigqueryConnection) {
|
|
418
|
-
throw new Error(
|
|
419
|
-
"Invalid connection configuration. No bigquery connection.",
|
|
420
|
-
);
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
const bigqueryConfig = connectionConfig.bigqueryConnection;
|
|
424
|
-
let serviceAccountKeyPath = undefined;
|
|
425
|
-
try {
|
|
426
|
-
if (bigqueryConfig.serviceAccountKeyJson) {
|
|
427
|
-
serviceAccountKeyPath = path.join(
|
|
428
|
-
TEMP_DIR_PATH,
|
|
429
|
-
`test-${uuidv4()}-service-account-key.json`,
|
|
430
|
-
);
|
|
431
|
-
await fs.writeFile(
|
|
432
|
-
serviceAccountKeyPath,
|
|
433
|
-
bigqueryConfig.serviceAccountKeyJson as string,
|
|
434
|
-
);
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
const bigqueryConnectionOptions = {
|
|
438
|
-
projectId: connectionConfig.bigqueryConnection.defaultProjectId,
|
|
439
|
-
serviceAccountKeyPath: serviceAccountKeyPath,
|
|
440
|
-
location: connectionConfig.bigqueryConnection.location,
|
|
441
|
-
maximumBytesBilled:
|
|
442
|
-
connectionConfig.bigqueryConnection.maximumBytesBilled,
|
|
443
|
-
timeoutMs:
|
|
444
|
-
connectionConfig.bigqueryConnection.queryTimeoutMilliseconds,
|
|
445
|
-
billingProjectId:
|
|
446
|
-
connectionConfig.bigqueryConnection.billingProjectId,
|
|
447
|
-
};
|
|
448
|
-
const bigqueryConnection = new BigQueryConnection(
|
|
449
|
-
"testConnection",
|
|
450
|
-
() => ({}),
|
|
451
|
-
bigqueryConnectionOptions,
|
|
452
|
-
);
|
|
453
|
-
|
|
454
|
-
await bigqueryConnection.test();
|
|
455
|
-
testResult = { status: "ok" };
|
|
456
|
-
} catch (error) {
|
|
457
|
-
if (error instanceof AxiosError) {
|
|
458
|
-
logAxiosError(error);
|
|
459
|
-
} else {
|
|
460
|
-
logger.error(error);
|
|
461
|
-
}
|
|
462
|
-
testResult = {
|
|
463
|
-
status: "failed",
|
|
464
|
-
errorMessage: (error as Error).message,
|
|
465
|
-
};
|
|
466
|
-
} finally {
|
|
467
|
-
try {
|
|
468
|
-
if (serviceAccountKeyPath) {
|
|
469
|
-
await fs.unlink(serviceAccountKeyPath);
|
|
470
|
-
}
|
|
471
|
-
} catch (cleanupError) {
|
|
472
|
-
logger.warn(
|
|
473
|
-
`Failed to cleanup temporary file ${serviceAccountKeyPath}:`,
|
|
474
|
-
cleanupError,
|
|
475
|
-
);
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
break;
|
|
619
|
+
try {
|
|
620
|
+
// Validate that connection name is provided
|
|
621
|
+
if (!connectionConfig.name) {
|
|
622
|
+
throw new Error("Connection name is required");
|
|
479
623
|
}
|
|
480
624
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
const trinoConfig = connectionConfig.trinoConnection;
|
|
487
|
-
if (
|
|
488
|
-
!trinoConfig.server ||
|
|
489
|
-
!trinoConfig.port ||
|
|
490
|
-
!trinoConfig.catalog ||
|
|
491
|
-
!trinoConfig.schema ||
|
|
492
|
-
!trinoConfig.user
|
|
493
|
-
) {
|
|
494
|
-
throw new Error(
|
|
495
|
-
"Trino connection requires server, port, catalog, schema, and user",
|
|
496
|
-
);
|
|
497
|
-
}
|
|
625
|
+
// Use createProjectConnections to create the connection, then test it
|
|
626
|
+
// TODO: Test duckdb connections?
|
|
627
|
+
const { malloyConnections } = await createProjectConnections(
|
|
628
|
+
[connectionConfig], // Pass the single connection config
|
|
629
|
+
);
|
|
498
630
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
{}
|
|
504
|
-
trinoConnectionOptions,
|
|
631
|
+
// Get the created connection
|
|
632
|
+
const connection = malloyConnections.get(connectionConfig.name);
|
|
633
|
+
if (!connection) {
|
|
634
|
+
throw new Error(
|
|
635
|
+
`Failed to create connection: ${connectionConfig.name}`,
|
|
505
636
|
);
|
|
506
|
-
|
|
507
|
-
try {
|
|
508
|
-
await trinoConnection.test();
|
|
509
|
-
testResult = { status: "ok" };
|
|
510
|
-
} catch (error) {
|
|
511
|
-
if (error instanceof AxiosError) {
|
|
512
|
-
logAxiosError(error);
|
|
513
|
-
} else {
|
|
514
|
-
logger.error(error);
|
|
515
|
-
}
|
|
516
|
-
testResult = {
|
|
517
|
-
status: "failed",
|
|
518
|
-
errorMessage: (error as Error).message,
|
|
519
|
-
};
|
|
520
|
-
}
|
|
521
|
-
break;
|
|
522
637
|
}
|
|
523
638
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
!connectionConfig.mysqlConnection.database
|
|
535
|
-
) {
|
|
536
|
-
throw new Error(
|
|
537
|
-
"MySQL connection requires: host, port, user, password, and database",
|
|
538
|
-
);
|
|
539
|
-
}
|
|
639
|
+
// Test the connection - cast to union type of connection classes that have test method
|
|
640
|
+
await (
|
|
641
|
+
connection as
|
|
642
|
+
| PostgresConnection
|
|
643
|
+
| BigQueryConnection
|
|
644
|
+
| SnowflakeConnection
|
|
645
|
+
| TrinoConnection
|
|
646
|
+
| MySQLConnection
|
|
647
|
+
| DuckDBConnection
|
|
648
|
+
).test();
|
|
540
649
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
"testConnection",
|
|
551
|
-
mysqlConnectionOptions,
|
|
552
|
-
);
|
|
553
|
-
try {
|
|
554
|
-
await mysqlConnection.test();
|
|
555
|
-
testResult = { status: "ok" };
|
|
556
|
-
} catch (error) {
|
|
557
|
-
if (error instanceof AxiosError) {
|
|
558
|
-
logAxiosError(error);
|
|
559
|
-
} else {
|
|
560
|
-
logger.error(error);
|
|
561
|
-
}
|
|
562
|
-
testResult = {
|
|
563
|
-
status: "failed",
|
|
564
|
-
errorMessage: (error as Error).message,
|
|
565
|
-
};
|
|
566
|
-
}
|
|
567
|
-
break;
|
|
650
|
+
return {
|
|
651
|
+
status: "ok",
|
|
652
|
+
errorMessage: "",
|
|
653
|
+
};
|
|
654
|
+
} catch (error) {
|
|
655
|
+
if (error instanceof AxiosError) {
|
|
656
|
+
logAxiosError(error);
|
|
657
|
+
} else {
|
|
658
|
+
logger.error(error);
|
|
568
659
|
}
|
|
569
660
|
|
|
570
|
-
default:
|
|
571
|
-
throw new BadRequestError(
|
|
572
|
-
`Unsupported connection type: ${connectionConfig.type}`,
|
|
573
|
-
);
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
if (testResult.status === "failed") {
|
|
577
661
|
return {
|
|
578
662
|
status: "failed",
|
|
579
|
-
errorMessage:
|
|
663
|
+
errorMessage: (error as Error).message,
|
|
580
664
|
};
|
|
581
665
|
}
|
|
582
|
-
|
|
583
|
-
return {
|
|
584
|
-
status: "ok",
|
|
585
|
-
errorMessage: "",
|
|
586
|
-
};
|
|
587
666
|
}
|