@malloy-publisher/server 0.0.90 → 0.0.92

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 (35) hide show
  1. package/.prettierignore +1 -0
  2. package/build.ts +9 -0
  3. package/dist/app/api-doc.yaml +3 -0
  4. package/dist/app/assets/{index-2BGzlUQa.js → index-DYO_URL-.js} +100 -100
  5. package/dist/app/assets/{index-D1X7Y0Ve.js → index-Dg-zTLb3.js} +1 -1
  6. package/dist/app/assets/{index.es49-D9XPPIF9.js → index.es50-CDMydA2o.js} +1 -1
  7. package/dist/app/assets/mui-UpaxdnvH.js +159 -0
  8. package/dist/app/index.html +2 -2
  9. package/dist/server.js +307 -98
  10. package/eslint.config.mjs +8 -0
  11. package/package.json +2 -1
  12. package/publisher.config.json +25 -5
  13. package/src/config.ts +25 -2
  14. package/src/constants.ts +1 -1
  15. package/src/controller/package.controller.ts +1 -0
  16. package/src/controller/watch-mode.controller.ts +19 -4
  17. package/src/data_styles.ts +10 -3
  18. package/src/mcp/prompts/index.ts +9 -1
  19. package/src/mcp/resources/package_resource.ts +2 -1
  20. package/src/mcp/resources/source_resource.ts +1 -0
  21. package/src/mcp/resources/view_resource.ts +1 -0
  22. package/src/server.ts +9 -5
  23. package/src/service/connection.ts +17 -4
  24. package/src/service/db_utils.ts +19 -11
  25. package/src/service/model.ts +2 -0
  26. package/src/service/package.spec.ts +76 -54
  27. package/src/service/project.ts +160 -45
  28. package/src/service/project_store.spec.ts +477 -165
  29. package/src/service/project_store.ts +319 -69
  30. package/src/service/scheduler.ts +3 -2
  31. package/src/utils.ts +0 -1
  32. package/tests/harness/e2e.ts +60 -58
  33. package/tests/harness/uris.ts +21 -24
  34. package/tests/integration/mcp/mcp_resource.integration.spec.ts +10 -0
  35. package/dist/app/assets/mui-YektUyEU.js +0 -161
@@ -13,12 +13,26 @@ import { logger } from "../logger";
13
13
  import { createConnections, InternalConnection } from "./connection";
14
14
  import { ApiConnection } from "./model";
15
15
  import { Package } from "./package";
16
+
17
+ enum PackageStatus {
18
+ LOADING = "loading",
19
+ SERVING = "serving",
20
+ UNLOADING = "unloading",
21
+ }
22
+
23
+ interface PackageInfo {
24
+ name: string;
25
+ loadTimestamp: number;
26
+ status: PackageStatus;
27
+ }
28
+
16
29
  type ApiPackage = components["schemas"]["Package"];
17
30
  type ApiProject = components["schemas"]["Project"];
18
31
 
