@malloy-publisher/server 0.0.192 → 0.0.193

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.
Files changed (34) hide show
  1. package/dist/app/api-doc.yaml +522 -1
  2. package/dist/app/assets/{HomePage-H1OH-VW5.js → HomePage-Di9MU3lS.js} +1 -1
  3. package/dist/app/assets/{MainPage-GL06aMke.js → MainPage-yZQo2HSL.js} +1 -1
  4. package/dist/app/assets/{ModelPage-Crau5hgZ.js → ModelPage-Dx2mHWeT.js} +1 -1
  5. package/dist/app/assets/{PackagePage-CbubRhgE.js → PackagePage-Q386Py9t.js} +1 -1
  6. package/dist/app/assets/{ProjectPage-DUlJkYJ4.js → ProjectPage-WR7wPQB-.js} +1 -1
  7. package/dist/app/assets/{RouteError-DrNXNihc.js → RouteError-stRGU4aW.js} +1 -1
  8. package/dist/app/assets/{WorkbookPage-CBBv7n5U.js → WorkbookPage-D3iX0djH.js} +1 -1
  9. package/dist/app/assets/{core-Dzx75uJR.es-DwnFZnyO.js → core-QH4HZQVz.es-CqlQLZdl.js} +1 -1
  10. package/dist/app/assets/{index-d5rvmoZ7.js → index-CVHzPJwN.js} +119 -119
  11. package/dist/app/assets/{index-CzjyS9cx.js → index-DavAceYD.js} +50 -50
  12. package/dist/app/assets/{index-HHdhLUpv.js → index-Y3Y-VRna.js} +1 -1
  13. package/dist/app/assets/{index.umd-CetYIBQY.js → index.umd-Bp8OIhfV.js} +46 -46
  14. package/dist/app/index.html +1 -1
  15. package/dist/server.mjs +1389 -984
  16. package/package.json +10 -10
  17. package/src/controller/connection.controller.ts +102 -27
  18. package/src/dto/connection.dto.spec.ts +4 -0
  19. package/src/dto/connection.dto.ts +46 -2
  20. package/src/server.ts +201 -2
  21. package/src/service/connection.spec.ts +250 -4
  22. package/src/service/connection.ts +326 -473
  23. package/src/service/connection_config.ts +514 -0
  24. package/src/service/connection_service.spec.ts +50 -0
  25. package/src/service/connection_service.ts +125 -32
  26. package/src/service/materialization_service.spec.ts +18 -12
  27. package/src/service/materialization_service.ts +54 -7
  28. package/src/service/model.ts +24 -27
  29. package/src/service/package.spec.ts +125 -1
  30. package/src/service/package.ts +86 -44
  31. package/src/service/project.ts +172 -94
  32. package/src/service/project_store.spec.ts +72 -0
  33. package/src/service/project_store.ts +98 -81
  34. package/tests/unit/duckdb/attached_databases.test.ts +1 -19
@@ -337,6 +337,78 @@ describe("ProjectStore Service", () => {
337
337
  expect(projects.map((p) => p.name)).toContain(projectName2);
338
338
  });
339
339
 
