@malloy-publisher/server 0.0.382-dev → 0.2.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.
Files changed (175) hide show
  1. package/README.docker.md +145 -20
  2. package/README.md +65 -12
  3. package/dist/app/api-doc.html +5 -0
  4. package/dist/app/api-doc.yaml +3903 -583
  5. package/dist/app/assets/EnvironmentPage-DiUgLuCH.js +1 -0
  6. package/dist/app/assets/HomePage-MhhjhnFj.js +1 -0
  7. package/dist/app/assets/LightMode-BEyvNUzT.js +1 -0
  8. package/dist/app/assets/MainPage-D4Cfee9o.js +2 -0
  9. package/dist/app/assets/MaterializationsPage-nGgGqwSN.js +1 -0
  10. package/dist/app/assets/ModelPage-DLAcEw-F.js +1 -0
  11. package/dist/app/assets/PackagePage-Df0ikH7w.js +1 -0
  12. package/dist/app/assets/RouteError-BFQwtroH.js +1 -0
  13. package/dist/app/assets/ThemeEditorPage-OoskoQYN.js +1 -0
  14. package/dist/app/assets/WorkbookPage-CpFgvUDC.js +1 -0
  15. package/dist/app/assets/{core-CjeTkq8O.es-BqRc6yhC.js → core-lb28vQkR.es-BoVSOc75.js} +1 -1
  16. package/dist/app/assets/github-dark-DenFmJkN.es-DHJKELXO.js +1 -0
  17. package/dist/app/assets/index-5eLCcNmP.css +1 -0
  18. package/dist/app/assets/index-6ypuFH6g.js +1774 -0
  19. package/dist/app/assets/index-B8pnU_V_.js +615 -0
  20. package/dist/app/assets/index-BImcHmVP.js +1 -0
  21. package/dist/app/assets/index-DSoCUF-f.js +2725 -0
  22. package/dist/app/assets/index-DfqzSItA.js +2 -0
  23. package/dist/app/assets/index-w_0OQJgZ.js +23 -0
  24. package/dist/app/index.html +8 -4
  25. package/dist/cpufeatures-1yrn0vtw.node +0 -0
  26. package/dist/default-publisher.config.json +23 -0
  27. package/dist/{instrumentation.js → instrumentation.mjs} +10631 -10573
  28. package/dist/package_load_worker.mjs +15219 -0
  29. package/dist/runtime/publisher.js +331 -0
  30. package/dist/{server.js → server.mjs} +115548 -49590
  31. package/dist/sshcrypto-vd2k5hq9.node +0 -0
  32. package/package.json +41 -28
  33. package/publisher.config.example.bigquery.json +33 -0
  34. package/publisher.config.example.duckdb.json +23 -0
  35. package/.eslintrc.json +0 -44
  36. package/.prettierignore +0 -1
  37. package/.prettierrc +0 -4
  38. package/build.ts +0 -38
  39. package/dist/app/assets/HomePage-Dn3E4CuB.js +0 -1
  40. package/dist/app/assets/MainPage-BzB3yoqi.js +0 -2
  41. package/dist/app/assets/ModelPage-C9O_sAXT.js +0 -1
  42. package/dist/app/assets/PackagePage-DcxKEjBX.js +0 -1
  43. package/dist/app/assets/ProjectPage-BDj307rF.js +0 -1
  44. package/dist/app/assets/RouteError-DAShbVCG.js +0 -1
  45. package/dist/app/assets/WorkbookPage-Cs_XYEaB.js +0 -1
  46. package/dist/app/assets/index-15BOvhp0.js +0 -456
  47. package/dist/app/assets/index-Bb2jqquW.js +0 -676
  48. package/dist/app/assets/index-CMlGQMcl.css +0 -1
  49. package/dist/app/assets/index-D68X76-7.js +0 -1276
  50. package/dist/app/assets/index.umd-DGBekgSu.js +0 -1145
  51. package/dxt/malloy_bridge.py +0 -354
  52. package/dxt/manifest.json +0 -22
  53. package/eslint.config.mjs +0 -76
  54. package/k6-tests/README.md +0 -144
  55. package/k6-tests/bun.lock +0 -797
  56. package/k6-tests/load-test/load-test-crud-connections.ts +0 -263
  57. package/k6-tests/load-test/load-test-crud-packages.ts +0 -461
  58. package/k6-tests/load-test/load-test-crud-projects.ts +0 -160
  59. package/k6-tests/load-test/load-test-crud.ts +0 -142
  60. package/k6-tests/load-test/load-test.ts +0 -784
  61. package/k6-tests/package.json +0 -27
  62. package/k6-tests/scripts/run-with-otel.sh +0 -50
  63. package/k6-tests/smoke-test/smoke-test.ts +0 -158
  64. package/k6-tests/tsconfig.json +0 -29
  65. package/k6-tests/utils/client_factory.ts +0 -128
  66. package/k6-tests/utils/common.ts +0 -489
  67. package/k6-tests/utils/logger.ts +0 -62
  68. package/k6-tests/utils/types.d.ts +0 -35
  69. package/malloy_mcp.dxt +0 -0
  70. package/publisher.config.json +0 -33
  71. package/src/config.spec.ts +0 -850
  72. package/src/config.ts +0 -305
  73. package/src/constants.ts +0 -10
  74. package/src/controller/compile.controller.ts +0 -35
  75. package/src/controller/connection.controller.ts +0 -515
  76. package/src/controller/database.controller.ts +0 -21
  77. package/src/controller/manifest.controller.ts +0 -29
  78. package/src/controller/materialization.controller.ts +0 -125
  79. package/src/controller/model.controller.ts +0 -107
  80. package/src/controller/package.controller.ts +0 -182
  81. package/src/controller/query.controller.ts +0 -60
  82. package/src/controller/watch-mode.controller.ts +0 -98
  83. package/src/data_styles.ts +0 -71
  84. package/src/dto/connection.dto.spec.ts +0 -131
  85. package/src/dto/connection.dto.ts +0 -174
  86. package/src/dto/index.ts +0 -2
  87. package/src/dto/package.dto.spec.ts +0 -42
  88. package/src/dto/package.dto.ts +0 -12
  89. package/src/dto/validate.spec.ts +0 -76
  90. package/src/dto/validate.ts +0 -31
  91. package/src/errors.ts +0 -116
  92. package/src/health.ts +0 -189
  93. package/src/instrumentation.ts +0 -148
  94. package/src/logger.ts +0 -154
  95. package/src/mcp/error_messages.ts +0 -199
  96. package/src/mcp/handler_utils.ts +0 -239
  97. package/src/mcp/mcp_constants.ts +0 -22
  98. package/src/mcp/prompts/handlers.ts +0 -84
  99. package/src/mcp/prompts/index.ts +0 -11
  100. package/src/mcp/prompts/prompt_definitions.ts +0 -160
  101. package/src/mcp/prompts/prompt_service.ts +0 -67
  102. package/src/mcp/prompts/utils.ts +0 -62
  103. package/src/mcp/resource_metadata.ts +0 -47
  104. package/src/mcp/resources/model_resource.ts +0 -153
  105. package/src/mcp/resources/notebook_resource.ts +0 -137
  106. package/src/mcp/resources/package_resource.ts +0 -370
  107. package/src/mcp/resources/project_resource.ts +0 -184
  108. package/src/mcp/resources/query_resource.ts +0 -117
  109. package/src/mcp/resources/source_resource.ts +0 -141
  110. package/src/mcp/resources/view_resource.ts +0 -136
  111. package/src/mcp/server.ts +0 -62
  112. package/src/mcp/tools/discovery_tools.ts +0 -258
  113. package/src/mcp/tools/execute_query_tool.ts +0 -265
  114. package/src/server.ts +0 -1195
  115. package/src/service/connection.spec.ts +0 -1331
  116. package/src/service/connection.ts +0 -1511
  117. package/src/service/connection_service.spec.ts +0 -641
  118. package/src/service/connection_service.ts +0 -181
  119. package/src/service/db_utils.spec.ts +0 -714
  120. package/src/service/db_utils.ts +0 -1227
  121. package/src/service/filter.spec.ts +0 -447
  122. package/src/service/filter.ts +0 -337
  123. package/src/service/filter_integration.spec.ts +0 -825
  124. package/src/service/gcs_s3_utils.ts +0 -427
  125. package/src/service/manifest_service.spec.ts +0 -201
  126. package/src/service/manifest_service.ts +0 -106
  127. package/src/service/materialization_service.spec.ts +0 -648
  128. package/src/service/materialization_service.ts +0 -929
  129. package/src/service/materialized_table_gc.spec.ts +0 -383
  130. package/src/service/materialized_table_gc.ts +0 -279
  131. package/src/service/model.spec.ts +0 -257
  132. package/src/service/model.ts +0 -1069
  133. package/src/service/package.spec.ts +0 -236
  134. package/src/service/package.ts +0 -463
  135. package/src/service/project.ts +0 -681
  136. package/src/service/project_compile.spec.ts +0 -197
  137. package/src/service/project_store.spec.ts +0 -859
  138. package/src/service/project_store.ts +0 -1531
  139. package/src/service/quoting.ts +0 -41
  140. package/src/service/resolve_project.ts +0 -13
  141. package/src/storage/BaseRepository.ts +0 -31
  142. package/src/storage/DatabaseInterface.ts +0 -180
  143. package/src/storage/StorageManager.mock.ts +0 -59
  144. package/src/storage/StorageManager.ts +0 -210
  145. package/src/storage/duckdb/ConnectionRepository.ts +0 -141
  146. package/src/storage/duckdb/DuckDBConnection.ts +0 -177
  147. package/src/storage/duckdb/DuckDBManifestStore.ts +0 -70
  148. package/src/storage/duckdb/DuckDBRepository.ts +0 -224
  149. package/src/storage/duckdb/ManifestRepository.ts +0 -119
  150. package/src/storage/duckdb/MaterializationRepository.ts +0 -249
  151. package/src/storage/duckdb/PackageRepository.ts +0 -135
  152. package/src/storage/duckdb/ProjectRepository.ts +0 -148
  153. package/src/storage/duckdb/manifest_store.spec.ts +0 -133
  154. package/src/storage/duckdb/schema.ts +0 -147
  155. package/src/storage/ducklake/DuckLakeManifestStore.ts +0 -146
  156. package/src/utils.ts +0 -13
  157. package/tests/fixtures/persist-test/data/orders.csv +0 -5
  158. package/tests/fixtures/persist-test/persist_test.malloy +0 -11
  159. package/tests/fixtures/persist-test/publisher.json +0 -5
  160. package/tests/fixtures/publisher.config.json +0 -15
  161. package/tests/harness/e2e.ts +0 -96
  162. package/tests/harness/mcp_test_setup.ts +0 -344
  163. package/tests/harness/mocks.ts +0 -37
  164. package/tests/harness/rest_e2e.ts +0 -68
  165. package/tests/harness/uris.ts +0 -31
  166. package/tests/integration/materialization/materialization_lifecycle.integration.spec.ts +0 -470
  167. package/tests/integration/mcp/mcp_execute_query_tool.integration.spec.ts +0 -434
  168. package/tests/integration/mcp/mcp_resource.integration.spec.ts +0 -663
  169. package/tests/integration/mcp/mcp_transport.integration.spec.ts +0 -122
  170. package/tests/integration/mcp/setup.spec.ts +0 -5
  171. package/tests/unit/duckdb/attached_databases.test.ts +0 -1132
  172. package/tests/unit/ducklake/ducklake.test.ts +0 -950
  173. package/tests/unit/mcp/prompt_definitions.test.ts +0 -102
  174. package/tests/unit/mcp/prompt_happy.test.ts +0 -51
  175. package/tsconfig.json +0 -32
