@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/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.86",
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 && NODE_ENV=production && bun build:app && bun build src/server.ts --outdir dist --target node --format cjs --external='@malloydata/db-duckdb' --external='@malloydata/malloy' --external='@malloydata/malloy-sql' --external='@malloydata/render' --external='@malloydata/db-bigquery' --external='@malloydata/db-mysql' --external='@malloydata/db-postgres' --external='@malloydata/db-snowflake' --external='@malloydata/db-trino' && NODE_ENV=production bun build src/instrumentation.ts --outdir dist --target node --format cjs && bun run build:app && echo '#!/usr/bin/env node' | cat - dist/server.js > temp && mv temp dist/server.js && chmod +x dist/server.js",
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 && bun run build:server && rm -rf ../server/dist/app && mkdir -p ../server/dist && cp -r dist ../server/dist/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",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "frozenConfig": false,
3
3
  "projects": {
4
- "malloy-samples": "./malloy-samples"
4
+ "malloy-samples": "https://github.com/ms2data/malloy-samples"
5
5
  }
6
6
  }
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 p = await project.getPackage(packageName, reload);
26
- return p.getPackageMetadata();
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 = "Publisher config can't be updated when publisher.config.json has `frozenConfig: true`",
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 || "localhost";
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
- mainServer.listen(PUBLISHER_PORT, PUBLISHER_HOST, () => {
709
- const address = mainServer.address() as AddressInfo;
710
- logger.info(
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
- "Running in development mode - proxying to React dev server at http://localhost:5173",
718
+ `Publisher server listening at http://${address.address}:${address.port}`,
716
719
  );
717
- }
718
- });
719
-
720
- const mcpHttpServer = mcpApp.listen(MCP_PORT, PUBLISHER_HOST, () => {
721
- logger.info(`MCP server listening at http://${PUBLISHER_HOST}:${MCP_PORT}`);
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"];
@@ -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"];
@@ -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 "../utils";
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";
@@ -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 (projectStore.listProjects().length === 0 && retries < maxRetries);
21
- if (projectStore.listProjects().length === 0) {
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(projectStore.getProject("this-one-does-not-exist", true)).rejects.toThrow(
170
- ProjectNotFoundError,
171
- );
174
+ expect(
175
+ projectStore.getProject("this-one-does-not-exist", true),
176
+ ).rejects.toThrow(ProjectNotFoundError);
172
177
  });
173
178
  });