19
32
  export class Project {
20
33
  private packages: Map<string, Package> = new Map();
21
34
  private packageMutexes = new Map<string, Mutex>();
35
+ private packageStatuses: Map<string, PackageInfo> = new Map();
22
36
  private malloyConnections: Map<string, BaseConnection>;
23
37
  private apiConnections: ApiConnection[];
24
38
  private internalConnections: InternalConnection[];
@@ -48,26 +62,38 @@ export class Project {
48
62
  }
49
63
 
50
64
  public async update(payload: ApiProject) {
51
- if (payload.name) {
52
- this.projectName = payload.name;
53
- this.packages.forEach((_package) => {
54
- _package.setProjectName(this.projectName);
55
- });
56
- this.metadata.name = this.projectName;
65
+ if (payload.readme !== undefined) {
66
+ this.metadata.readme = payload.readme;
57
67
  }
58
- if (payload.resource) {
59
- this.projectPath = payload.resource.replace(
60
- `${API_PREFIX}/projects/`,
61
- "",
68
+
69
+ // Handle connections update
70
+ // TODO: Update project connections should have its own API endpoint
71
+ if (payload.connections) {
72
+ logger.info(
73
+ `Updating ${payload.connections.length} connections for project ${this.projectName}`,
74
+ );
75
+
76
+ // Reload connections with full config
77
+ const { malloyConnections, apiConnections } = await createConnections(
78
+ this.projectPath,
79
+ payload.connections,
80
+ );
81
+
82
+ // Update the project's connection maps
83
+ this.malloyConnections = malloyConnections;
84
+ this.apiConnections = apiConnections;
85
+ this.internalConnections = apiConnections;
86
+
87
+ logger.info(
88
+ `Successfully updated connections for project ${this.projectName}`,
89
+ {
90
+ malloyConnections: malloyConnections.size,
91
+ apiConnections: apiConnections.length,
92
+ internalConnections: apiConnections.length,
93
+ },
62
94
  );
63
- if (!(await fs.promises.exists(this.projectPath))) {
64
- throw new ProjectNotFoundError(
65
- `Project path "${this.projectPath}" not found`,
66
- );
67
- }
68
- this.metadata.resource = payload.resource;
69
95
  }
70
- this.metadata.readme = payload.readme;
96
+
71
97
  return this;
72
98
  }
73
99
 
@@ -81,10 +107,15 @@ export class Project {
81
107
  `Project path ${projectPath} not found`,
82
108
  );
83
109
  }
84
- const { malloyConnections, apiConnections } = await createConnections(
85
- projectPath,
86
- defaultConnections,
87
- );
110
+
111
+ let malloyConnections: Map<string, BaseConnection> = new Map();
112
+ let apiConnections: InternalConnection[] = [];
113
+
114
+ logger.info(`Creating project with connection configuration`);
115
+ const result = await createConnections(projectPath, defaultConnections);
116
+ malloyConnections = result.malloyConnections;
117
+ apiConnections = result.apiConnections;
118
+
88
119
  logger.info(
89
120
  `Loaded ${malloyConnections.size + apiConnections.length} connections for project ${projectName}`,
90
121
  {
@@ -92,6 +123,7 @@ export class Project {
92
123
  apiConnections,
93
124
  },
94
125
  );
126
+
95
127
  return new Project(
96
128
  projectName,
97
129
  projectPath,
@@ -185,8 +217,11 @@ export class Project {
185
217
  packageDirectories.map(async (directory) => {
186
218
  try {
187
219
  return (
188
- await this.getPackage(directory.name, false)
189
- ).getPackageMetadata();
220
+ this.packageStatuses.get(directory.name)?.status ===
221
+ PackageStatus.LOADING
222
+ ? undefined
223
+ : await this.getPackage(directory.name, false)
224
+ )?.getPackageMetadata();
190
225
  } catch (error) {
191
226
  logger.error(
192
227
  `Failed to load package: ${directory.name} due to : ${error}`,
@@ -201,7 +236,14 @@ export class Project {
201
236
  const filteredMetadata = packageMetadata.filter(
202
237
  (metadata) => metadata,
203
238
  ) as ApiPackage[];
204
- return filteredMetadata;
239
+
240
+ // Filter out packages that are being unloaded
241
+ const finalMetadata = filteredMetadata.filter((metadata) => {
242
+ const packageStatus = this.packageStatuses.get(metadata.name || "");
243
+ return packageStatus?.status !== PackageStatus.UNLOADING;
244
+ });
245
+
246
+ return finalMetadata;
205
247
  } catch (error) {
206
248
  logger.error("Error listing packages", { error });
207
249
  console.error(error);
@@ -213,22 +255,35 @@ export class Project {
213
255
  packageName: string,
214
256
  reload: boolean = false,
215
257
  ): Promise<Package> {
258
+ // Check if package is already loaded first
259
+ const _package = this.packages.get(packageName);
260
+ if (_package !== undefined && !reload) {
261
+ return _package;
262
+ }
263
+
216
264
  // We need to acquire the mutex to prevent a thundering herd of requests from creating the
217
265
  // package multiple times.
218
266
  let packageMutex = this.packageMutexes.get(packageName);
219
267
  if (packageMutex?.isLocked()) {
220
268
  await packageMutex.waitForUnlock();
221
- return this.packages.get(packageName)!;
269
+ const existingPackage = this.packages.get(packageName);
270
+ if (existingPackage) {
271
+ return existingPackage;
272
+ }
222
273
  }
223
274
  packageMutex = new Mutex();
224
275
  this.packageMutexes.set(packageName, packageMutex);
225
276
 
226
277
  return packageMutex.runExclusive(async () => {
227
- const _package = this.packages.get(packageName);
228
- if (_package !== undefined && !reload) {
229
- return _package;
278
+ // Double-check after acquiring mutex
279
+ const existingPackage = this.packages.get(packageName);
280
+ if (existingPackage !== undefined && !reload) {
281
+ return existingPackage;
230
282
  }
231
283
 
284
+ // Set package status to loading
285
+ this.setPackageStatus(packageName, PackageStatus.LOADING);
286
+
232
287
  try {
233
288
  const _package = await Package.create(
234
289
  this.projectName,
@@ -237,21 +292,28 @@ export class Project {
237
292
  this.malloyConnections,
238
293
  );
239
294
  this.packages.set(packageName, _package);
295
+
296
+ // Set package status to serving
297
+ this.setPackageStatus(packageName, PackageStatus.SERVING);
298
+
240
299
  return _package;
241
300
  } catch (error) {
301
+ // Clean up on error - mutex will be automatically released by runExclusive
242
302
  this.packages.delete(packageName);
303
+ this.packageStatuses.delete(packageName);
243
304
  throw error;
244
- } finally {
245
- packageMutex.release();
246
- this.packageMutexes.delete(packageName);
247
305
  }
306
+ // Mutex is automatically released here by runExclusive
248
307
  });
249
308
  }
250
309
 
251
310
  public async addPackage(packageName: string) {
252
311
  const packagePath = path.join(this.projectPath, packageName);
253
312
  if (
254
- !(await fs.promises.exists(packagePath)) ||
313
+ !(await fs.promises
314
+ .access(packagePath)
315
+ .then(() => true)
316
+ .catch(() => false)) ||
255
317
  !(await fs.promises.stat(packagePath)).isDirectory()
256
318
  ) {
257
319
  throw new PackageNotFoundError(`Package ${packageName} not found`);
@@ -263,15 +325,23 @@ export class Project {
263
325
  malloyConnections: this.malloyConnections,
264
326
  },
265
327
  );
266
- this.packages.set(
267
- packageName,
268
- await Package.create(
269
- this.projectName,
328
+ this.setPackageStatus(packageName, PackageStatus.LOADING);
329
+ try {
330
+ this.packages.set(
270
331
  packageName,
271
- packagePath,
272
- this.malloyConnections,
273
- ),
274
- );
332
+ await Package.create(
333
+ this.projectName,
334
+ packageName,
335
+ packagePath,
336
+ this.malloyConnections,
337
+ ),
338
+ );
339
+ } catch (error) {
340
+ logger.error("Error adding package", { error });
341
+ this.deletePackageStatus(packageName);
342
+ throw error;
343
+ }
344
+ this.setPackageStatus(packageName, PackageStatus.SERVING);
275
345
  return this.packages.get(packageName);
276
346
  }
277
347
 
@@ -292,15 +362,60 @@ export class Project {
292
362
  return _package.getPackageMetadata();
293
363
  }
294
364
 
295
- public async deletePackage(packageName: string) {
365
+ public getPackageStatus(packageName: string): PackageInfo | undefined {
366
+ return this.packageStatuses.get(packageName);
367
+ }
368
+
369
+ public setPackageStatus(packageName: string, status: PackageStatus): void {
370
+ const currentStatus = this.packageStatuses.get(packageName);
371
+ this.packageStatuses.set(packageName, {
372
+ name: packageName,
373
+ loadTimestamp: currentStatus?.loadTimestamp || Date.now(),
374
+ status: status,
375
+ });
376
+ }
377
+
378
+ public deletePackageStatus(packageName: string): void {
379
+ this.packageStatuses.delete(packageName);
380
+ }
381
+
382
+ public async deletePackage(packageName: string): Promise<void> {
296
383
  const _package = this.packages.get(packageName);
297
384
  if (!_package) {
298
- throw new PackageNotFoundError(`Package ${packageName} not found`);
385
+ return;
299
386
  }
300
- await fs.promises.rm(path.join(this.projectPath, packageName), {
301
- recursive: true,
302
- });
387
+ const packageStatus = this.packageStatuses.get(packageName);
388
+
389
+ if (packageStatus?.status === PackageStatus.LOADING) {
390
+ logger.error("Package loading. Can't unload.", {
391
+ projectName: this.projectName,
392
+ packageName,
393
+ });
394
+ throw new Error(
395
+ "Package loading. Can't unload. " +
396
+ this.projectName +
397
+ " " +
398
+ packageName,
399
+ );
400
+ } else if (packageStatus?.status === PackageStatus.SERVING) {
401
+ this.setPackageStatus(packageName, PackageStatus.UNLOADING);
402
+ }
403
+
404
+ try {
405
+ await fs.promises.rm(path.join(this.projectPath, packageName), {
406
+ recursive: true,
407
+ force: true,
408
+ });
409
+ } catch (err) {
410
+ logger.error(
411
+ "Error removing package directory while unloading package",
412
+ { error: err, projectName: this.projectName, packageName },
413
+ );
414
+ }
415
+
416
+ // Remove from internal tracking
303
417
  this.packages.delete(packageName);
418
+ this.packageStatuses.delete(packageName);
304
419
  }
305
420
 
306
421
  public async serialize(): Promise<ApiProject> {