340
+ it("should skip a project with invalid startup connection config", async () => {
341
+ const validProjectName = "valid-project";
342
+ const invalidProjectName = "invalid-motherduck-project";
343
+ const validProjectPath = path.join(serverRootPath, validProjectName);
344
+ const invalidProjectPath = path.join(serverRootPath, invalidProjectName);
345
+
346
+ mkdirSync(validProjectPath, { recursive: true });
347
+ mkdirSync(invalidProjectPath, { recursive: true });
348
+ writeFileSync(
349
+ path.join(validProjectPath, "publisher.json"),
350
+ JSON.stringify({
351
+ name: validProjectName,
352
+ description: "Valid project",
353
+ }),
354
+ );
355
+ writeFileSync(
356
+ path.join(invalidProjectPath, "publisher.json"),
357
+ JSON.stringify({
358
+ name: invalidProjectName,
359
+ description: "Invalid project",
360
+ }),
361
+ );
362
+
363
+ const publisherConfigPath = path.join(
364
+ serverRootPath,
365
+ "publisher.config.json",
366
+ );
367
+ writeFileSync(
368
+ publisherConfigPath,
369
+ JSON.stringify({
370
+ frozenConfig: false,
371
+ projects: [
372
+ {
373
+ name: invalidProjectName,
374
+ packages: [
375
+ {
376
+ name: invalidProjectName,
377
+ location: invalidProjectPath,
378
+ },
379
+ ],
380
+ connections: [
381
+ {
382
+ name: "motherduck",
383
+ type: "motherduck",
384
+ motherduckConnection: {},
385
+ },
386
+ ],
387
+ },
388
+ {
389
+ name: validProjectName,
390
+ packages: [
391
+ {
392
+ name: validProjectName,
393
+ location: validProjectPath,
394
+ },
395
+ ],
396
+ connections: [],
397
+ },
398
+ ],
399
+ }),
400
+ );
401
+
402
+ const newProjectStore = new ProjectStore(serverRootPath);
403
+ await newProjectStore.finishedInitialization;
404
+
405
+ const projects = await newProjectStore.listProjects();
406
+ expect(projects.map((p) => p.name)).toEqual([validProjectName]);
407
+ await expect(
408
+ newProjectStore.getProject(invalidProjectName),
409
+ ).rejects.toThrow();
410
+ });
411
+
340
412
  it("should handle project updates", async () => {
341
413
  // Create a project directory
342
414
  const projectPath = path.join(serverRootPath, projectName);
@@ -117,6 +117,33 @@ export class ProjectStore {
117
117
  this.finishedInitialization = this.initialize();
118
118
  }
119
119
 
120
+ private async addConfiguredProject(project: ProcessedProject) {
121
+ try {
122
+ await this.addProject(
123
+ {
124
+ name: project.name,
125
+ resource: `${API_PREFIX}/projects/${project.name}`,
126
+ connections: project.connections,
127
+ packages: project.packages,
128
+ },
129
+ true,
130
+ );
131
+ } catch (error) {
132
+ this.logProjectInitializationError(project.name, error);
133
+ }
134
+ }
135
+
136
+ private logProjectInitializationError(
137
+ projectName: string | undefined,
138
+ error: unknown,
139
+ ) {
140
+ const label = projectName ? ` "${projectName}"` : "";
141
+ logger.error(
142
+ `Error initializing project${label}; skipping project`,
143
+ this.extractErrorDataFromError(error),
144
+ );
145
+ }
146
+
120
147
  private async initialize() {
121
148
  const reInit = process.env.INITIALIZE_STORAGE === "true";
122
149
  const initialTime = performance.now();
@@ -140,15 +167,7 @@ export class ProjectStore {
140
167
  // Load projects from config file
141
168
  await Promise.all(
142
169
  projectManifest.projects.map(async (project) => {
143
- await this.addProject(
144
- {
145
- name: project.name,
146
- resource: `${API_PREFIX}/projects/${project.name}`,
147
- connections: project.connections,
148
- packages: project.packages,
149
- },
150
- true,
151
- );
170
+ await this.addConfiguredProject(project);
152
171
  }),
153
172
  );
154
173
  } else {
@@ -159,88 +178,87 @@ export class ProjectStore {
159
178
  // Load projects from database
160
179
  await Promise.all(
161
180
  existingProjects.map(async (dbProject) => {
162
- // Check if project files exist on disk
163
- const projectExists = await fs.promises
164
- .access(dbProject.path)
165
- .then(() => true)
166
- .catch(() => false);
167
-
168
- if (!projectExists) {
169
- // Try to find in config and reload
170
- const projectConfig = projectManifest.projects.find(
171
- (p) => p.name === dbProject.name,
172
- );
173
-
174
- if (projectConfig) {
175
- const projectInstance = await this.addProject(
176
- {
177
- name: projectConfig.name,
178
- resource: `${API_PREFIX}/projects/${projectConfig.name}`,
179
- connections: projectConfig.connections,
180
- packages: projectConfig.packages,
181
- },
182
- true,
181
+ try {
182
+ // Check if project files exist on disk
183
+ const projectExists = await fs.promises
184
+ .access(dbProject.path)
185
+ .then(() => true)
186
+ .catch(() => false);
187
+
188
+ if (!projectExists) {
189
+ // Try to find in config and reload
190
+ const projectConfig = projectManifest.projects.find(
191
+ (p) => p.name === dbProject.name,
183
192
  );
184
193
 
185
- // Update database with new path
186
- await repository.updateProject(dbProject.id, {
187
- path: projectInstance.metadata.location,
188
- });
189
-
190
- return projectInstance.listPackages();
191
- } else {
192
- logger.error(
193
- `Project "${dbProject.name}" not found in config and files missing`,
194
- );
195
- return;
194
+ if (projectConfig) {
195
+ const projectInstance = await this.addProject(
196
+ {
197
+ name: projectConfig.name,
198
+ resource: `${API_PREFIX}/projects/${projectConfig.name}`,
199
+ connections: projectConfig.connections,
200
+ packages: projectConfig.packages,
201
+ },
202
+ true,
203
+ );
204
+
205
+ // Update database with new path
206
+ await repository.updateProject(dbProject.id, {
207
+ path: projectInstance.metadata.location,
208
+ });
209
+
210
+ return projectInstance.listPackages();
211
+ } else {
212
+ logger.error(
213
+ `Project "${dbProject.name}" not found in config and files missing`,
214
+ );
215
+ return;
216
+ }
196
217
  }
197
- }
198
218
 
199
- // Get connections from database
200
- const connections = await repository.listConnections(
201
- dbProject.id,
202
- );
203
-
204
- const projectInstance = await Project.create(
205
- dbProject.name,
206
- dbProject.path,
207
- connections.map((conn) => ({
208
- name: conn.name,
209
- type: conn.type,
210
- resource: `${API_PREFIX}/connections/${conn.name}`,
211
- ...conn.config,
212
- })),
213
- );
214
-
215
- this.projects.set(dbProject.name, projectInstance);
216
-
217
- // Get packages from database
218
- const packages = await repository.listPackages(
219
- dbProject.id,
220
- );
221
- packages.forEach((pkg) => {
222
- projectInstance.setPackageStatus(
223
- pkg.name,
224
- PackageStatus.SERVING,
219
+ // Get connections from database
220
+ const connections = await repository.listConnections(
221
+ dbProject.id,
222
+ );
223
+
224
+ const projectInstance = await Project.create(
225
+ dbProject.name,
226
+ dbProject.path,
227
+ connections.map((conn) => ({
228
+ name: conn.name,
229
+ type: conn.type,
230
+ resource: `${API_PREFIX}/connections/${conn.name}`,
231
+ ...conn.config,
232
+ })),
225
233
  );
226
- });
227
234
 
228
- return projectInstance.listPackages();
235
+ // Get packages from database
236
+ const packages = await repository.listPackages(
237
+ dbProject.id,
238
+ );
239
+ packages.forEach((pkg) => {
240
+ projectInstance.setPackageStatus(
241
+ pkg.name,
242
+ PackageStatus.SERVING,
243
+ );
244
+ });
245
+
246
+ this.projects.set(dbProject.name, projectInstance);
247
+
248
+ return projectInstance.listPackages();
249
+ } catch (error) {
250
+ this.logProjectInitializationError(
251
+ dbProject.name,
252
+ error,
253
+ );
254
+ }
229
255
  }),
230
256
  );
231
257
  } else {
232
258
  // Fallback to config file if database is empty
233
259
  await Promise.all(
234
260
  projectManifest.projects.map(async (project) => {
235
- await this.addProject(
236
- {
237
- name: project.name,
238
- resource: `${API_PREFIX}/projects/${project.name}`,
239
- connections: project.connections,
240
- packages: project.packages,
241
- },
242
- true,
243
- );
261
+ await this.addConfiguredProject(project);
244
262
  }),
245
263
  );
246
264
  }
@@ -256,7 +274,6 @@ export class ProjectStore {
256
274
  markNotReady();
257
275
  const errorData = this.extractErrorDataFromError(error);
258
276
  logger.error("Error initializing project store", errorData);
259
- process.exit(1);
260
277
  }
261
278
  }
262
279
 
@@ -856,7 +873,7 @@ export class ProjectStore {
856
873
  const projectPath = project.metadata?.location;
857
874
 
858
875
  // Close all connections before removing the project
859
- project.closeAllConnections();
876
+ await project.closeAllConnections();
860
877
 
861
878
  this.projects.delete(projectName);
862
879
  await this.deleteProjectFromDatabase(projectName);
@@ -838,7 +838,7 @@ describe("createProjectConnections - DuckDB", () => {
838
838
  await expect(
839
839
  createProjectConnections(connections, PROJECT_TEST_DIR),
840
840
  ).rejects.toThrow(
841
- "DuckDB attached databases names cannot conflict with connection name",
841
+ "DuckDB attached database names cannot conflict with connection name",
842
842
  );
843
843
  });
844
844
 
@@ -888,24 +888,6 @@ describe("createProjectConnections - DuckDB", () => {
888
888
  ).rejects.toThrow("DuckDB connection name cannot be 'duckdb'");
889
889
  });
890
890
 
891
- it("should throw when DuckDB connection has no attached databases", async () => {
892
- const connections: ApiConnection[] = [
893
- {
894
- name: "no_attached_db",
895
- type: "duckdb",
896
- duckdbConnection: {
897
- attachedDatabases: [],
898
- },
899
- },
900
- ];
901
-
902
- await expect(
903
- createProjectConnections(connections, PROJECT_TEST_DIR),
904
- ).rejects.toThrow(
905
- "DuckDB connection must have at least one attached database",
906
- );
907
- });
908
-
909
891
  it("should throw on unsupported connection type", async () => {
910
892
  const connections = [
911
893
  {