@malloy-publisher/server 0.0.205 → 0.0.206
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 +363 -395
- package/dist/app/assets/{EnvironmentPage-CAge6UHD.js → EnvironmentPage-BYwBeC2F.js} +1 -1
- package/dist/app/assets/{HomePage-DhTe8qpa.js → HomePage-ivu4vdpj.js} +1 -1
- package/dist/app/assets/{MainPage-CeTxxGex.js → MainPage-B2DnHEDU.js} +2 -2
- package/dist/app/assets/{MaterializationsPage-CpDHB70t.js → MaterializationsPage-BZEuwF9P.js} +1 -1
- package/dist/app/assets/{ModelPage-D9sSMb75.js → ModelPage-Dpu3bfPg.js} +1 -1
- package/dist/app/assets/{PackagePage-LRqQWrFY.js → PackagePage-B8PwcRHt.js} +1 -1
- package/dist/app/assets/{RouteError-xT6kuCNw.js → RouteError-BhbywAeC.js} +1 -1
- package/dist/app/assets/{WorkbookPage-DsIh9svZ.js → WorkbookPage-C-JXsJG0.js} +1 -1
- package/dist/app/assets/{core-C2sQrwVu.es-Bjem0hym.js → core-pPlPr7jK.es-CNEOlxKB.js} +1 -1
- package/dist/app/assets/{index-BdOZDcce.js → index-BHEm8Egc.js} +1 -1
- package/dist/app/assets/{index-DHHAcY5o.js → index-BsvDrV14.js} +1 -1
- package/dist/app/assets/{index-RX3QOTde.js → index-ChR1fKR2.js} +127 -127
- package/dist/app/assets/{index.umd-D2WH3D-f.js → index.umd-BVLPYNuj.js} +1 -1
- package/dist/app/index.html +1 -1
- package/dist/instrumentation.mjs +18 -8
- package/dist/package_load_worker.mjs +19 -2
- package/dist/server.mjs +1151 -1264
- package/package.json +1 -1
- package/src/constants.ts +12 -0
- package/src/controller/materialization.controller.ts +79 -35
- package/src/controller/package.controller.spec.ts +179 -0
- package/src/controller/package.controller.ts +60 -73
- package/src/dto/package.dto.ts +16 -1
- package/src/errors.spec.ts +12 -0
- package/src/errors.ts +18 -0
- package/src/health.spec.ts +34 -1
- package/src/instrumentation.ts +33 -17
- package/src/materialization_metrics.ts +66 -0
- package/src/package_load/package_load_pool.ts +7 -1
- package/src/package_load/package_load_worker.ts +44 -4
- package/src/package_load/protocol.ts +7 -1
- package/src/server-old.ts +7 -149
- package/src/server.ts +9 -188
- package/src/service/authorize_integration.spec.ts +67 -0
- package/src/service/environment.ts +212 -12
- package/src/service/environment_store.spec.ts +0 -81
- package/src/service/environment_store.ts +0 -23
- package/src/service/explore_visibility.spec.ts +434 -0
- package/src/service/exports_probe.spec.ts +107 -0
- package/src/service/manifest_loader.spec.ts +99 -0
- package/src/service/manifest_loader.ts +95 -0
- package/src/service/materialization_service.spec.ts +324 -512
- package/src/service/materialization_service.ts +816 -656
- package/src/service/model.ts +400 -13
- package/src/service/package.spec.ts +14 -2
- package/src/service/package.ts +254 -14
- package/src/service/package_rollback.spec.ts +190 -0
- package/src/service/package_worker_path.spec.ts +223 -0
- package/src/service/query_boundary.spec.ts +470 -0
- package/src/storage/DatabaseInterface.ts +35 -57
- package/src/storage/StorageManager.mock.ts +0 -9
- package/src/storage/StorageManager.ts +7 -290
- package/src/storage/duckdb/DuckDBRepository.ts +2 -35
- package/src/storage/duckdb/MaterializationRepository.ts +52 -27
- package/src/storage/duckdb/schema.ts +4 -20
- package/tests/integration/materialization/manifest_binding.integration.spec.ts +175 -0
- package/tests/integration/materialization/materialization_lifecycle.integration.spec.ts +277 -266
- package/tests/unit/duckdb/legacy_schema_migration.test.ts +7 -4
- package/src/controller/manifest.controller.ts +0 -38
- package/src/service/manifest_service.spec.ts +0 -206
- package/src/service/manifest_service.ts +0 -117
- package/src/service/materialized_table_gc.spec.ts +0 -384
- package/src/service/materialized_table_gc.ts +0 -231
- package/src/storage/duckdb/DuckDBManifestStore.ts +0 -70
- package/src/storage/duckdb/ManifestRepository.ts +0 -120
- package/src/storage/duckdb/manifest_store.spec.ts +0 -133
- package/src/storage/ducklake/DuckLakeManifestStore.ts +0 -155
- package/tests/unit/storage/StorageManager.test.ts +0 -166
|
@@ -7,8 +7,14 @@ import * as fs from "fs";
|
|
|
7
7
|
import * as path from "path";
|
|
8
8
|
import { pathToFileURL } from "url";
|
|
9
9
|
import { components } from "../api";
|
|
10
|
-
import { API_PREFIX, README_NAME } from "../constants";
|
|
11
10
|
import {
|
|
11
|
+
API_PREFIX,
|
|
12
|
+
normalizeModelPath,
|
|
13
|
+
NOTEBOOK_FILE_SUFFIX,
|
|
14
|
+
README_NAME,
|
|
15
|
+
} from "../constants";
|
|
16
|
+
import {
|
|
17
|
+
BadRequestError,
|
|
12
18
|
ConnectionNotFoundError,
|
|
13
19
|
EnvironmentNotFoundError,
|
|
14
20
|
PackageNotFoundError,
|
|
@@ -29,6 +35,7 @@ import {
|
|
|
29
35
|
EnvironmentMalloyConfig,
|
|
30
36
|
InternalConnection,
|
|
31
37
|
} from "./connection";
|
|
38
|
+
import { fetchManifestEntries } from "./manifest_loader";
|
|
32
39
|
import { ApiConnection } from "./model";
|
|
33
40
|
import { Package } from "./package";
|
|
34
41
|
import type { PackageMemoryGovernor } from "./package_memory_governor";
|
|
@@ -187,10 +194,6 @@ export class Environment {
|
|
|
187
194
|
await this.writeEnvironmentReadme(payload.readme);
|
|
188
195
|
}
|
|
189
196
|
|
|
190
|
-
if (payload.materializationStorage !== undefined) {
|
|
191
|
-
this.metadata.materializationStorage = payload.materializationStorage;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
197
|
// Handle connections update
|
|
195
198
|
// TODO: Update environment connections should have its own API endpoint
|
|
196
199
|
if (payload.connections) {
|
|
@@ -296,6 +299,17 @@ export class Environment {
|
|
|
296
299
|
): Promise<{ problems: LogMessage[]; sql?: string }> {
|
|
297
300
|
assertSafePackageName(packageName);
|
|
298
301
|
assertSafeRelativeModelPath(modelName);
|
|
302
|
+
// /compile appends the submitted source to the TARGET MODEL's content for
|
|
303
|
+
// namespace context. A notebook (.malloynb) is markdown + cells, not a
|
|
304
|
+
// model, so compiling against it only yields a confusing parse error —
|
|
305
|
+
// reject it up front with an actionable message. (Notebooks remain public
|
|
306
|
+
// for discovery/query; this is specific to the compile context.)
|
|
307
|
+
if (modelName.endsWith(NOTEBOOK_FILE_SUFFIX)) {
|
|
308
|
+
throw new BadRequestError(
|
|
309
|
+
`Cannot compile against a notebook ("${modelName}"). ` +
|
|
310
|
+
`/compile takes a .malloy model path for namespace context.`,
|
|
311
|
+
);
|
|
312
|
+
}
|
|
299
313
|
// Hold the per-package mutex for the duration of every disk read —
|
|
300
314
|
// both the explicit `fs.readFile(modelPath)` below and the implicit
|
|
301
315
|
// import resolution that `runtime.loadModel` does through the URL
|
|
@@ -365,6 +379,14 @@ export class Environment {
|
|
|
365
379
|
// and compilation surfaces its own error.
|
|
366
380
|
const gateModel = pkg.getModel(modelName);
|
|
367
381
|
if (gateModel) {
|
|
382
|
+
// Query boundary first (the *what* axis): /compile compiles ad-hoc
|
|
383
|
+
// text against a model, so gate it like an ad-hoc query — a
|
|
384
|
+
// non-`explores` model file, or text whose surface-resolved target
|
|
385
|
+
// is a non-curated model source (under queryableSources:
|
|
386
|
+
// "declared"), is rejected with a generic 404 before compilation
|
|
387
|
+
// can leak schema/SQL. Text the early gate can't pin is settled by
|
|
388
|
+
// the compiled backstop below.
|
|
389
|
+
gateModel.assertQueryBoundaryEarly(undefined, undefined, source);
|
|
368
390
|
await gateModel.assertAuthorizedForText(source, givens ?? {});
|
|
369
391
|
}
|
|
370
392
|
|
|
@@ -392,8 +414,8 @@ export class Environment {
|
|
|
392
414
|
// gate or extract beyond the early text gate already applied.
|
|
393
415
|
}
|
|
394
416
|
|
|
395
|
-
// Compiled-source
|
|
396
|
-
// the source the COMPILED final query actually reads, closing
|
|
417
|
+
// Compiled-source backstops — run REGARDLESS of includeSql. They
|
|
418
|
+
// gate the source the COMPILED final query actually reads, closing
|
|
397
419
|
// named-query / multi-statement indirection the early surface-syntax
|
|
398
420
|
// gate misses (e.g. `run: ungated\nrun: gated` — the early gate only
|
|
399
421
|
// matches the FIRST `run:`, but the LAST statement is what executes).
|
|
@@ -401,9 +423,26 @@ export class Environment {
|
|
|
401
423
|
// (field-not-found errors leak its columns), so this must not be
|
|
402
424
|
// conditional on SQL extraction. (A `source: x is gated` alias makes
|
|
403
425
|
// a new ungated source — that's the documented "extend doesn't
|
|
404
|
-
// inherit authorize" footgun, the same as the query path.)
|
|
405
|
-
|
|
406
|
-
// the
|
|
426
|
+
// inherit authorize" footgun, the same as the query path.)
|
|
427
|
+
|
|
428
|
+
// Boundary backstop (the *what* axis, 404) before the authorize
|
|
429
|
+
// one (the *who* axis, 403). /compile text is always ad-hoc — the
|
|
430
|
+
// early gate can only positively deny, never fully clear — so the
|
|
431
|
+
// compiled final query's run target is the authority. Self-gates
|
|
432
|
+
// internally (no-ops when the boundary is inert: "all" / no
|
|
433
|
+
// explores), so it is deliberately NOT guarded by hasAuthorize().
|
|
434
|
+
// Text that compiles only source definitions (no final query) has
|
|
435
|
+
// no run target and nothing to gate.
|
|
436
|
+
if (queryMaterializer && gateModel) {
|
|
437
|
+
await gateModel.assertQueryBoundaryForRunnable(
|
|
438
|
+
queryMaterializer,
|
|
439
|
+
source,
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Authorize backstop (the *who* axis, 403). Only run when the model
|
|
444
|
+
// declares gates so ungated compiles don't pay for the extra
|
|
445
|
+
// final-query compile.
|
|
407
446
|
if (queryMaterializer && gateModel?.hasAuthorize()) {
|
|
408
447
|
await gateModel.assertAuthorizedForRunnable(
|
|
409
448
|
queryMaterializer,
|
|
@@ -792,6 +831,7 @@ export class Environment {
|
|
|
792
831
|
packagePath,
|
|
793
832
|
() => this.malloyConfig.malloyConfig,
|
|
794
833
|
);
|
|
834
|
+
await this.bindManifestIfConfigured(_package);
|
|
795
835
|
if (existingPackage !== undefined && reload) {
|
|
796
836
|
this.retireConnectionGeneration(`package ${packageName}`, () =>
|
|
797
837
|
existingPackage.getMalloyConfig().shutdown("close"),
|
|
@@ -897,6 +937,7 @@ export class Environment {
|
|
|
897
937
|
public async installPackage(
|
|
898
938
|
packageName: string,
|
|
899
939
|
downloader: (stagingPath: string) => Promise<void>,
|
|
940
|
+
validate?: (pkg: Package) => string | undefined,
|
|
900
941
|
): Promise<Package> {
|
|
901
942
|
assertSafePackageName(packageName);
|
|
902
943
|
const stagingPath = this.allocateStagingPath(packageName);
|
|
@@ -963,6 +1004,15 @@ export class Environment {
|
|
|
963
1004
|
canonicalPath,
|
|
964
1005
|
() => this.malloyConfig.malloyConfig,
|
|
965
1006
|
);
|
|
1007
|
+
// Strict-reject hook (publish/update only — reload passes no
|
|
1008
|
+
// validator and stays fail-safe). Throw INSIDE the try so the
|
|
1009
|
+
// catch below rolls the swap back: the just-installed tree is
|
|
1010
|
+
// wiped and the retired tree (if any) is restored, so a rejected
|
|
1011
|
+
// publish/update never leaves the bad tree served on disk.
|
|
1012
|
+
const validationMsg = validate?.(newPackage);
|
|
1013
|
+
if (validationMsg) {
|
|
1014
|
+
throw new BadRequestError(validationMsg);
|
|
1015
|
+
}
|
|
966
1016
|
logger.debug("install.phase2.committed", {
|
|
967
1017
|
environmentName: this.environmentName,
|
|
968
1018
|
packageName,
|
|
@@ -1008,6 +1058,11 @@ export class Environment {
|
|
|
1008
1058
|
throw err;
|
|
1009
1059
|
}
|
|
1010
1060
|
|
|
1061
|
+
// Best-effort manifest bind happens after the swap commits, outside the
|
|
1062
|
+
// rollback window: a manifest that can't be fetched must not undo an
|
|
1063
|
+
// otherwise-successful install (the package serves live instead).
|
|
1064
|
+
await this.bindManifestIfConfigured(newPackage);
|
|
1065
|
+
|
|
1011
1066
|
this.packages.set(packageName, newPackage);
|
|
1012
1067
|
this.setPackageStatus(packageName, PackageStatus.SERVING);
|
|
1013
1068
|
|
|
@@ -1062,6 +1117,47 @@ export class Environment {
|
|
|
1062
1117
|
});
|
|
1063
1118
|
}
|
|
1064
1119
|
|
|
1120
|
+
/**
|
|
1121
|
+
* If the freshly-loaded package declares a `manifestLocation`, fetch the
|
|
1122
|
+
* control-plane-computed build manifest and rebind its models so persist
|
|
1123
|
+
* references resolve to the materialized tables. Best-effort: a fetch/bind
|
|
1124
|
+
* failure logs a warning and leaves the package serving live (the models are
|
|
1125
|
+
* already loaded without a manifest). Callers must hold the package lock —
|
|
1126
|
+
* this rebinds `pkg` in place rather than re-entering {@link withPackageLock}.
|
|
1127
|
+
*/
|
|
1128
|
+
private async bindManifestIfConfigured(pkg: Package): Promise<void> {
|
|
1129
|
+
const manifestLocation = pkg.getPackageMetadata().manifestLocation;
|
|
1130
|
+
if (!manifestLocation) {
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
await this.bindManifest(pkg, manifestLocation);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
/** Fetch + bind a specific manifest URI onto an already-loaded package. */
|
|
1137
|
+
private async bindManifest(
|
|
1138
|
+
pkg: Package,
|
|
1139
|
+
manifestLocation: string,
|
|
1140
|
+
): Promise<void> {
|
|
1141
|
+
const packageName = pkg.getPackageName();
|
|
1142
|
+
try {
|
|
1143
|
+
const entries = await fetchManifestEntries(manifestLocation);
|
|
1144
|
+
await pkg.reloadAllModels(entries);
|
|
1145
|
+
logger.info("Bound build manifest to package", {
|
|
1146
|
+
environmentName: this.environmentName,
|
|
1147
|
+
packageName,
|
|
1148
|
+
manifestLocation,
|
|
1149
|
+
entryCount: Object.keys(entries).length,
|
|
1150
|
+
});
|
|
1151
|
+
} catch (err) {
|
|
1152
|
+
logger.warn("Failed to bind build manifest; serving live", {
|
|
1153
|
+
environmentName: this.environmentName,
|
|
1154
|
+
packageName,
|
|
1155
|
+
manifestLocation,
|
|
1156
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1065
1161
|
/**
|
|
1066
1162
|
* Read a model's source text from disk, holding the per-package mutex
|
|
1067
1163
|
* so the read is serialized against {@link installPackage} /
|
|
@@ -1086,7 +1182,13 @@ export class Environment {
|
|
|
1086
1182
|
|
|
1087
1183
|
private async writePackageManifest(
|
|
1088
1184
|
packageName: string,
|
|
1089
|
-
metadata: {
|
|
1185
|
+
metadata: {
|
|
1186
|
+
name: string;
|
|
1187
|
+
description?: string;
|
|
1188
|
+
explores?: string[];
|
|
1189
|
+
queryableSources?: "declared" | "all";
|
|
1190
|
+
manifestLocation?: string | null;
|
|
1191
|
+
},
|
|
1090
1192
|
): Promise<void> {
|
|
1091
1193
|
const packagePath = safeJoinUnderRoot(this.environmentPath, packageName);
|
|
1092
1194
|
const manifestPath = safeJoinUnderRoot(packagePath, "publisher.json");
|
|
@@ -1101,11 +1203,23 @@ export class Environment {
|
|
|
1101
1203
|
logger.warn(`Could not read manifest for ${packageName}`);
|
|
1102
1204
|
}
|
|
1103
1205
|
|
|
1104
|
-
// Update with new metadata
|
|
1206
|
+
// Update with new metadata. `explores`/`queryableSources` are only
|
|
1207
|
+
// overwritten when the caller explicitly provides them; otherwise the
|
|
1208
|
+
// existing on-disk value is preserved via the spread (an undefined here
|
|
1209
|
+
// must not erase it).
|
|
1105
1210
|
const updatedManifest = {
|
|
1106
1211
|
...existingManifest,
|
|
1107
1212
|
name: metadata.name,
|
|
1108
1213
|
description: metadata.description,
|
|
1214
|
+
...(metadata.explores !== undefined
|
|
1215
|
+
? { explores: metadata.explores }
|
|
1216
|
+
: {}),
|
|
1217
|
+
...(metadata.queryableSources !== undefined
|
|
1218
|
+
? { queryableSources: metadata.queryableSources }
|
|
1219
|
+
: {}),
|
|
1220
|
+
...(metadata.manifestLocation !== undefined
|
|
1221
|
+
? { manifestLocation: metadata.manifestLocation }
|
|
1222
|
+
: {}),
|
|
1109
1223
|
};
|
|
1110
1224
|
|
|
1111
1225
|
// Write back to file
|
|
@@ -1132,18 +1246,72 @@ export class Environment {
|
|
|
1132
1246
|
if (body.name) {
|
|
1133
1247
|
_package.setName(body.name);
|
|
1134
1248
|
}
|
|
1249
|
+
// Preserve `explores` across a metadata PATCH. `setPackageMetadata`
|
|
1250
|
+
// replaces the whole object, so a name/description-only update must
|
|
1251
|
+
// carry the existing discovery surface through — otherwise the
|
|
1252
|
+
// in-memory `explores` is wiped and `listModels()` silently starts
|
|
1253
|
+
// serving every model until the next reload. When the body explicitly
|
|
1254
|
+
// carries `explores`, honor the new set instead.
|
|
1255
|
+
const existing = _package.getPackageMetadata();
|
|
1256
|
+
// Normalize API-body explores through the same helper the worker uses
|
|
1257
|
+
// for on-disk explores, so `["./index.malloy"]` / backslash paths
|
|
1258
|
+
// validate and persist identically regardless of input channel (no
|
|
1259
|
+
// misleading publish-time 400, no publish-vs-reload divergence).
|
|
1260
|
+
const normalizedExplores = body.explores?.map(normalizeModelPath);
|
|
1261
|
+
const explores =
|
|
1262
|
+
normalizedExplores !== undefined
|
|
1263
|
+
? normalizedExplores
|
|
1264
|
+
: existing.explores;
|
|
1265
|
+
const queryableSources =
|
|
1266
|
+
body.queryableSources !== undefined
|
|
1267
|
+
? body.queryableSources
|
|
1268
|
+
: existing.queryableSources;
|
|
1269
|
+
// Preserve the existing manifestLocation unless the body explicitly
|
|
1270
|
+
// sets it (including to null, which clears it and reverts to live).
|
|
1271
|
+
const manifestLocation =
|
|
1272
|
+
body.manifestLocation !== undefined
|
|
1273
|
+
? body.manifestLocation
|
|
1274
|
+
: existing.manifestLocation;
|
|
1135
1275
|
_package.setPackageMetadata({
|
|
1136
1276
|
name: body.name,
|
|
1137
1277
|
description: body.description,
|
|
1138
1278
|
resource: body.resource,
|
|
1139
1279
|
location: body.location,
|
|
1280
|
+
explores,
|
|
1281
|
+
queryableSources,
|
|
1282
|
+
manifestLocation,
|
|
1140
1283
|
});
|
|
1141
1284
|
|
|
1285
|
+
// Strict-reject, symmetric with the publish path
|
|
1286
|
+
// (package.controller.addPackage): validate the resulting explores
|
|
1287
|
+
// against the live model set and restore the prior metadata before
|
|
1288
|
+
// rejecting, so a bad update neither persists nor mutates the served
|
|
1289
|
+
// surface.
|
|
1290
|
+
const invalidMsg = _package.formatInvalidExplores();
|
|
1291
|
+
if (invalidMsg) {
|
|
1292
|
+
_package.setPackageMetadata(existing);
|
|
1293
|
+
throw new BadRequestError(invalidMsg);
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1142
1296
|
await this.writePackageManifest(packageName, {
|
|
1143
1297
|
name: packageName,
|
|
1144
1298
|
description: body.description,
|
|
1299
|
+
explores: normalizedExplores,
|
|
1300
|
+
queryableSources: body.queryableSources,
|
|
1301
|
+
manifestLocation: body.manifestLocation,
|
|
1145
1302
|
});
|
|
1146
1303
|
|
|
1304
|
+
// When the body changes manifestLocation, apply it now so the new
|
|
1305
|
+
// binding takes effect without a separate reload: a URI rebinds models
|
|
1306
|
+
// to the materialized tables; null/empty reverts the package to live.
|
|
1307
|
+
if (body.manifestLocation !== undefined) {
|
|
1308
|
+
if (body.manifestLocation) {
|
|
1309
|
+
await this.bindManifest(_package, body.manifestLocation);
|
|
1310
|
+
} else {
|
|
1311
|
+
await _package.reloadAllModels({});
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1147
1315
|
return _package.getPackageMetadata();
|
|
1148
1316
|
});
|
|
1149
1317
|
}
|
|
@@ -1245,6 +1413,38 @@ export class Environment {
|
|
|
1245
1413
|
});
|
|
1246
1414
|
}
|
|
1247
1415
|
|
|
1416
|
+
/**
|
|
1417
|
+
* Evict a package from the in-memory caches WITHOUT touching its on-disk
|
|
1418
|
+
* directory — the non-destructive counterpart to {@link deletePackage}.
|
|
1419
|
+
*
|
|
1420
|
+
* Used to roll back a no-location `addPackage` (which registers a
|
|
1421
|
+
* *pre-existing*, user-owned directory) when post-load validation rejects
|
|
1422
|
+
* it: deleting the tree there would destroy content the publisher never
|
|
1423
|
+
* created. This still drains and closes the connections the just-created
|
|
1424
|
+
* `Package` opened, so the duckdb handle isn't leaked.
|
|
1425
|
+
*/
|
|
1426
|
+
public async unloadPackage(packageName: string): Promise<void> {
|
|
1427
|
+
assertSafePackageName(packageName);
|
|
1428
|
+
return this.withPackageLock(packageName, async () => {
|
|
1429
|
+
const _package = this.packages.get(packageName);
|
|
1430
|
+
if (!_package) {
|
|
1431
|
+
return;
|
|
1432
|
+
}
|
|
1433
|
+
if (
|
|
1434
|
+
this.packageStatuses.get(packageName)?.status ===
|
|
1435
|
+
PackageStatus.SERVING
|
|
1436
|
+
) {
|
|
1437
|
+
this.setPackageStatus(packageName, PackageStatus.UNLOADING);
|
|
1438
|
+
}
|
|
1439
|
+
// Same 30s connection drain as deletePackage — just no fs rename/rm.
|
|
1440
|
+
this.retireConnectionGeneration(`package ${packageName}`, () =>
|
|
1441
|
+
_package.getMalloyConfig().shutdown("close"),
|
|
1442
|
+
);
|
|
1443
|
+
this.packages.delete(packageName);
|
|
1444
|
+
this.packageStatuses.delete(packageName);
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1248
1448
|
public updateConnections(
|
|
1249
1449
|
malloyConfig: EnvironmentMalloyConfig,
|
|
1250
1450
|
_apiConnections?: ApiConnection[],
|
|
@@ -11,12 +11,6 @@ import { EnvironmentStore } from "./environment_store";
|
|
|
11
11
|
|
|
12
12
|
type MockData = Record<string, unknown>;
|
|
13
13
|
|
|
14
|
-
const initializeDuckLakeCalls: Array<{
|
|
15
|
-
environmentId: string;
|
|
16
|
-
environmentName: string;
|
|
17
|
-
config: { catalogUrl: string; dataPath: string };
|
|
18
|
-
}> = [];
|
|
19
|
-
|
|
20
14
|
mock.module("../storage/StorageManager", () => {
|
|
21
15
|
return {
|
|
22
16
|
StorageManager: class MockStorageManager {
|
|
@@ -24,18 +18,6 @@ mock.module("../storage/StorageManager", () => {
|
|
|
24
18
|
return;
|
|
25
19
|
}
|
|
26
20
|
|
|
27
|
-
async initializeDuckLakeForEnvironment(
|
|
28
|
-
environmentId: string,
|
|
29
|
-
environmentName: string,
|
|
30
|
-
config: { catalogUrl: string; dataPath: string },
|
|
31
|
-
): Promise<void> {
|
|
32
|
-
initializeDuckLakeCalls.push({
|
|
33
|
-
environmentId,
|
|
34
|
-
environmentName,
|
|
35
|
-
config,
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
|
|
39
21
|
getRepository() {
|
|
40
22
|
return {
|
|
41
23
|
// ===== PROJECT METHODS =====
|
|
@@ -488,69 +470,6 @@ describe("EnvironmentStore Service", () => {
|
|
|
488
470
|
expect(readmeContent).toBe("Updated README content");
|
|
489
471
|
});
|
|
490
472
|
|
|
491
|
-
it("should propagate materializationStorage on addEnvironment for new environment", async () => {
|
|
492
|
-
writeFileSync(
|
|
493
|
-
path.join(serverRootPath, "publisher.config.json"),
|
|
494
|
-
JSON.stringify({ frozenConfig: false, environments: [] }),
|
|
495
|
-
);
|
|
496
|
-
|
|
497
|
-
await environmentStore.finishedInitialization;
|
|
498
|
-
|
|
499
|
-
const materializationStorage = {
|
|
500
|
-
catalogUrl:
|
|
501
|
-
"postgres:host=localhost port=5432 dbname=ducklake user=u password=p",
|
|
502
|
-
dataPath: "gs://test-bucket",
|
|
503
|
-
};
|
|
504
|
-
|
|
505
|
-
initializeDuckLakeCalls.length = 0;
|
|
506
|
-
const project = await environmentStore.addEnvironment({
|
|
507
|
-
name: projectName,
|
|
508
|
-
materializationStorage,
|
|
509
|
-
});
|
|
510
|
-
|
|
511
|
-
expect(project.metadata.materializationStorage).toEqual(
|
|
512
|
-
materializationStorage,
|
|
513
|
-
);
|
|
514
|
-
expect(initializeDuckLakeCalls).toHaveLength(1);
|
|
515
|
-
expect(initializeDuckLakeCalls[0].config).toEqual(materializationStorage);
|
|
516
|
-
});
|
|
517
|
-
|
|
518
|
-
it("should propagate materializationStorage on update", async () => {
|
|
519
|
-
const projectPath = path.join(serverRootPath, projectName);
|
|
520
|
-
mkdirSync(projectPath, { recursive: true });
|
|
521
|
-
writeFileSync(
|
|
522
|
-
path.join(projectPath, "publisher.json"),
|
|
523
|
-
JSON.stringify({ name: projectName, description: "Test package" }),
|
|
524
|
-
);
|
|
525
|
-
writeFileSync(
|
|
526
|
-
path.join(serverRootPath, "publisher.config.json"),
|
|
527
|
-
JSON.stringify({
|
|
528
|
-
frozenConfig: false,
|
|
529
|
-
environments: [
|
|
530
|
-
{
|
|
531
|
-
name: projectName,
|
|
532
|
-
packages: [{ name: projectName, location: projectPath }],
|
|
533
|
-
},
|
|
534
|
-
],
|
|
535
|
-
}),
|
|
536
|
-
);
|
|
537
|
-
|
|
538
|
-
await environmentStore.finishedInitialization;
|
|
539
|
-
const project = await environmentStore.getEnvironment(projectName);
|
|
540
|
-
|
|
541
|
-
const materializationStorage = {
|
|
542
|
-
catalogUrl:
|
|
543
|
-
"postgres:host=localhost port=5432 dbname=ducklake user=u password=p",
|
|
544
|
-
dataPath: "gs://test-bucket",
|
|
545
|
-
};
|
|
546
|
-
|
|
547
|
-
await project.update({ name: projectName, materializationStorage });
|
|
548
|
-
|
|
549
|
-
expect(project.metadata.materializationStorage).toEqual(
|
|
550
|
-
materializationStorage,
|
|
551
|
-
);
|
|
552
|
-
});
|
|
553
|
-
|
|
554
473
|
it(
|
|
555
474
|
"should handle project reload",
|
|
556
475
|
async () => {
|
|
@@ -452,25 +452,6 @@ export class EnvironmentStore {
|
|
|
452
452
|
dbEnvironment = await repository.createEnvironment(environmentData);
|
|
453
453
|
}
|
|
454
454
|
|
|
455
|
-
// Initialize DuckLake manifest storage if configured on the environment.
|
|
456
|
-
const materializationStorage = environment.metadata
|
|
457
|
-
?.materializationStorage as
|
|
458
|
-
| { catalogUrl?: string; dataPath?: string }
|
|
459
|
-
| undefined;
|
|
460
|
-
if (
|
|
461
|
-
materializationStorage?.catalogUrl &&
|
|
462
|
-
materializationStorage?.dataPath
|
|
463
|
-
) {
|
|
464
|
-
await this.storageManager.initializeDuckLakeForEnvironment(
|
|
465
|
-
dbEnvironment.id,
|
|
466
|
-
dbEnvironment.name,
|
|
467
|
-
{
|
|
468
|
-
catalogUrl: materializationStorage.catalogUrl,
|
|
469
|
-
dataPath: materializationStorage.dataPath,
|
|
470
|
-
},
|
|
471
|
-
);
|
|
472
|
-
}
|
|
473
|
-
|
|
474
455
|
return dbEnvironment;
|
|
475
456
|
}
|
|
476
457
|
|
|
@@ -938,10 +919,6 @@ export class EnvironmentStore {
|
|
|
938
919
|
|
|
939
920
|
if (!newEnvironment.metadata) newEnvironment.metadata = {};
|
|
940
921
|
newEnvironment.metadata.location = absoluteEnvironmentPath;
|
|
941
|
-
if (environment.materializationStorage !== undefined) {
|
|
942
|
-
newEnvironment.metadata.materializationStorage =
|
|
943
|
-
environment.materializationStorage;
|
|
944
|
-
}
|
|
945
922
|
|
|
946
923
|
this.environments.set(environmentName, newEnvironment);
|
|
947
924
|
|