@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.
- package/.prettierignore +1 -0
- package/build.ts +9 -0
- package/dist/app/api-doc.yaml +3 -0
- package/dist/app/assets/{index-2BGzlUQa.js → index-DYO_URL-.js} +100 -100
- package/dist/app/assets/{index-D1X7Y0Ve.js → index-Dg-zTLb3.js} +1 -1
- package/dist/app/assets/{index.es49-D9XPPIF9.js → index.es50-CDMydA2o.js} +1 -1
- package/dist/app/assets/mui-UpaxdnvH.js +159 -0
- package/dist/app/index.html +2 -2
- package/dist/server.js +307 -98
- package/eslint.config.mjs +8 -0
- package/package.json +2 -1
- package/publisher.config.json +25 -5
- package/src/config.ts +25 -2
- package/src/constants.ts +1 -1
- package/src/controller/package.controller.ts +1 -0
- package/src/controller/watch-mode.controller.ts +19 -4
- package/src/data_styles.ts +10 -3
- package/src/mcp/prompts/index.ts +9 -1
- package/src/mcp/resources/package_resource.ts +2 -1
- package/src/mcp/resources/source_resource.ts +1 -0
- package/src/mcp/resources/view_resource.ts +1 -0
- package/src/server.ts +9 -5
- package/src/service/connection.ts +17 -4
- package/src/service/db_utils.ts +19 -11
- package/src/service/model.ts +2 -0
- package/src/service/package.spec.ts +76 -54
- package/src/service/project.ts +160 -45
- package/src/service/project_store.spec.ts +477 -165
- package/src/service/project_store.ts +319 -69
- package/src/service/scheduler.ts +3 -2
- package/src/utils.ts +0 -1
- package/tests/harness/e2e.ts +60 -58
- package/tests/harness/uris.ts +21 -24
- package/tests/integration/mcp/mcp_resource.integration.spec.ts +10 -0
- package/dist/app/assets/mui-YektUyEU.js +0 -161
package/src/service/project.ts
CHANGED
|
@@ -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.
|
|
52
|
-
this.
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
189
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
|
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.
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
this.projectName,
|
|
328
|
+
this.setPackageStatus(packageName, PackageStatus.LOADING);
|
|
329
|
+
try {
|
|
330
|
+
this.packages.set(
|
|
270
331
|
packageName,
|
|
271
|
-
|
|
272
|
-
|
|
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
|
|
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
|
-
|
|
385
|
+
return;
|
|
299
386
|
}
|
|
300
|
-
|
|
301
|
-
|
|
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> {
|