@malloy-publisher/server 0.0.192 → 0.0.194
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 +1 -0
- package/dist/app/api-doc.yaml +558 -1
- package/dist/app/assets/{HomePage-H1OH-VW5.js → HomePage-DbZS0N7G.js} +1 -1
- package/dist/app/assets/MainPage-CBuWkbmr.js +2 -0
- package/dist/app/assets/{ModelPage-Crau5hgZ.js → ModelPage-Bt37smot.js} +1 -1
- package/dist/app/assets/{PackagePage-CbubRhgE.js → PackagePage-DLZe50WG.js} +1 -1
- package/dist/app/assets/{ProjectPage-DUlJkYJ4.js → ProjectPage-FQTEPXP4.js} +1 -1
- package/dist/app/assets/{RouteError-DrNXNihc.js → RouteError-DefbDO7F.js} +1 -1
- package/dist/app/assets/{WorkbookPage-CBBv7n5U.js → WorkbookPage-CkAo16ar.js} +1 -1
- package/dist/app/assets/{core-Dzx75uJR.es-DwnFZnyO.js → core-BrfQApxh.es-DnvCX4oH.js} +14 -14
- package/dist/app/assets/index-5eLCcNmP.css +1 -0
- package/dist/app/assets/{index-d5rvmoZ7.js → index-Bu0ub036.js} +119 -119
- package/dist/app/assets/index-CkzK3JIl.js +40 -0
- package/dist/app/assets/index-CoA6HIGS.js +1742 -0
- package/dist/app/assets/{index.umd-CetYIBQY.js → index.umd-B6Ms2PpL.js} +46 -46
- package/dist/app/index.html +2 -2
- package/dist/server.mjs +1529 -985
- package/package.json +11 -10
- package/src/config.ts +7 -2
- package/src/controller/connection.controller.ts +102 -27
- package/src/dto/connection.dto.spec.ts +55 -0
- package/src/dto/connection.dto.ts +87 -2
- package/src/server.ts +201 -2
- package/src/service/connection.spec.ts +250 -4
- package/src/service/connection.ts +328 -473
- package/src/service/connection_config.spec.ts +123 -0
- package/src/service/connection_config.ts +562 -0
- package/src/service/connection_service.spec.ts +50 -0
- package/src/service/connection_service.ts +125 -32
- package/src/service/db_utils.spec.ts +161 -0
- package/src/service/db_utils.ts +131 -0
- 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
- package/dist/app/assets/MainPage-GL06aMke.js +0 -2
- package/dist/app/assets/index-CMlGQMcl.css +0 -1
- package/dist/app/assets/index-CzjyS9cx.js +0 -1276
- package/dist/app/assets/index-HHdhLUpv.js +0 -676
package/src/service/project.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { LogMessage } from "@malloydata/malloy";
|
|
2
|
-
import {
|
|
3
|
-
import { BaseConnection } from "@malloydata/malloy/connection";
|
|
2
|
+
import { MalloyError, Runtime } from "@malloydata/malloy";
|
|
4
3
|
import { Mutex } from "async-mutex";
|
|
5
4
|
import * as fs from "fs";
|
|
6
5
|
import * as path from "path";
|
|
@@ -14,9 +13,10 @@ import {
|
|
|
14
13
|
import { logger } from "../logger";
|
|
15
14
|
import { URL_READER } from "../utils";
|
|
16
15
|
import {
|
|
17
|
-
|
|
16
|
+
buildProjectMalloyConfig,
|
|
18
17
|
deleteDuckLakeConnectionFile,
|
|
19
18
|
InternalConnection,
|
|
19
|
+
ProjectMalloyConfig,
|
|
20
20
|
} from "./connection";
|
|
21
21
|
import { ApiConnection } from "./model";
|
|
22
22
|
import { Package } from "./package";
|
|
@@ -35,12 +35,27 @@ interface PackageInfo {
|
|
|
35
35
|
|
|
36
36
|
type ApiPackage = components["schemas"]["Package"];
|
|
37
37
|
type ApiProject = components["schemas"]["Project"];
|
|
38
|
+
type RetiredConnectionGeneration = {
|
|
39
|
+
label: string;
|
|
40
|
+
releaseConnections: () => Promise<void>;
|
|
41
|
+
timer?: ReturnType<typeof setTimeout>;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const RETIRED_CONNECTION_DRAIN_MS = 30_000;
|
|
38
45
|
|
|
39
46
|
export class Project {
|
|
40
47
|
private packages: Map<string, Package> = new Map();
|
|
48
|
+
// Lock ordering: connectionMutex (project) MUST be acquired before any
|
|
49
|
+
// packageMutex. Connection updates may invalidate cached package
|
|
50
|
+
// MalloyConfigs and force reloads, so the project lock is the outer one.
|
|
51
|
+
// Never acquire connectionMutex while holding a packageMutex — that's the
|
|
52
|
+
// AB/BA deadlock path.
|
|
41
53
|
private packageMutexes = new Map<string, Mutex>();
|
|
42
54
|
private packageStatuses: Map<string, PackageInfo> = new Map();
|
|
43
|
-
private
|
|
55
|
+
private malloyConfig: ProjectMalloyConfig;
|
|
56
|
+
private connectionMutex = new Mutex();
|
|
57
|
+
private retiredConnectionGenerations =
|
|
58
|
+
new Set<RetiredConnectionGeneration>();
|
|
44
59
|
private apiConnections: ApiConnection[];
|
|
45
60
|
private projectPath: string;
|
|
46
61
|
private projectName: string;
|
|
@@ -49,12 +64,12 @@ export class Project {
|
|
|
49
64
|
constructor(
|
|
50
65
|
projectName: string,
|
|
51
66
|
projectPath: string,
|
|
52
|
-
|
|
67
|
+
malloyConfig: ProjectMalloyConfig,
|
|
53
68
|
apiConnections: InternalConnection[],
|
|
54
69
|
) {
|
|
55
70
|
this.projectName = projectName;
|
|
56
71
|
this.projectPath = projectPath;
|
|
57
|
-
this.
|
|
72
|
+
this.malloyConfig = malloyConfig;
|
|
58
73
|
this.apiConnections = apiConnections;
|
|
59
74
|
this.metadata = {
|
|
60
75
|
resource: `${API_PREFIX}/projects/${this.projectName}`,
|
|
@@ -87,30 +102,28 @@ export class Project {
|
|
|
87
102
|
// Handle connections update
|
|
88
103
|
// TODO: Update project connections should have its own API endpoint
|
|
89
104
|
if (payload.connections) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
105
|
+
const payloadConnections = payload.connections;
|
|
106
|
+
await this.runConnectionUpdateExclusive(async () => {
|
|
107
|
+
logger.info(
|
|
108
|
+
`Updating ${payloadConnections.length} connections for project ${this.projectName}`,
|
|
109
|
+
);
|
|
110
|
+
const isUpdateConnectionRequest = true;
|
|
111
|
+
const nextMalloyConfig = buildProjectMalloyConfig(
|
|
112
|
+
payloadConnections,
|
|
98
113
|
this.projectPath,
|
|
99
114
|
isUpdateConnectionRequest,
|
|
100
115
|
);
|
|
101
116
|
|
|
102
|
-
|
|
103
|
-
this.malloyConnections = malloyConnections;
|
|
104
|
-
this.apiConnections = apiConnections;
|
|
117
|
+
this.updateConnections(nextMalloyConfig);
|
|
105
118
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
);
|
|
119
|
+
logger.info(
|
|
120
|
+
`Successfully updated connections for project ${this.projectName}`,
|
|
121
|
+
{
|
|
122
|
+
apiConnections: this.apiConnections.length,
|
|
123
|
+
internalConnections: this.apiConnections.length,
|
|
124
|
+
},
|
|
125
|
+
);
|
|
126
|
+
});
|
|
114
127
|
}
|
|
115
128
|
|
|
116
129
|
return this;
|
|
@@ -128,22 +141,20 @@ export class Project {
|
|
|
128
141
|
}
|
|
129
142
|
|
|
130
143
|
logger.info(`Creating project with connection configuration`);
|
|
131
|
-
const
|
|
132
|
-
await createProjectConnections(connections, projectPath);
|
|
144
|
+
const malloyConfig = buildProjectMalloyConfig(connections, projectPath);
|
|
133
145
|
|
|
134
146
|
logger.info(
|
|
135
|
-
`Loaded ${
|
|
147
|
+
`Loaded ${malloyConfig.apiConnections.length} connections for project ${projectName}`,
|
|
136
148
|
{
|
|
137
|
-
|
|
138
|
-
apiConnections,
|
|
149
|
+
apiConnections: malloyConfig.apiConnections,
|
|
139
150
|
},
|
|
140
151
|
);
|
|
141
152
|
|
|
142
153
|
const project = new Project(
|
|
143
154
|
projectName,
|
|
144
155
|
projectPath,
|
|
145
|
-
|
|
146
|
-
apiConnections,
|
|
156
|
+
malloyConfig,
|
|
157
|
+
malloyConfig.apiConnections,
|
|
147
158
|
);
|
|
148
159
|
|
|
149
160
|
return project;
|
|
@@ -203,10 +214,14 @@ export class Project {
|
|
|
203
214
|
},
|
|
204
215
|
};
|
|
205
216
|
|
|
206
|
-
|
|
217
|
+
const pkg = await this.getPackage(packageName);
|
|
218
|
+
|
|
219
|
+
// Initialize Runtime with the package's active MalloyConfig so compile
|
|
220
|
+
// checks see the same package-scoped duckdb as execution. This runtime
|
|
221
|
+
// borrows the package config; the package/project lifecycle owns release.
|
|
207
222
|
const runtime = new Runtime({
|
|
208
223
|
urlReader: interceptingReader,
|
|
209
|
-
|
|
224
|
+
config: pkg.getMalloyConfig(),
|
|
210
225
|
});
|
|
211
226
|
|
|
212
227
|
// Attempt to compile
|
|
@@ -254,14 +269,66 @@ export class Project {
|
|
|
254
269
|
return connection;
|
|
255
270
|
}
|
|
256
271
|
|
|
257
|
-
public getMalloyConnection(connectionName: string)
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
272
|
+
public async getMalloyConnection(connectionName: string) {
|
|
273
|
+
return this.malloyConfig.malloyConfig.connections.lookupConnection(
|
|
274
|
+
connectionName,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
public getProjectMalloyConfig() {
|
|
279
|
+
return this.malloyConfig.malloyConfig;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
public async runConnectionUpdateExclusive<T>(
|
|
283
|
+
fn: () => Promise<T>,
|
|
284
|
+
): Promise<T> {
|
|
285
|
+
return this.connectionMutex.runExclusive(fn);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private retireConnectionGeneration(
|
|
289
|
+
label: string,
|
|
290
|
+
releaseConnections: () => Promise<void>,
|
|
291
|
+
): void {
|
|
292
|
+
const generation: RetiredConnectionGeneration = {
|
|
293
|
+
label,
|
|
294
|
+
releaseConnections,
|
|
295
|
+
};
|
|
296
|
+
generation.timer = setTimeout(() => {
|
|
297
|
+
void this.releaseRetiredConnectionGeneration(generation);
|
|
298
|
+
}, RETIRED_CONNECTION_DRAIN_MS);
|
|
299
|
+
(
|
|
300
|
+
generation.timer as ReturnType<typeof setTimeout> & {
|
|
301
|
+
unref?: () => void;
|
|
302
|
+
}
|
|
303
|
+
).unref?.();
|
|
304
|
+
this.retiredConnectionGenerations.add(generation);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private async releaseRetiredConnectionGeneration(
|
|
308
|
+
generation: RetiredConnectionGeneration,
|
|
309
|
+
): Promise<void> {
|
|
310
|
+
if (!this.retiredConnectionGenerations.delete(generation)) return;
|
|
311
|
+
|
|
312
|
+
if (generation.timer) {
|
|
313
|
+
clearTimeout(generation.timer);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
await generation.releaseConnections();
|
|
318
|
+
} catch (error) {
|
|
319
|
+
logger.error(
|
|
320
|
+
`Error releasing retired connection generation ${generation.label}`,
|
|
321
|
+
{ error },
|
|
262
322
|
);
|
|
263
323
|
}
|
|
264
|
-
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private async releaseAllRetiredConnectionGenerations(): Promise<void> {
|
|
327
|
+
await Promise.all(
|
|
328
|
+
[...this.retiredConnectionGenerations].map((generation) =>
|
|
329
|
+
this.releaseRetiredConnectionGeneration(generation),
|
|
330
|
+
),
|
|
331
|
+
);
|
|
265
332
|
}
|
|
266
333
|
|
|
267
334
|
public async listPackages(): Promise<ApiPackage[]> {
|
|
@@ -356,8 +423,13 @@ export class Project {
|
|
|
356
423
|
this.projectName,
|
|
357
424
|
packageName,
|
|
358
425
|
packagePath,
|
|
359
|
-
this.
|
|
426
|
+
() => this.malloyConfig.malloyConfig,
|
|
360
427
|
);
|
|
428
|
+
if (existingPackage !== undefined && reload) {
|
|
429
|
+
this.retireConnectionGeneration(`package ${packageName}`, () =>
|
|
430
|
+
existingPackage.getMalloyConfig().releaseConnections(),
|
|
431
|
+
);
|
|
432
|
+
}
|
|
361
433
|
this.packages.set(packageName, _package);
|
|
362
434
|
|
|
363
435
|
// Set package status to serving
|
|
@@ -391,7 +463,7 @@ export class Project {
|
|
|
391
463
|
`Adding package ${packageName} to project ${this.projectName}`,
|
|
392
464
|
{
|
|
393
465
|
packagePath,
|
|
394
|
-
|
|
466
|
+
malloyConfig: this.malloyConfig.malloyConfig,
|
|
395
467
|
},
|
|
396
468
|
);
|
|
397
469
|
this.setPackageStatus(packageName, PackageStatus.LOADING);
|
|
@@ -402,7 +474,7 @@ export class Project {
|
|
|
402
474
|
this.projectName,
|
|
403
475
|
packageName,
|
|
404
476
|
packagePath,
|
|
405
|
-
this.
|
|
477
|
+
() => this.malloyConfig.malloyConfig,
|
|
406
478
|
),
|
|
407
479
|
);
|
|
408
480
|
} catch (error) {
|
|
@@ -514,6 +586,8 @@ export class Project {
|
|
|
514
586
|
this.setPackageStatus(packageName, PackageStatus.UNLOADING);
|
|
515
587
|
}
|
|
516
588
|
|
|
589
|
+
await _package.getMalloyConfig().releaseConnections();
|
|
590
|
+
|
|
517
591
|
try {
|
|
518
592
|
await fs.promises.rm(path.join(this.projectPath, packageName), {
|
|
519
593
|
recursive: true,
|
|
@@ -532,33 +606,37 @@ export class Project {
|
|
|
532
606
|
}
|
|
533
607
|
|
|
534
608
|
public updateConnections(
|
|
535
|
-
|
|
536
|
-
|
|
609
|
+
malloyConfig: ProjectMalloyConfig,
|
|
610
|
+
_apiConnections?: ApiConnection[],
|
|
611
|
+
afterPreviousRelease?: () => Promise<void>,
|
|
537
612
|
): void {
|
|
538
|
-
|
|
539
|
-
this.
|
|
613
|
+
const previousMalloyConfig = this.malloyConfig;
|
|
614
|
+
this.malloyConfig = malloyConfig;
|
|
615
|
+
this.apiConnections = malloyConfig.apiConnections;
|
|
616
|
+
|
|
617
|
+
if (previousMalloyConfig !== malloyConfig) {
|
|
618
|
+
this.retireConnectionGeneration(
|
|
619
|
+
`project ${this.projectName}`,
|
|
620
|
+
async () => {
|
|
621
|
+
await previousMalloyConfig.releaseConnections();
|
|
622
|
+
await afterPreviousRelease?.();
|
|
623
|
+
},
|
|
624
|
+
);
|
|
625
|
+
} else {
|
|
626
|
+
void afterPreviousRelease?.();
|
|
627
|
+
}
|
|
540
628
|
}
|
|
541
629
|
|
|
542
630
|
public async deleteConnection(connectionName: string): Promise<void> {
|
|
543
|
-
this.malloyConnections.get(connectionName)?.close();
|
|
544
|
-
const isDeleted = this.malloyConnections.delete(connectionName);
|
|
545
|
-
|
|
546
631
|
const index = this.apiConnections.findIndex(
|
|
547
632
|
(conn) => conn.name === connectionName,
|
|
548
633
|
);
|
|
549
634
|
|
|
550
|
-
const connectionType = this.apiConnections[index]?.type;
|
|
551
|
-
if (connectionType === "duckdb") {
|
|
552
|
-
await this.deleteDuckDBConnection(connectionName);
|
|
553
|
-
} else if (connectionType === "ducklake") {
|
|
554
|
-
await this.deleteDuckLakeConnection(connectionName);
|
|
555
|
-
}
|
|
556
|
-
|
|
557
635
|
if (index !== -1) {
|
|
558
636
|
this.apiConnections.splice(index, 1);
|
|
559
637
|
}
|
|
560
638
|
|
|
561
|
-
if (
|
|
639
|
+
if (index !== -1) {
|
|
562
640
|
logger.info(
|
|
563
641
|
`Removed connection ${connectionName} from project ${this.projectName}`,
|
|
564
642
|
);
|
|
@@ -569,24 +647,36 @@ export class Project {
|
|
|
569
647
|
}
|
|
570
648
|
}
|
|
571
649
|
|
|
572
|
-
public closeAllConnections(): void {
|
|
573
|
-
//
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
650
|
+
public async closeAllConnections(): Promise<void> {
|
|
651
|
+
// Release the package-scoped MalloyConfigs (each holds the package's own
|
|
652
|
+
// sandbox `duckdb` connection) before tearing down the project config
|
|
653
|
+
// they wrap. Without this, hard unload leaks per-package DuckDB handles.
|
|
654
|
+
const packageReleases = await Promise.allSettled(
|
|
655
|
+
Array.from(this.packages.values(), (pkg) =>
|
|
656
|
+
pkg.getMalloyConfig().releaseConnections(),
|
|
657
|
+
),
|
|
658
|
+
);
|
|
659
|
+
for (const result of packageReleases) {
|
|
660
|
+
if (result.status === "rejected") {
|
|
581
661
|
logger.error(
|
|
582
|
-
`Error closing
|
|
583
|
-
{ error },
|
|
662
|
+
`Error closing package connections for project ${this.projectName}`,
|
|
663
|
+
{ error: result.reason },
|
|
584
664
|
);
|
|
585
665
|
}
|
|
586
666
|
}
|
|
667
|
+
this.packages.clear();
|
|
668
|
+
this.packageStatuses.clear();
|
|
669
|
+
|
|
670
|
+
try {
|
|
671
|
+
await this.malloyConfig.releaseConnections();
|
|
672
|
+
} catch (error) {
|
|
673
|
+
logger.error(
|
|
674
|
+
`Error closing connections for project ${this.projectName}`,
|
|
675
|
+
{ error },
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
await this.releaseAllRetiredConnectionGenerations();
|
|
587
679
|
|
|
588
|
-
// Clear connection maps
|
|
589
|
-
this.malloyConnections.clear();
|
|
590
680
|
this.apiConnections = [];
|
|
591
681
|
|
|
592
682
|
logger.info(`Closed all connections for project ${this.projectName}`);
|
|
@@ -605,29 +695,17 @@ export class Project {
|
|
|
605
695
|
this.projectPath,
|
|
606
696
|
`${connectionName}.duckdb`,
|
|
607
697
|
);
|
|
608
|
-
|
|
609
|
-
.
|
|
610
|
-
.
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
logger.error(
|
|
620
|
-
`Failed to remove DuckDB connection file ${connectionName} from project ${this.projectName}`,
|
|
621
|
-
{ error },
|
|
622
|
-
);
|
|
623
|
-
});
|
|
624
|
-
})
|
|
625
|
-
.catch((error) => {
|
|
626
|
-
logger.error(
|
|
627
|
-
`Failed to remove DuckDB connection file ${connectionName} from project ${this.projectName}`,
|
|
628
|
-
{ error },
|
|
629
|
-
);
|
|
630
|
-
});
|
|
698
|
+
try {
|
|
699
|
+
await fs.promises.rm(duckdbPath, { force: true });
|
|
700
|
+
logger.info(
|
|
701
|
+
`Removed DuckDB connection file ${connectionName} from project ${this.projectName}`,
|
|
702
|
+
);
|
|
703
|
+
} catch (error) {
|
|
704
|
+
logger.error(
|
|
705
|
+
`Failed to remove DuckDB connection file ${connectionName} from project ${this.projectName}`,
|
|
706
|
+
{ error },
|
|
707
|
+
);
|
|
708
|
+
}
|
|
631
709
|
}
|
|
632
710
|
|
|
633
711
|
public async deleteDuckLakeConnection(
|
|
@@ -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);
|