@malloy-publisher/server 0.0.85 → 0.0.87

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.
@@ -2,40 +2,62 @@ import * as fs from "fs/promises";
2
2
  import * as path from "path";
3
3
  import { components } from "../api";
4
4
  import { API_PREFIX } from "../constants";
5
- import { ProjectNotFoundError } from "../errors";
5
+ import { FrozenConfigError, ProjectNotFoundError } from "../errors";
6
6
  import { logger } from "../logger";
7
+ import { isPublisherConfigFrozen } from "../utils";
7
8
  import { Project } from "./project";
8
9
  type ApiProject = components["schemas"]["Project"];
9
10
 
10
11
  export class ProjectStore {
11
12
  private serverRootPath: string;
12
13
  private projects: Map<string, Project> = new Map();
14
+ public publisherConfigIsFrozen: boolean;
13
15
 
14
16
  constructor(serverRootPath: string) {
15
17
  this.serverRootPath = serverRootPath;
18
+ void this.initialize();
16
19
  }
17
20
 
18
- public async listProjects(): Promise<ApiProject[]> {
19
- const projectManifest = await ProjectStore.getProjectManifest(
20
- this.serverRootPath,
21
- );
22
- if (!projectManifest.projects) {
23
- return [];
24
- } else {
25
- return Object.keys(projectManifest.projects).map((projectName) => ({
26
- name: projectName,
27
- resource: `${API_PREFIX}/projects/${projectName}`,
28
- })) as ApiProject[];
21
+ private async initialize() {
22
+ try {
23
+ this.publisherConfigIsFrozen = isPublisherConfigFrozen(
24
+ this.serverRootPath,
25
+ );
26
+ const projectManifest = await ProjectStore.reloadProjectManifest(
27
+ this.serverRootPath,
28
+ );
29
+ for (const projectName of Object.keys(projectManifest.projects)) {
30
+ const projectPath = projectManifest.projects[projectName];
31
+ const absoluteProjectPath = path.join(
32
+ this.serverRootPath,
33
+ projectPath,
34
+ );
35
+ const project = await Project.create(
36
+ projectName,
37
+ absoluteProjectPath,
38
+ );
39
+ this.projects.set(projectName, project);
40
+ }
41
+ logger.info("Project store successfully initialized");
42
+ } catch (error) {
43
+ logger.error("Error initializing project store", { error });
44
+ process.exit(1);
29
45
  }
30
46
  }
31
47
 
48
+ public listProjects() {
49
+ return Array.from(this.projects.values()).map(
50
+ (project) => project.metadata,
51
+ );
52
+ }
53
+
32
54
  public async getProject(
33
55
  projectName: string,
34
56
  reload: boolean,
35
57
  ): Promise<Project> {
36
58
  let project = this.projects.get(projectName);
37
59
  if (project === undefined || reload) {
38
- const projectManifest = await ProjectStore.getProjectManifest(
60
+ const projectManifest = await ProjectStore.reloadProjectManifest(
39
61
  this.serverRootPath,
40
62
  );
41
63
  if (
@@ -43,22 +65,75 @@ export class ProjectStore {
43
65
  !projectManifest.projects[projectName]
44
66
  ) {
45
67
  throw new ProjectNotFoundError(
46
- `Project ${projectName} not found in publisher.config.json`,
68
+ `Project "${projectName}" not found in publisher`,
47
69
  );
48
70
  }
49
- project = await Project.create(
50
- projectName,
51
- path.join(
52
- this.serverRootPath,
53
- projectManifest.projects[projectName],
54
- ),
71
+ project = await this.addProject({
72
+ name: projectName,
73
+ resource: `${API_PREFIX}/projects/${projectName}`,
74
+ });
75
+ }
76
+ return project;
77
+ }
78
+
79
+ public async addProject(project: ApiProject) {
80
+ if (this.publisherConfigIsFrozen) {
81
+ throw new FrozenConfigError();
82
+ }
83
+ const projectName = project.name;
84
+ if (!projectName) {
85
+ throw new Error("Project name is required");
86
+ }
87
+ const projectManifest = await ProjectStore.reloadProjectManifest(
88
+ this.serverRootPath,
89
+ );
90
+ const projectPath = projectManifest.projects[projectName];
91
+ if (!projectPath) {
92
+ throw new ProjectNotFoundError(
93
+ `Project "${projectName}" not found in publisher.config.json`,
94
+ );
95
+ }
96
+ const absoluteProjectPath = path.join(this.serverRootPath, projectPath);
97
+ if (!(await fs.stat(absoluteProjectPath)).isDirectory()) {
98
+ throw new ProjectNotFoundError(
99
+ `Project ${projectName} not found in ${absoluteProjectPath}`,
55
100
  );
56
- this.projects.set(projectName, project);
57
101
  }
102
+ const newProject = await Project.create(projectName, absoluteProjectPath);
103
+ this.projects.set(projectName, newProject);
104
+ return newProject;
105
+ }
106
+
107
+ public async updateProject(project: ApiProject) {
108
+ if (this.publisherConfigIsFrozen) {
109
+ throw new FrozenConfigError();
110
+ }
111
+ const projectName = project.name;
112
+ if (!projectName) {
113
+ throw new Error("Project name is required");
114
+ }
115
+ const existingProject = this.projects.get(projectName);
116
+ if (!existingProject) {
117
+ throw new ProjectNotFoundError(`Project ${projectName} not found`);
118
+ }
119
+ const updatedProject = await existingProject.update(project);
120
+ this.projects.set(projectName, updatedProject);
121
+ return updatedProject;
122
+ }
123
+
124
+ public async deleteProject(projectName: string) {
125
+ if (this.publisherConfigIsFrozen) {
126
+ throw new FrozenConfigError();
127
+ }
128
+ const project = this.projects.get(projectName);
129
+ if (!project) {
130
+ throw new ProjectNotFoundError(`Project ${projectName} not found`);
131
+ }
132
+ this.projects.delete(projectName);
58
133
  return project;
59
134
  }
60
135
 
61
- private static async getProjectManifest(
136
+ private static async reloadProjectManifest(
62
137
  serverRootPath: string,
63
138
  ): Promise<{ projects: { [key: string]: string } }> {
64
139
  try {
package/src/utils.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { URLReader } from "@malloydata/malloy";
2
- import { promises as fs } from "fs";
2
+ import * as fs from "fs";
3
+ import path from "path";
3
4
  import { fileURLToPath } from "url";
4
5
 
5
6
  export const PACKAGE_MANIFEST_NAME = "publisher.json";
@@ -15,6 +16,17 @@ export const URL_READER: URLReader = {
15
16
  if (url.protocol == "file:") {
16
17
  path = fileURLToPath(url);
17
18
  }
18
- return fs.readFile(path, "utf8");
19
+ return fs.promises.readFile(path, "utf8");
19
20
  },
20
21
  };
22
+
23
+ export const isPublisherConfigFrozen = (serverRoot: string) => {
24
+ const publisherConfigPath = path.join(serverRoot, "publisher.config.json");
25
+ if (!fs.existsSync(publisherConfigPath)) {
26
+ return false;
27
+ }
28
+ const publisherConfig = JSON.parse(
29
+ fs.readFileSync(publisherConfigPath, "utf8"),
30
+ );
31
+ return Boolean(publisherConfig.frozenConfig);
32
+ };