@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,681 +0,0 @@
1
- import type { LogMessage } from "@malloydata/malloy";
2
- import { FixedConnectionMap, MalloyError, Runtime } from "@malloydata/malloy";
3
- import { BaseConnection } from "@malloydata/malloy/connection";
4
- import { Mutex } from "async-mutex";
5
- import * as fs from "fs";
6
- import * as path from "path";
7
- import { components } from "../api";
8
- import { API_PREFIX, README_NAME } from "../constants";
9
- import {
10
- ConnectionNotFoundError,
11
- PackageNotFoundError,
12
- ProjectNotFoundError,
13
- } from "../errors";
14
- import { logger } from "../logger";
15
- import { URL_READER } from "../utils";
16
- import {
17
- createProjectConnections,
18
- deleteDuckLakeConnectionFile,
19
- InternalConnection,
20
- } from "./connection";
21
- import { ApiConnection } from "./model";
22
- import { Package } from "./package";
23
-
24
- export enum PackageStatus {
25
- LOADING = "loading",
26
- SERVING = "serving",
27
- UNLOADING = "unloading",
28
- }
29
-
30
- interface PackageInfo {
31
- name: string;
32
- loadTimestamp: number;
33
- status: PackageStatus;
34
- }
35
-
36
- type ApiPackage = components["schemas"]["Package"];
37
- type ApiProject = components["schemas"]["Project"];
38
-
39
- export class Project {
40
- private packages: Map<string, Package> = new Map();
41
- private packageMutexes = new Map<string, Mutex>();
42
- private packageStatuses: Map<string, PackageInfo> = new Map();
43
- private malloyConnections: Map<string, BaseConnection>;
44
- private apiConnections: ApiConnection[];
45
- private projectPath: string;
46
- private projectName: string;
47
- public metadata: ApiProject;
48
-
49
- constructor(
50
- projectName: string,
51
- projectPath: string,
52
- malloyConnections: Map<string, BaseConnection>,
53
- apiConnections: InternalConnection[],
54
- ) {
55
- this.projectName = projectName;
56
- this.projectPath = projectPath;
57
- this.malloyConnections = malloyConnections;
58
- this.apiConnections = apiConnections;
59
- this.metadata = {
60
- resource: `${API_PREFIX}/projects/${this.projectName}`,
61
- name: this.projectName,
62
- location: this.projectPath,
63
- };
64
- void this.reloadProjectMetadata();
65
- }
66
-
67
- private async writeProjectReadme(readme?: string): Promise<void> {
68
- if (readme === undefined) return;
69
-
70
- const readmePath = path.join(this.projectPath, "README.md");
71
-
72
- try {
73
- await fs.promises.writeFile(readmePath, readme, "utf-8");
74
- logger.info(`Updated README.md for project ${this.projectName}`);
75
- } catch (err) {
76
- logger.error(`Failed to write README.md`, { error: err });
77
- throw new Error(`Failed to update project README`);
78
- }
79
- }
80
-
81
- public async update(payload: ApiProject) {
82
- if (payload.readme !== undefined) {
83
- this.metadata.readme = payload.readme;
84
- await this.writeProjectReadme(payload.readme);
85
- }
86
-
87
- // Handle connections update
88
- // TODO: Update project connections should have its own API endpoint
89
- if (payload.connections) {
90
- logger.info(
91
- `Updating ${payload.connections.length} connections for project ${this.projectName}`,
92
- );
93
- const isUpdateConnectionRequest = true;
94
- // Reload connections with full config
95
- const { malloyConnections, apiConnections } =
96
- await createProjectConnections(
97
- payload.connections,
98
- this.projectPath,
99
- isUpdateConnectionRequest,
100
- );
101
-
102
- // Update the project's connection maps
103
- this.malloyConnections = malloyConnections;
104
- this.apiConnections = apiConnections;
105
-
106
- logger.info(
107
- `Successfully updated connections for project ${this.projectName}`,
108
- {
109
- malloyConnections: malloyConnections.size,
110
- apiConnections: apiConnections.length,
111
- internalConnections: apiConnections.length,
112
- },
113
- );
114
- }
115
-
116
- return this;
117
- }
118
-
119
- static async create(
120
- projectName: string,
121
- projectPath: string,
122
- connections: ApiConnection[],
123
- ): Promise<Project> {
124
- if (!(await fs.promises.stat(projectPath))?.isDirectory()) {
125
- throw new ProjectNotFoundError(
126
- `Project path ${projectPath} not found`,
127
- );
128
- }
129
-
130
- logger.info(`Creating project with connection configuration`);
131
- const { malloyConnections, apiConnections } =
132
- await createProjectConnections(connections, projectPath);
133
-
134
- logger.info(
135
- `Loaded ${malloyConnections.size + apiConnections.length} connections for project ${projectName}`,
136
- {
137
- malloyConnections,
138
- apiConnections,
139
- },
140
- );
141
-
142
- const project = new Project(
143
- projectName,
144
- projectPath,
145
- malloyConnections,
146
- apiConnections,
147
- );
148
-
149
- return project;
150
- }
151
-
152
- public async reloadProjectMetadata(): Promise<ApiProject> {
153
- let readme = "";
154
- try {
155
- readme = (
156
- await fs.promises.readFile(path.join(this.projectPath, README_NAME))
157
- ).toString();
158
- } catch {
159
- // Readme not found, so we'll just return an empty string
160
- }
161
- this.metadata = {
162
- ...this.metadata,
163
- resource: `${API_PREFIX}/projects/${this.projectName}`,
164
- name: this.projectName,
165
- readme,
166
- };
167
- return this.metadata;
168
- }
169
-
170
- public async compileSource(
171
- packageName: string,
172
- modelName: string,
173
- source: string,
174
- includeSql: boolean = false,
175
- ): Promise<{ problems: LogMessage[]; sql?: string }> {
176
- // Place the virtual file in the model's directory so relative imports resolve correctly.
177
- const modelDir = path.dirname(
178
- path.join(this.projectPath, packageName, modelName),
179
- );
180
- const virtualUri = `file://${path.join(modelDir, "__compile_check.malloy")}`;
181
- const virtualUrl = new URL(virtualUri);
182
-
183
- // Read the full model file so the submitted source inherits the model's
184
- // complete namespace — imports, source definitions, queries, etc.
185
- const modelPath = path.join(this.projectPath, packageName, modelName);
186
- let modelContent = "";
187
- try {
188
- modelContent = await fs.promises.readFile(modelPath, "utf8");
189
- } catch {
190
- // If the model file can't be read, proceed with empty content
191
- // and let compilation surface any errors naturally.
192
- }
193
- const fullSource = modelContent ? `${modelContent}\n${source}` : source;
194
-
195
- // Create a URL Reader that serves the source string for the virtual file,
196
- // but falls back to the disk for everything else (imports).
197
- const interceptingReader = {
198
- readURL: async (url: URL) => {
199
- if (url.toString() === virtualUri) {
200
- return fullSource;
201
- }
202
- return URL_READER.readURL(url);
203
- },
204
- };
205
-
206
- // Initialize Runtime with the project's active connections
207
- const runtime = new Runtime({
208
- urlReader: interceptingReader,
209
- connections: new FixedConnectionMap(this.malloyConnections, "duckdb"),
210
- });
211
-
212
- // Attempt to compile
213
- try {
214
- const modelMaterializer = runtime.loadModel(virtualUrl);
215
- const model = await modelMaterializer.getModel();
216
-
217
- // If includeSql is requested and compilation succeeded, attempt to extract SQL
218
- let sql: string | undefined;
219
- if (includeSql) {
220
- try {
221
- const queryMaterializer = modelMaterializer.loadFinalQuery();
222
- sql = await queryMaterializer.getSQL();
223
- } catch {
224
- // Source may not contain a runnable query (e.g. only source definitions),
225
- // in which case we simply omit the sql field.
226
- }
227
- }
228
-
229
- // If successful, return any non-fatal warnings
230
- return { problems: model.problems, sql };
231
- } catch (error) {
232
- // If parsing/compilation fails, return the errors
233
- if (error instanceof MalloyError) {
234
- return { problems: error.problems };
235
- }
236
- // If it's a system error (e.g. file not found), throw it up
237
- throw error;
238
- }
239
- }
240
-
241
- public listApiConnections(): ApiConnection[] {
242
- return this.apiConnections;
243
- }
244
-
245
- public getApiConnection(connectionName: string): ApiConnection {
246
- const connection = this.apiConnections.find(
247
- (connection) => connection.name === connectionName,
248
- );
249
- if (!connection) {
250
- throw new ConnectionNotFoundError(
251
- `Connection ${connectionName} not found`,
252
- );
253
- }
254
- return connection;
255
- }
256
-
257
- public getMalloyConnection(connectionName: string): BaseConnection {
258
- const connection = this.malloyConnections.get(connectionName);
259
- if (!connection) {
260
- throw new ConnectionNotFoundError(
261
- `Connection ${connectionName} not found`,
262
- );
263
- }
264
- return connection;
265
- }
266
-
267
- public async listPackages(): Promise<ApiPackage[]> {
268
- logger.debug("Listing packages", { projectPath: this.projectPath });
269
- try {
270
- const packageMetadata = await Promise.all(
271
- Array.from(this.packageStatuses.keys()).map(async (packageName) => {
272
- try {
273
- const packageMetadata = (
274
- this.packageStatuses.get(packageName)?.status ===
275
- PackageStatus.LOADING
276
- ? undefined
277
- : await this.getPackage(packageName, false)
278
- )?.getPackageMetadata();
279
- if (packageMetadata) {
280
- packageMetadata.name = packageName;
281
- }
282
- return packageMetadata;
283
- } catch (error) {
284
- logger.error(
285
- `Failed to load package: ${packageName} due to : ${error}`,
286
- );
287
- // Directory did not contain a valid package.json file -- therefore, it's not a package.
288
- // Or it timed out
289
- return undefined;
290
- }
291
- }),
292
- );
293
- // Get rid of undefined entries (i.e, directories without publisher.json files).
294
- const filteredMetadata = packageMetadata.filter(
295
- (metadata) => metadata,
296
- ) as ApiPackage[];
297
-
298
- // Filter out packages that are being unloaded
299
- const finalMetadata = filteredMetadata.filter((metadata) => {
300
- const packageStatus = this.packageStatuses.get(metadata.name || "");
301
- return packageStatus?.status !== PackageStatus.UNLOADING;
302
- });
303
-
304
- return finalMetadata;
305
- } catch (error) {
306
- logger.error("Error listing packages", { error });
307
- console.error(error);
308
- throw error;
309
- }
310
- }
311
-
312
- public async getPackage(
313
- packageName: string,
314
- reload: boolean = false,
315
- ): Promise<Package> {
316
- // Check if package is already loaded first
317
- const _package = this.packages.get(packageName);
318
- if (_package !== undefined && !reload) {
319
- return _package;
320
- }
321
-
322
- // We need to acquire the mutex to prevent a thundering herd of requests from creating the
323
- // package multiple times.
324
- let packageMutex = this.packageMutexes.get(packageName);
325
- if (packageMutex?.isLocked()) {
326
- logger.debug(
327
- `Package ${packageName} is being loaded, waiting for unlock...`,
328
- );
329
- await packageMutex.waitForUnlock();
330
- logger.debug(`Package ${packageName} unlocked`);
331
- const existingPackage = this.packages.get(packageName);
332
- if (existingPackage) {
333
- logger.debug(`Package ${packageName} loaded by another request`);
334
- return existingPackage;
335
- }
336
- // If package still doesn't exist after unlock, it might have failed to load
337
- // Continue to try loading it ourselves
338
- }
339
- packageMutex = new Mutex();
340
- this.packageMutexes.set(packageName, packageMutex);
341
-
342
- return packageMutex.runExclusive(async () => {
343
- // Double-check after acquiring mutex
344
- const existingPackage = this.packages.get(packageName);
345
- if (existingPackage !== undefined && !reload) {
346
- return existingPackage;
347
- }
348
-
349
- // Set package status to loading
350
- this.setPackageStatus(packageName, PackageStatus.LOADING);
351
-
352
- try {
353
- logger.debug(`Loading package ${packageName}...`);
354
- const packagePath = path.join(this.projectPath, packageName);
355
- const _package = await Package.create(
356
- this.projectName,
357
- packageName,
358
- packagePath,
359
- this.malloyConnections,
360
- );
361
- this.packages.set(packageName, _package);
362
-
363
- // Set package status to serving
364
- this.setPackageStatus(packageName, PackageStatus.SERVING);
365
- logger.debug(`Successfully loaded package ${packageName}`);
366
-
367
- return _package;
368
- } catch (error) {
369
- logger.error(`Failed to load package ${packageName}`, { error });
370
- // Clean up on error - mutex will be automatically released by runExclusive
371
- this.packages.delete(packageName);
372
- this.packageStatuses.delete(packageName);
373
- throw error;
374
- }
375
- // Mutex is automatically released here by runExclusive
376
- });
377
- }
378
-
379
- public async addPackage(packageName: string) {
380
- const packagePath = path.join(this.projectPath, packageName);
381
- if (
382
- !(await fs.promises
383
- .access(packagePath)
384
- .then(() => true)
385
- .catch(() => false)) ||
386
- !(await fs.promises.stat(packagePath))?.isDirectory()
387
- ) {
388
- throw new PackageNotFoundError(`Package ${packageName} not found`);
389
- }
390
- logger.info(
391
- `Adding package ${packageName} to project ${this.projectName}`,
392
- {
393
- packagePath,
394
- malloyConnections: this.malloyConnections,
395
- },
396
- );
397
- this.setPackageStatus(packageName, PackageStatus.LOADING);
398
- try {
399
- this.packages.set(
400
- packageName,
401
- await Package.create(
402
- this.projectName,
403
- packageName,
404
- packagePath,
405
- this.malloyConnections,
406
- ),
407
- );
408
- } catch (error) {
409
- logger.error("Error adding package", { error });
410
- this.deletePackageStatus(packageName);
411
- throw error;
412
- }
413
- this.setPackageStatus(packageName, PackageStatus.SERVING);
414
- return this.packages.get(packageName);
415
- }
416
-
417
- private async writePackageManifest(
418
- packageName: string,
419
- metadata: { name: string; description?: string },
420
- ): Promise<void> {
421
- const packagePath = path.join(this.projectPath, packageName);
422
- const manifestPath = path.join(packagePath, "publisher.json");
423
-
424
- try {
425
- // Read existing manifest
426
- let existingManifest: Record<string, unknown> = {};
427
- try {
428
- const content = await fs.promises.readFile(manifestPath, "utf-8");
429
- existingManifest = JSON.parse(content);
430
- } catch (_err) {
431
- logger.warn(`Could not read manifest for ${packageName}`);
432
- }
433
-
434
- // Update with new metadata
435
- const updatedManifest = {
436
- ...existingManifest,
437
- name: metadata.name,
438
- description: metadata.description,
439
- };
440
-
441
- // Write back to file
442
- await fs.promises.writeFile(
443
- manifestPath,
444
- JSON.stringify(updatedManifest, null, 2),
445
- "utf-8",
446
- );
447
-
448
- logger.info(`Updated publisher.json for ${packageName}`);
449
- } catch (error) {
450
- logger.error(`Failed to update publisher.json`, { error });
451
- throw new Error(`Failed to update package manifest`);
452
- }
453
- }
454
-
455
- public async updatePackage(packageName: string, body: ApiPackage) {
456
- const _package = this.packages.get(packageName);
457
- if (!_package) {
458
- throw new PackageNotFoundError(`Package ${packageName} not found`);
459
- }
460
- if (body.name) {
461
- _package.setName(body.name);
462
- }
463
- _package.setPackageMetadata({
464
- name: body.name,
465
- description: body.description,
466
- resource: body.resource,
467
- location: body.location,
468
- });
469
-
470
- await this.writePackageManifest(packageName, {
471
- name: packageName,
472
- description: body.description,
473
- });
474
-
475
- return _package.getPackageMetadata();
476
- }
477
-
478
- public getPackageStatus(packageName: string): PackageInfo | undefined {
479
- return this.packageStatuses.get(packageName);
480
- }
481
-
482
- public setPackageStatus(packageName: string, status: PackageStatus): void {
483
- const currentStatus = this.packageStatuses.get(packageName);
484
- this.packageStatuses.set(packageName, {
485
- name: packageName,
486
- loadTimestamp: currentStatus?.loadTimestamp || Date.now(),
487
- status: status,
488
- });
489
- }
490
-
491
- public deletePackageStatus(packageName: string): void {
492
- this.packageStatuses.delete(packageName);
493
- }
494
-
495
- public async deletePackage(packageName: string): Promise<void> {
496
- const _package = this.packages.get(packageName);
497
- if (!_package) {
498
- return;
499
- }
500
- const packageStatus = this.packageStatuses.get(packageName);
501
-
502
- if (packageStatus?.status === PackageStatus.LOADING) {
503
- logger.error("Package loading. Can't unload.", {
504
- projectName: this.projectName,
505
- packageName,
506
- });
507
- throw new Error(
508
- "Package loading. Can't unload. " +
509
- this.projectName +
510
- " " +
511
- packageName,
512
- );
513
- } else if (packageStatus?.status === PackageStatus.SERVING) {
514
- this.setPackageStatus(packageName, PackageStatus.UNLOADING);
515
- }
516
-
517
- try {
518
- await fs.promises.rm(path.join(this.projectPath, packageName), {
519
- recursive: true,
520
- force: true,
521
- });
522
- } catch (err) {
523
- logger.error(
524
- "Error removing package directory while unloading package",
525
- { error: err, projectName: this.projectName, packageName },
526
- );
527
- }
528
-
529
- // Remove from internal tracking
530
- this.packages.delete(packageName);
531
- this.packageStatuses.delete(packageName);
532
- }
533
-
534
- public updateConnections(
535
- malloyConnections: Map<string, BaseConnection>,
536
- apiConnections: ApiConnection[],
537
- ): void {
538
- this.malloyConnections = malloyConnections;
539
- this.apiConnections = apiConnections;
540
- }
541
-
542
- public async deleteConnection(connectionName: string): Promise<void> {
543
- this.malloyConnections.get(connectionName)?.close();
544
- const isDeleted = this.malloyConnections.delete(connectionName);
545
-
546
- const index = this.apiConnections.findIndex(
547
- (conn) => conn.name === connectionName,
548
- );
549
-
550
- const connectionType = this.apiConnections[index]?.type;
551
- if (connectionType === "duckdb") {
552
- await this.deleteDuckDBConnection(connectionName);
553
- } else if (connectionType === "ducklake") {
554
- await this.deleteDuckLakeConnection(connectionName);
555
- }
556
-
557
- if (index !== -1) {
558
- this.apiConnections.splice(index, 1);
559
- }
560
-
561
- if (isDeleted || index !== -1) {
562
- logger.info(
563
- `Removed connection ${connectionName} from project ${this.projectName}`,
564
- );
565
- } else {
566
- logger.warn(
567
- `Connection ${connectionName} not found in project ${this.projectName}`,
568
- );
569
- }
570
- }
571
-
572
- public closeAllConnections(): void {
573
- // Close all Malloy connections
574
- for (const [connectionName, connection] of this.malloyConnections) {
575
- try {
576
- connection.close();
577
- logger.info(
578
- `Closed connection ${connectionName} for project ${this.projectName}`,
579
- );
580
- } catch (error) {
581
- logger.error(
582
- `Error closing connection ${connectionName} for project ${this.projectName}`,
583
- { error },
584
- );
585
- }
586
- }
587
-
588
- // Clear connection maps
589
- this.malloyConnections.clear();
590
- this.apiConnections = [];
591
-
592
- logger.info(`Closed all connections for project ${this.projectName}`);
593
- }
594
-
595
- public async serialize(): Promise<ApiProject> {
596
- return {
597
- ...this.metadata,
598
- connections: this.listApiConnections(),
599
- packages: await this.listPackages(),
600
- };
601
- }
602
-
603
- public async deleteDuckDBConnection(connectionName: string): Promise<void> {
604
- const duckdbPath = path.join(
605
- this.projectPath,
606
- `${connectionName}.duckdb`,
607
- );
608
- fs.promises
609
- .access(duckdbPath)
610
- .then(() => {
611
- fs.promises
612
- .rm(duckdbPath)
613
- .then(() => {
614
- logger.info(
615
- `Removed DuckDB connection file ${connectionName} from project ${this.projectName}`,
616
- );
617
- })
618
- .catch((error) => {
619
- logger.error(
620
- `Failed to remove DuckDB connection file ${connectionName} from project ${this.projectName}`,
621
- { error },
622
- );
623
- });
624
- })
625
- .catch((error) => {
626
- logger.error(
627
- `Failed to remove DuckDB connection file ${connectionName} from project ${this.projectName}`,
628
- { error },
629
- );
630
- });
631
- }
632
-
633
- public async deleteDuckLakeConnection(
634
- connectionName: string,
635
- ): Promise<void> {
636
- await deleteDuckLakeConnectionFile(connectionName, this.projectPath);
637
- logger.info(
638
- `Removed DuckLake connection ${connectionName} from project ${this.projectName}`,
639
- );
640
- }
641
- }
642
-
643
- /**
644
- * Extracts the preamble from a Malloy model file — the leading block of
645
- * `##!` pragmas, `import` statements, blank lines, and comments that appear
646
- * before any `source:`, `query:`, or `run:` definition. This allows a
647
- * submitted query to inherit the model's import context.
648
- */
649
- export async function extractPreamble(modelPath: string): Promise<string> {
650
- try {
651
- const content = await fs.promises.readFile(modelPath, "utf8");
652
- return extractPreambleFromSource(content);
653
- } catch {
654
- // If the model file can't be read, return empty preamble
655
- // and let the compilation surface any import errors naturally.
656
- return "";
657
- }
658
- }
659
-
660
- /**
661
- * Extracts the preamble from Malloy source text. Exported for testing.
662
- */
663
- export function extractPreambleFromSource(content: string): string {
664
- const lines = content.split("\n");
665
- const preambleLines: string[] = [];
666
-
667
- for (const line of lines) {
668
- const trimmed = line.trim();
669
- // Stop at the first source/query/run definition
670
- if (
671
- trimmed.startsWith("source:") ||
672
- trimmed.startsWith("query:") ||
673
- trimmed.startsWith("run:")
674
- ) {
675
- break;
676
- }
677
- preambleLines.push(line);
678
- }
679
-
680
- return preambleLines.join("\n").trimEnd();
681
- }