@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.
Files changed (28) hide show
  1. package/dist/app/api-doc.yaml +324 -335
  2. package/dist/app/assets/{HomePage-BxFnfH3M.js → HomePage-z6NLKLPp.js} +1 -1
  3. package/dist/app/assets/{MainPage-D301Y0mT.js → MainPage-C9McOjLb.js} +2 -2
  4. package/dist/app/assets/{ModelPage-Df8ivC1J.js → ModelPage-DjlTuT2G.js} +1 -1
  5. package/dist/app/assets/{PackagePage-CE41SCV_.js → PackagePage-CDh_gnAZ.js} +1 -1
  6. package/dist/app/assets/ProjectPage-vyvZZWAB.js +1 -0
  7. package/dist/app/assets/{RouteError-l_WGtNhS.js → RouteError-FbxztVnz.js} +1 -1
  8. package/dist/app/assets/{WorkbookPage-CY-1oBvt.js → WorkbookPage-DNXFxaeZ.js} +1 -1
  9. package/dist/app/assets/{index-D5BBaLz8.js → index-BMyI9XZS.js} +1 -1
  10. package/dist/app/assets/{index-DlZbNvNc.js → index-DHFp2DLx.js} +1 -1
  11. package/dist/app/assets/{index-DjbXd602.js → index-a6hx_UrL.js} +113 -113
  12. package/dist/app/assets/{index.umd-DQiSWsWe.js → index.umd-Cv1NyZL8.js} +1 -1
  13. package/dist/app/index.html +1 -1
  14. package/dist/server.js +35395 -144722
  15. package/k6-tests/common.ts +12 -3
  16. package/package.json +1 -1
  17. package/src/controller/connection.controller.ts +82 -72
  18. package/src/controller/query.controller.ts +1 -1
  19. package/src/server.ts +6 -48
  20. package/src/service/connection.ts +384 -305
  21. package/src/service/db_utils.ts +416 -301
  22. package/src/service/package.spec.ts +8 -97
  23. package/src/service/package.ts +24 -46
  24. package/src/service/project.ts +8 -24
  25. package/src/service/project_store.ts +0 -1
  26. package/dist/app/assets/ProjectPage-DA66xbmQ.js +0 -1
  27. package/src/controller/schedule.controller.ts +0 -21
  28. 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
- export async function createConnections(
88
- basePath: string,
89
- defaultConnections: ApiConnection[] = [],
90
- projectName?: string,
91
- serverRootPath?: string,
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 allConnections) {
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
- let testResult: { status: "ok" | "failed"; errorMessage?: string };
307
-
308
- switch (connectionConfig.type) {
309
- case "postgres": {
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
- case "trino": {
482
- if (!connectionConfig.trinoConnection) {
483
- throw new Error("Trino connection configuration is missing.");
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
- const trinoConnectionOptions =
500
- validateAndBuildTrinoConfig(trinoConfig);
501
- const trinoConnection = new TrinoConnection(
502
- "testConnection",
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
- case "mysql": {
525
- if (!connectionConfig.mysqlConnection) {
526
- throw new Error("MySQL connection configuration is missing.");
527
- }
528
-
529
- if (
530
- !connectionConfig.mysqlConnection.host ||
531
- !connectionConfig.mysqlConnection.port ||
532
- !connectionConfig.mysqlConnection.user ||
533
- !connectionConfig.mysqlConnection.password ||
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
- const mysqlConfig = connectionConfig.mysqlConnection;
542
- const mysqlConnectionOptions = {
543
- host: mysqlConfig.host,
544
- port: mysqlConfig.port,
545
- user: mysqlConfig.user,
546
- password: mysqlConfig.password,
547
- database: mysqlConfig.database,
548
- };
549
- const mysqlConnection = new MySQLConnection(
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: testResult.errorMessage || "Connection test failed",
663
+ errorMessage: (error as Error).message,
580
664
  };
581
665
  }
582
-
583
- return {
584
- status: "ok",
585
- errorMessage: "",
586
- };
587
666
  }