@malloy-publisher/server 0.0.205 → 0.0.207
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 +394 -395
- package/dist/app/assets/{EnvironmentPage-CAge6UHD.js → EnvironmentPage-BScgHmkw.js} +1 -1
- package/dist/app/assets/{HomePage-DhTe8qpa.js → HomePage-CGedji_w.js} +1 -1
- package/dist/app/assets/{MainPage-CeTxxGex.js → MainPage-DWfF4jXW.js} +2 -2
- package/dist/app/assets/{MaterializationsPage-CpDHB70t.js → MaterializationsPage-B9PDlk8c.js} +1 -1
- package/dist/app/assets/{ModelPage-D9sSMb75.js → ModelPage-BiNOgK_e.js} +1 -1
- package/dist/app/assets/{PackagePage-LRqQWrFY.js → PackagePage-DAN5V7gu.js} +1 -1
- package/dist/app/assets/{RouteError-xT6kuCNw.js → RouteError-CEnIzuKN.js} +1 -1
- package/dist/app/assets/{WorkbookPage-DsIh9svZ.js → WorkbookPage-gA1ceqHP.js} +1 -1
- package/dist/app/assets/{core-C2sQrwVu.es-Bjem0hym.js → core-AOmIKwkc.es-Dclu1Fga.js} +1 -1
- package/dist/app/assets/{index-BdOZDcce.js → index-DGGe8UpP.js} +1 -1
- package/dist/app/assets/{index-RX3QOTde.js → index-DtlPzNxc.js} +127 -127
- package/dist/app/assets/{index-DHHAcY5o.js → index-uu6UpHd2.js} +1 -1
- package/dist/app/assets/{index.umd-D2WH3D-f.js → index.umd-DDq93YX4.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 +1272 -1299
- 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 +121 -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 +270 -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 +584 -500
- package/src/service/materialization_service.ts +839 -657
- package/src/service/model.ts +419 -15
- package/src/service/package.spec.ts +14 -2
- package/src/service/package.ts +339 -29
- 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 +194 -0
- package/tests/integration/materialization/materialization_lifecycle.integration.spec.ts +369 -264
- 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
package/src/health.spec.ts
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
afterAll,
|
|
3
|
+
afterEach,
|
|
4
|
+
beforeEach,
|
|
5
|
+
describe,
|
|
6
|
+
expect,
|
|
7
|
+
it,
|
|
8
|
+
spyOn,
|
|
9
|
+
} from "bun:test";
|
|
2
10
|
import { Server } from "http";
|
|
3
11
|
import { performGracefulShutdownAfterDrain } from "./health";
|
|
4
12
|
import { logger } from "./logger";
|
|
13
|
+
import { __setPackageLoadPoolForTests } from "./package_load/package_load_pool";
|
|
5
14
|
|
|
6
15
|
// Regression test for the graceful-shutdown ordering bug that caused
|
|
7
16
|
// [winston] Attempt to write logs with no transports: {"message":"Waiting 50 seconds..."}
|
|
@@ -16,6 +25,18 @@ describe("performGracefulShutdownAfterDrain: shutdown ordering", () => {
|
|
|
16
25
|
const originalExit = process.exit;
|
|
17
26
|
let callOrder: string[];
|
|
18
27
|
|
|
28
|
+
afterAll(async () => {
|
|
29
|
+
// performGracefulShutdownAfterDrain drains the package-load worker pool
|
|
30
|
+
// via getPackageLoadPool().shutdown() — which lazily CREATES the global
|
|
31
|
+
// singleton and leaves it installed in its shut-down state. In prod the
|
|
32
|
+
// process exits right after, so that's fine; in this shared test process
|
|
33
|
+
// it poisons every later spec that touches the worker path
|
|
34
|
+
// (Package.create → "PackageLoadPool is shutting down"), with failures
|
|
35
|
+
// that come and go with bun's platform-dependent file order. Reset the
|
|
36
|
+
// singleton so the next user lazily creates a fresh pool.
|
|
37
|
+
await __setPackageLoadPoolForTests(null);
|
|
38
|
+
});
|
|
39
|
+
|
|
19
40
|
beforeEach(() => {
|
|
20
41
|
callOrder = [];
|
|
21
42
|
|
|
@@ -41,6 +62,18 @@ describe("performGracefulShutdownAfterDrain: shutdown ordering", () => {
|
|
|
41
62
|
process.exit = originalExit;
|
|
42
63
|
});
|
|
43
64
|
|
|
65
|
+
// performGracefulShutdownAfterDrain lazily creates and shuts down the
|
|
66
|
+
// module-level PackageLoadPool singleton. If we leave that shut-down
|
|
67
|
+
// pool installed, sibling specs that run later in the same Bun process
|
|
68
|
+
// (e.g. package.spec.ts, package_race.spec.ts) inherit a dead pool and
|
|
69
|
+
// fail with "PackageLoadPool is shutting down". Reset the singleton so
|
|
70
|
+
// the next getPackageLoadPool() spins up a fresh one. The leaked-pool
|
|
71
|
+
// ordering is filesystem-readdir dependent, which is why it only
|
|
72
|
+
// surfaced on Linux CI.
|
|
73
|
+
afterAll(async () => {
|
|
74
|
+
await __setPackageLoadPoolForTests(null);
|
|
75
|
+
});
|
|
76
|
+
|
|
44
77
|
const fakeServer = (): Server => ({ listening: false }) as unknown as Server;
|
|
45
78
|
|
|
46
79
|
it("logs the 'Waiting ...' message before closing the logger", async () => {
|
package/src/instrumentation.ts
CHANGED
|
@@ -121,8 +121,20 @@ const httpRequestCount = meter.createCounter("http_server_requests_total", {
|
|
|
121
121
|
// /health/liveness probe (a pure synchronous 200 handler) can fail under K8s,
|
|
122
122
|
// so we surface p50/p99/max so an operator can correlate liveness restarts
|
|
123
123
|
// with sustained event-loop pressure (large Malloy compiles, GC, etc.).
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
//
|
|
125
|
+
// `monitorEventLoopDelay` is a Node-only API; some runtimes (e.g. Bun on
|
|
126
|
+
// certain platforms, used by the integration test suite) don't implement it.
|
|
127
|
+
// Degrade gracefully there rather than crashing module load: the metrics are
|
|
128
|
+
// simply not registered.
|
|
129
|
+
const eventLoopHistogram = (() => {
|
|
130
|
+
try {
|
|
131
|
+
const histogram = monitorEventLoopDelay({ resolution: 20 });
|
|
132
|
+
histogram.enable();
|
|
133
|
+
return histogram;
|
|
134
|
+
} catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
})();
|
|
126
138
|
|
|
127
139
|
const eventLoopLagP50 = meter.createObservableGauge(
|
|
128
140
|
"publisher_event_loop_lag_p50_ms",
|
|
@@ -150,21 +162,25 @@ const eventLoopLagMax = meter.createObservableGauge(
|
|
|
150
162
|
);
|
|
151
163
|
|
|
152
164
|
// Sample all three in one batch so the histogram reset can't race the reads.
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
);
|
|
165
|
+
// Skipped entirely when the runtime lacks event-loop-delay monitoring.
|
|
166
|
+
if (eventLoopHistogram) {
|
|
167
|
+
const histogram = eventLoopHistogram;
|
|
168
|
+
meter.addBatchObservableCallback(
|
|
169
|
+
(observableResult) => {
|
|
170
|
+
observableResult.observe(
|
|
171
|
+
eventLoopLagP50,
|
|
172
|
+
histogram.percentile(50) / 1e6,
|
|
173
|
+
);
|
|
174
|
+
observableResult.observe(
|
|
175
|
+
eventLoopLagP99,
|
|
176
|
+
histogram.percentile(99) / 1e6,
|
|
177
|
+
);
|
|
178
|
+
observableResult.observe(eventLoopLagMax, histogram.max / 1e6);
|
|
179
|
+
histogram.reset();
|
|
180
|
+
},
|
|
181
|
+
[eventLoopLagP50, eventLoopLagP99, eventLoopLagMax],
|
|
182
|
+
);
|
|
183
|
+
}
|
|
168
184
|
|
|
169
185
|
const IGNORED_PATHS = new Set([
|
|
170
186
|
"/health",
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized telemetry for the two-round materialization protocol.
|
|
3
|
+
*
|
|
4
|
+
* Operators need to answer "are builds completing, how long do the two
|
|
5
|
+
* rounds take, and where are they failing?" without grepping logs. The
|
|
6
|
+
* counter carries `round` (`round1`|`round2`) and `outcome`
|
|
7
|
+
* (`success`|`failed`|`cancelled`); the histogram carries `round` so a
|
|
8
|
+
* dashboard can render Round 1 (compile+plan) vs Round 2 (build) latency
|
|
9
|
+
* side by side.
|
|
10
|
+
*
|
|
11
|
+
* Lazy init for the same reason as {@link ./query_cap_metrics}: instruments
|
|
12
|
+
* created before `setGlobalMeterProvider` bind to a NoOp meter
|
|
13
|
+
* (https://github.com/open-telemetry/opentelemetry-js/issues/3505).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { metrics, type Counter, type Histogram } from "@opentelemetry/api";
|
|
17
|
+
|
|
18
|
+
export type MaterializationRound = "round1" | "round2" | "auto";
|
|
19
|
+
export type MaterializationOutcome = "success" | "failed" | "cancelled";
|
|
20
|
+
/** Manifest bind outcome: timeout is split out from generic failure on purpose. */
|
|
21
|
+
export type ManifestBindOutcome = "success" | "failure" | "timeout";
|
|
22
|
+
|
|
23
|
+
let roundCounter: Counter | null = null;
|
|
24
|
+
let roundDuration: Histogram | null = null;
|
|
25
|
+
let manifestBindCounter: Counter | null = null;
|
|
26
|
+
let sourceBuildDuration: Histogram | null = null;
|
|
27
|
+
let dropTablesCounter: Counter | null = null;
|
|
28
|
+
|
|
29
|
+
function ensureTelemetry(): { counter: Counter; duration: Histogram } {
|
|
30
|
+
if (roundCounter && roundDuration) {
|
|
31
|
+
return { counter: roundCounter, duration: roundDuration };
|
|
32
|
+
}
|
|
33
|
+
const meter = metrics.getMeter("publisher");
|
|
34
|
+
if (!roundCounter) {
|
|
35
|
+
roundCounter = meter.createCounter(
|
|
36
|
+
"publisher_materialization_rounds_total",
|
|
37
|
+
{
|
|
38
|
+
description:
|
|
39
|
+
"Materialization rounds completed. Labels: round ('round1'|'round2'|'auto'), outcome ('success'|'failed'|'cancelled').",
|
|
40
|
+
},
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
if (!roundDuration) {
|
|
44
|
+
roundDuration = meter.createHistogram(
|
|
45
|
+
"publisher_materialization_round_duration_ms",
|
|
46
|
+
{
|
|
47
|
+
description:
|
|
48
|
+
"Wall-clock duration of a materialization round. Label: round ('round1'|'round2'|'auto').",
|
|
49
|
+
unit: "ms",
|
|
50
|
+
},
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
return { counter: roundCounter, duration: roundDuration };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Record the outcome and duration of a materialization round. */
|
|
57
|
+
export function recordMaterializationRound(
|
|
58
|
+
round: MaterializationRound,
|
|
59
|
+
outcome: MaterializationOutcome,
|
|
60
|
+
durationMs: number,
|
|
61
|
+
): void {
|
|
62
|
+
const { counter, duration } = ensureTelemetry();
|
|
63
|
+
counter.add(1, { round, outcome });
|
|
64
|
+
duration.record(durationMs, { round });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Record a manifest-bind attempt (publisher binding a control-plane manifest to
|
|
69
|
+
* a package at load). `timeout` is distinguished from `failure` so operators can
|
|
70
|
+
* tell an unreachable/slow manifest store from a malformed manifest.
|
|
71
|
+
*/
|
|
72
|
+
export function recordManifestBind(outcome: ManifestBindOutcome): void {
|
|
73
|
+
if (!manifestBindCounter) {
|
|
74
|
+
manifestBindCounter = metrics
|
|
75
|
+
.getMeter("publisher")
|
|
76
|
+
.createCounter("publisher_materialization_manifest_bind_total", {
|
|
77
|
+
description:
|
|
78
|
+
"Manifest bind attempts. Label: outcome ('success'|'failure'|'timeout').",
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
manifestBindCounter.add(1, { outcome });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Record the wall-clock duration of building one persist source's table. */
|
|
85
|
+
export function recordSourceBuildDuration(durationMs: number): void {
|
|
86
|
+
if (!sourceBuildDuration) {
|
|
87
|
+
sourceBuildDuration = metrics
|
|
88
|
+
.getMeter("publisher")
|
|
89
|
+
.createHistogram(
|
|
90
|
+
"publisher_materialization_source_build_duration_ms",
|
|
91
|
+
{
|
|
92
|
+
description:
|
|
93
|
+
"Wall-clock duration of building a single persist source.",
|
|
94
|
+
unit: "ms",
|
|
95
|
+
},
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
sourceBuildDuration.record(durationMs);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Record a best-effort physical-table drop on materialization delete. */
|
|
102
|
+
export function recordDropTables(outcome: "success" | "failure"): void {
|
|
103
|
+
if (!dropTablesCounter) {
|
|
104
|
+
dropTablesCounter = metrics
|
|
105
|
+
.getMeter("publisher")
|
|
106
|
+
.createCounter("publisher_materialization_drop_tables_total", {
|
|
107
|
+
description:
|
|
108
|
+
"Physical tables dropped on delete. Label: outcome ('success'|'failure').",
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
dropTablesCounter.add(1, { outcome });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Visible for tests. Drops cached instruments so a fresh MeterProvider can capture emissions. */
|
|
115
|
+
export function resetMaterializationTelemetryForTesting(): void {
|
|
116
|
+
roundCounter = null;
|
|
117
|
+
roundDuration = null;
|
|
118
|
+
manifestBindCounter = null;
|
|
119
|
+
sourceBuildDuration = null;
|
|
120
|
+
dropTablesCounter = null;
|
|
121
|
+
}
|
|
@@ -228,7 +228,13 @@ export interface LoadPackageJob {
|
|
|
228
228
|
* don't allocate a Map for models the caller never looks at.
|
|
229
229
|
*/
|
|
230
230
|
export interface LoadPackageOutcome {
|
|
231
|
-
packageMetadata: {
|
|
231
|
+
packageMetadata: {
|
|
232
|
+
name?: string;
|
|
233
|
+
description?: string;
|
|
234
|
+
explores?: string[];
|
|
235
|
+
queryableSources?: "declared" | "all";
|
|
236
|
+
manifestLocation?: string | null;
|
|
237
|
+
};
|
|
232
238
|
models: Array<
|
|
233
239
|
Omit<SerializedModel, "modelDef" | "sourceInfos"> & {
|
|
234
240
|
modelDef?: ModelDef;
|
|
@@ -75,6 +75,7 @@ import { fileURLToPath, pathToFileURL } from "url";
|
|
|
75
75
|
|
|
76
76
|
import {
|
|
77
77
|
MODEL_FILE_SUFFIX,
|
|
78
|
+
normalizeModelPath,
|
|
78
79
|
NOTEBOOK_FILE_SUFFIX,
|
|
79
80
|
PACKAGE_MANIFEST_NAME,
|
|
80
81
|
} from "../constants";
|
|
@@ -375,16 +376,38 @@ function buildWorkerMalloyConfig(job: LoadPackageRequest): MalloyConfig {
|
|
|
375
376
|
// stays decoupled from the main-thread service module graph)
|
|
376
377
|
// ──────────────────────────────────────────────────────────────────────
|
|
377
378
|
|
|
378
|
-
async function readPackageMetadata(
|
|
379
|
-
|
|
380
|
-
|
|
379
|
+
async function readPackageMetadata(packagePath: string): Promise<{
|
|
380
|
+
name?: string;
|
|
381
|
+
description?: string;
|
|
382
|
+
explores?: string[];
|
|
383
|
+
queryableSources?: "declared" | "all";
|
|
384
|
+
manifestLocation?: string | null;
|
|
385
|
+
}> {
|
|
381
386
|
const manifestPath = path.join(packagePath, PACKAGE_MANIFEST_NAME);
|
|
382
387
|
const contents = await fs.promises.readFile(manifestPath, "utf8");
|
|
383
388
|
const parsed = JSON.parse(contents) as {
|
|
384
389
|
name?: string;
|
|
385
390
|
description?: string;
|
|
391
|
+
explores?: string[];
|
|
392
|
+
queryableSources?: unknown;
|
|
393
|
+
manifestLocation?: unknown;
|
|
394
|
+
};
|
|
395
|
+
return {
|
|
396
|
+
name: parsed.name,
|
|
397
|
+
description: parsed.description,
|
|
398
|
+
explores: Array.isArray(parsed.explores)
|
|
399
|
+
? parsed.explores.map(normalizeModelPath)
|
|
400
|
+
: undefined,
|
|
401
|
+
// Default + invalid fall back to "declared" (fail-safe: queryable ==
|
|
402
|
+
// discoverable). Only an explicit "all" opts out of the query boundary.
|
|
403
|
+
queryableSources: parsed.queryableSources === "all" ? "all" : "declared",
|
|
404
|
+
// URI (gs://, s3://, file://, or local path) of the control-plane-computed
|
|
405
|
+
// build manifest. The main thread fetches + binds it after load.
|
|
406
|
+
manifestLocation:
|
|
407
|
+
typeof parsed.manifestLocation === "string"
|
|
408
|
+
? parsed.manifestLocation
|
|
409
|
+
: null,
|
|
386
410
|
};
|
|
387
|
-
return { name: parsed.name, description: parsed.description };
|
|
388
411
|
}
|
|
389
412
|
|
|
390
413
|
async function listPackageFiles(packagePath: string): Promise<string[]> {
|
|
@@ -400,6 +423,18 @@ function filterModelPaths(allRelative: string[]): string[] {
|
|
|
400
423
|
);
|
|
401
424
|
}
|
|
402
425
|
|
|
426
|
+
// `normalizeModelPath` (shared, from ../constants) runs here at parse time so
|
|
427
|
+
// the keys stored in Package.models are already normalized, and the API
|
|
428
|
+
// publish/update path normalizes its `explores` input through the same helper —
|
|
429
|
+
// so on-disk and API-written manifests share one representation.
|
|
430
|
+
|
|
431
|
+
// explores validation is intentionally NOT done here. The worker is the
|
|
432
|
+
// shared load path (startup, reload, AND publish), but the policy differs by
|
|
433
|
+
// context — strict-reject at publish, warn-and-fail-safe at load — so it lives
|
|
434
|
+
// on the main thread (see Package.getInvalidExplores / loadViaWorker and the
|
|
435
|
+
// publish path in package.controller). An unmatched entry simply lists nothing
|
|
436
|
+
// (listModels filters by set membership), which is the fail-safe outcome.
|
|
437
|
+
|
|
403
438
|
// ──────────────────────────────────────────────────────────────────────
|
|
404
439
|
// Model compile (mirrors service/model.ts but produces SerializedModel)
|
|
405
440
|
// ──────────────────────────────────────────────────────────────────────
|
|
@@ -558,6 +593,10 @@ async function compileMalloyModel(
|
|
|
558
593
|
modelDef,
|
|
559
594
|
modelInfo: modelDefToModelInfo(modelDef),
|
|
560
595
|
sourceInfos,
|
|
596
|
+
// `sources`/`queries` ship complete (authorize + filter enforcement and
|
|
597
|
+
// join resolution read the full set); the Model's discovery accessors
|
|
598
|
+
// filter them down to the export closure (`modelDef.exports`) to match
|
|
599
|
+
// `modelInfo`/`sourceInfos`.
|
|
561
600
|
sources,
|
|
562
601
|
queries,
|
|
563
602
|
filterMap: Array.from(filterMap.entries()),
|
|
@@ -800,6 +839,7 @@ async function loadPackage(
|
|
|
800
839
|
|
|
801
840
|
const allFiles = await listPackageFiles(job.packagePath);
|
|
802
841
|
const modelPaths = filterModelPaths(allFiles);
|
|
842
|
+
|
|
803
843
|
const models = await Promise.all(
|
|
804
844
|
modelPaths.map((modelPath) =>
|
|
805
845
|
compileOneModel(job, malloyConfig, modelPath),
|
|
@@ -173,7 +173,13 @@ export interface SerializedNotebookCell {
|
|
|
173
173
|
export interface LoadPackageResult {
|
|
174
174
|
type: "load-package-result";
|
|
175
175
|
requestId: string;
|
|
176
|
-
packageMetadata: {
|
|
176
|
+
packageMetadata: {
|
|
177
|
+
name?: string;
|
|
178
|
+
description?: string;
|
|
179
|
+
explores?: string[];
|
|
180
|
+
queryableSources?: "declared" | "all";
|
|
181
|
+
manifestLocation?: string | null;
|
|
182
|
+
};
|
|
177
183
|
models: SerializedModel[];
|
|
178
184
|
/** Wall-clock ms inside the worker for the full package load. */
|
|
179
185
|
loadDurationMs: number;
|
package/src/server-old.ts
CHANGED
|
@@ -30,7 +30,6 @@ import { ParsedQs } from "qs";
|
|
|
30
30
|
import { CompileController } from "./controller/compile.controller";
|
|
31
31
|
import { ConnectionController } from "./controller/connection.controller";
|
|
32
32
|
import { DatabaseController } from "./controller/database.controller";
|
|
33
|
-
import { ManifestController } from "./controller/manifest.controller";
|
|
34
33
|
import { MaterializationController } from "./controller/materialization.controller";
|
|
35
34
|
import { ModelController } from "./controller/model.controller";
|
|
36
35
|
import { PackageController } from "./controller/package.controller";
|
|
@@ -57,7 +56,6 @@ export interface LegacyControllerSet {
|
|
|
57
56
|
queryController: QueryController;
|
|
58
57
|
compileController: CompileController;
|
|
59
58
|
materializationController: MaterializationController;
|
|
60
|
-
manifestController: ManifestController;
|
|
61
59
|
}
|
|
62
60
|
|
|
63
61
|
// ─── response/body field mappers ───────────────────────────────────────────
|
|
@@ -97,7 +95,6 @@ export function registerLegacyRoutes(
|
|
|
97
95
|
queryController,
|
|
98
96
|
compileController,
|
|
99
97
|
materializationController,
|
|
100
|
-
manifestController,
|
|
101
98
|
} = controllers;
|
|
102
99
|
|
|
103
100
|
// body-parser is already registered on the main app for `${API_PREFIX}/*`
|
|
@@ -379,26 +376,7 @@ export function registerLegacyRoutes(
|
|
|
379
376
|
},
|
|
380
377
|
);
|
|
381
378
|
|
|
382
|
-
// sqlSource (
|
|
383
|
-
app.get(
|
|
384
|
-
`${LEGACY_API_PREFIX}/projects/:projectName/connections/:connectionName/sqlSource`,
|
|
385
|
-
async (req, res) => {
|
|
386
|
-
try {
|
|
387
|
-
res.status(200).json(
|
|
388
|
-
await connectionController.getConnectionSqlSource(
|
|
389
|
-
req.params.projectName,
|
|
390
|
-
req.params.connectionName,
|
|
391
|
-
req.query.sqlStatement as string,
|
|
392
|
-
),
|
|
393
|
-
);
|
|
394
|
-
} catch (error) {
|
|
395
|
-
logger.error(error);
|
|
396
|
-
const { json, status } = internalErrorToHttpError(error as Error);
|
|
397
|
-
res.status(status).json(json);
|
|
398
|
-
}
|
|
399
|
-
},
|
|
400
|
-
);
|
|
401
|
-
|
|
379
|
+
// sqlSource (POST), per-project + per-package
|
|
402
380
|
app.post(
|
|
403
381
|
`${LEGACY_API_PREFIX}/projects/:projectName/connections/:connectionName/sqlSource`,
|
|
404
382
|
async (req, res) => {
|
|
@@ -418,26 +396,6 @@ export function registerLegacyRoutes(
|
|
|
418
396
|
},
|
|
419
397
|
);
|
|
420
398
|
|
|
421
|
-
app.get(
|
|
422
|
-
`${LEGACY_API_PREFIX}/projects/:projectName/packages/:packageName/connections/:connectionName/sqlSource`,
|
|
423
|
-
async (req, res) => {
|
|
424
|
-
try {
|
|
425
|
-
res.status(200).json(
|
|
426
|
-
await connectionController.getConnectionSqlSource(
|
|
427
|
-
req.params.projectName,
|
|
428
|
-
req.params.connectionName,
|
|
429
|
-
req.query.sqlStatement as string,
|
|
430
|
-
req.params.packageName,
|
|
431
|
-
),
|
|
432
|
-
);
|
|
433
|
-
} catch (error) {
|
|
434
|
-
logger.error(error);
|
|
435
|
-
const { json, status } = internalErrorToHttpError(error as Error);
|
|
436
|
-
res.status(status).json(json);
|
|
437
|
-
}
|
|
438
|
-
},
|
|
439
|
-
);
|
|
440
|
-
|
|
441
399
|
app.post(
|
|
442
400
|
`${LEGACY_API_PREFIX}/projects/:projectName/packages/:packageName/connections/:connectionName/sqlSource`,
|
|
443
401
|
async (req, res) => {
|
|
@@ -567,48 +525,7 @@ export function registerLegacyRoutes(
|
|
|
567
525
|
},
|
|
568
526
|
);
|
|
569
527
|
|
|
570
|
-
//
|
|
571
|
-
app.get(
|
|
572
|
-
`${LEGACY_API_PREFIX}/projects/:projectName/connections/:connectionName/temporaryTable`,
|
|
573
|
-
queryConcurrency(),
|
|
574
|
-
async (req, res) => {
|
|
575
|
-
try {
|
|
576
|
-
res.status(200).json(
|
|
577
|
-
await connectionController.getConnectionTemporaryTable(
|
|
578
|
-
req.params.projectName,
|
|
579
|
-
req.params.connectionName,
|
|
580
|
-
req.query.sqlStatement as string,
|
|
581
|
-
),
|
|
582
|
-
);
|
|
583
|
-
} catch (error) {
|
|
584
|
-
logger.error(error);
|
|
585
|
-
const { json, status } = internalErrorToHttpError(error as Error);
|
|
586
|
-
res.status(status).json(json);
|
|
587
|
-
}
|
|
588
|
-
},
|
|
589
|
-
);
|
|
590
|
-
|
|
591
|
-
app.get(
|
|
592
|
-
`${LEGACY_API_PREFIX}/projects/:projectName/packages/:packageName/connections/:connectionName/temporaryTable`,
|
|
593
|
-
queryConcurrency(),
|
|
594
|
-
async (req, res) => {
|
|
595
|
-
try {
|
|
596
|
-
res.status(200).json(
|
|
597
|
-
await connectionController.getConnectionTemporaryTable(
|
|
598
|
-
req.params.projectName,
|
|
599
|
-
req.params.connectionName,
|
|
600
|
-
req.query.sqlStatement as string,
|
|
601
|
-
req.params.packageName,
|
|
602
|
-
),
|
|
603
|
-
);
|
|
604
|
-
} catch (error) {
|
|
605
|
-
logger.error(error);
|
|
606
|
-
const { json, status } = internalErrorToHttpError(error as Error);
|
|
607
|
-
res.status(status).json(json);
|
|
608
|
-
}
|
|
609
|
-
},
|
|
610
|
-
);
|
|
611
|
-
|
|
528
|
+
// sqlTemporaryTable (POST), per-project + per-package
|
|
612
529
|
app.post(
|
|
613
530
|
`${LEGACY_API_PREFIX}/projects/:projectName/connections/:connectionName/sqlTemporaryTable`,
|
|
614
531
|
queryConcurrency(),
|
|
@@ -674,11 +591,9 @@ export function registerLegacyRoutes(
|
|
|
674
591
|
`${LEGACY_API_PREFIX}/projects/:projectName/packages`,
|
|
675
592
|
async (req, res) => {
|
|
676
593
|
try {
|
|
677
|
-
const autoLoadManifest = req.query.autoLoadManifest === "true";
|
|
678
594
|
const _package = await packageController.addPackage(
|
|
679
595
|
req.params.projectName,
|
|
680
596
|
req.body,
|
|
681
|
-
{ autoLoadManifest },
|
|
682
597
|
);
|
|
683
598
|
res.status(200).json(_package?.getPackageMetadata());
|
|
684
599
|
} catch (error) {
|
|
@@ -1024,34 +939,18 @@ export function registerLegacyRoutes(
|
|
|
1024
939
|
},
|
|
1025
940
|
);
|
|
1026
941
|
|
|
1027
|
-
app.post(
|
|
1028
|
-
`${LEGACY_API_PREFIX}/projects/:projectName/packages/:packageName/materializations/teardown`,
|
|
1029
|
-
async (req, res) => {
|
|
1030
|
-
try {
|
|
1031
|
-
const result = await materializationController.teardownPackage(
|
|
1032
|
-
req.params.projectName,
|
|
1033
|
-
req.params.packageName,
|
|
1034
|
-
req.body || {},
|
|
1035
|
-
);
|
|
1036
|
-
res.status(200).json(result);
|
|
1037
|
-
} catch (error) {
|
|
1038
|
-
const { json, status } = internalErrorToHttpError(error as Error);
|
|
1039
|
-
res.status(status).json(json);
|
|
1040
|
-
}
|
|
1041
|
-
},
|
|
1042
|
-
);
|
|
1043
|
-
|
|
1044
942
|
app.post(
|
|
1045
943
|
`${LEGACY_API_PREFIX}/projects/:projectName/packages/:packageName/materializations/:materializationId`,
|
|
1046
944
|
async (req, res) => {
|
|
1047
945
|
try {
|
|
1048
946
|
const action = req.query.action;
|
|
1049
|
-
if (action === "
|
|
947
|
+
if (action === "build") {
|
|
1050
948
|
const build =
|
|
1051
|
-
await materializationController.
|
|
949
|
+
await materializationController.buildMaterialization(
|
|
1052
950
|
req.params.projectName,
|
|
1053
951
|
req.params.packageName,
|
|
1054
952
|
req.params.materializationId,
|
|
953
|
+
req.body || {},
|
|
1055
954
|
);
|
|
1056
955
|
res.status(202).json(remapMaterializationResponse(build));
|
|
1057
956
|
} else if (action === "stop") {
|
|
@@ -1064,7 +963,7 @@ export function registerLegacyRoutes(
|
|
|
1064
963
|
res.status(200).json(remapMaterializationResponse(build));
|
|
1065
964
|
} else {
|
|
1066
965
|
throw new BadRequestError(
|
|
1067
|
-
`Unsupported action '${String(action ?? "")}'. Expected '
|
|
966
|
+
`Unsupported action '${String(action ?? "")}'. Expected 'build' or 'stop'.`,
|
|
1068
967
|
);
|
|
1069
968
|
}
|
|
1070
969
|
} catch (error) {
|
|
@@ -1082,6 +981,7 @@ export function registerLegacyRoutes(
|
|
|
1082
981
|
req.params.projectName,
|
|
1083
982
|
req.params.packageName,
|
|
1084
983
|
req.params.materializationId,
|
|
984
|
+
{ dropTables: req.query.dropTables === "true" },
|
|
1085
985
|
);
|
|
1086
986
|
res.status(204).send();
|
|
1087
987
|
} catch (error) {
|
|
@@ -1091,48 +991,6 @@ export function registerLegacyRoutes(
|
|
|
1091
991
|
},
|
|
1092
992
|
);
|
|
1093
993
|
|
|
1094
|
-
// ── manifest ────────────────────────────────────────────────────────────
|
|
1095
|
-
app.get(
|
|
1096
|
-
`${LEGACY_API_PREFIX}/projects/:projectName/packages/:packageName/manifest`,
|
|
1097
|
-
async (req, res) => {
|
|
1098
|
-
try {
|
|
1099
|
-
const manifest = await manifestController.getManifest(
|
|
1100
|
-
req.params.projectName,
|
|
1101
|
-
req.params.packageName,
|
|
1102
|
-
);
|
|
1103
|
-
res.status(200).json(manifest);
|
|
1104
|
-
} catch (error) {
|
|
1105
|
-
logger.error("Get manifest error", { error });
|
|
1106
|
-
const { json, status } = internalErrorToHttpError(error as Error);
|
|
1107
|
-
res.status(status).json(json);
|
|
1108
|
-
}
|
|
1109
|
-
},
|
|
1110
|
-
);
|
|
1111
|
-
|
|
1112
|
-
app.post(
|
|
1113
|
-
`${LEGACY_API_PREFIX}/projects/:projectName/packages/:packageName/manifest`,
|
|
1114
|
-
async (req, res) => {
|
|
1115
|
-
try {
|
|
1116
|
-
const action = req.query.action;
|
|
1117
|
-
if (action === "reload") {
|
|
1118
|
-
const manifest = await manifestController.reloadManifest(
|
|
1119
|
-
req.params.projectName,
|
|
1120
|
-
req.params.packageName,
|
|
1121
|
-
);
|
|
1122
|
-
res.status(200).json(manifest);
|
|
1123
|
-
} else {
|
|
1124
|
-
throw new BadRequestError(
|
|
1125
|
-
`Unsupported action '${String(action ?? "")}'. Expected 'reload'.`,
|
|
1126
|
-
);
|
|
1127
|
-
}
|
|
1128
|
-
} catch (error) {
|
|
1129
|
-
logger.error("Manifest action error", { error });
|
|
1130
|
-
const { json, status } = internalErrorToHttpError(error as Error);
|
|
1131
|
-
res.status(status).json(json);
|
|
1132
|
-
}
|
|
1133
|
-
},
|
|
1134
|
-
);
|
|
1135
|
-
|
|
1136
994
|
logger.info(
|
|
1137
995
|
"Legacy /projects/* routes registered for backwards compatibility",
|
|
1138
996
|
);
|