@malloy-publisher/server 0.0.86 → 0.0.88
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 +25 -0
- package/dist/app/api-doc.yaml +83 -0
- package/dist/app/assets/{RenderedResult-BAZuT25g-_eLjVkSo.js → RenderedResult-BAZuT25g-DLMDvQic.js} +2 -2
- package/dist/app/assets/{index-BvRGZn2o.js → index-CNRCklos.js} +211 -206
- package/dist/app/assets/index-CcIq0aEZ.css +1 -0
- package/dist/app/assets/index-CobAY3LE.js +427 -0
- package/dist/app/assets/index-lhN38COk.js +211 -0
- package/dist/app/assets/index.umd-CSPhcx55.js +2105 -0
- package/dist/app/index.html +2 -2
- package/dist/instrumentation.js +67969 -35371
- package/dist/server.js +49297 -40630
- package/package.json +7 -4
- package/publisher.config.json +1 -1
- package/src/config.ts +20 -0
- package/src/constants.ts +6 -0
- package/src/controller/package.controller.ts +41 -2
- package/src/controller/watch-mode.controller.ts +83 -0
- package/src/errors.ts +2 -1
- package/src/server.ts +22 -14
- package/src/service/connection.ts +7 -7
- package/src/service/model.ts +6 -6
- package/src/service/package.ts +4 -4
- package/src/service/project.ts +6 -4
- package/src/service/project_store.spec.ts +19 -14
- package/src/service/project_store.ts +262 -47
- package/src/utils.ts +0 -18
- package/dist/app/assets/index-C-lp8bCy.js +0 -210
- package/dist/app/assets/index-EI2L5apg.js +0 -432
- package/dist/app/assets/index-RMQQYOAi.css +0 -1
- package/dist/app/assets/index.umd-DBkg1U_t.js +0 -2078
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@malloy-publisher/server",
|
|
3
3
|
"description": "Malloy Publisher Server",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.88",
|
|
5
5
|
"main": "dist/server.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"malloy-publisher": "dist/server.js"
|
|
@@ -11,16 +11,17 @@
|
|
|
11
11
|
},
|
|
12
12
|
"scripts": {
|
|
13
13
|
"test": "bun test ./**/*.spec.ts",
|
|
14
|
-
"build": "bun generate-api-types &&
|
|
14
|
+
"build": "bun generate-api-types && bun build:app && NODE_ENV=production bun run build.ts",
|
|
15
15
|
"start": "NODE_ENV=production node ./dist/server.js",
|
|
16
16
|
"start:dev": "NODE_ENV=development bun --watch src/server.ts",
|
|
17
17
|
"start:instrumented": "node --require ./dist/instrumentation.js ./dist/server.js",
|
|
18
18
|
"lint": "bunx eslint \"./src/**/*.ts\"",
|
|
19
19
|
"format": "bunx prettier --write '**/*.{ts,tsx}'",
|
|
20
|
-
"build:app": "cd ../app &&
|
|
20
|
+
"build:app": "cd ../app && NODE_ENV=production bunx vite build",
|
|
21
21
|
"generate-api-types": "bunx openapi-typescript ../../api-doc.yaml --output src/api.ts"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
+
"@google-cloud/storage": "^7.16.0",
|
|
24
25
|
"@malloydata/db-bigquery": "^0.0.295",
|
|
25
26
|
"@malloydata/db-duckdb": "^0.0.295",
|
|
26
27
|
"@malloydata/db-mysql": "^0.0.295",
|
|
@@ -37,7 +38,9 @@
|
|
|
37
38
|
"@opentelemetry/sdk-node": "^0.200.0",
|
|
38
39
|
"@opentelemetry/sdk-trace-node": "^2.0.0",
|
|
39
40
|
"async-mutex": "^0.5.0",
|
|
41
|
+
"aws-sdk": "^2.1692.0",
|
|
40
42
|
"body-parser": "^1.20.2",
|
|
43
|
+
"chokidar": "^4.0.3",
|
|
41
44
|
"class-transformer": "^0.5.1",
|
|
42
45
|
"class-validator": "^0.14.1",
|
|
43
46
|
"cors": "^2.8.5",
|
|
@@ -48,6 +51,7 @@
|
|
|
48
51
|
"morgan": "^1.10.0",
|
|
49
52
|
"node-cron": "^3.0.3",
|
|
50
53
|
"recursive-readdir": "^2.2.3",
|
|
54
|
+
"simple-git": "^3.28.0",
|
|
51
55
|
"uuid": "^11.0.3"
|
|
52
56
|
},
|
|
53
57
|
"devDependencies": {
|
|
@@ -65,7 +69,6 @@
|
|
|
65
69
|
"@types/supertest": "^6.0.2",
|
|
66
70
|
"@typescript-eslint/eslint-plugin": "^8.6.0",
|
|
67
71
|
"@typescript-eslint/parser": "^8.6.0",
|
|
68
|
-
"aws-sdk": "^2.1692.0",
|
|
69
72
|
"eslint-config-prettier": "^9.1.0",
|
|
70
73
|
"eslint-plugin-prettier": "^5.2.1",
|
|
71
74
|
"mock-aws-s3": "^4.0.2",
|
package/publisher.config.json
CHANGED
package/src/config.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { PUBLISHER_CONFIG_NAME } from "./constants";
|
|
4
|
+
|
|
5
|
+
type FilesystemPath = `./${string}` | `../${string}` | `/${string}`;
|
|
6
|
+
type GcsPath = `gs://${string}`;
|
|
7
|
+
export type PublisherConfig = {
|
|
8
|
+
frozenConfig: boolean;
|
|
9
|
+
projects: Record<string, FilesystemPath | GcsPath>;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const getPublisherConfig = (serverRoot: string): PublisherConfig => {
|
|
13
|
+
const publisherConfigPath = path.join(serverRoot, PUBLISHER_CONFIG_NAME);
|
|
14
|
+
return JSON.parse(fs.readFileSync(publisherConfigPath, "utf8"));
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const isPublisherConfigFrozen = (serverRoot: string) => {
|
|
18
|
+
const publisherConfig = getPublisherConfig(serverRoot);
|
|
19
|
+
return Boolean(publisherConfig.frozenConfig);
|
|
20
|
+
};
|
package/src/constants.ts
CHANGED
|
@@ -1,2 +1,8 @@
|
|
|
1
1
|
export const API_PREFIX = "/api/v0";
|
|
2
2
|
export const README_NAME = "README.md";
|
|
3
|
+
export const PUBLISHER_CONFIG_NAME = "publisher.config.json";
|
|
4
|
+
export const PACKAGE_MANIFEST_NAME = "publisher.json";
|
|
5
|
+
export const CONNECTIONS_MANIFEST_NAME = "publisher.connections.json";
|
|
6
|
+
export const MODEL_FILE_SUFFIX = ".malloy";
|
|
7
|
+
export const NOTEBOOK_FILE_SUFFIX = ".malloynb";
|
|
8
|
+
export const ROW_LIMIT = 1000;
|
|
@@ -22,8 +22,12 @@ export class PackageController {
|
|
|
22
22
|
reload: boolean,
|
|
23
23
|
): Promise<ApiPackage> {
|
|
24
24
|
const project = await this.projectStore.getProject(projectName, false);
|
|
25
|
-
const
|
|
26
|
-
|
|
25
|
+
const _package = await project.getPackage(packageName, reload);
|
|
26
|
+
const packageLocation = _package.getPackageMetadata().location;
|
|
27
|
+
if (reload && packageLocation) {
|
|
28
|
+
await this.downloadPackage(projectName, packageName, packageLocation);
|
|
29
|
+
}
|
|
30
|
+
return _package.getPackageMetadata();
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
async addPackage(projectName: string, body: ApiPackage) {
|
|
@@ -34,6 +38,9 @@ export class PackageController {
|
|
|
34
38
|
throw new BadRequestError("Package name is required");
|
|
35
39
|
}
|
|
36
40
|
const project = await this.projectStore.getProject(projectName, false);
|
|
41
|
+
if (body.location) {
|
|
42
|
+
await this.downloadPackage(projectName, body.name, body.location);
|
|
43
|
+
}
|
|
37
44
|
return project.addPackage(body.name);
|
|
38
45
|
}
|
|
39
46
|
|
|
@@ -54,6 +61,38 @@ export class PackageController {
|
|
|
54
61
|
throw new FrozenConfigError();
|
|
55
62
|
}
|
|
56
63
|
const project = await this.projectStore.getProject(projectName, false);
|
|
64
|
+
if (body.location) {
|
|
65
|
+
await this.downloadPackage(projectName, packageName, body.location);
|
|
66
|
+
}
|
|
57
67
|
return project.updatePackage(packageName, body);
|
|
58
68
|
}
|
|
69
|
+
|
|
70
|
+
private async downloadPackage(
|
|
71
|
+
projectName: string,
|
|
72
|
+
packageName: string,
|
|
73
|
+
packageLocation: string,
|
|
74
|
+
) {
|
|
75
|
+
const absoluteTargetPath = `/etc/publisher/${projectName}/${packageName}`;
|
|
76
|
+
if (
|
|
77
|
+
packageLocation.startsWith("https://") ||
|
|
78
|
+
packageLocation.startsWith("git@")
|
|
79
|
+
) {
|
|
80
|
+
await this.projectStore.downloadGitHubDirectory(
|
|
81
|
+
packageLocation,
|
|
82
|
+
absoluteTargetPath,
|
|
83
|
+
);
|
|
84
|
+
} else if (packageLocation.startsWith("gs://")) {
|
|
85
|
+
await this.projectStore.downloadGcsDirectory(
|
|
86
|
+
packageLocation,
|
|
87
|
+
projectName,
|
|
88
|
+
absoluteTargetPath,
|
|
89
|
+
);
|
|
90
|
+
} else if (packageLocation.startsWith("s3://")) {
|
|
91
|
+
await this.projectStore.downloadS3Directory(
|
|
92
|
+
packageLocation,
|
|
93
|
+
projectName,
|
|
94
|
+
absoluteTargetPath,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
59
98
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import chokidar, { FSWatcher } from "chokidar";
|
|
2
|
+
import { RequestHandler } from "express";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { components } from "../api";
|
|
5
|
+
import { logger } from "../logger";
|
|
6
|
+
import { ProjectStore } from "../service/project_store";
|
|
7
|
+
|
|
8
|
+
type StartWatchReq = components["schemas"]["StartWatchRequest"];
|
|
9
|
+
type WatchStatusRes = components["schemas"]["WatchStatus"];
|
|
10
|
+
type Handler<Req = {}, Res = void> = RequestHandler<{}, Res, Req>;
|
|
11
|
+
|
|
12
|
+
export class WatchModeController {
|
|
13
|
+
watchingPath: string | null;
|
|
14
|
+
watchingProjectName: string | null;
|
|
15
|
+
watcher: FSWatcher;
|
|
16
|
+
|
|
17
|
+
constructor(private projectStore: ProjectStore) {
|
|
18
|
+
this.watchingPath = null;
|
|
19
|
+
this.watchingProjectName = null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public getWatchStatus: Handler<{}, WatchStatusRes> = async (_req, res) => {
|
|
23
|
+
return res.json({
|
|
24
|
+
enabled: !!this.watchingPath,
|
|
25
|
+
watchingPath: this.watchingPath,
|
|
26
|
+
projectName: this.watchingProjectName ?? undefined,
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
public startWatching: Handler<StartWatchReq> = async (req, res) => {
|
|
31
|
+
const projectManifest = await ProjectStore.reloadProjectManifest(
|
|
32
|
+
this.projectStore.serverRootPath,
|
|
33
|
+
);
|
|
34
|
+
this.watchingProjectName = req.body.projectName;
|
|
35
|
+
this.watchingPath = path.join(
|
|
36
|
+
this.projectStore.serverRootPath,
|
|
37
|
+
projectManifest.projects[req.body.projectName],
|
|
38
|
+
);
|
|
39
|
+
this.watcher = chokidar.watch(this.watchingPath, {
|
|
40
|
+
ignored: (path, stats) =>
|
|
41
|
+
!!stats?.isFile() &&
|
|
42
|
+
!path.endsWith(".malloy") &&
|
|
43
|
+
!path.endsWith(".md"),
|
|
44
|
+
ignoreInitial: true,
|
|
45
|
+
});
|
|
46
|
+
const reloadProject = async () => {
|
|
47
|
+
// Overwrite the project with it's existing metadata to trigger a re-read
|
|
48
|
+
const project = await this.projectStore.getProject(
|
|
49
|
+
req.body.projectName,
|
|
50
|
+
true,
|
|
51
|
+
);
|
|
52
|
+
await this.projectStore.addProject(project.metadata);
|
|
53
|
+
logger.info(`Reloaded ${req.body.projectName}`);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
this.watcher.on("add", async (path) => {
|
|
57
|
+
logger.info(
|
|
58
|
+
`Detected new file ${path}, reloading ${req.body.projectName}`,
|
|
59
|
+
);
|
|
60
|
+
await reloadProject();
|
|
61
|
+
});
|
|
62
|
+
this.watcher.on("unlink", async (path) => {
|
|
63
|
+
logger.info(
|
|
64
|
+
`Detected deletion of ${path}, reloading ${req.body.projectName}`,
|
|
65
|
+
);
|
|
66
|
+
await reloadProject();
|
|
67
|
+
});
|
|
68
|
+
this.watcher.on("change", async (path) => {
|
|
69
|
+
logger.info(
|
|
70
|
+
`Detected change on ${path}, reloading ${req.body.projectName}`,
|
|
71
|
+
);
|
|
72
|
+
await reloadProject();
|
|
73
|
+
});
|
|
74
|
+
res.json();
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
public stopWatchMode: Handler = async (_req, res) => {
|
|
78
|
+
this.watcher.close();
|
|
79
|
+
this.watchingPath = null;
|
|
80
|
+
this.watchingProjectName = null;
|
|
81
|
+
res.json();
|
|
82
|
+
};
|
|
83
|
+
}
|
package/src/errors.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { MalloyError } from "@malloydata/malloy";
|
|
2
|
+
import { PUBLISHER_CONFIG_NAME } from "./constants";
|
|
2
3
|
|
|
3
4
|
export function internalErrorToHttpError(error: Error) {
|
|
4
5
|
if (error instanceof BadRequestError) {
|
|
@@ -84,7 +85,7 @@ export class ModelCompilationError extends Error {
|
|
|
84
85
|
|
|
85
86
|
export class FrozenConfigError extends Error {
|
|
86
87
|
constructor(
|
|
87
|
-
message =
|
|
88
|
+
message = `Publisher config can't be updated when ${PUBLISHER_CONFIG_NAME} has { "frozenConfig": true }`,
|
|
88
89
|
) {
|
|
89
90
|
super(message);
|
|
90
91
|
}
|
package/src/server.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { ModelController } from "./controller/model.controller";
|
|
|
12
12
|
import { PackageController } from "./controller/package.controller";
|
|
13
13
|
import { QueryController } from "./controller/query.controller";
|
|
14
14
|
import { ScheduleController } from "./controller/schedule.controller";
|
|
15
|
+
import { WatchModeController } from "./controller/watch-mode.controller";
|
|
15
16
|
import { internalErrorToHttpError, NotImplementedError } from "./errors";
|
|
16
17
|
import { logger, loggerMiddleware } from "./logger";
|
|
17
18
|
import { initializeMcpServer } from "./mcp/server";
|
|
@@ -62,7 +63,7 @@ function parseArgs() {
|
|
|
62
63
|
parseArgs();
|
|
63
64
|
|
|
64
65
|
const PUBLISHER_PORT = Number(process.env.PUBLISHER_PORT || 4000);
|
|
65
|
-
const PUBLISHER_HOST = process.env.PUBLISHER_HOST || "
|
|
66
|
+
const PUBLISHER_HOST = process.env.PUBLISHER_HOST || "0.0.0.0";
|
|
66
67
|
const MCP_PORT = Number(process.env.MCP_PORT || 4040);
|
|
67
68
|
const MCP_ENDPOINT = "/mcp";
|
|
68
69
|
// Find the app directory - handle NPX vs local execution
|
|
@@ -83,6 +84,7 @@ app.use(loggerMiddleware);
|
|
|
83
84
|
app.use(cors());
|
|
84
85
|
|
|
85
86
|
const projectStore = new ProjectStore(SERVER_ROOT);
|
|
87
|
+
const watchModeController = new WatchModeController(projectStore);
|
|
86
88
|
const connectionController = new ConnectionController(projectStore);
|
|
87
89
|
const modelController = new ModelController(projectStore);
|
|
88
90
|
const packageController = new PackageController(projectStore);
|
|
@@ -206,6 +208,10 @@ const setVersionIdError = (res: express.Response) => {
|
|
|
206
208
|
app.use(cors());
|
|
207
209
|
app.use(bodyParser.json());
|
|
208
210
|
|
|
211
|
+
app.get(`${API_PREFIX}/watch-mode/status`, watchModeController.getWatchStatus);
|
|
212
|
+
app.post(`${API_PREFIX}/watch-mode/start`, watchModeController.startWatching);
|
|
213
|
+
app.post(`${API_PREFIX}/watch-mode/stop`, watchModeController.stopWatchMode);
|
|
214
|
+
|
|
209
215
|
app.get(`${API_PREFIX}/projects`, async (_req, res) => {
|
|
210
216
|
try {
|
|
211
217
|
res.status(200).json(await projectStore.listProjects());
|
|
@@ -705,20 +711,22 @@ app.use(
|
|
|
705
711
|
);
|
|
706
712
|
|
|
707
713
|
const mainServer = http.createServer(app);
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
`Publisher server listening at http://${address.address}:${address.port}`,
|
|
712
|
-
);
|
|
713
|
-
if (isDevelopment) {
|
|
714
|
+
projectStore.finishedInitialization.then(() => {
|
|
715
|
+
mainServer.listen(PUBLISHER_PORT, PUBLISHER_HOST, () => {
|
|
716
|
+
const address = mainServer.address() as AddressInfo;
|
|
714
717
|
logger.info(
|
|
715
|
-
|
|
718
|
+
`Publisher server listening at http://${address.address}:${address.port}`,
|
|
716
719
|
);
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
720
|
+
if (isDevelopment) {
|
|
721
|
+
logger.info(
|
|
722
|
+
"Running in development mode - proxying to React dev server at http://localhost:5173",
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
mcpApp.listen(MCP_PORT, PUBLISHER_HOST, () => {
|
|
727
|
+
logger.info(
|
|
728
|
+
`MCP server listening at http://${PUBLISHER_HOST}:${MCP_PORT}`,
|
|
729
|
+
);
|
|
730
|
+
});
|
|
722
731
|
});
|
|
723
732
|
|
|
724
|
-
export { app, mainServer as httpServer, mcpApp, mcpHttpServer };
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { PostgresConnection } from "@malloydata/db-postgres";
|
|
2
1
|
import { BigQueryConnection } from "@malloydata/db-bigquery";
|
|
2
|
+
import { MySQLConnection } from "@malloydata/db-mysql";
|
|
3
|
+
import { PostgresConnection } from "@malloydata/db-postgres";
|
|
3
4
|
import { SnowflakeConnection } from "@malloydata/db-snowflake";
|
|
4
5
|
import { TrinoConnection } from "@malloydata/db-trino";
|
|
5
|
-
import { MySQLConnection } from "@malloydata/db-mysql";
|
|
6
|
-
import { v4 as uuidv4 } from "uuid";
|
|
7
6
|
import { Connection } from "@malloydata/malloy";
|
|
8
|
-
import { components } from "../api";
|
|
9
|
-
import path from "path";
|
|
10
|
-
import fs from "fs/promises";
|
|
11
|
-
import { CONNECTIONS_MANIFEST_NAME } from "../utils";
|
|
12
7
|
import { BaseConnection } from "@malloydata/malloy/connection";
|
|
8
|
+
import fs from "fs/promises";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import { v4 as uuidv4 } from "uuid";
|
|
11
|
+
import { components } from "../api";
|
|
12
|
+
import { CONNECTIONS_MANIFEST_NAME } from "../constants";
|
|
13
13
|
|
|
14
14
|
type ApiConnection = components["schemas"]["Connection"];
|
|
15
15
|
type ApiConnectionAttributes = components["schemas"]["ConnectionAttributes"];
|
package/src/service/model.ts
CHANGED
|
@@ -25,6 +25,11 @@ import { metrics } from "@opentelemetry/api";
|
|
|
25
25
|
import * as fs from "fs/promises";
|
|
26
26
|
import * as path from "path";
|
|
27
27
|
import { components } from "../api";
|
|
28
|
+
import {
|
|
29
|
+
MODEL_FILE_SUFFIX,
|
|
30
|
+
NOTEBOOK_FILE_SUFFIX,
|
|
31
|
+
ROW_LIMIT,
|
|
32
|
+
} from "../constants";
|
|
28
33
|
import { HackyDataStylesAccumulator } from "../data_styles";
|
|
29
34
|
import {
|
|
30
35
|
BadRequestError,
|
|
@@ -32,12 +37,7 @@ import {
|
|
|
32
37
|
ModelNotFoundError,
|
|
33
38
|
} from "../errors";
|
|
34
39
|
import { logger } from "../logger";
|
|
35
|
-
import {
|
|
36
|
-
MODEL_FILE_SUFFIX,
|
|
37
|
-
NOTEBOOK_FILE_SUFFIX,
|
|
38
|
-
ROW_LIMIT,
|
|
39
|
-
URL_READER,
|
|
40
|
-
} from "../utils";
|
|
40
|
+
import { URL_READER } from "../utils";
|
|
41
41
|
|
|
42
42
|
type ApiCompiledModel = components["schemas"]["CompiledModel"];
|
|
43
43
|
type ApiNotebookCell = components["schemas"]["NotebookCell"];
|
package/src/service/package.ts
CHANGED
|
@@ -11,14 +11,14 @@ import {
|
|
|
11
11
|
import { metrics } from "@opentelemetry/api";
|
|
12
12
|
import recursive from "recursive-readdir";
|
|
13
13
|
import { components } from "../api";
|
|
14
|
-
import { API_PREFIX } from "../constants";
|
|
15
|
-
import { PackageNotFoundError } from "../errors";
|
|
16
|
-
import { logger } from "../logger";
|
|
17
14
|
import {
|
|
15
|
+
API_PREFIX,
|
|
18
16
|
MODEL_FILE_SUFFIX,
|
|
19
17
|
NOTEBOOK_FILE_SUFFIX,
|
|
20
18
|
PACKAGE_MANIFEST_NAME,
|
|
21
|
-
} from "../
|
|
19
|
+
} from "../constants";
|
|
20
|
+
import { PackageNotFoundError } from "../errors";
|
|
21
|
+
import { logger } from "../logger";
|
|
22
22
|
import { createConnections } from "./connection";
|
|
23
23
|
import { Model } from "./model";
|
|
24
24
|
import { Scheduler } from "./scheduler";
|
package/src/service/project.ts
CHANGED
|
@@ -41,6 +41,7 @@ export class Project {
|
|
|
41
41
|
this.metadata = {
|
|
42
42
|
resource: `${API_PREFIX}/projects/${this.projectName}`,
|
|
43
43
|
name: this.projectName,
|
|
44
|
+
location: this.projectPath,
|
|
44
45
|
};
|
|
45
46
|
void this.reloadProjectMetadata();
|
|
46
47
|
}
|
|
@@ -243,10 +244,7 @@ export class Project {
|
|
|
243
244
|
return this.packages.get(packageName);
|
|
244
245
|
}
|
|
245
246
|
|
|
246
|
-
public async updatePackage(
|
|
247
|
-
packageName: string,
|
|
248
|
-
body: { resource?: string; name?: string; description?: string },
|
|
249
|
-
) {
|
|
247
|
+
public async updatePackage(packageName: string, body: ApiPackage) {
|
|
250
248
|
const _package = this.packages.get(packageName);
|
|
251
249
|
if (!_package) {
|
|
252
250
|
throw new PackageNotFoundError(`Package ${packageName} not found`);
|
|
@@ -258,6 +256,7 @@ export class Project {
|
|
|
258
256
|
name: body.name,
|
|
259
257
|
description: body.description,
|
|
260
258
|
resource: body.resource,
|
|
259
|
+
location: body.location,
|
|
261
260
|
});
|
|
262
261
|
return _package.getPackageMetadata();
|
|
263
262
|
}
|
|
@@ -267,6 +266,9 @@ export class Project {
|
|
|
267
266
|
if (!_package) {
|
|
268
267
|
throw new PackageNotFoundError(`Package ${packageName} not found`);
|
|
269
268
|
}
|
|
269
|
+
await fs.rm(path.join(this.projectPath, packageName), {
|
|
270
|
+
recursive: true,
|
|
271
|
+
});
|
|
270
272
|
this.packages.delete(packageName);
|
|
271
273
|
}
|
|
272
274
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { describe, expect, it, mock, spyOn } from "bun:test";
|
|
2
2
|
import * as fs from "fs/promises";
|
|
3
3
|
import path from "path";
|
|
4
|
+
import { isPublisherConfigFrozen } from "../config";
|
|
4
5
|
import { FrozenConfigError, ProjectNotFoundError } from "../errors";
|
|
5
|
-
import { isPublisherConfigFrozen } from "../utils";
|
|
6
6
|
import { ProjectStore } from "./project_store";
|
|
7
7
|
|
|
8
8
|
describe("ProjectStore", () => {
|
|
@@ -17,8 +17,11 @@ describe("ProjectStore", () => {
|
|
|
17
17
|
do {
|
|
18
18
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
19
19
|
retries++;
|
|
20
|
-
} while (
|
|
21
|
-
|
|
20
|
+
} while (
|
|
21
|
+
(await projectStore.listProjects()).length === 0 &&
|
|
22
|
+
retries < maxRetries
|
|
23
|
+
);
|
|
24
|
+
if ((await projectStore.listProjects()).length === 0) {
|
|
22
25
|
throw new Error("Timed out initializing ProjectStore");
|
|
23
26
|
}
|
|
24
27
|
}
|
|
@@ -27,7 +30,7 @@ describe("ProjectStore", () => {
|
|
|
27
30
|
mock(isPublisherConfigFrozen).mockReturnValue(true);
|
|
28
31
|
const projectStore = new ProjectStore(serverRoot);
|
|
29
32
|
await waitForInitialization(projectStore);
|
|
30
|
-
expect(projectStore.listProjects()).toEqual([
|
|
33
|
+
expect(await projectStore.listProjects()).toEqual([
|
|
31
34
|
{
|
|
32
35
|
name: "malloy-samples",
|
|
33
36
|
readme: expect.stringContaining("# Malloy Analysis Examples"),
|
|
@@ -39,11 +42,13 @@ describe("ProjectStore", () => {
|
|
|
39
42
|
it("should list projects from memory by default", async () => {
|
|
40
43
|
mock(isPublisherConfigFrozen).mockReturnValue(true);
|
|
41
44
|
const projectStore = new ProjectStore(serverRoot);
|
|
45
|
+
await waitForInitialization(projectStore);
|
|
46
|
+
|
|
42
47
|
// Mock fs.readFile & fs.readdir to track calls
|
|
43
48
|
const readFileSpy = spyOn(fs, "readFile");
|
|
44
49
|
const readdirSpy = spyOn(fs, "readdir");
|
|
45
50
|
// Call listProjects, which should use memory and not call fs.readFile
|
|
46
|
-
projectStore.listProjects();
|
|
51
|
+
await projectStore.listProjects();
|
|
47
52
|
|
|
48
53
|
expect(readFileSpy).not.toHaveBeenCalled();
|
|
49
54
|
expect(readdirSpy).not.toHaveBeenCalled();
|
|
@@ -72,7 +77,7 @@ describe("ProjectStore", () => {
|
|
|
72
77
|
name: "malloy-samples",
|
|
73
78
|
readme: "Updated README",
|
|
74
79
|
});
|
|
75
|
-
expect(projectStore.listProjects()).toEqual([
|
|
80
|
+
expect(await projectStore.listProjects()).toEqual([
|
|
76
81
|
{
|
|
77
82
|
name: "malloy-samples",
|
|
78
83
|
readme: "Updated README",
|
|
@@ -80,7 +85,7 @@ describe("ProjectStore", () => {
|
|
|
80
85
|
},
|
|
81
86
|
]);
|
|
82
87
|
await projectStore.deleteProject("malloy-samples");
|
|
83
|
-
expect(projectStore.listProjects()).toEqual([]);
|
|
88
|
+
expect(await projectStore.listProjects()).toEqual([]);
|
|
84
89
|
await projectStore.addProject({
|
|
85
90
|
name: "malloy-samples",
|
|
86
91
|
});
|
|
@@ -94,7 +99,7 @@ describe("ProjectStore", () => {
|
|
|
94
99
|
|
|
95
100
|
// After a while, it'll resolve the async promise where the readme gets loaded in memory
|
|
96
101
|
await waitForInitialization(projectStore);
|
|
97
|
-
expect(projectStore.listProjects()).toEqual([
|
|
102
|
+
expect(await projectStore.listProjects()).toEqual([
|
|
98
103
|
{
|
|
99
104
|
name: "malloy-samples",
|
|
100
105
|
readme: expect.stringContaining("# Malloy Analysis Examples"),
|
|
@@ -110,7 +115,7 @@ describe("ProjectStore", () => {
|
|
|
110
115
|
const projectStore = new ProjectStore(serverRoot);
|
|
111
116
|
// Initialization should succeed
|
|
112
117
|
await waitForInitialization(projectStore);
|
|
113
|
-
expect(projectStore.listProjects()).toEqual([
|
|
118
|
+
expect(await projectStore.listProjects()).toEqual([
|
|
114
119
|
{
|
|
115
120
|
name: "malloy-samples",
|
|
116
121
|
readme: expect.stringContaining("# Malloy Analysis Examples"),
|
|
@@ -138,7 +143,7 @@ describe("ProjectStore", () => {
|
|
|
138
143
|
);
|
|
139
144
|
|
|
140
145
|
// Failed methods should not modify the in-memory hashmap
|
|
141
|
-
expect(projectStore.listProjects()).toEqual([
|
|
146
|
+
expect(await projectStore.listProjects()).toEqual([
|
|
142
147
|
{
|
|
143
148
|
name: "malloy-samples",
|
|
144
149
|
readme: expect.stringContaining("# Malloy Analysis Examples"),
|
|
@@ -154,7 +159,7 @@ describe("ProjectStore", () => {
|
|
|
154
159
|
const projectStore = new ProjectStore(serverRoot);
|
|
155
160
|
await waitForInitialization(projectStore);
|
|
156
161
|
await projectStore.deleteProject("malloy-samples");
|
|
157
|
-
expect(projectStore.listProjects()).toEqual([]);
|
|
162
|
+
expect(await projectStore.listProjects()).toEqual([]);
|
|
158
163
|
const readFileSpy = spyOn(fs, "readFile");
|
|
159
164
|
await projectStore.getProject("malloy-samples", true);
|
|
160
165
|
expect(readFileSpy).toHaveBeenCalled();
|
|
@@ -166,8 +171,8 @@ describe("ProjectStore", () => {
|
|
|
166
171
|
}));
|
|
167
172
|
const projectStore = new ProjectStore(serverRoot);
|
|
168
173
|
await waitForInitialization(projectStore);
|
|
169
|
-
expect(
|
|
170
|
-
|
|
171
|
-
);
|
|
174
|
+
expect(
|
|
175
|
+
projectStore.getProject("this-one-does-not-exist", true),
|
|
176
|
+
).rejects.toThrow(ProjectNotFoundError);
|
|
172
177
|
});
|
|
173
178
|
});
|