@malloy-publisher/server 0.0.195 → 0.0.197-dev

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 (100) hide show
  1. package/dist/app/api-doc.yaml +213 -214
  2. package/dist/app/assets/EnvironmentPage-1j6QDWAy.js +1 -0
  3. package/dist/app/assets/HomePage-DMop21VG.js +1 -0
  4. package/dist/app/assets/MainPage-BbE8ETz1.js +2 -0
  5. package/dist/app/assets/ModelPage-D2jvfe3t.js +1 -0
  6. package/dist/app/assets/PackagePage-BbnhGoD3.js +1 -0
  7. package/dist/app/assets/{RouteError-DefbDO7F.js → RouteError-D3LGEZ3i.js} +1 -1
  8. package/dist/app/assets/WorkbookPage-DttVIj4u.js +1 -0
  9. package/dist/app/assets/{core-BrfQApxh.es-DnvCX4oH.js → core-w79IMXAG.es-Bd0UlzOL.js} +1 -1
  10. package/dist/app/assets/{index-Bu0ub036.js → index-5K9YjIxF.js} +117 -117
  11. package/dist/app/assets/{index-CkzK3JIl.js → index-C513UodQ.js} +1 -1
  12. package/dist/app/assets/{index-CoA6HIGS.js → index-DIgzgp69.js} +1 -1
  13. package/dist/app/assets/{index.umd-B6Ms2PpL.js → index.umd-BMeMPq_9.js} +1 -1
  14. package/dist/app/index.html +1 -1
  15. package/dist/server.mjs +1352 -1310
  16. package/package.json +2 -2
  17. package/publisher.config.json +2 -2
  18. package/src/config.spec.ts +74 -66
  19. package/src/config.ts +50 -47
  20. package/src/controller/compile.controller.ts +10 -7
  21. package/src/controller/connection.controller.ts +79 -58
  22. package/src/controller/database.controller.ts +10 -7
  23. package/src/controller/manifest.controller.ts +23 -14
  24. package/src/controller/materialization.controller.ts +14 -14
  25. package/src/controller/model.controller.ts +35 -20
  26. package/src/controller/package.controller.ts +83 -49
  27. package/src/controller/query.controller.ts +11 -8
  28. package/src/controller/watch-mode.controller.ts +35 -29
  29. package/src/errors.ts +2 -2
  30. package/src/mcp/error_messages.ts +2 -2
  31. package/src/mcp/handler_utils.ts +23 -20
  32. package/src/mcp/mcp_constants.ts +1 -1
  33. package/src/mcp/prompts/handlers.ts +3 -3
  34. package/src/mcp/prompts/prompt_service.ts +5 -5
  35. package/src/mcp/prompts/utils.ts +12 -12
  36. package/src/mcp/resource_metadata.ts +3 -3
  37. package/src/mcp/resources/environment_resource.ts +187 -0
  38. package/src/mcp/resources/model_resource.ts +19 -17
  39. package/src/mcp/resources/notebook_resource.ts +13 -13
  40. package/src/mcp/resources/package_resource.ts +30 -27
  41. package/src/mcp/resources/query_resource.ts +15 -10
  42. package/src/mcp/resources/source_resource.ts +10 -10
  43. package/src/mcp/resources/view_resource.ts +11 -11
  44. package/src/mcp/server.ts +16 -14
  45. package/src/mcp/tools/discovery_tools.ts +67 -49
  46. package/src/mcp/tools/execute_query_tool.ts +14 -14
  47. package/src/server.ts +175 -159
  48. package/src/service/connection.spec.ts +158 -133
  49. package/src/service/connection.ts +42 -39
  50. package/src/service/connection_config.spec.ts +13 -11
  51. package/src/service/connection_config.ts +28 -19
  52. package/src/service/connection_service.spec.ts +63 -43
  53. package/src/service/connection_service.ts +106 -89
  54. package/src/service/{project.ts → environment.ts} +92 -77
  55. package/src/service/{project_compile.spec.ts → environment_compile.spec.ts} +1 -1
  56. package/src/service/{project_store.spec.ts → environment_store.spec.ts} +99 -83
  57. package/src/service/{project_store.ts → environment_store.ts} +373 -327
  58. package/src/service/manifest_service.spec.ts +15 -15
  59. package/src/service/manifest_service.ts +26 -21
  60. package/src/service/materialization_service.spec.ts +93 -59
  61. package/src/service/materialization_service.ts +71 -62
  62. package/src/service/materialized_table_gc.spec.ts +15 -15
  63. package/src/service/materialized_table_gc.ts +3 -3
  64. package/src/service/model.ts +4 -4
  65. package/src/service/package.spec.ts +2 -2
  66. package/src/service/package.ts +23 -21
  67. package/src/service/resolve_environment.ts +15 -0
  68. package/src/storage/DatabaseInterface.ts +34 -25
  69. package/src/storage/StorageManager.mock.ts +3 -3
  70. package/src/storage/StorageManager.ts +64 -28
  71. package/src/storage/duckdb/ConnectionRepository.ts +13 -11
  72. package/src/storage/duckdb/DuckDBConnection.ts +1 -1
  73. package/src/storage/duckdb/DuckDBManifestStore.ts +6 -6
  74. package/src/storage/duckdb/DuckDBRepository.ts +47 -47
  75. package/src/storage/duckdb/{ProjectRepository.ts → EnvironmentRepository.ts} +35 -35
  76. package/src/storage/duckdb/ManifestRepository.ts +21 -20
  77. package/src/storage/duckdb/MaterializationRepository.ts +31 -28
  78. package/src/storage/duckdb/PackageRepository.ts +11 -11
  79. package/src/storage/duckdb/manifest_store.spec.ts +2 -2
  80. package/src/storage/duckdb/schema.ts +20 -20
  81. package/src/storage/ducklake/DuckLakeManifestStore.ts +20 -11
  82. package/tests/fixtures/publisher.config.json +1 -1
  83. package/tests/harness/e2e.ts +1 -1
  84. package/tests/harness/mcp_test_setup.ts +12 -24
  85. package/tests/harness/mocks.ts +10 -8
  86. package/tests/integration/materialization/materialization_lifecycle.integration.spec.ts +4 -4
  87. package/tests/integration/mcp/mcp_execute_query_tool.integration.spec.ts +28 -49
  88. package/tests/integration/mcp/mcp_resource.integration.spec.ts +39 -47
  89. package/tests/integration/mcp/mcp_transport.integration.spec.ts +1 -1
  90. package/tests/unit/duckdb/attached_databases.test.ts +51 -33
  91. package/tests/unit/ducklake/ducklake.test.ts +24 -22
  92. package/tests/unit/mcp/prompt_happy.test.ts +8 -8
  93. package/dist/app/assets/HomePage-DbZS0N7G.js +0 -1
  94. package/dist/app/assets/MainPage-CBuWkbmr.js +0 -2
  95. package/dist/app/assets/ModelPage-Bt37smot.js +0 -1
  96. package/dist/app/assets/PackagePage-DLZe50WG.js +0 -1
  97. package/dist/app/assets/ProjectPage-FQTEPXP4.js +0 -1
  98. package/dist/app/assets/WorkbookPage-CkAo16ar.js +0 -1
  99. package/src/mcp/resources/project_resource.ts +0 -184
  100. package/src/service/resolve_project.ts +0 -13
