@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.
- package/dist/app/api-doc.yaml +522 -1
- package/dist/app/assets/{HomePage-H1OH-VW5.js → HomePage-Di9MU3lS.js} +1 -1
- package/dist/app/assets/{MainPage-GL06aMke.js → MainPage-yZQo2HSL.js} +1 -1
- package/dist/app/assets/{ModelPage-Crau5hgZ.js → ModelPage-Dx2mHWeT.js} +1 -1
- package/dist/app/assets/{PackagePage-CbubRhgE.js → PackagePage-Q386Py9t.js} +1 -1
- package/dist/app/assets/{ProjectPage-DUlJkYJ4.js → ProjectPage-WR7wPQB-.js} +1 -1
- package/dist/app/assets/{RouteError-DrNXNihc.js → RouteError-stRGU4aW.js} +1 -1
- package/dist/app/assets/{WorkbookPage-CBBv7n5U.js → WorkbookPage-D3iX0djH.js} +1 -1
- package/dist/app/assets/{core-Dzx75uJR.es-DwnFZnyO.js → core-QH4HZQVz.es-CqlQLZdl.js} +1 -1
- package/dist/app/assets/{index-d5rvmoZ7.js → index-CVHzPJwN.js} +119 -119
- package/dist/app/assets/{index-CzjyS9cx.js → index-DavAceYD.js} +50 -50
- package/dist/app/assets/{index-HHdhLUpv.js → index-Y3Y-VRna.js} +1 -1
- package/dist/app/assets/{index.umd-CetYIBQY.js → index.umd-Bp8OIhfV.js} +46 -46
- package/dist/app/index.html +1 -1
- package/dist/server.mjs +1389 -984
- package/package.json +10 -10
- package/src/controller/connection.controller.ts +102 -27
- package/src/dto/connection.dto.spec.ts +4 -0
- package/src/dto/connection.dto.ts +46 -2
- package/src/server.ts +201 -2
- package/src/service/connection.spec.ts +250 -4
- package/src/service/connection.ts +326 -473
- package/src/service/connection_config.ts +514 -0
- package/src/service/connection_service.spec.ts +50 -0
- package/src/service/connection_service.ts +125 -32
- package/src/service/materialization_service.spec.ts +18 -12
- package/src/service/materialization_service.ts +54 -7
- package/src/service/model.ts +24 -27
- package/src/service/package.spec.ts +125 -1
- package/src/service/package.ts +86 -44
- package/src/service/project.ts +172 -94
- package/src/service/project_store.spec.ts +72 -0
- package/src/service/project_store.ts +98 -81
- 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.
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
.
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
{
|