@@ -1,236 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it } from "bun:test";
2
- import { Stats } from "fs";
3
- import fs from "fs/promises";
4
- import { join } from "path";
5
- import sinon from "sinon";
6
- import { PackageNotFoundError } from "../errors";
7
- import { Model } from "./model";
8
- import { Package } from "./package";
9
-
10
- // Minimal partial types for mocking
11
- type PartialModel = Pick<Model, "getPath">;
12
-
13
- describe("service/package", () => {
14
- const testPackageDirectory = "testPackage";
15
-
16
- beforeEach(async () => {
17
- await fs.mkdir(testPackageDirectory, { recursive: true });
18
- await fs.writeFile(join(testPackageDirectory, "model1.model"), "");
19
- // Create a simple parquet file with schema "name: string, value: int"
20
- const parquetBuffer = Buffer.from("Name,Value\nJohn,10\nJane,20\nJim,30");
21
-
22
- await fs.writeFile(
23
- join(testPackageDirectory, "database.csv"),
24
- parquetBuffer,
25
- );
26
- const publisherContent = JSON.stringify({ description: "Test package" });
27
- await fs.writeFile(
28
- join(testPackageDirectory, "publisher.json"),
29
- publisherContent,
30
- );
31
- });
32
-
33
- afterEach(async () => {
34
- sinon.restore();
35
- // On Windows, DuckDB connections may still have file handles open,
36
- // causing EBUSY errors. Retry deletion with exponential backoff.
37
- const maxRetries = 3;
38
- const delay = 50; // Start with 50ms
39
- let lastError: unknown;
40
-
41
- for (let attempt = 0; attempt < maxRetries; attempt++) {
42
- try {
43
- await fs.rm(testPackageDirectory, { recursive: true, force: true });
44
- return; // Success, exit retry loop
45
- } catch (error) {
46
- lastError = error;
47
- const errnoError = error as NodeJS.ErrnoException;
48
- // Only retry on EBUSY errors (Windows file handle still open)
49
- if (errnoError.code !== "EBUSY") {
50
- throw error; // Non-EBUSY errors should be thrown immediately
51
- }
52
- // Exponential backoff: 50ms, 100ms, 200ms, 400ms, 800ms
53
- if (attempt < maxRetries - 1) {
54
- await new Promise((resolve) => setTimeout(resolve, delay));
55
- }
56
- }
57
- }
58
- // If we've exhausted retries, ignore EBUSY errors as they don't affect test results
59
- // and the files will be cleaned up eventually
60
- if ((lastError as NodeJS.ErrnoException).code !== "EBUSY") {
61
- throw lastError;
62
- }
63
- });
64
-
65
- it("should create a package instance", async () => {
66
- // Using 'as any' for simplified mock Map value in test
67
- const pkg = new Package(
68
- "testProject",
69
- "testPackage",
70
- testPackageDirectory,
71
- { name: "testPackage", description: "Test package" },
72
- [],
73
- new Map([
74
- [
75
- "model1.malloy",
76
- { getPath: () => "model1.malloy" } as unknown as Model,
77
- ],
78
- [
79
- "model2.malloynb",
80
- { getPath: () => "model2.malloynb" } as unknown as Model,
81
- ],
82
- ]),
83
- );
84
-
85
- expect(pkg).toBeInstanceOf(Package);
86
- });
87
-
88
- describe("instance methods", () => {
89
- describe("create", () => {
90
- it("should throw PackageNotFoundError if the package manifest does not exist", async () => {
91
- await fs.rm(join(testPackageDirectory, "publisher.json"));
92
- sinon.stub(fs, "stat").rejects(new Error("File not found"));
93
-
94
- await expect(
95
- Package.create(
96
- "testProject",
97
- "testPackage",
98
- testPackageDirectory,
99
- new Map(),
100
- ),
101
- ).rejects.toThrowError(
102
- new PackageNotFoundError(
103
- "Package manifest for testPackage does not exist.",
104
- ),
105
- );
106
- });
107
- it(
108
- "should return a Package object if the package exists",
109
- async () => {
110
- sinon.stub(fs, "stat").resolves();
111
- const readFileStub = sinon
112
- .stub(fs, "readFile")
113
- .resolves(
114
- Buffer.from(
115
- JSON.stringify({ description: "Test package" }),
116
- ),
117
- );
118
-
119
- // Still use Partial<Model> for the stub resolution type
120
- sinon
121
- .stub(Model, "create")
122
- // @ts-expect-error PartialModel is a partial type
123
- .resolves({ getPath: () => "model1.model" } as PartialModel);
124
-
125
- readFileStub.restore();
126
- readFileStub.resolves(Buffer.from(JSON.stringify([])));
127
-
128
- const packageInstance = await Package.create(
129
- "testProject",
130
- "testPackage",
131
- testPackageDirectory,
132
- new Map(),
133
- );
134
-
135
- expect(packageInstance).toBeInstanceOf(Package);
136
- expect(packageInstance.getPackageName()).toBe("testPackage");
137
- expect(packageInstance.getPackageMetadata().description).toBe(
138
- "Test package",
139
- );
140
- expect(packageInstance.listDatabases()).toEqual([
141
- {
142
- path: "database.csv",
143
- type: "embedded",
144
- info: {
145
- name: "database.csv",
146
- columns: [
147
- { name: "Name", type: "string" },
148
- { name: "Value", type: "number" },
149
- ],
150
- rowCount: 3,
151
- },
152
- },
153
- ]);
154
- expect(packageInstance.listModels()).toBeEmpty();
155
- },
156
- { timeout: 20000 },
157
- );
158
- });
159
-
160
- describe("listModels", () => {
161
- it("should return a list of models with their paths and types", async () => {
162
- // Using 'as any' for simplified mock Map value in test
163
- const packageInstance = new Package(
164
- "testProject",
165
- "testPackage",
166
- testPackageDirectory,
167
- { name: "testPackage", description: "Test package" },
168
- [],
169
- new Map([
170
- [
171
- "model1.malloy",
172
- {
173
- getPath: () => "model1.malloy",
174
- getModel: () => "foo",
175
- } as unknown as Model,
176
- ],
177
- [
178
- "model2.malloynb",
179
- {
180
- getPath: () => "model2.malloynb",
181
- getNotebookError: () => {
182
- return {
183
- message: "This is the error",
184
- };
185
- },
186
- } as unknown as Model,
187
- ],
188
- ]),
189
- );
190
-
191
- const models = await packageInstance.listModels();
192
- expect(models).toEqual([
193
- {
194
- // @ts-expect-error TODO: Fix missing projectName type in API
195
- projectName: "testProject",
196
- packageName: "testPackage",
197
- path: "model1.malloy",
198
- error: undefined,
199
- },
200
- ]);
201
-
202
- const notebooks = await packageInstance.listNotebooks();
203
- expect(notebooks).toEqual([
204
- {
205
- // @ts-expect-error TODO: Fix missing projectName type in API
206
- projectName: "testProject",
207
- packageName: "testPackage",
208
- path: "model2.malloynb",
209
- error: "This is the error",
210
- },
211
- ]);
212
- });
213
- });
214
-
215
- describe("getDatabaseInfo", () => {
216
- it("should return the size of the database file", async () => {
217
- sinon.stub(fs, "stat").resolves({ size: 13 } as Stats);
218
-
219
- // @ts-expect-error Accessing private static method for testing
220
- const info = await Package.getDatabaseInfo(
221
- testPackageDirectory,
222
- "database.csv",
223
- );
224
-
225
- expect(info).toEqual({
226
- name: "database.csv",
227
- columns: [
228
- { name: "Name", type: "string" },
229
- { name: "Value", type: "number" },
230
- ],
231
- rowCount: 3,
232
- });
233
- });
234
- });
235
- });
236
- });
@@ -1,463 +0,0 @@
1
- import * as fs from "fs/promises";
2
- import * as path from "path";
3
-
4
- import { DuckDBConnection } from "@malloydata/db-duckdb";
5
- import {
6
- Connection,
7
- ConnectionRuntime,
8
- EmptyURLReader,
9
- SourceDef,
10
- } from "@malloydata/malloy";
11
- import { metrics } from "@opentelemetry/api";
12
- import recursive from "recursive-readdir";
13
- import { components } from "../api";
14
- import {
15
- API_PREFIX,
16
- MODEL_FILE_SUFFIX,
17
- NOTEBOOK_FILE_SUFFIX,
18
- PACKAGE_MANIFEST_NAME,
19
- } from "../constants";
20
- import { PackageNotFoundError } from "../errors";
21
- import { formatDuration, logger } from "../logger";
22
- import { BuildManifest } from "../storage/DatabaseInterface";
23
- import { Model } from "./model";
24
-
25
- type ApiDatabase = components["schemas"]["Database"];
26
- type ApiModel = components["schemas"]["Model"];
27
- type ApiNotebook = components["schemas"]["Notebook"];
28
- export type ApiPackage = components["schemas"]["Package"];
29
- type ApiColumn = components["schemas"]["Column"];
30
- type ApiTableDescription = components["schemas"]["TableDescription"];
31
-
32
- const ENABLE_LIST_MODEL_COMPILATION = true;
33
- export class Package {
34
- private projectName: string;
35
- private packageName: string;
36
- private packageMetadata: ApiPackage;
37
- private databases: ApiDatabase[];
38
- private models: Map<string, Model> = new Map();
39
- private packagePath: string;
40
- private connections: Map<string, Connection> = new Map();
41
- private static meter = metrics.getMeter("publisher");
42
- private static packageLoadHistogram = this.meter.createHistogram(
43
- "malloy_package_load_duration",
44
- {
45
- description: "Time taken to load a Malloy package",
46
- unit: "ms",
47
- },
48
- );
49
-
50
- constructor(
51
- projectName: string,
52
- packageName: string,
53
- packagePath: string,
54
- packageMetadata: ApiPackage,
55
- databases: ApiDatabase[],
56
- models: Map<string, Model>,
57
- connections: Map<string, Connection> = new Map(),
58
- ) {
59
- this.projectName = projectName;
60
- this.packageName = packageName;
61
- this.packagePath = packagePath;
62
- this.packageMetadata = packageMetadata;
63
- this.databases = databases;
64
- this.models = models;
65
- this.connections = connections;
66
- }
67
-
68
- static async create(
69
- projectName: string,
70
- packageName: string,
71
- packagePath: string,
72
- projectConnections: Map<string, Connection>,
73
- ): Promise<Package> {
74
- const startTime = performance.now();
75
- await Package.validatePackageManifestExistsOrThrowError(packagePath);
76
- const manifestValidationTime = performance.now();
77
- logger.info("Package manifest validation completed", {
78
- packageName,
79
- duration: formatDuration(manifestValidationTime - startTime),
80
- });
81
-
82
- try {
83
- const packageConfig = await Package.readPackageConfig(packagePath);
84
- const packageConfigTime = performance.now();
85
- logger.info("Package config read completed", {
86
- packageName,
87
- duration: formatDuration(
88
- packageConfigTime - manifestValidationTime,
89
- ),
90
- });
91
- packageConfig.resource = `${API_PREFIX}/projects/${projectName}/packages/${packageName}`;
92
-
93
- const databases = await Package.readDatabases(packagePath);
94
- const databasesTime = performance.now();
95
- logger.info("Databases read completed", {
96
- packageName,
97
- databaseCount: databases.length,
98
- duration: formatDuration(databasesTime - packageConfigTime),
99
- });
100
- const connections = new Map<string, Connection>(projectConnections);
101
-
102
- // Add a duckdb connection for the package.
103
- const duckdbConnection = new DuckDBConnection(
104
- "duckdb",
105
- ":memory:",
106
- packagePath,
107
- );
108
- connections.set("duckdb", duckdbConnection);
109
-
110
- const models = await Package.loadModels(
111
- packageName,
112
- packagePath,
113
- connections,
114
- );
115
- const modelsTime = performance.now();
116
- logger.info("Models loaded", {
117
- packageName,
118
- modelCount: models.size,
119
- duration: formatDuration(modelsTime - databasesTime),
120
- });
121
- for (const [modelPath, model] of models.entries()) {
122
- const maybeModel = model as unknown as {
123
- compilationError?: unknown;
124
- };
125
- if (maybeModel.compilationError) {
126
- const err = maybeModel.compilationError;
127
- const message =
128
- err instanceof Error
129
- ? err.message
130
- : `Unknown compilation error in ${modelPath}`;
131
-
132
- logger.error("Model compilation failed", {
133
- packageName,
134
- modelPath,
135
- error: message,
136
- });
137
-
138
- this.packageLoadHistogram.record(performance.now() - startTime, {
139
- malloy_package_name: packageName,
140
- status: "compilation_error",
141
- });
142
- throw err;
143
- }
144
- }
145
- const endTime = performance.now();
146
- const executionTime = endTime - startTime;
147
- this.packageLoadHistogram.record(executionTime, {
148
- malloy_package_name: packageName,
149
- status: "success",
150
- });
151
- logger.info(`Successfully loaded package ${packageName}`, {
152
- packageName,
153
- duration: formatDuration(executionTime),
154
- });
155
- return new Package(
156
- projectName,
157
- packageName,
158
- packagePath,
159
- packageConfig,
160
- databases,
161
- models,
162
- connections,
163
- );
164
- } catch (error) {
165
- logger.error(`Error loading package ${packageName}`, { error });
166
- console.error(error);
167
- const endTime = performance.now();
168
- const executionTime = endTime - startTime;
169
- this.packageLoadHistogram.record(executionTime, {
170
- malloy_package_name: packageName,
171
- status: "error",
172
- });
173
- // Clean up package directory on failure
174
- try {
175
- await fs.rm(packagePath, {
176
- recursive: true,
177
- force: true,
178
- });
179
- logger.info(`Cleaned up failed package directory: ${packagePath}`);
180
- } catch (cleanupError) {
181
- logger.warn(`Failed to clean up package directory ${packagePath}`, {
182
- error: cleanupError,
183
- });
184
- }
185
- throw error;
186
- }
187
- }
188
-
189
- public getPackageName(): string {
190
- return this.packageName;
191
- }
192
-
193
- public getPackagePath(): string {
194
- return this.packagePath;
195
- }
196
-
197
- public getPackageMetadata(): ApiPackage {
198
- return this.packageMetadata;
199
- }
200
-
201
- public listDatabases(): ApiDatabase[] {
202
- return this.databases;
203
- }
204
-
205
- public getModel(modelPath: string): Model | undefined {
206
- return this.models.get(modelPath);
207
- }
208
-
209
- public getModelPaths(): string[] {
210
- return Array.from(this.models.keys());
211
- }
212
-
213
- /**
214
- * Recompile every model in the package with the given build manifest
215
- * so queries resolve persist references to materialized tables.
216
- *
217
- * Builds a fresh map off to the side and swaps it in at the end. If any
218
- * recompile fails the whole call rejects before the swap and the live
219
- * `this.models` reference remains untouched — no half-loaded state is
220
- * ever observable to concurrent readers.
221
- */
222
- public async reloadAllModels(
223
- buildManifest: BuildManifest["entries"],
224
- ): Promise<void> {
225
- const modelPaths = Array.from(this.models.keys());
226
- logger.info("Reloading all models with build manifest", {
227
- packageName: this.packageName,
228
- modelCount: modelPaths.length,
229
- manifestEntryCount: Object.keys(buildManifest).length,
230
- });
231
-
232
- const reloaded = await Promise.all(
233
- modelPaths.map((modelPath) =>
234
- Model.create(
235
- this.packageName,
236
- this.packagePath,
237
- modelPath,
238
- this.connections,
239
- { buildManifest },
240
- ),
241
- ),
242
- );
243
- const nextModels = new Map<string, Model>();
244
- for (const model of reloaded) {
245
- nextModels.set(model.getPath(), model);
246
- }
247
- this.models = nextModels;
248
- }
249
-
250
- public getConnections(): Map<string, Connection> {
251
- return this.connections;
252
- }
253
-
254
- public getMalloyConnection(connectionName: string): Connection {
255
- const connection = this.connections.get(connectionName);
256
- if (!connection) {
257
- throw new Error(
258
- `Connection ${connectionName} not found in package ${this.packageName}`,
259
- );
260
- }
261
- return connection;
262
- }
263
-
264
- public async getModelFileText(modelPath: string): Promise<string> {
265
- const model = this.getModel(modelPath);
266
- if (!model) {
267
- throw new Error(`Model not found: ${modelPath}`);
268
- }
269
- return await model.getFileText(this.packagePath);
270
- }
271
-
272
- public async listModels(): Promise<ApiModel[]> {
273
- const values = await Promise.all(
274
- Array.from(this.models.keys())
275
- .filter((modelPath) => {
276
- return modelPath.endsWith(MODEL_FILE_SUFFIX);
277
- })
278
- .map(async (modelPath) => {
279
- let error: string | undefined;
280
- if (ENABLE_LIST_MODEL_COMPILATION) {
281
- try {
282
- await this.models.get(modelPath)?.getModel();
283
- } catch (modelError) {
284
- error =
285
- modelError instanceof Error
286
- ? modelError.message
287
- : undefined;
288
- }
289
- }
290
- return {
291
- projectName: this.projectName,
292
- path: modelPath,
293
- packageName: this.packageName,
294
- error,
295
- };
296
- }),
297
- );
298
- return values;
299
- }
300
-
301
- public async listNotebooks(): Promise<ApiNotebook[]> {
302
- return await Promise.all(
303
- Array.from(this.models.keys())
304
- .filter((modelPath) => {
305
- return modelPath.endsWith(NOTEBOOK_FILE_SUFFIX);
306
- })
307
- .map(async (modelPath) => {
308
- let error: Error | undefined;
309
- if (ENABLE_LIST_MODEL_COMPILATION) {
310
- error = this.models.get(modelPath)?.getNotebookError();
311
- }
312
- return {
313
- projectName: this.projectName,
314
- packageName: this.packageName,
315
- path: modelPath,
316
- error: error?.message,
317
- };
318
- }),
319
- );
320
- }
321
-
322
- private static async loadModels(
323
- packageName: string,
324
- packagePath: string,
325
- connections: Map<string, Connection>,
326
- ): Promise<Map<string, Model>> {
327
- const modelPaths = await Package.getModelPaths(packagePath);
328
- const models = await Promise.all(
329
- modelPaths.map((modelPath) =>
330
- Model.create(packageName, packagePath, modelPath, connections),
331
- ),
332
- );
333
- return new Map(models.map((model) => [model.getPath(), model]));
334
- }
335
-
336
- private static async getModelPaths(packagePath: string): Promise<string[]> {
337
- let files = undefined;
338
- try {
339
- files = await recursive(packagePath);
340
- } catch (error) {
341
- logger.error(error);
342
- throw new PackageNotFoundError(
343
- `Package config for ${packagePath} does not exist.`,
344
- );
345
- }
346
- return files
347
- .map((fullPath: string) => {
348
- return path.relative(packagePath, fullPath).replace(/\\/g, "/");
349
- })
350
- .filter(
351
- (modelPath: string) =>
352
- modelPath.endsWith(MODEL_FILE_SUFFIX) ||
353
- modelPath.endsWith(NOTEBOOK_FILE_SUFFIX),
354
- );
355
- }
356
-
357
- private static async validatePackageManifestExistsOrThrowError(
358
- packagePath: string,
359
- ) {
360
- const packageConfigPath = path.join(packagePath, PACKAGE_MANIFEST_NAME);
361
- try {
362
- await fs.stat(packageConfigPath);
363
- } catch {
364
- logger.error(`Can't find ${packageConfigPath}`);
365
- throw new PackageNotFoundError(
366
- `Package manifest for ${packagePath} does not exist.`,
367
- );
368
- }
369
- }
370
-
371
- private static async readPackageConfig(
372
- packagePath: string,
373
- ): Promise<ApiPackage> {
374
- const packageConfigPath = path.join(packagePath, PACKAGE_MANIFEST_NAME);
375
- const packageConfigContents = await fs.readFile(packageConfigPath);
376
- // TODO: Validate package manifest. Define manifest type in public API.
377
- const packageManifest = JSON.parse(packageConfigContents.toString());
378
- return {
379
- name: packageManifest.name,
380
- description: packageManifest.description,
381
- };
382
- }
383
-
384
- private static async readDatabases(
385
- packagePath: string,
386
- ): Promise<ApiDatabase[]> {
387
- return await Promise.all(
388
- (await Package.getDatabasePaths(packagePath)).map(
389
- async (databasePath) => {
390
- const databaseInfo = await Package.getDatabaseInfo(
391
- packagePath,
392
- databasePath,
393
- );
394
-
395
- return {
396
- path: databasePath,
397
- info: databaseInfo,
398
- type: "embedded",
399
- };
400
- },
401
- ),
402
- );
403
- }
404
-
405
- private static async getDatabasePaths(
406
- packagePath: string,
407
- ): Promise<string[]> {
408
- let files = undefined;
409
- files = await recursive(packagePath);
410
- return files
411
- .map((fullPath: string) => {
412
- return path.relative(packagePath, fullPath).replace(/\\/g, "/");
413
- })
414
- .filter(
415
- (modelPath: string) =>
416
- modelPath.endsWith(".parquet") || modelPath.endsWith(".csv"),
417
- );
418
- }
419
-
420
- private static async getDatabaseInfo(
421
- packagePath: string,
422
- databasePath: string,
423
- ): Promise<ApiTableDescription> {
424
- const fullPath = path.join(packagePath, databasePath);
425
-
426
- // Create a DuckDB source then:
427
- // 1. Load the model and get the table schema from model
428
- // 2. Run a query to get the row count from the table
429
- const runtime = new ConnectionRuntime({
430
- urlReader: new EmptyURLReader(),
431
- connections: [new DuckDBConnection("duckdb")],
432
- });
433
- // Normalize path to use forward slashes for cross-platform compatibility
434
- // DuckDB on Windows supports forward slashes, and this avoids escaping issues
435
- const normalizedPath = fullPath.replace(/\\/g, "/");
436
- const model = runtime.loadModel(
437
- `source: temp is duckdb.table('${normalizedPath}')`,
438
- );
439
- const modelDef = await model.getModel();
440
- const fields = (modelDef._modelDef.contents["temp"] as SourceDef).fields;
441
- const schema = fields.map((field): ApiColumn => {
442
- return { type: field.type, name: field.name };
443
- });
444
- const runner = model.loadQuery(
445
- "run: temp->{aggregate: row_count is count()}",
446
- );
447
- const result = await runner.run();
448
- const rowCount = result.data.value[0].row_count?.valueOf() as number;
449
- return { name: databasePath, rowCount, columns: schema };
450
- }
451
-
452
- public setName(name: string) {
453
- this.packageName = name;
454
- }
455
-
456
- public setProjectName(projectName: string) {
457
- this.projectName = projectName;
458
- }
459
-
460
- public setPackageMetadata(packageMetadata: ApiPackage) {
461
- this.packageMetadata = packageMetadata;
462
- }
463
- }