@malloy-publisher/server 0.0.87 → 0.0.89
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/build.ts +26 -0
- package/dist/app/api-doc.yaml +126 -5
- package/dist/app/assets/RenderedResult-BAZuT25g-QakVAbYy.js +2 -0
- package/dist/app/assets/{index-BbW5TZg_.js → index-Bq29VQqL.js} +2 -2
- package/dist/app/assets/{index-2xWCh-ya.css → index-CcIq0aEZ.css} +1 -1
- package/dist/app/assets/index-DZMePHJ5.js +251 -0
- package/dist/app/assets/{index-CIfV3yj1.js → index-TslDWlxH.js} +6 -6
- package/dist/app/assets/{index.umd-x-naS8R7.js → index.umd-BN4_E5KD.js} +259 -259
- package/dist/app/assets/mui-BEbinrI-.js +161 -0
- package/dist/app/assets/vendor-c5ypKtDW.js +17 -0
- package/dist/app/index.html +4 -2
- package/dist/instrumentation.js +67818 -35196
- package/dist/server.js +80404 -82231
- package/package.json +11 -5
- package/publisher.config.json +1 -1
- package/src/config.ts +20 -0
- package/src/constants.ts +14 -0
- package/src/controller/connection.controller.ts +21 -4
- package/src/controller/package.controller.ts +52 -2
- package/src/controller/schedule.controller.ts +3 -3
- package/src/controller/watch-mode.controller.ts +83 -0
- package/src/errors.ts +2 -1
- package/src/logger.ts +9 -0
- package/src/server.ts +33 -19
- package/src/service/connection.ts +159 -161
- package/src/service/model.ts +6 -6
- package/src/service/package.spec.ts +12 -10
- package/src/service/package.ts +15 -8
- package/src/service/project.ts +77 -36
- package/src/service/project_store.spec.ts +83 -56
- package/src/service/project_store.ts +330 -50
- package/src/utils.ts +0 -18
- package/tests/harness/mcp_test_setup.ts +5 -5
- package/dist/app/assets/RenderedResult-BAZuT25g-BMU632YI.js +0 -2
- package/dist/app/assets/index-C7whj6wK.js +0 -432
package/src/service/project.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { BaseConnection } from "@malloydata/malloy/connection";
|
|
2
2
|
import { Mutex } from "async-mutex";
|
|
3
|
-
import * as fs from "fs
|
|
3
|
+
import * as fs from "fs";
|
|
4
4
|
import * as path from "path";
|
|
5
5
|
import { components } from "../api";
|
|
6
|
-
import { API_PREFIX, README_NAME } from "../constants";
|
|
6
|
+
import { API_PREFIX, PACKAGE_MANIFEST_NAME, README_NAME } from "../constants";
|
|
7
7
|
import {
|
|
8
8
|
ConnectionNotFoundError,
|
|
9
9
|
PackageNotFoundError,
|
|
10
10
|
ProjectNotFoundError,
|
|
11
11
|
} from "../errors";
|
|
12
|
+
import { logger } from "../logger";
|
|
12
13
|
import { createConnections, InternalConnection } from "./connection";
|
|
13
14
|
import { ApiConnection } from "./model";
|
|
14
15
|
import { Package } from "./package";
|
|
@@ -41,6 +42,7 @@ export class Project {
|
|
|
41
42
|
this.metadata = {
|
|
42
43
|
resource: `${API_PREFIX}/projects/${this.projectName}`,
|
|
43
44
|
name: this.projectName,
|
|
45
|
+
location: this.projectPath,
|
|
44
46
|
};
|
|
45
47
|
void this.reloadProjectMetadata();
|
|
46
48
|
}
|
|
@@ -58,7 +60,7 @@ export class Project {
|
|
|
58
60
|
`${API_PREFIX}/projects/`,
|
|
59
61
|
"",
|
|
60
62
|
);
|
|
61
|
-
if (!(await fs.exists(this.projectPath))) {
|
|
63
|
+
if (!(await fs.promises.exists(this.projectPath))) {
|
|
62
64
|
throw new ProjectNotFoundError(
|
|
63
65
|
`Project path "${this.projectPath}" not found`,
|
|
64
66
|
);
|
|
@@ -72,14 +74,24 @@ export class Project {
|
|
|
72
74
|
static async create(
|
|
73
75
|
projectName: string,
|
|
74
76
|
projectPath: string,
|
|
77
|
+
defaultConnections: ApiConnection[],
|
|
75
78
|
): Promise<Project> {
|
|
76
|
-
if (!(await fs.stat(projectPath)).isDirectory()) {
|
|
79
|
+
if (!(await fs.promises.stat(projectPath)).isDirectory()) {
|
|
77
80
|
throw new ProjectNotFoundError(
|
|
78
81
|
`Project path ${projectPath} not found`,
|
|
79
82
|
);
|
|
80
83
|
}
|
|
81
|
-
const { malloyConnections, apiConnections } =
|
|
82
|
-
|
|
84
|
+
const { malloyConnections, apiConnections } = await createConnections(
|
|
85
|
+
projectPath,
|
|
86
|
+
defaultConnections,
|
|
87
|
+
);
|
|
88
|
+
logger.info(
|
|
89
|
+
`Loaded ${malloyConnections.size + apiConnections.length} connections for project ${projectName}`,
|
|
90
|
+
{
|
|
91
|
+
malloyConnections,
|
|
92
|
+
apiConnections,
|
|
93
|
+
},
|
|
94
|
+
);
|
|
83
95
|
return new Project(
|
|
84
96
|
projectName,
|
|
85
97
|
projectPath,
|
|
@@ -103,7 +115,7 @@ export class Project {
|
|
|
103
115
|
let readme = "";
|
|
104
116
|
try {
|
|
105
117
|
readme = (
|
|
106
|
-
await fs.readFile(path.join(this.projectPath, README_NAME))
|
|
118
|
+
await fs.promises.readFile(path.join(this.projectPath, README_NAME))
|
|
107
119
|
).toString();
|
|
108
120
|
} catch {
|
|
109
121
|
// Readme not found, so we'll just return an empty string
|
|
@@ -157,27 +169,33 @@ export class Project {
|
|
|
157
169
|
}
|
|
158
170
|
|
|
159
171
|
public async listPackages(): Promise<ApiPackage[]> {
|
|
172
|
+
logger.info("Listing packages", { projectPath: this.projectPath });
|
|
160
173
|
try {
|
|
161
|
-
const files = await fs.readdir(this.projectPath, {
|
|
174
|
+
const files = await fs.promises.readdir(this.projectPath, {
|
|
162
175
|
withFileTypes: true,
|
|
163
176
|
});
|
|
177
|
+
const packageDirectories = files.filter(
|
|
178
|
+
(file) =>
|
|
179
|
+
file.isDirectory() &&
|
|
180
|
+
fs.existsSync(
|
|
181
|
+
path.join(this.projectPath, file.name, PACKAGE_MANIFEST_NAME),
|
|
182
|
+
),
|
|
183
|
+
);
|
|
164
184
|
const packageMetadata = await Promise.all(
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
}),
|
|
185
|
+
packageDirectories.map(async (directory) => {
|
|
186
|
+
try {
|
|
187
|
+
return (
|
|
188
|
+
await this.getPackage(directory.name, false)
|
|
189
|
+
).getPackageMetadata();
|
|
190
|
+
} catch (error) {
|
|
191
|
+
logger.error(
|
|
192
|
+
`Failed to load package: ${directory.name} due to : ${error}`,
|
|
193
|
+
);
|
|
194
|
+
// Directory did not contain a valid package.json file -- therefore, it's not a package.
|
|
195
|
+
// Or it timed out
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
}),
|
|
181
199
|
);
|
|
182
200
|
// Get rid of undefined entries (i.e, directories without publisher.json files).
|
|
183
201
|
const filteredMetadata = packageMetadata.filter(
|
|
@@ -185,23 +203,27 @@ export class Project {
|
|
|
185
203
|
) as ApiPackage[];
|
|
186
204
|
return filteredMetadata;
|
|
187
205
|
} catch (error) {
|
|
188
|
-
|
|
206
|
+
logger.error("Error listing packages", { error });
|
|
207
|
+
console.error(error);
|
|
208
|
+
throw error;
|
|
189
209
|
}
|
|
190
210
|
}
|
|
191
211
|
|
|
192
212
|
public async getPackage(
|
|
193
213
|
packageName: string,
|
|
194
|
-
reload: boolean,
|
|
214
|
+
reload: boolean = false,
|
|
195
215
|
): Promise<Package> {
|
|
196
216
|
// We need to acquire the mutex to prevent a thundering herd of requests from creating the
|
|
197
217
|
// package multiple times.
|
|
198
218
|
let packageMutex = this.packageMutexes.get(packageName);
|
|
199
|
-
if (
|
|
200
|
-
packageMutex
|
|
201
|
-
this.
|
|
219
|
+
if (packageMutex?.isLocked()) {
|
|
220
|
+
await packageMutex.waitForUnlock();
|
|
221
|
+
return this.packages.get(packageName)!;
|
|
202
222
|
}
|
|
223
|
+
packageMutex = new Mutex();
|
|
224
|
+
this.packageMutexes.set(packageName, packageMutex);
|
|
203
225
|
|
|
204
|
-
return
|
|
226
|
+
return packageMutex.runExclusive(async () => {
|
|
205
227
|
const _package = this.packages.get(packageName);
|
|
206
228
|
if (_package !== undefined && !reload) {
|
|
207
229
|
return _package;
|
|
@@ -219,6 +241,9 @@ export class Project {
|
|
|
219
241
|
} catch (error) {
|
|
220
242
|
this.packages.delete(packageName);
|
|
221
243
|
throw error;
|
|
244
|
+
} finally {
|
|
245
|
+
packageMutex.release();
|
|
246
|
+
this.packageMutexes.delete(packageName);
|
|
222
247
|
}
|
|
223
248
|
});
|
|
224
249
|
}
|
|
@@ -226,11 +251,18 @@ export class Project {
|
|
|
226
251
|
public async addPackage(packageName: string) {
|
|
227
252
|
const packagePath = path.join(this.projectPath, packageName);
|
|
228
253
|
if (
|
|
229
|
-
!(await fs.exists(packagePath)) ||
|
|
230
|
-
!(await fs.stat(packagePath)).isDirectory()
|
|
254
|
+
!(await fs.promises.exists(packagePath)) ||
|
|
255
|
+
!(await fs.promises.stat(packagePath)).isDirectory()
|
|
231
256
|
) {
|
|
232
257
|
throw new PackageNotFoundError(`Package ${packageName} not found`);
|
|
233
258
|
}
|
|
259
|
+
logger.info(
|
|
260
|
+
`Adding package ${packageName} to project ${this.projectName}`,
|
|
261
|
+
{
|
|
262
|
+
packagePath,
|
|
263
|
+
malloyConnections: this.malloyConnections,
|
|
264
|
+
},
|
|
265
|
+
);
|
|
234
266
|
this.packages.set(
|
|
235
267
|
packageName,
|
|
236
268
|
await Package.create(
|
|
@@ -243,10 +275,7 @@ export class Project {
|
|
|
243
275
|
return this.packages.get(packageName);
|
|
244
276
|
}
|
|
245
277
|
|
|
246
|
-
public async updatePackage(
|
|
247
|
-
packageName: string,
|
|
248
|
-
body: { resource?: string; name?: string; description?: string },
|
|
249
|
-
) {
|
|
278
|
+
public async updatePackage(packageName: string, body: ApiPackage) {
|
|
250
279
|
const _package = this.packages.get(packageName);
|
|
251
280
|
if (!_package) {
|
|
252
281
|
throw new PackageNotFoundError(`Package ${packageName} not found`);
|
|
@@ -258,6 +287,7 @@ export class Project {
|
|
|
258
287
|
name: body.name,
|
|
259
288
|
description: body.description,
|
|
260
289
|
resource: body.resource,
|
|
290
|
+
location: body.location,
|
|
261
291
|
});
|
|
262
292
|
return _package.getPackageMetadata();
|
|
263
293
|
}
|
|
@@ -267,6 +297,17 @@ export class Project {
|
|
|
267
297
|
if (!_package) {
|
|
268
298
|
throw new PackageNotFoundError(`Package ${packageName} not found`);
|
|
269
299
|
}
|
|
300
|
+
await fs.promises.rm(path.join(this.projectPath, packageName), {
|
|
301
|
+
recursive: true,
|
|
302
|
+
});
|
|
270
303
|
this.packages.delete(packageName);
|
|
271
304
|
}
|
|
305
|
+
|
|
306
|
+
public async serialize(): Promise<ApiProject> {
|
|
307
|
+
return {
|
|
308
|
+
...this.metadata,
|
|
309
|
+
connections: this.listApiConnections(),
|
|
310
|
+
packages: await this.listPackages(),
|
|
311
|
+
};
|
|
312
|
+
}
|
|
272
313
|
}
|
|
@@ -1,37 +1,52 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
afterAll,
|
|
3
|
+
beforeAll,
|
|
4
|
+
describe,
|
|
5
|
+
expect,
|
|
6
|
+
it,
|
|
7
|
+
mock,
|
|
8
|
+
spyOn,
|
|
9
|
+
} from "bun:test";
|
|
10
|
+
import { rmSync } from "fs";
|
|
2
11
|
import * as fs from "fs/promises";
|
|
3
12
|
import path from "path";
|
|
13
|
+
import { isPublisherConfigFrozen } from "../config";
|
|
14
|
+
import { publisherPath } from "../constants";
|
|
4
15
|
import { FrozenConfigError, ProjectNotFoundError } from "../errors";
|
|
5
|
-
import {
|
|
16
|
+
import { logger } from "../logger";
|
|
6
17
|
import { ProjectStore } from "./project_store";
|
|
18
|
+
import sinon from "sinon";
|
|
7
19
|
|
|
8
20
|
describe("ProjectStore", () => {
|
|
9
21
|
const serverRoot = path.resolve(
|
|
10
22
|
process.cwd(),
|
|
11
23
|
process.env.SERVER_ROOT || ".",
|
|
12
24
|
);
|
|
25
|
+
let loggerStub: sinon.SinonStub;
|
|
13
26
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
it("should load all projects from publisher.config.json within a second on initialization", async () => {
|
|
27
|
+
beforeAll(() => {
|
|
28
|
+
rmSync(path.resolve(publisherPath, "malloy-samples"), {
|
|
29
|
+
recursive: true,
|
|
30
|
+
force: true,
|
|
31
|
+
});
|
|
32
|
+
loggerStub = sinon.stub(logger, "info").returns(logger);
|
|
33
|
+
});
|
|
34
|
+
afterAll(() => {
|
|
35
|
+
loggerStub.restore();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("should load all projects from publisher.config.json on initialization", async () => {
|
|
27
39
|
mock(isPublisherConfigFrozen).mockReturnValue(true);
|
|
28
40
|
const projectStore = new ProjectStore(serverRoot);
|
|
29
|
-
|
|
30
|
-
|
|
41
|
+
mock(projectStore.downloadGitHubDirectory).mockResolvedValue(undefined);
|
|
42
|
+
await projectStore.finishedInitialization;
|
|
43
|
+
expect(await projectStore.listProjects()).toEqual([
|
|
31
44
|
{
|
|
32
45
|
name: "malloy-samples",
|
|
33
|
-
readme: expect.
|
|
46
|
+
readme: expect.any(String),
|
|
34
47
|
resource: "/api/v0/projects/malloy-samples",
|
|
48
|
+
packages: expect.any(Array),
|
|
49
|
+
connections: expect.any(Array),
|
|
35
50
|
},
|
|
36
51
|
]);
|
|
37
52
|
});
|
|
@@ -39,14 +54,16 @@ describe("ProjectStore", () => {
|
|
|
39
54
|
it("should list projects from memory by default", async () => {
|
|
40
55
|
mock(isPublisherConfigFrozen).mockReturnValue(true);
|
|
41
56
|
const projectStore = new ProjectStore(serverRoot);
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
57
|
+
const projects = await projectStore.listProjects();
|
|
58
|
+
expect(projects).toEqual([
|
|
59
|
+
{
|
|
60
|
+
name: "malloy-samples",
|
|
61
|
+
readme: expect.any(String),
|
|
62
|
+
resource: "/api/v0/projects/malloy-samples",
|
|
63
|
+
packages: expect.any(Array),
|
|
64
|
+
connections: expect.any(Array),
|
|
65
|
+
},
|
|
66
|
+
]);
|
|
50
67
|
});
|
|
51
68
|
|
|
52
69
|
it("should list projects from disk if reload is true", async () => {
|
|
@@ -63,24 +80,31 @@ describe("ProjectStore", () => {
|
|
|
63
80
|
});
|
|
64
81
|
|
|
65
82
|
it("should allow modifying the in-memory hashmap if config is not frozen", async () => {
|
|
66
|
-
mock.module("../
|
|
83
|
+
mock.module("../config", () => ({
|
|
67
84
|
isPublisherConfigFrozen: () => false,
|
|
68
85
|
}));
|
|
69
86
|
const projectStore = new ProjectStore(serverRoot);
|
|
70
|
-
|
|
87
|
+
mock(projectStore.downloadGitHubDirectory).mockResolvedValue(undefined);
|
|
88
|
+
await projectStore.finishedInitialization;
|
|
71
89
|
await projectStore.updateProject({
|
|
72
90
|
name: "malloy-samples",
|
|
73
91
|
readme: "Updated README",
|
|
74
92
|
});
|
|
75
|
-
|
|
76
|
-
|
|
93
|
+
let projects = await projectStore.listProjects();
|
|
94
|
+
projects = await projectStore.listProjects();
|
|
95
|
+
let malloySamplesProject = projects.find(
|
|
96
|
+
(p) => p.name === "malloy-samples",
|
|
97
|
+
);
|
|
98
|
+
expect(malloySamplesProject).toBeDefined();
|
|
99
|
+
expect(malloySamplesProject).toMatchObject(
|
|
100
|
+
expect.objectContaining({
|
|
77
101
|
name: "malloy-samples",
|
|
78
102
|
readme: "Updated README",
|
|
79
103
|
resource: "/api/v0/projects/malloy-samples",
|
|
80
|
-
},
|
|
81
|
-
|
|
104
|
+
}),
|
|
105
|
+
);
|
|
82
106
|
await projectStore.deleteProject("malloy-samples");
|
|
83
|
-
expect(projectStore.listProjects()).toEqual([]);
|
|
107
|
+
expect(await projectStore.listProjects()).toEqual([]);
|
|
84
108
|
await projectStore.addProject({
|
|
85
109
|
name: "malloy-samples",
|
|
86
110
|
});
|
|
@@ -90,31 +114,32 @@ describe("ProjectStore", () => {
|
|
|
90
114
|
).toHaveProperty("metadata", {
|
|
91
115
|
name: "malloy-samples",
|
|
92
116
|
resource: "/api/v0/projects/malloy-samples",
|
|
117
|
+
location: expect.any(String),
|
|
93
118
|
});
|
|
94
119
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
expect(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
},
|
|
103
|
-
]);
|
|
120
|
+
projects = await projectStore.listProjects();
|
|
121
|
+
malloySamplesProject = projects.find((p) => p.name === "malloy-samples");
|
|
122
|
+
expect(malloySamplesProject).toBeDefined();
|
|
123
|
+
expect(malloySamplesProject).toMatchObject({
|
|
124
|
+
name: "malloy-samples",
|
|
125
|
+
resource: "/api/v0/projects/malloy-samples",
|
|
126
|
+
});
|
|
104
127
|
});
|
|
105
128
|
|
|
106
129
|
it("should not allow modifying the in-memory hashmap if config is frozen", async () => {
|
|
107
|
-
mock.module("../
|
|
130
|
+
mock.module("../config", () => ({
|
|
108
131
|
isPublisherConfigFrozen: () => true,
|
|
109
132
|
}));
|
|
110
133
|
const projectStore = new ProjectStore(serverRoot);
|
|
111
134
|
// Initialization should succeed
|
|
112
|
-
await
|
|
113
|
-
expect(projectStore.listProjects()).toEqual([
|
|
135
|
+
await projectStore.finishedInitialization;
|
|
136
|
+
expect(await projectStore.listProjects()).toEqual([
|
|
114
137
|
{
|
|
115
138
|
name: "malloy-samples",
|
|
116
|
-
readme: expect.
|
|
139
|
+
readme: expect.any(String),
|
|
117
140
|
resource: "/api/v0/projects/malloy-samples",
|
|
141
|
+
packages: expect.any(Array),
|
|
142
|
+
connections: expect.any(Array),
|
|
118
143
|
},
|
|
119
144
|
]);
|
|
120
145
|
// Adding a project should fail
|
|
@@ -138,36 +163,38 @@ describe("ProjectStore", () => {
|
|
|
138
163
|
);
|
|
139
164
|
|
|
140
165
|
// Failed methods should not modify the in-memory hashmap
|
|
141
|
-
expect(projectStore.listProjects()).toEqual([
|
|
166
|
+
expect(await projectStore.listProjects()).toEqual([
|
|
142
167
|
{
|
|
143
168
|
name: "malloy-samples",
|
|
144
|
-
readme: expect.
|
|
169
|
+
readme: expect.any(String),
|
|
145
170
|
resource: "/api/v0/projects/malloy-samples",
|
|
171
|
+
packages: expect.any(Array),
|
|
172
|
+
connections: expect.any(Array),
|
|
146
173
|
},
|
|
147
174
|
]);
|
|
148
175
|
});
|
|
149
176
|
|
|
150
177
|
it("should always try to reload a project if it's not in the hashmap", async () => {
|
|
151
|
-
mock.module("../
|
|
178
|
+
mock.module("../config", () => ({
|
|
152
179
|
isPublisherConfigFrozen: () => false,
|
|
153
180
|
}));
|
|
154
181
|
const projectStore = new ProjectStore(serverRoot);
|
|
155
|
-
await
|
|
182
|
+
await projectStore.finishedInitialization;
|
|
156
183
|
await projectStore.deleteProject("malloy-samples");
|
|
157
|
-
expect(projectStore.listProjects()).toEqual([]);
|
|
184
|
+
expect(await projectStore.listProjects()).toEqual([]);
|
|
158
185
|
const readFileSpy = spyOn(fs, "readFile");
|
|
159
186
|
await projectStore.getProject("malloy-samples", true);
|
|
160
187
|
expect(readFileSpy).toHaveBeenCalled();
|
|
161
188
|
});
|
|
162
189
|
|
|
163
190
|
it("should throw a NotFound error when reloading a project that is not in disk", async () => {
|
|
164
|
-
mock.module("../
|
|
191
|
+
mock.module("../config", () => ({
|
|
165
192
|
isPublisherConfigFrozen: () => false,
|
|
166
193
|
}));
|
|
167
194
|
const projectStore = new ProjectStore(serverRoot);
|
|
168
|
-
await
|
|
169
|
-
expect(
|
|
170
|
-
|
|
171
|
-
);
|
|
195
|
+
await projectStore.finishedInitialization;
|
|
196
|
+
expect(
|
|
197
|
+
projectStore.getProject("this-one-does-not-exist", true),
|
|
198
|
+
).rejects.toThrow(ProjectNotFoundError);
|
|
172
199
|
});
|
|
173
200
|
});
|