@@ -20,7 +20,8 @@ interface PackageContentEntry {
20
20
  }
21
21
 
22
22
  // --- Test Suite ---
23
- describe("MCP Resource Handlers (E2E Integration)", () => {
23
+ // Serial: one shared MCP client; concurrent it() blocks can interleave StreamableHTTP requests.
24
+ describe.serial("MCP Resource Handlers (E2E Integration)", () => {
24
25
  let env: McpE2ETestEnvironment | null = null;
25
26
  let mcpClient: Client;
26
27
 
@@ -36,29 +37,29 @@ describe("MCP Resource Handlers (E2E Integration)", () => {
36
37
  });
37
38
 
38
39
  // --- Test Constants ---
39
- const homeProjectUri = "malloy://project/malloy-samples";
40
- const faaPackageUri = "malloy://project/malloy-samples/package/faa";
40
+ const homeProjectUri = "malloy://environment/malloy-samples";
41
+ const faaPackageUri = "malloy://environment/malloy-samples/package/faa";
41
42
  const flightsModelUri =
42
- "malloy://project/malloy-samples/package/faa/models/flights.malloy";
43
+ "malloy://environment/malloy-samples/package/faa/models/flights.malloy";
43
44
  const FLIGHTS_SOURCE = "flights";
44
45
  const FLIGHTS_CARRIER_QUERY = "flights_by_carrier";
45
46
  const FLIGHTS_MONTH_VIEW = "flights_by_month";
46
47
  const OVERVIEW_NOTEBOOK = "overview.malloynb";
47
48
  const nonExistentPackageUri =
48
- "malloy://project/malloy-samples/package/nonexistent";
49
+ "malloy://environment/malloy-samples/package/nonexistent";
49
50
  const nonExistentModelUri =
50
- "malloy://project/malloy-samples/package/faa/models/nonexistent.malloy";
51
- const nonExistentProjectUri = "malloy://project/invalid_project";
51
+ "malloy://environment/malloy-samples/package/faa/models/nonexistent.malloy";
52
+ const nonExistentProjectUri = "malloy://environment/invalid_project";
52
53
  const invalidUri = "invalid://format";
53
54
 
54
- const validSourceUri = `malloy://project/malloy-samples/package/faa/models/flights.malloy/sources/${FLIGHTS_SOURCE}`;
55
- const validQueryUri = `malloy://project/malloy-samples/package/faa/models/flights.malloy/queries/${FLIGHTS_CARRIER_QUERY}`;
56
- const validViewUri = `malloy://project/malloy-samples/package/faa/models/flights.malloy/sources/${FLIGHTS_SOURCE}/views/${FLIGHTS_MONTH_VIEW}`;
57
- const validNotebookUri = `malloy://project/malloy-samples/package/faa/notebooks/${OVERVIEW_NOTEBOOK}`;
58
- const nonExistentSourceUri = `malloy://project/malloy-samples/package/faa/models/flights.malloy/sources/non_existent_source`;
59
- const nonExistentQueryUri = `malloy://project/malloy-samples/package/faa/models/flights.malloy/queries/non_existent_query`;
60
- const nonExistentViewUri = `malloy://project/malloy-samples/package/faa/models/flights.malloy/sources/${FLIGHTS_SOURCE}/views/non_existent_view`;
61
- const nonExistentNotebookUri = `malloy://project/malloy-samples/package/faa/notebooks/non_existent.malloynb`;
55
+ const validSourceUri = `malloy://environment/malloy-samples/package/faa/models/flights.malloy/sources/${FLIGHTS_SOURCE}`;
56
+ const validQueryUri = `malloy://environment/malloy-samples/package/faa/models/flights.malloy/queries/${FLIGHTS_CARRIER_QUERY}`;
57
+ const validViewUri = `malloy://environment/malloy-samples/package/faa/models/flights.malloy/sources/${FLIGHTS_SOURCE}/views/${FLIGHTS_MONTH_VIEW}`;
58
+ const validNotebookUri = `malloy://environment/malloy-samples/package/faa/notebooks/${OVERVIEW_NOTEBOOK}`;
59
+ const nonExistentSourceUri = `malloy://environment/malloy-samples/package/faa/models/flights.malloy/sources/non_existent_source`;
60
+ const nonExistentQueryUri = `malloy://environment/malloy-samples/package/faa/models/flights.malloy/queries/non_existent_query`;
61
+ const nonExistentViewUri = `malloy://environment/malloy-samples/package/faa/models/flights.malloy/sources/${FLIGHTS_SOURCE}/views/non_existent_view`;
62
+ const nonExistentNotebookUri = `malloy://environment/malloy-samples/package/faa/notebooks/non_existent.malloynb`;
62
63
 
63
64
  describe("client.listResources", () => {
64
65
  it(
@@ -85,17 +86,17 @@ describe("MCP Resource Handlers (E2E Integration)", () => {
85
86
  expect(faaPackageEntry).toBeDefined();
86
87
  expect(faaPackageEntry?.name).toBe("faa");
87
88
 
88
- const firstResource = result.resources[0];
89
- expect(firstResource).toBeDefined();
90
- expect(firstResource.uri).toBeDefined();
91
- expect(typeof firstResource.uri).toBe("string");
92
- expect(firstResource.uri).toMatch(/^malloy:\/\//);
93
- expect(firstResource.name).toBeDefined();
94
- expect(typeof firstResource.name).toBe("string");
95
- expect(firstResource.description).toBeDefined();
96
- expect(typeof firstResource.description).toBe("string");
97
- if (firstResource.mimeType) {
98
- expect(typeof firstResource.mimeType).toBe("string");
89
+ // Assert shape on a known row (order of listResources is not guaranteed).
90
+ const sample = faaPackageEntry!;
91
+ expect(sample.uri).toBeDefined();
92
+ expect(typeof sample.uri).toBe("string");
93
+ expect(sample.uri).toMatch(/^malloy:\/\//);
94
+ expect(sample.name).toBeDefined();
95
+ expect(typeof sample.name).toBe("string");
96
+ expect(sample.description).toBeDefined();
97
+ expect(typeof sample.description).toBe("string");
98
+ if (sample.mimeType) {
99
+ expect(typeof sample.mimeType).toBe("string");
99
100
  }
100
101
  },
101
102
  { timeout: 30000 },
@@ -216,7 +217,7 @@ describe("MCP Resource Handlers (E2E Integration)", () => {
216
217
  );
217
218
  if (flightsEntry) {
218
219
  expect(flightsEntry.uri).toBe(
219
- "malloy://project/malloy-samples/package/faa/sources/flights.malloy",
220
+ "malloy://environment/malloy-samples/package/faa/sources/flights.malloy",
220
221
  );
221
222
  expect(flightsEntry.metadata).toBeDefined();
222
223
  expect(flightsEntry.metadata!.description).toContain(
@@ -231,7 +232,7 @@ describe("MCP Resource Handlers (E2E Integration)", () => {
231
232
  );
232
233
  if (notebookEntry) {
233
234
  expect(notebookEntry.uri).toBe(
234
- "malloy://project/malloy-samples/package/faa/notebooks/aircraft_analysis.malloynb",
235
+ "malloy://environment/malloy-samples/package/faa/notebooks/aircraft_analysis.malloynb",
235
236
  );
236
237
  expect(notebookEntry.metadata).toBeDefined();
237
238
  expect(notebookEntry.metadata!.description).toContain(
@@ -504,7 +505,7 @@ describe("MCP Resource Handlers (E2E Integration)", () => {
504
505
  // Adjust test to expect the generic "Resource not found" error, as the specific
505
506
  // "not a notebook" detail isn't easily surfaced in the standard error format.
506
507
  expect(errorPayload.error).toMatch(/Notebook 'overview.malloynb'/);
507
- expect(errorPayload.error).toMatch(/project 'malloy-samples'/); // Check project name
508
+ expect(errorPayload.error).toMatch(/environment 'malloy-samples'/);
508
509
  expect(errorPayload.suggestions).toBeDefined();
509
510
  expect(Array.isArray(errorPayload.suggestions)).toBe(true);
510
511
  expect(errorPayload.suggestions.length).toBeGreaterThan(0);
@@ -531,8 +532,7 @@ describe("MCP Resource Handlers (E2E Integration)", () => {
531
532
  expect(errorPayload.error).toMatch(/Resource not found/i);
532
533
  // Check for the specific source name in the message
533
534
  expect(errorPayload.error).toMatch(/Source 'non_existent_source'/);
534
- // Adjust project name expectation
535
- expect(errorPayload.error).toMatch(/project 'malloy-samples'/);
535
+ expect(errorPayload.error).toMatch(/environment 'malloy-samples'/);
536
536
  expect(errorPayload.suggestions).toBeDefined();
537
537
  expect(Array.isArray(errorPayload.suggestions)).toBe(true);
538
538
  expect(errorPayload.suggestions.length).toBeGreaterThan(0);
@@ -557,8 +557,7 @@ describe("MCP Resource Handlers (E2E Integration)", () => {
557
557
  expect(errorPayload.error).toMatch(/Resource not found/i);
558
558
  // Check for the specific query name in the message
559
559
  expect(errorPayload.error).toMatch(/Query 'non_existent_query'/);
560
- // Adjust project name expectation
561
- expect(errorPayload.error).toMatch(/project 'malloy-samples'/);
560
+ expect(errorPayload.error).toMatch(/environment 'malloy-samples'/);
562
561
  expect(errorPayload.suggestions).toBeDefined();
563
562
  expect(Array.isArray(errorPayload.suggestions)).toBe(true);
564
563
  expect(errorPayload.suggestions.length).toBeGreaterThan(0);
@@ -583,8 +582,7 @@ describe("MCP Resource Handlers (E2E Integration)", () => {
583
582
  expect(errorPayload.error).toMatch(/Resource not found/i);
584
583
  // Check for the specific view name in the message
585
584
  expect(errorPayload.error).toMatch(/View 'non_existent_view'/);
586
- // Adjust project name expectation
587
- expect(errorPayload.error).toMatch(/project 'malloy-samples'/);
585
+ expect(errorPayload.error).toMatch(/environment 'malloy-samples'/);
588
586
  expect(errorPayload.suggestions).toBeDefined();
589
587
  expect(Array.isArray(errorPayload.suggestions)).toBe(true);
590
588
  expect(errorPayload.suggestions.length).toBeGreaterThan(0);
@@ -610,8 +608,7 @@ describe("MCP Resource Handlers (E2E Integration)", () => {
610
608
  // Check for the specific notebook name and context in the message
611
609
  // Adjust test to expect the generic "Resource not found" error
612
610
  expect(errorPayload.error).toMatch(/Notebook 'non_existent.malloynb'/);
613
- // Adjust project name expectation
614
- expect(errorPayload.error).toMatch(/project 'malloy-samples'/);
611
+ expect(errorPayload.error).toMatch(/environment 'malloy-samples'/);
615
612
  expect(errorPayload.suggestions).toBeDefined();
616
613
  expect(Array.isArray(errorPayload.suggestions)).toBe(true);
617
614
  expect(errorPayload.suggestions.length).toBeGreaterThan(0);
@@ -622,7 +619,7 @@ describe("MCP Resource Handlers (E2E Integration)", () => {
622
619
 
623
620
  it("should return structured app error when requesting view from wrong source", async () => {
624
621
  if (!env) throw new Error("Test environment not initialized");
625
- const wrongSourceUri = `malloy://project/malloy-samples/package/faa/models/flights.malloy/sources/aircraft/views/${FLIGHTS_MONTH_VIEW}`;
622
+ const wrongSourceUri = `malloy://environment/malloy-samples/package/faa/models/flights.malloy/sources/aircraft/views/${FLIGHTS_MONTH_VIEW}`;
626
623
  const result = await mcpClient.readResource({ uri: wrongSourceUri });
627
624
  expect(result.isError).toBe(true);
628
625
  expect(result.contents).toBeDefined();
@@ -632,22 +629,17 @@ describe("MCP Resource Handlers (E2E Integration)", () => {
632
629
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
633
630
  const errorPayload = JSON.parse((result.contents![0] as any).text);
634
631
  expect(errorPayload.error).toBeDefined();
635
- // Adjust expectation: The primary error should be the project not found
636
632
  expect(errorPayload.error).toMatch(/Resource not found/i);
637
- expect(errorPayload.error).toMatch(/project 'malloy-samples'/);
638
- // Remove checks for view/source name in the error for this specific case
633
+ expect(errorPayload.error).toMatch(/View 'flights_by_month'/);
634
+ expect(errorPayload.error).toMatch(/source 'aircraft'/);
635
+ expect(errorPayload.error).toMatch(/environment 'malloy-samples'/);
639
636
  expect(errorPayload.suggestions).toBeDefined();
640
637
  expect(Array.isArray(errorPayload.suggestions)).toBe(true);
641
- expect(errorPayload.suggestions.length).toBeGreaterThan(0);
642
-
643
- // The suggestions come as full sentences, so we check for expected content
644
638
  expect(errorPayload.suggestions.length).toBe(3);
645
639
  expect(errorPayload.suggestions[0]).toContain(
646
640
  "Verify the identifier or URI",
647
641
  );
648
- expect(errorPayload.suggestions[0]).toContain(
649
- "project 'malloy-samples'",
650
- );
642
+ expect(errorPayload.suggestions[0]).toContain("flights_by_month");
651
643
  expect(errorPayload.suggestions[0]).toContain("is spelled correctly");
652
644
  expect(errorPayload.suggestions[0]).toContain(
653
645
  "Check capitalization and path separators",
@@ -18,7 +18,7 @@ import {
18
18
  // --- Test Suite ---
19
19
  // Note: These tests assume interaction via a standard MCP client.
20
20
  // Tests for raw HTTP edge cases (non-JSON, non-RPC) are omitted based on this assumption.
21
- describe("MCP Transport Tests (E2E Integration)", () => {
21
+ describe.serial("MCP Transport Tests (E2E Integration)", () => {
22
22
  let env: McpE2ETestEnvironment | null = null;
23
23
  let mcpClient: Client<Request, Notification, Result>; // Convenience variable
24
24
 
@@ -12,7 +12,7 @@ import fs from "fs/promises";
12
12
  import os from "os";
13
13
  import path from "path";
14
14
  import type { components } from "../../../src/api";
15
- import { createProjectConnections } from "../../../src/service/connection";
15
+ import { createEnvironmentConnections } from "../../../src/service/connection";
16
16
 
17
17
  type ApiConnection = components["schemas"]["Connection"];
18
18
 
@@ -355,7 +355,7 @@ describe("DuckDB Attached Databases", () => {
355
355
  });
356
356
  });
357
357
 
358
- describe("createProjectConnections - DuckDB", () => {
358
+ describe("createEnvironmentConnections - DuckDB", () => {
359
359
  const PROJECT_TEST_DIR = path.join(os.tmpdir(), "duckdb-project-tests");
360
360
  let createdConnections: Map<string, unknown> = new Map();
361
361
 
@@ -402,7 +402,7 @@ describe("createProjectConnections - DuckDB", () => {
402
402
  ];
403
403
 
404
404
  const { malloyConnections, apiConnections } =
405
- await createProjectConnections(connections, PROJECT_TEST_DIR);
405
+ await createEnvironmentConnections(connections, PROJECT_TEST_DIR);
406
406
 
407
407
  createdConnections = malloyConnections;
408
408
 
@@ -433,7 +433,7 @@ describe("createProjectConnections - DuckDB", () => {
433
433
  ];
434
434
 
435
435
  const { malloyConnections, apiConnections } =
436
- await createProjectConnections(connections, PROJECT_TEST_DIR);
436
+ await createEnvironmentConnections(connections, PROJECT_TEST_DIR);
437
437
 
438
438
  createdConnections = malloyConnections;
439
439
 
@@ -462,7 +462,7 @@ describe("createProjectConnections - DuckDB", () => {
462
462
  ];
463
463
 
464
464
  const { malloyConnections, apiConnections } =
465
- await createProjectConnections(connections, PROJECT_TEST_DIR);
465
+ await createEnvironmentConnections(connections, PROJECT_TEST_DIR);
466
466
 
467
467
  createdConnections = malloyConnections;
468
468
 
@@ -502,7 +502,7 @@ describe("createProjectConnections - DuckDB", () => {
502
502
  },
503
503
  ];
504
504
 
505
- const { malloyConnections } = await createProjectConnections(
505
+ const { malloyConnections } = await createEnvironmentConnections(
506
506
  connections,
507
507
  PROJECT_TEST_DIR,
508
508
  );
@@ -533,7 +533,7 @@ describe("createProjectConnections - DuckDB", () => {
533
533
  },
534
534
  ];
535
535
 
536
- const { malloyConnections } = await createProjectConnections(
536
+ const { malloyConnections } = await createEnvironmentConnections(
537
537
  connections,
538
538
  PROJECT_TEST_DIR,
539
539
  );
@@ -591,7 +591,7 @@ describe("createProjectConnections - DuckDB", () => {
591
591
  },
592
592
  ];
593
593
 
594
- const { malloyConnections } = await createProjectConnections(
594
+ const { malloyConnections } = await createEnvironmentConnections(
595
595
  connections,
596
596
  PROJECT_TEST_DIR,
597
597
  );
@@ -624,7 +624,7 @@ describe("createProjectConnections - DuckDB", () => {
624
624
  ];
625
625
 
626
626
  await expect(
627
- createProjectConnections(connections, PROJECT_TEST_DIR),
627
+ createEnvironmentConnections(connections, PROJECT_TEST_DIR),
628
628
  ).rejects.toThrow();
629
629
  });
630
630
 
@@ -637,7 +637,7 @@ describe("createProjectConnections - DuckDB", () => {
637
637
  ] as ApiConnection[];
638
638
 
639
639
  await expect(
640
- createProjectConnections(connections, PROJECT_TEST_DIR),
640
+ createEnvironmentConnections(connections, PROJECT_TEST_DIR),
641
641
  ).rejects.toThrow("DuckDB connection configuration is missing");
642
642
  });
643
643
 
@@ -658,7 +658,7 @@ describe("createProjectConnections - DuckDB", () => {
658
658
  ];
659
659
 
660
660
  await expect(
661
- createProjectConnections(connections, PROJECT_TEST_DIR),
661
+ createEnvironmentConnections(connections, PROJECT_TEST_DIR),
662
662
  ).rejects.toThrow("Unsupported database type");
663
663
  });
664
664
 
@@ -682,7 +682,7 @@ describe("createProjectConnections - DuckDB", () => {
682
682
  ];
683
683
 
684
684
  await expect(
685
- createProjectConnections(connections, PROJECT_TEST_DIR),
685
+ createEnvironmentConnections(connections, PROJECT_TEST_DIR),
686
686
  ).rejects.toThrow("service account key required");
687
687
  });
688
688
 
@@ -704,7 +704,7 @@ describe("createProjectConnections - DuckDB", () => {
704
704
  ] as ApiConnection[];
705
705
 
706
706
  await expect(
707
- createProjectConnections(connections, PROJECT_TEST_DIR),
707
+ createEnvironmentConnections(connections, PROJECT_TEST_DIR),
708
708
  ).rejects.toThrow("keyId and secret are required");
709
709
  });
710
710
 
@@ -726,7 +726,7 @@ describe("createProjectConnections - DuckDB", () => {
726
726
  ] as ApiConnection[];
727
727
 
728
728
  await expect(
729
- createProjectConnections(connections, PROJECT_TEST_DIR),
729
+ createEnvironmentConnections(connections, PROJECT_TEST_DIR),
730
730
  ).rejects.toThrow("accessKeyId and secretAccessKey are required");
731
731
  });
732
732
 
@@ -748,7 +748,7 @@ describe("createProjectConnections - DuckDB", () => {
748
748
  ] as ApiConnection[];
749
749
 
750
750
  await expect(
751
- createProjectConnections(connections, PROJECT_TEST_DIR),
751
+ createEnvironmentConnections(connections, PROJECT_TEST_DIR),
752
752
  ).rejects.toThrow("PostgreSQL connection configuration is required");
753
753
  });
754
754
 
@@ -761,7 +761,7 @@ describe("createProjectConnections - DuckDB", () => {
761
761
  ] as ApiConnection[];
762
762
 
763
763
  await expect(
764
- createProjectConnections(connections, PROJECT_TEST_DIR),
764
+ createEnvironmentConnections(connections, PROJECT_TEST_DIR),
765
765
  ).rejects.toThrow("DuckLake connection configuration is missing");
766
766
  });
767
767
 
@@ -785,7 +785,7 @@ describe("createProjectConnections - DuckDB", () => {
785
785
  ];
786
786
 
787
787
  await expect(
788
- createProjectConnections(connections, PROJECT_TEST_DIR),
788
+ createEnvironmentConnections(connections, PROJECT_TEST_DIR),
789
789
  ).rejects.toThrow("username is required");
790
790
  });
791
791
 
@@ -806,7 +806,7 @@ describe("createProjectConnections - DuckDB", () => {
806
806
  ];
807
807
 
808
808
  await expect(
809
- createProjectConnections(connections, PROJECT_TEST_DIR),
809
+ createEnvironmentConnections(connections, PROJECT_TEST_DIR),
810
810
  ).rejects.toThrow("PostgreSQL connection configuration missing");
811
811
  });
812
812
 
@@ -836,7 +836,7 @@ describe("createProjectConnections - DuckDB", () => {
836
836
  ];
837
837
 
838
838
  await expect(
839
- createProjectConnections(connections, PROJECT_TEST_DIR),
839
+ createEnvironmentConnections(connections, PROJECT_TEST_DIR),
840
840
  ).rejects.toThrow(
841
841
  "DuckDB attached database names cannot conflict with connection name",
842
842
  );
@@ -854,7 +854,7 @@ describe("createProjectConnections - DuckDB", () => {
854
854
  ];
855
855
 
856
856
  await expect(
857
- createProjectConnections(connections, PROJECT_TEST_DIR),
857
+ createEnvironmentConnections(connections, PROJECT_TEST_DIR),
858
858
  ).rejects.toThrow("DuckDB connection name cannot be 'duckdb'");
859
859
  });
860
860
 
@@ -884,10 +884,28 @@ describe("createProjectConnections - DuckDB", () => {
884
884
  ];
885
885
 
886
886
  await expect(
887
- createProjectConnections(connections, PROJECT_TEST_DIR),
887
+ createEnvironmentConnections(connections, PROJECT_TEST_DIR),
888
888
  ).rejects.toThrow("DuckDB connection name cannot be 'duckdb'");
889
889
  });
890
890
 
891
+ it("should throw when DuckDB connection has no attached databases", async () => {
892
+ const connections: ApiConnection[] = [
893
+ {
894
+ name: "no_attached_db",
895
+ type: "duckdb",
896
+ duckdbConnection: {
897
+ attachedDatabases: [],
898
+ },
899
+ },
900
+ ];
901
+
902
+ await expect(
903
+ createEnvironmentConnections(connections, PROJECT_TEST_DIR),
904
+ ).rejects.toThrow(
905
+ "DuckDB connection must have at least one attached database",
906
+ );
907
+ });
908
+
891
909
  it("should throw on unsupported connection type", async () => {
892
910
  const connections = [
893
911
  {
@@ -897,7 +915,7 @@ describe("createProjectConnections - DuckDB", () => {
897
915
  ] as ApiConnection[];
898
916
 
899
917
  await expect(
900
- createProjectConnections(connections, PROJECT_TEST_DIR),
918
+ createEnvironmentConnections(connections, PROJECT_TEST_DIR),
901
919
  ).rejects.toThrow("Unsupported connection type");
902
920
  });
903
921
 
@@ -921,13 +939,13 @@ describe("createProjectConnections - DuckDB", () => {
921
939
  ] as ApiConnection[];
922
940
 
923
941
  await expect(
924
- createProjectConnections(connections, PROJECT_TEST_DIR),
942
+ createEnvironmentConnections(connections, PROJECT_TEST_DIR),
925
943
  ).rejects.toThrow();
926
944
  });
927
945
  });
928
946
  });
929
947
 
930
- describe("createProjectConnections - Other Connection Types", () => {
948
+ describe("createEnvironmentConnections - Other Connection Types", () => {
931
949
  const PROJECT_TEST_DIR = path.join(
932
950
  os.tmpdir(),
933
951
  "connection-validation-tests",
@@ -970,7 +988,7 @@ describe("createProjectConnections - Other Connection Types", () => {
970
988
  ] as ApiConnection[];
971
989
 
972
990
  await expect(
973
- createProjectConnections(connections, PROJECT_TEST_DIR),
991
+ createEnvironmentConnections(connections, PROJECT_TEST_DIR),
974
992
  ).rejects.toThrow("Snowflake connection configuration is missing");
975
993
  });
976
994
 
@@ -988,7 +1006,7 @@ describe("createProjectConnections - Other Connection Types", () => {
988
1006
  ];
989
1007
 
990
1008
  await expect(
991
- createProjectConnections(connections, PROJECT_TEST_DIR),
1009
+ createEnvironmentConnections(connections, PROJECT_TEST_DIR),
992
1010
  ).rejects.toThrow("Snowflake account is required");
993
1011
  });
994
1012
 
@@ -1006,7 +1024,7 @@ describe("createProjectConnections - Other Connection Types", () => {
1006
1024
  ];
1007
1025
 
1008
1026
  await expect(
1009
- createProjectConnections(connections, PROJECT_TEST_DIR),
1027
+ createEnvironmentConnections(connections, PROJECT_TEST_DIR),
1010
1028
  ).rejects.toThrow("Snowflake username is required");
1011
1029
  });
1012
1030
 
@@ -1024,7 +1042,7 @@ describe("createProjectConnections - Other Connection Types", () => {
1024
1042
  ];
1025
1043
 
1026
1044
  await expect(
1027
- createProjectConnections(connections, PROJECT_TEST_DIR),
1045
+ createEnvironmentConnections(connections, PROJECT_TEST_DIR),
1028
1046
  ).rejects.toThrow(
1029
1047
  "Snowflake password or private key or private key path is required",
1030
1048
  );
@@ -1044,7 +1062,7 @@ describe("createProjectConnections - Other Connection Types", () => {
1044
1062
  ];
1045
1063
 
1046
1064
  await expect(
1047
- createProjectConnections(connections, PROJECT_TEST_DIR),
1065
+ createEnvironmentConnections(connections, PROJECT_TEST_DIR),
1048
1066
  ).rejects.toThrow("Snowflake warehouse is required");
1049
1067
  });
1050
1068
  });
@@ -1059,7 +1077,7 @@ describe("createProjectConnections - Other Connection Types", () => {
1059
1077
  ] as ApiConnection[];
1060
1078
 
1061
1079
  await expect(
1062
- createProjectConnections(connections, PROJECT_TEST_DIR),
1080
+ createEnvironmentConnections(connections, PROJECT_TEST_DIR),
1063
1081
  ).rejects.toThrow("Trino connection configuration is missing");
1064
1082
  });
1065
1083
 
@@ -1076,7 +1094,7 @@ describe("createProjectConnections - Other Connection Types", () => {
1076
1094
  ];
1077
1095
 
1078
1096
  await expect(
1079
- createProjectConnections(connections, PROJECT_TEST_DIR),
1097
+ createEnvironmentConnections(connections, PROJECT_TEST_DIR),
1080
1098
  ).rejects.toThrow(
1081
1099
  'Invalid Trino connection: expected "http://server:port" or "https://server:port"',
1082
1100
  );
@@ -1093,7 +1111,7 @@ describe("createProjectConnections - Other Connection Types", () => {
1093
1111
  ] as ApiConnection[];
1094
1112
 
1095
1113
  await expect(
1096
- createProjectConnections(connections, PROJECT_TEST_DIR),
1114
+ createEnvironmentConnections(connections, PROJECT_TEST_DIR),
1097
1115
  ).rejects.toThrow("MotherDuck connection configuration is missing");
1098
1116
  });
1099
1117
 
@@ -1107,7 +1125,7 @@ describe("createProjectConnections - Other Connection Types", () => {
1107
1125
  ];
1108
1126
 
1109
1127
  await expect(
1110
- createProjectConnections(connections, PROJECT_TEST_DIR),
1128
+ createEnvironmentConnections(connections, PROJECT_TEST_DIR),
1111
1129
  ).rejects.toThrow("MotherDuck access token is required");
1112
1130
  });
1113
1131
  });
@@ -4,7 +4,7 @@ import fs from "fs/promises";
4
4
  import path from "path";
5
5
  import { components } from "../../../src/api";
6
6
  import {
7
- createProjectConnections,
7
+ createEnvironmentConnections,
8
8
  deleteDuckLakeConnectionFile,
9
9
  testConnectionConfig,
10
10
  } from "../../../src/service/connection";
@@ -143,7 +143,7 @@ describe("DuckLake Connection Tests", () => {
143
143
  };
144
144
 
145
145
  const { malloyConnections, apiConnections } =
146
- await createProjectConnections(
146
+ await createEnvironmentConnections(
147
147
  [ducklakeConnection],
148
148
  testProjectPath,
149
149
  );
@@ -212,7 +212,7 @@ describe("DuckLake Connection Tests", () => {
212
212
  },
213
213
  };
214
214
 
215
- const { malloyConnections } = await createProjectConnections(
215
+ const { malloyConnections } = await createEnvironmentConnections(
216
216
  [ducklakeConnection],
217
217
  testProjectPath,
218
218
  );
@@ -268,7 +268,7 @@ describe("DuckLake Connection Tests", () => {
268
268
  },
269
269
  };
270
270
 
271
- const { malloyConnections } = await createProjectConnections(
271
+ const { malloyConnections } = await createEnvironmentConnections(
272
272
  [ducklakeConnection],
273
273
  testProjectPath,
274
274
  );
@@ -333,7 +333,7 @@ describe("DuckLake Connection Tests", () => {
333
333
  },
334
334
  };
335
335
 
336
- const { malloyConnections } = await createProjectConnections(
336
+ const { malloyConnections } = await createEnvironmentConnections(
337
337
  [ducklakeConnection],
338
338
  testProjectPath,
339
339
  );
@@ -400,7 +400,7 @@ describe("DuckLake Connection Tests", () => {
400
400
  },
401
401
  };
402
402
 
403
- const { malloyConnections } = await createProjectConnections(
403
+ const { malloyConnections } = await createEnvironmentConnections(
404
404
  [ducklakeConnection],
405
405
  testProjectPath,
406
406
  );
@@ -470,7 +470,7 @@ describe("DuckLake Connection Tests", () => {
470
470
  },
471
471
  };
472
472
 
473
- const { malloyConnections } = await createProjectConnections(
473
+ const { malloyConnections } = await createEnvironmentConnections(
474
474
  [ducklakeConnection],
475
475
  testProjectPath,
476
476
  );
@@ -529,7 +529,7 @@ describe("DuckLake Connection Tests", () => {
529
529
  },
530
530
  };
531
531
 
532
- const { malloyConnections } = await createProjectConnections(
532
+ const { malloyConnections } = await createEnvironmentConnections(
533
533
  [ducklakeConnection],
534
534
  testProjectPath,
535
535
  );
@@ -553,7 +553,7 @@ describe("DuckLake Connection Tests", () => {
553
553
  describe("Error Handling", () => {
554
554
  it("should throw error if DuckLake catalog connection is missing", async () => {
555
555
  await expect(
556
- createProjectConnections(
556
+ createEnvironmentConnections(
557
557
  [
558
558
  {
559
559
  name: "ducklake_no_catalog",
@@ -576,7 +576,7 @@ describe("DuckLake Connection Tests", () => {
576
576
 
577
577
  it("should throw error if DuckLake storage bucketUrl is missing", async () => {
578
578
  await expect(
579
- createProjectConnections(
579
+ createEnvironmentConnections(
580
580
  [
581
581
  {
582
582
  name: "ducklake_no_bucket",
@@ -607,7 +607,7 @@ describe("DuckLake Connection Tests", () => {
607
607
 
608
608
  it("should throw error if DuckLake connection config is missing", async () => {
609
609
  await expect(
610
- createProjectConnections(
610
+ createEnvironmentConnections(
611
611
  [
612
612
  {
613
613
  name: "ducklake_missing_config",
@@ -653,19 +653,21 @@ describe("DuckLake Connection Tests", () => {
653
653
  };
654
654
 
655
655
  // Create connection twice - second should handle already attached gracefully
656
- const { malloyConnections: conn1 } = await createProjectConnections(
657
- [ducklakeConnection],
658
- testProjectPath,
659
- );
656
+ const { malloyConnections: conn1 } =
657
+ await createEnvironmentConnections(
658
+ [ducklakeConnection],
659
+ testProjectPath,
660
+ );
660
661
  const connection1 = conn1.get(
661
662
  "ducklake_duplicate_test",
662
663
  ) as DuckDBConnection;
663
664
  createdConnections.push(connection1);
664
665
 
665
- const { malloyConnections: conn2 } = await createProjectConnections(
666
- [ducklakeConnection],
667
- testProjectPath,
668
- );
666
+ const { malloyConnections: conn2 } =
667
+ await createEnvironmentConnections(
668
+ [ducklakeConnection],
669
+ testProjectPath,
670
+ );
669
671
  const connection2 = conn2.get(
670
672
  "ducklake_duplicate_test",
671
673
  ) as DuckDBConnection;
@@ -714,7 +716,7 @@ describe("DuckLake Connection Tests", () => {
714
716
  },
715
717
  };
716
718
 
717
- const { malloyConnections } = await createProjectConnections(
719
+ const { malloyConnections } = await createEnvironmentConnections(
718
720
  [ducklakeConnection],
719
721
  testProjectPath,
720
722
  );
@@ -771,7 +773,7 @@ describe("DuckLake Connection Tests", () => {
771
773
  },
772
774
  };
773
775
 
774
- const { malloyConnections } = await createProjectConnections(
776
+ const { malloyConnections } = await createEnvironmentConnections(
775
777
  [ducklakeConnection],
776
778
  testProjectPath,
777
779
  );
@@ -903,7 +905,7 @@ describe("DuckLake Connection Tests", () => {
903
905
  return;
904
906
  }
905
907
 
906
- const { apiConnections } = await createProjectConnections(
908
+ const { apiConnections } = await createEnvironmentConnections(
907
909
  [
908
910
  {
909
911
  name: "ducklake_attrs_test",