@malloy-publisher/server 0.0.196 → 0.0.197
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/README.docker.md +88 -20
- package/README.md +15 -0
- package/build.ts +16 -0
- package/dist/app/api-doc.yaml +20 -3
- package/dist/app/assets/EnvironmentPage-BVkQH_xQ.js +1 -0
- package/dist/app/assets/HomePage-BgH9UkjK.js +1 -0
- package/dist/app/assets/MainPage-DiBxABem.js +2 -0
- package/dist/app/assets/ModelPage-oS70fj83.js +1 -0
- package/dist/app/assets/PackagePage-F_qLDAdv.js +1 -0
- package/dist/app/assets/RouteError-WqpffppN.js +1 -0
- package/dist/app/assets/WorkbookPage-_YmC-ebR.js +1 -0
- package/dist/app/assets/{core-w79IMXAG.es-Bd0UlzOL.js → core-B8L9xCYT.es-BcRLJTnC.js} +14 -14
- package/dist/app/assets/index-BMViiwtJ.js +451 -0
- package/dist/app/assets/{index-C513UodQ.js → index-C3XPaTaS.js} +15 -15
- package/dist/app/assets/index-rg8Ok8nl.js +1803 -0
- package/dist/app/assets/{index.umd-BMeMPq_9.js → index.umd-CCAfKkxY.js} +1 -1
- package/dist/app/index.html +2 -3
- package/dist/default-publisher.config.json +23 -0
- package/dist/instrumentation.mjs +1 -3
- package/dist/server.mjs +334 -165
- package/package.json +11 -12
- package/publisher.config.example.bigquery.json +33 -0
- package/publisher.config.example.duckdb.json +23 -0
- package/publisher.config.json +1 -11
- package/src/config.spec.ts +118 -0
- package/src/config.ts +78 -2
- package/src/controller/connection.controller.ts +1 -1
- package/src/default-publisher.config.json +23 -0
- package/src/errors.spec.ts +42 -0
- package/src/errors.ts +8 -0
- package/src/health.ts +26 -0
- package/src/logger.ts +1 -3
- package/src/pg_helpers.spec.ts +226 -0
- package/src/pg_helpers.ts +129 -0
- package/src/server.ts +20 -0
- package/src/service/connection.spec.ts +6 -4
- package/src/service/connection.ts +8 -3
- package/src/service/connection_config.ts +2 -2
- package/src/service/environment.ts +53 -25
- package/src/service/environment_store.spec.ts +19 -0
- package/src/service/environment_store.ts +21 -2
- package/src/service/package.ts +4 -3
- package/src/storage/StorageManager.ts +71 -11
- package/src/utils.ts +11 -0
- package/tests/unit/duckdb/attached_databases.test.ts +5 -5
- package/tests/unit/storage/StorageManager.test.ts +166 -0
- package/dist/app/assets/EnvironmentPage-1j6QDWAy.js +0 -1
- package/dist/app/assets/HomePage-DMop21VG.js +0 -1
- package/dist/app/assets/MainPage-BbE8ETz1.js +0 -2
- package/dist/app/assets/ModelPage-D2jvfe3t.js +0 -1
- package/dist/app/assets/PackagePage-BbnhGoD3.js +0 -1
- package/dist/app/assets/RouteError-D3LGEZ3i.js +0 -1
- package/dist/app/assets/WorkbookPage-DttVIj4u.js +0 -1
- package/dist/app/assets/index-5K9YjIxF.js +0 -456
- package/dist/app/assets/index-DIgzgp69.js +0 -1742
|
@@ -25,7 +25,12 @@ import {
|
|
|
25
25
|
FrozenConfigError,
|
|
26
26
|
PackageNotFoundError,
|
|
27
27
|
} from "../errors";
|
|
28
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
getOperationalState,
|
|
30
|
+
markDegraded,
|
|
31
|
+
markNotReady,
|
|
32
|
+
markReady,
|
|
33
|
+
} from "../health";
|
|
29
34
|
import { formatDuration, logger } from "../logger";
|
|
30
35
|
import { Connection } from "../storage/DatabaseInterface";
|
|
31
36
|
import { StorageConfig, StorageManager } from "../storage/StorageManager";
|
|
@@ -96,6 +101,7 @@ export class EnvironmentStore {
|
|
|
96
101
|
public publisherConfigIsFrozen: boolean;
|
|
97
102
|
public finishedInitialization: Promise<void>;
|
|
98
103
|
private isInitialized: boolean = false;
|
|
104
|
+
private failedEnvironments: Array<{ name: string; error: string }> = [];
|
|
99
105
|
public storageManager: StorageManager;
|
|
100
106
|
private s3Client = new S3({
|
|
101
107
|
followRegionRedirects: true,
|
|
@@ -142,6 +148,10 @@ export class EnvironmentStore {
|
|
|
142
148
|
`Error initializing environment${label}; skipping environment`,
|
|
143
149
|
this.extractErrorDataFromError(error),
|
|
144
150
|
);
|
|
151
|
+
this.failedEnvironments.push({
|
|
152
|
+
name: environmentName ?? "<unknown>",
|
|
153
|
+
error: error instanceof Error ? error.message : String(error),
|
|
154
|
+
});
|
|
145
155
|
}
|
|
146
156
|
|
|
147
157
|
private async initialize() {
|
|
@@ -275,7 +285,11 @@ export class EnvironmentStore {
|
|
|
275
285
|
}
|
|
276
286
|
|
|
277
287
|
this.isInitialized = true;
|
|
278
|
-
|
|
288
|
+
if (this.failedEnvironments.length > 0) {
|
|
289
|
+
markDegraded();
|
|
290
|
+
} else {
|
|
291
|
+
markReady();
|
|
292
|
+
}
|
|
279
293
|
const initializationDuration = performance.now() - initialTime;
|
|
280
294
|
logger.info(
|
|
281
295
|
`Environment store successfully initialized in ${formatDuration(initializationDuration)}`,
|
|
@@ -689,6 +703,11 @@ export class EnvironmentStore {
|
|
|
689
703
|
frozenConfig: isPublisherConfigFrozen(this.serverRootPath),
|
|
690
704
|
operationalState:
|
|
691
705
|
getOperationalState() as components["schemas"]["ServerStatus"]["operationalState"],
|
|
706
|
+
...(this.failedEnvironments.length > 0 && {
|
|
707
|
+
failedEnvironments: [
|
|
708
|
+
...this.failedEnvironments,
|
|
709
|
+
] as components["schemas"]["ServerStatus"]["failedEnvironments"],
|
|
710
|
+
}),
|
|
692
711
|
};
|
|
693
712
|
|
|
694
713
|
const environments = await this.listEnvironments(true);
|
package/src/service/package.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
import { PackageNotFoundError } from "../errors";
|
|
25
25
|
import { formatDuration, logger } from "../logger";
|
|
26
26
|
import { BuildManifest } from "../storage/DatabaseInterface";
|
|
27
|
+
import { ignoreDotfiles } from "../utils";
|
|
27
28
|
import { Model } from "./model";
|
|
28
29
|
|
|
29
30
|
type ApiDatabase = components["schemas"]["Database"];
|
|
@@ -42,6 +43,7 @@ type PackageConnectionInput =
|
|
|
42
43
|
| (() => MalloyConfig);
|
|
43
44
|
|
|
44
45
|
const ENABLE_LIST_MODEL_COMPILATION = true;
|
|
46
|
+
|
|
45
47
|
export class Package {
|
|
46
48
|
private environmentName: string;
|
|
47
49
|
private packageName: string;
|
|
@@ -380,7 +382,7 @@ export class Package {
|
|
|
380
382
|
private static async getModelPaths(packagePath: string): Promise<string[]> {
|
|
381
383
|
let files = undefined;
|
|
382
384
|
try {
|
|
383
|
-
files = await recursive(packagePath);
|
|
385
|
+
files = await recursive(packagePath, [ignoreDotfiles]);
|
|
384
386
|
} catch (error) {
|
|
385
387
|
logger.error(error);
|
|
386
388
|
throw new PackageNotFoundError(
|
|
@@ -449,8 +451,7 @@ export class Package {
|
|
|
449
451
|
private static async getDatabasePaths(
|
|
450
452
|
packagePath: string,
|
|
451
453
|
): Promise<string[]> {
|
|
452
|
-
|
|
453
|
-
files = await recursive(packagePath);
|
|
454
|
+
const files = await recursive(packagePath, [ignoreDotfiles]);
|
|
454
455
|
return files
|
|
455
456
|
.map((fullPath: string) => {
|
|
456
457
|
return path.relative(packagePath, fullPath).replace(/\\/g, "/");
|
|
@@ -1,5 +1,13 @@
|
|
|
1
|
+
import { Mutex } from "async-mutex";
|
|
1
2
|
import * as crypto from "crypto";
|
|
3
|
+
import { ConnectionAuthError } from "../errors";
|
|
2
4
|
import { logger } from "../logger";
|
|
5
|
+
import {
|
|
6
|
+
handlePgAttachError,
|
|
7
|
+
pgConnectTimeoutSeconds,
|
|
8
|
+
redactPgSecrets,
|
|
9
|
+
withPgConnectTimeout,
|
|
10
|
+
} from "../pg_helpers";
|
|
3
11
|
import {
|
|
4
12
|
DatabaseConnection,
|
|
5
13
|
ManifestStore,
|
|
@@ -78,6 +86,13 @@ export class StorageManager {
|
|
|
78
86
|
*/
|
|
79
87
|
private attachedCatalogs = new Map<string, string>();
|
|
80
88
|
|
|
89
|
+
// Serializes DuckLake catalog attaches. Concurrent POST /environments calls
|
|
90
|
+
// hitting the same DuckDB connection would otherwise race on extension
|
|
91
|
+
// autoload (httpfs/azure/etc.), where multiple connections download the
|
|
92
|
+
// extension to `.tmp-<uuid>` files in parallel; only one wins the rename
|
|
93
|
+
// and the rest crash with "Could not remove file ... No such file or directory".
|
|
94
|
+
private duckLakeAttachMutex: Mutex = new Mutex();
|
|
95
|
+
|
|
81
96
|
private config: StorageConfig;
|
|
82
97
|
|
|
83
98
|
constructor(config: StorageConfig) {
|
|
@@ -141,14 +156,18 @@ export class StorageManager {
|
|
|
141
156
|
}
|
|
142
157
|
|
|
143
158
|
const key = configKey(config);
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
159
|
+
const catalogName = await this.duckLakeAttachMutex.runExclusive(
|
|
160
|
+
async () => {
|
|
161
|
+
const existing = this.attachedCatalogs.get(key);
|
|
162
|
+
if (existing) return existing;
|
|
163
|
+
// Catalog name derived from the config so multiple configs can coexist as
|
|
164
|
+
// separate ATTACHments without colliding on the name.
|
|
165
|
+
const name = catalogNameForConfig(config);
|
|
166
|
+
await this.attachDuckLakeCatalog(config, name);
|
|
167
|
+
this.attachedCatalogs.set(key, name);
|
|
168
|
+
return name;
|
|
169
|
+
},
|
|
170
|
+
);
|
|
152
171
|
|
|
153
172
|
const store = new DuckLakeManifestStore(
|
|
154
173
|
this.duckDbConnection,
|
|
@@ -178,12 +197,31 @@ export class StorageManager {
|
|
|
178
197
|
await connection.run("INSTALL postgres; LOAD postgres;");
|
|
179
198
|
}
|
|
180
199
|
|
|
181
|
-
|
|
200
|
+
// For PG-backed catalogs, inject connect_timeout so a wedged libpq
|
|
201
|
+
// handshake fails the caller in seconds rather than hanging the
|
|
202
|
+
// worker until the K8s liveness probe trips (the 2026-05 incident).
|
|
203
|
+
// Non-PG catalogs (e.g. SQLite, MySQL) pass through unchanged.
|
|
204
|
+
const catalogUrl = isPostgres
|
|
205
|
+
? withPgConnectTimeout(config.catalogUrl, pgConnectTimeoutSeconds())
|
|
206
|
+
: config.catalogUrl;
|
|
207
|
+
|
|
208
|
+
const escapedCatalogUrl = escapeSQL(catalogUrl);
|
|
182
209
|
const escapedDataPath = escapeSQL(config.dataPath);
|
|
183
210
|
const isCloudStorage =
|
|
184
211
|
config.dataPath.startsWith("gs://") ||
|
|
185
212
|
config.dataPath.startsWith("s3://");
|
|
186
213
|
|
|
214
|
+
// Pre-install httpfs explicitly so the ATTACH below doesn't trigger
|
|
215
|
+
// DuckDB's autoloader. The autoloader downloads extensions to
|
|
216
|
+
// `<ext>.tmp-<uuid>` and races when multiple connections within the
|
|
217
|
+
// same process hit it concurrently — losers fail with
|
|
218
|
+
// "Could not remove file ... No such file or directory" on cleanup
|
|
219
|
+
// of their .tmp file. INSTALL/LOAD here is idempotent and serialized
|
|
220
|
+
// by the caller's mutex.
|
|
221
|
+
if (isCloudStorage) {
|
|
222
|
+
await connection.run("INSTALL httpfs; LOAD httpfs;");
|
|
223
|
+
}
|
|
224
|
+
|
|
187
225
|
let attachCmd = `ATTACH 'ducklake:${escapedCatalogUrl}' AS ${catalogName}`;
|
|
188
226
|
const attachOpts: string[] = [
|
|
189
227
|
`DATA_PATH '${escapedDataPath}'`,
|
|
@@ -193,13 +231,35 @@ export class StorageManager {
|
|
|
193
231
|
// sidestepping object-storage auth issues entirely for this path.
|
|
194
232
|
"DATA_INLINING_ROW_LIMIT 100000",
|
|
195
233
|
];
|
|
234
|
+
|
|
196
235
|
if (isCloudStorage) {
|
|
197
236
|
attachOpts.push("OVERRIDE_DATA_PATH true");
|
|
198
237
|
}
|
|
199
238
|
attachCmd += ` (${attachOpts.join(", ")});`;
|
|
200
239
|
|
|
201
|
-
logger.info(
|
|
202
|
-
|
|
240
|
+
logger.info(
|
|
241
|
+
`Attaching DuckLake manifest catalog: ${redactPgSecrets(attachCmd)}`,
|
|
242
|
+
);
|
|
243
|
+
try {
|
|
244
|
+
await connection.run(attachCmd);
|
|
245
|
+
} catch (error) {
|
|
246
|
+
const outcome = handlePgAttachError(
|
|
247
|
+
error,
|
|
248
|
+
`DuckLake catalog credentials rejected for ${catalogName}`,
|
|
249
|
+
);
|
|
250
|
+
if (outcome.action === "swallow") {
|
|
251
|
+
logger.info(
|
|
252
|
+
`DuckLake catalog ${catalogName} is already attached, skipping`,
|
|
253
|
+
);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (outcome.error instanceof ConnectionAuthError) {
|
|
257
|
+
logger.warn("DuckLake catalog credentials rejected", {
|
|
258
|
+
catalogName,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
throw outcome.error;
|
|
262
|
+
}
|
|
203
263
|
}
|
|
204
264
|
|
|
205
265
|
getRepository(): ResourceRepository {
|
package/src/utils.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { URLReader } from "@malloydata/malloy";
|
|
2
2
|
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
3
4
|
import { fileURLToPath } from "url";
|
|
4
5
|
|
|
5
6
|
export const URL_READER: URLReader = {
|
|
@@ -11,3 +12,13 @@ export const URL_READER: URLReader = {
|
|
|
11
12
|
return fs.promises.readFile(path, "utf8");
|
|
12
13
|
},
|
|
13
14
|
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Skip dotfiles/dotdirs (.vscode, .git, .DS_Store, etc.) when walking a
|
|
18
|
+
* package tree. These come from editors/VCS, never contain Malloy models
|
|
19
|
+
* or databases, and have been a source of spurious ENOENTs when their
|
|
20
|
+
* contents disappear mid-scan.
|
|
21
|
+
*/
|
|
22
|
+
export function ignoreDotfiles(file: string): boolean {
|
|
23
|
+
return path.basename(file).startsWith(".");
|
|
24
|
+
}
|
|
@@ -855,7 +855,7 @@ describe("createEnvironmentConnections - DuckDB", () => {
|
|
|
855
855
|
|
|
856
856
|
await expect(
|
|
857
857
|
createEnvironmentConnections(connections, PROJECT_TEST_DIR),
|
|
858
|
-
).rejects.toThrow(
|
|
858
|
+
).rejects.toThrow(/'duckdb' is reserved/);
|
|
859
859
|
});
|
|
860
860
|
|
|
861
861
|
it("should throw when DuckDB connection name is 'duckdb' with attached databases", async () => {
|
|
@@ -885,10 +885,12 @@ describe("createEnvironmentConnections - DuckDB", () => {
|
|
|
885
885
|
|
|
886
886
|
await expect(
|
|
887
887
|
createEnvironmentConnections(connections, PROJECT_TEST_DIR),
|
|
888
|
-
).rejects.toThrow(
|
|
888
|
+
).rejects.toThrow(/'duckdb' is reserved/);
|
|
889
889
|
});
|
|
890
890
|
|
|
891
891
|
it("should throw when DuckDB connection has no attached databases", async () => {
|
|
892
|
+
// Env-level DuckDB requires at least one attached foreign db;
|
|
893
|
+
// the per-package "duckdb" sandbox covers the plain-in-memory case.
|
|
892
894
|
const connections: ApiConnection[] = [
|
|
893
895
|
{
|
|
894
896
|
name: "no_attached_db",
|
|
@@ -901,9 +903,7 @@ describe("createEnvironmentConnections - DuckDB", () => {
|
|
|
901
903
|
|
|
902
904
|
await expect(
|
|
903
905
|
createEnvironmentConnections(connections, PROJECT_TEST_DIR),
|
|
904
|
-
).rejects.toThrow(
|
|
905
|
-
"DuckDB connection must have at least one attached database",
|
|
906
|
-
);
|
|
906
|
+
).rejects.toThrow(/has no attached databases/);
|
|
907
907
|
});
|
|
908
908
|
|
|
909
909
|
it("should throw on unsupported connection type", async () => {
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// Unit tests for the catch-block wiring in
|
|
2
|
+
// StorageManager.attachDuckLakeCatalog. The pure helpers from
|
|
3
|
+
// `pg_helpers.ts` are tested directly in `pg_helpers.spec.ts`; this file
|
|
4
|
+
// covers the integration — that the helpers are invoked in the right
|
|
5
|
+
// order and the right places inside `initializeDuckLakeForEnvironment`.
|
|
6
|
+
//
|
|
7
|
+
// Stubs DuckDBConnection.run instead of using a real DuckDB so we can
|
|
8
|
+
// inject libpq-style errors at the ATTACH boundary without standing up a
|
|
9
|
+
// real Postgres.
|
|
10
|
+
//
|
|
11
|
+
// Lives under `tests/unit/` (not `src/`) on purpose: the `src/` unit-spec
|
|
12
|
+
// process imports `service/environment_store.spec.ts`, which calls
|
|
13
|
+
// `mock.module("../storage/StorageManager", ...)`. Bun's module mocks
|
|
14
|
+
// persist process-wide across spec files, so a sibling spec in `src/` that
|
|
15
|
+
// `import`s the real StorageManager would get the mock instead. Running
|
|
16
|
+
// here puts us in the separate `test:integration` process with a clean
|
|
17
|
+
// module cache.
|
|
18
|
+
import { describe, expect, it } from "bun:test";
|
|
19
|
+
import { ConnectionAuthError } from "../../../src/errors";
|
|
20
|
+
import { StorageManager } from "../../../src/storage/StorageManager";
|
|
21
|
+
|
|
22
|
+
interface PrivateStorageManager {
|
|
23
|
+
duckDbConnection: { run: (sql: string) => Promise<unknown> } | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const PG_CONFIG = {
|
|
27
|
+
catalogUrl: "postgres:host=h user=u password=hunter2 dbname=catalog",
|
|
28
|
+
dataPath: "gs://bucket/path",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function setupWithStubbedConn(runHandler: (sql: string) => Promise<unknown>): {
|
|
32
|
+
sm: StorageManager;
|
|
33
|
+
calls: string[];
|
|
34
|
+
} {
|
|
35
|
+
const sm = new StorageManager({ type: "duckdb" });
|
|
36
|
+
const calls: string[] = [];
|
|
37
|
+
const stub = {
|
|
38
|
+
run: async (sql: string): Promise<unknown> => {
|
|
39
|
+
calls.push(sql);
|
|
40
|
+
return runHandler(sql);
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
(sm as unknown as PrivateStorageManager).duckDbConnection = stub;
|
|
44
|
+
return { sm, calls };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe("StorageManager.attachDuckLakeCatalog wiring", () => {
|
|
48
|
+
it("classifies libpq auth failure on ATTACH as ConnectionAuthError", async () => {
|
|
49
|
+
const { sm } = setupWithStubbedConn(async (sql) => {
|
|
50
|
+
if (sql.startsWith("ATTACH")) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
'IO Error: Unable to connect to Postgres at "host=h user=u password=hunter2 ...": FATAL: password authentication failed for user "u"',
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return undefined;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
await expect(
|
|
59
|
+
sm.initializeDuckLakeForEnvironment("env-1", "env-name", PG_CONFIG),
|
|
60
|
+
).rejects.toBeInstanceOf(ConnectionAuthError);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("redacts the embedded password in the classified error", async () => {
|
|
64
|
+
const { sm } = setupWithStubbedConn(async (sql) => {
|
|
65
|
+
if (sql.startsWith("ATTACH")) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
"password authentication failed: tried host=h password=hunter2",
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
await sm.initializeDuckLakeForEnvironment(
|
|
75
|
+
"env-1",
|
|
76
|
+
"env-name",
|
|
77
|
+
PG_CONFIG,
|
|
78
|
+
);
|
|
79
|
+
throw new Error("expected ATTACH to throw");
|
|
80
|
+
} catch (e) {
|
|
81
|
+
expect(e).toBeInstanceOf(ConnectionAuthError);
|
|
82
|
+
expect((e as Error).message).toContain("password=***");
|
|
83
|
+
expect((e as Error).message).not.toContain("hunter2");
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("injects connect_timeout into PG catalogUrl before ATTACH", async () => {
|
|
88
|
+
const { sm, calls } = setupWithStubbedConn(async () => undefined);
|
|
89
|
+
|
|
90
|
+
await sm.initializeDuckLakeForEnvironment("env-1", "env-name", PG_CONFIG);
|
|
91
|
+
|
|
92
|
+
const attachSql = calls.find((s) => s.startsWith("ATTACH"));
|
|
93
|
+
expect(attachSql).toBeDefined();
|
|
94
|
+
expect(attachSql).toContain("connect_timeout=");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("honors PG_CONNECT_TIMEOUT_SECONDS env override in the emitted SQL", async () => {
|
|
98
|
+
const original = process.env.PG_CONNECT_TIMEOUT_SECONDS;
|
|
99
|
+
process.env.PG_CONNECT_TIMEOUT_SECONDS = "17";
|
|
100
|
+
try {
|
|
101
|
+
const { sm, calls } = setupWithStubbedConn(async () => undefined);
|
|
102
|
+
await sm.initializeDuckLakeForEnvironment(
|
|
103
|
+
"env-1",
|
|
104
|
+
"env-name",
|
|
105
|
+
PG_CONFIG,
|
|
106
|
+
);
|
|
107
|
+
const attachSql = calls.find((s) => s.startsWith("ATTACH"));
|
|
108
|
+
expect(attachSql).toContain("connect_timeout=17");
|
|
109
|
+
} finally {
|
|
110
|
+
if (original === undefined) {
|
|
111
|
+
delete process.env.PG_CONNECT_TIMEOUT_SECONDS;
|
|
112
|
+
} else {
|
|
113
|
+
process.env.PG_CONNECT_TIMEOUT_SECONDS = original;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("leaves a non-PG catalogUrl untouched (no connect_timeout)", async () => {
|
|
119
|
+
const { sm, calls } = setupWithStubbedConn(async () => undefined);
|
|
120
|
+
const sqliteConfig = {
|
|
121
|
+
catalogUrl: "sqlite:/tmp/x.db",
|
|
122
|
+
dataPath: "/tmp/data",
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
await sm.initializeDuckLakeForEnvironment(
|
|
126
|
+
"env-1",
|
|
127
|
+
"env-name",
|
|
128
|
+
sqliteConfig,
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const attachSql = calls.find((s) => s.startsWith("ATTACH"));
|
|
132
|
+
expect(attachSql).toBeDefined();
|
|
133
|
+
expect(attachSql).not.toContain("connect_timeout");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("rethrows non-auth errors from ATTACH unchanged (preserves cause)", async () => {
|
|
137
|
+
const original = new Error("disk I/O error: read failed");
|
|
138
|
+
const { sm } = setupWithStubbedConn(async (sql) => {
|
|
139
|
+
if (sql.startsWith("ATTACH")) throw original;
|
|
140
|
+
return undefined;
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
await expect(
|
|
144
|
+
sm.initializeDuckLakeForEnvironment("env-1", "env-name", PG_CONFIG),
|
|
145
|
+
).rejects.toBe(original);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("does not call connect_timeout injection when catalogUrl lacks postgres: prefix", async () => {
|
|
149
|
+
// Sanity: the isPostgres branch is detected purely by string prefix.
|
|
150
|
+
// A keyword-form string without the prefix shouldn't be misclassified.
|
|
151
|
+
const { sm, calls } = setupWithStubbedConn(async () => undefined);
|
|
152
|
+
const ambiguousConfig = {
|
|
153
|
+
catalogUrl: "host=h dbname=d", // no scheme prefix at all
|
|
154
|
+
dataPath: "/tmp/data",
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
await sm.initializeDuckLakeForEnvironment(
|
|
158
|
+
"env-1",
|
|
159
|
+
"env-name",
|
|
160
|
+
ambiguousConfig,
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
const attachSql = calls.find((s) => s.startsWith("ATTACH"));
|
|
164
|
+
expect(attachSql).not.toContain("connect_timeout");
|
|
165
|
+
});
|
|
166
|
+
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{d as r,t as a,j as e,E as i,J as o}from"./index-5K9YjIxF.js";function m(){const s=r(),{environmentName:n}=a();if(n){const t=i({environmentName:n});return e.jsx(o,{onSelectPackage:s,resourceUri:t})}else return e.jsx("div",{children:e.jsx("h2",{children:"Missing environment name"})})}export{m as default};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{d as n,j as t,a as o}from"./index-5K9YjIxF.js";function s(){const a=n();return t.jsx(o,{onClickEnvironment:a})}export{s as default};
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{u as V,g as F,r as g,R as _,b as X,c as j,e as M,f as A,j as r,s as m,h as w,i as v,k as I,P as Y,m as R,l as J,n as z,B as K,o as N,p as Z,T as U,q,t as oo,d as eo,v as f,C as S,w as ro,x as to,I as ao,M as so,y as $,S as no,z as lo,A as io,O as co,D as po}from"./index-5K9YjIxF.js";function uo(o,e,t,a,n){const[s,i]=g.useState(()=>n&&t?t(o).matches:a?a(o).matches:e);return X(()=>{if(!t)return;const p=t(o),u=()=>{i(p.matches)};return u(),p.addEventListener("change",u),()=>{p.removeEventListener("change",u)}},[o,t]),s}const go={..._},L=go.useSyncExternalStore;function xo(o,e,t,a,n){const s=g.useCallback(()=>e,[e]),i=g.useMemo(()=>{if(n&&t)return()=>t(o).matches;if(a!==null){const{matches:c}=a(o);return()=>c}return s},[s,o,a,n,t]),[p,u]=g.useMemo(()=>{if(t===null)return[s,()=>()=>{}];const c=t(o);return[()=>c.matches,l=>(c.addEventListener("change",l),()=>{c.removeEventListener("change",l)})]},[s,t,o]);return L(u,p,i)}function O(o={}){const{themeId:e}=o;return function(a,n={}){let s=V();s&&e&&(s=s[e]||s);const i=typeof window<"u"&&typeof window.matchMedia<"u",{defaultMatches:p=!1,matchMedia:u=i?window.matchMedia:null,ssrMatchMedia:d=null,noSsr:c=!1}=F({name:"MuiUseMediaQuery",props:n,theme:s});let l=typeof a=="function"?a(s):a;return l=l.replace(/^@media( ?)/m,""),l.includes("print")&&console.warn(["MUI: You have provided a `print` query to the `useMediaQuery` hook.","Using the print media query to modify print styles can lead to unexpected results.","Consider using the `displayPrint` field in the `sx` prop instead.","More information about `displayPrint` on our docs: https://mui.com/system/display/#display-in-print."].join(`
|
|
2
|
-
`)),(L!==void 0?xo:uo)(l,p,u,d,c)}}O();function mo(o){return j("MuiAppBar",o)}M("MuiAppBar",["root","positionFixed","positionAbsolute","positionSticky","positionStatic","positionRelative","colorDefault","colorPrimary","colorSecondary","colorInherit","colorTransparent","colorError","colorInfo","colorSuccess","colorWarning"]);const fo=o=>{const{color:e,position:t,classes:a}=o,n={root:["root",`color${v(e)}`,`position${v(t)}`]};return I(n,mo,a)},D=(o,e)=>o?`${o?.replace(")","")}, ${e})`:e,bo=m(Y,{name:"MuiAppBar",slot:"Root",overridesResolver:(o,e)=>{const{ownerState:t}=o;return[e.root,e[`position${v(t.position)}`],e[`color${v(t.color)}`]]}})(R(({theme:o})=>({display:"flex",flexDirection:"column",width:"100%",boxSizing:"border-box",flexShrink:0,variants:[{props:{position:"fixed"},style:{position:"fixed",zIndex:(o.vars||o).zIndex.appBar,top:0,left:"auto",right:0,"@media print":{position:"absolute"}}},{props:{position:"absolute"},style:{position:"absolute",zIndex:(o.vars||o).zIndex.appBar,top:0,left:"auto",right:0}},{props:{position:"sticky"},style:{position:"sticky",zIndex:(o.vars||o).zIndex.appBar,top:0,left:"auto",right:0}},{props:{position:"static"},style:{position:"static"}},{props:{position:"relative"},style:{position:"relative"}},{props:{color:"inherit"},style:{"--AppBar-color":"inherit"}},{props:{color:"default"},style:{"--AppBar-background":o.vars?o.vars.palette.AppBar.defaultBg:o.palette.grey[100],"--AppBar-color":o.vars?o.vars.palette.text.primary:o.palette.getContrastText(o.palette.grey[100]),...o.applyStyles("dark",{"--AppBar-background":o.vars?o.vars.palette.AppBar.defaultBg:o.palette.grey[900],"--AppBar-color":o.vars?o.vars.palette.text.primary:o.palette.getContrastText(o.palette.grey[900])})}},...Object.entries(o.palette).filter(J(["contrastText"])).map(([e])=>({props:{color:e},style:{"--AppBar-background":(o.vars??o).palette[e].main,"--AppBar-color":(o.vars??o).palette[e].contrastText}})),{props:e=>e.enableColorOnDark===!0&&!["inherit","transparent"].includes(e.color),style:{backgroundColor:"var(--AppBar-background)",color:"var(--AppBar-color)"}},{props:e=>e.enableColorOnDark===!1&&!["inherit","transparent"].includes(e.color),style:{backgroundColor:"var(--AppBar-background)",color:"var(--AppBar-color)",...o.applyStyles("dark",{backgroundColor:o.vars?D(o.vars.palette.AppBar.darkBg,"var(--AppBar-background)"):null,color:o.vars?D(o.vars.palette.AppBar.darkColor,"var(--AppBar-color)"):null})}},{props:{color:"transparent"},style:{"--AppBar-background":"transparent","--AppBar-color":"inherit",backgroundColor:"var(--AppBar-background)",color:"var(--AppBar-color)",...o.applyStyles("dark",{backgroundImage:"none"})}}]}))),ho=g.forwardRef(function(e,t){const a=A({props:e,name:"MuiAppBar"}),{className:n,color:s="primary",enableColorOnDark:i=!1,position:p="fixed",...u}=a,d={...a,color:s,position:p,enableColorOnDark:i},c=fo(d);return r.jsx(bo,{square:!0,component:"header",ownerState:d,elevation:4,className:w(c.root,n,p==="fixed"&&"mui-fixed"),ref:t,...u})}),yo=z(r.jsx("path",{d:"M6 10c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm12 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm-6 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"})),vo=m(K,{name:"MuiBreadcrumbCollapsed"})(R(({theme:o})=>({display:"flex",marginLeft:`calc(${o.spacing(1)} * 0.5)`,marginRight:`calc(${o.spacing(1)} * 0.5)`,...o.palette.mode==="light"?{backgroundColor:o.palette.grey[100],color:o.palette.grey[700]}:{backgroundColor:o.palette.grey[700],color:o.palette.grey[100]},borderRadius:2,"&:hover, &:focus":{...o.palette.mode==="light"?{backgroundColor:o.palette.grey[200]}:{backgroundColor:o.palette.grey[600]}},"&:active":{boxShadow:o.shadows[0],...o.palette.mode==="light"?{backgroundColor:N(o.palette.grey[200],.12)}:{backgroundColor:N(o.palette.grey[600],.12)}}}))),ko=m(yo)({width:24,height:16});function Bo(o){const{slots:e={},slotProps:t={},...a}=o,n=o;return r.jsx("li",{children:r.jsx(vo,{focusRipple:!0,...a,ownerState:n,children:r.jsx(ko,{as:e.CollapsedIcon,ownerState:n,...t.collapsedIcon})})})}function Co(o){return j("MuiBreadcrumbs",o)}const So=M("MuiBreadcrumbs",["root","ol","li","separator"]),jo=o=>{const{classes:e}=o;return I({root:["root"],li:["li"],ol:["ol"],separator:["separator"]},Co,e)},Mo=m(U,{name:"MuiBreadcrumbs",slot:"Root",overridesResolver:(o,e)=>[{[`& .${So.li}`]:e.li},e.root]})({}),Ao=m("ol",{name:"MuiBreadcrumbs",slot:"Ol"})({display:"flex",flexWrap:"wrap",alignItems:"center",padding:0,margin:0,listStyle:"none"}),wo=m("li",{name:"MuiBreadcrumbs",slot:"Separator"})({display:"flex",userSelect:"none",marginLeft:8,marginRight:8});function Io(o,e,t,a){return o.reduce((n,s,i)=>(i<o.length-1?n=n.concat(s,r.jsx(wo,{"aria-hidden":!0,className:e,ownerState:a,children:t},`separator-${i}`)):n.push(s),n),[])}const Ro=g.forwardRef(function(e,t){const a=A({props:e,name:"MuiBreadcrumbs"}),{children:n,className:s,component:i="nav",slots:p={},slotProps:u={},expandText:d="Show path",itemsAfterCollapse:c=1,itemsBeforeCollapse:l=1,maxItems:h=8,separator:k="/",...Q}=a,[T,W]=g.useState(!1),b={...a,component:i,expanded:T,expandText:d,itemsAfterCollapse:c,itemsBeforeCollapse:l,maxItems:h,separator:k},y=jo(b),H=Z({elementType:p.CollapsedIcon,externalSlotProps:u.collapsedIcon,ownerState:b}),P=g.useRef(null),G=x=>{const C=()=>{W(!0);const E=P.current.querySelector("a[href],button,[tabindex]");E&&E.focus()};return l+c>=x.length?x:[...x.slice(0,l),r.jsx(Bo,{"aria-label":d,slots:{CollapsedIcon:p.CollapsedIcon},slotProps:{collapsedIcon:H},onClick:C},"ellipsis"),...x.slice(x.length-c,x.length)]},B=g.Children.toArray(n).filter(x=>g.isValidElement(x)).map((x,C)=>r.jsx("li",{className:y.li,children:x},`child-${C}`));return r.jsx(Mo,{ref:t,component:i,color:"textSecondary",className:w(y.root,s),ownerState:b,...Q,children:r.jsx(Ao,{className:y.ol,ref:P,ownerState:b,children:Io(T||h&&B.length<=h?B:G(B),y.separator,k,b)})})});function zo(o){return j("MuiToolbar",o)}M("MuiToolbar",["root","gutters","regular","dense"]);const To=o=>{const{classes:e,disableGutters:t,variant:a}=o;return I({root:["root",!t&&"gutters",a]},zo,e)},Po=m("div",{name:"MuiToolbar",slot:"Root",overridesResolver:(o,e)=>{const{ownerState:t}=o;return[e.root,!t.disableGutters&&e.gutters,e[t.variant]]}})(R(({theme:o})=>({position:"relative",display:"flex",alignItems:"center",variants:[{props:({ownerState:e})=>!e.disableGutters,style:{paddingLeft:o.spacing(2),paddingRight:o.spacing(2),[o.breakpoints.up("sm")]:{paddingLeft:o.spacing(3),paddingRight:o.spacing(3)}}},{props:{variant:"dense"},style:{minHeight:48}},{props:{variant:"regular"},style:o.mixins.toolbar}]}))),Eo=g.forwardRef(function(e,t){const a=A({props:e,name:"MuiToolbar"}),{className:n,component:s="div",disableGutters:i=!1,variant:p="regular",...u}=a,d={...a,component:s,disableGutters:i,variant:p},c=To(d);return r.jsx(Po,{as:s,className:w(c.root,n),ref:t,ownerState:d,...u})}),No=O({themeId:q}),$o=z(r.jsx("path",{d:"M10 6 8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"})),Do=z(r.jsx("path",{d:"M3 18h18v-2H3zm0-5h18v-2H3zm0-7v2h18V6z"}));function Uo(){const o=oo(),e=o["*"],t=eo();return r.jsx(f,{sx:{display:"flex",alignItems:"center"},children:r.jsxs(Ro,{"aria-label":"breadcrumb",separator:r.jsx($o,{sx:{fontSize:14,color:"text.secondary"}}),sx:{"& .MuiBreadcrumbs-separator":{margin:"0 6px"}},children:[o.environmentName&&r.jsx(S,{onClick:a=>t(`/${o.environmentName}/`,a),label:o.environmentName,size:"medium",sx:{backgroundColor:"background.paper",color:"primary.main",fontWeight:500,height:"32px",fontSize:"1rem",cursor:"pointer","&:hover":{backgroundColor:"primary.100"}}}),o.packageName&&r.jsx(S,{onClick:a=>t(`/${o.environmentName}/${o.packageName}/`,a),label:o.packageName,size:"medium",sx:{backgroundColor:"background.paper",color:"primary.main",fontWeight:500,height:"32px",fontSize:"1rem",cursor:"pointer","&:hover":{backgroundColor:"secondary.100"}}}),e&&r.jsx(S,{onClick:a=>t(`/${o.environmentName}/${o.packageName}/${e}`,a),label:e,size:"medium",sx:{backgroundColor:"background.paper",color:"primary.main",fontWeight:500,height:"32px",fontSize:"1rem",cursor:"pointer","&:hover":{backgroundColor:"grey.200"}}})]})})}function Lo({logoHeader:o,endCap:e}){const t=ro(),a=to(),n=No(a.breakpoints.down("sm")),[s,i]=g.useState(null),p=!!s,u=l=>{i(l.currentTarget)},d=()=>i(null),c=[{label:"Malloy Docs",link:"https://docs.malloydata.dev/documentation/",sx:{color:"primary.main"}},{label:"Publisher Docs",link:"https://github.com/malloydata/publisher/blob/main/README.md",sx:{color:"primary.main"}},{label:"Publisher API",link:"/api-doc.html",sx:{color:"primary.main"}}];return r.jsxs(ho,{position:"sticky",elevation:0,sx:{backgroundColor:"background.paper",borderBottom:"1px solid",borderColor:"divider"},children:[r.jsxs(Eo,{sx:{justifyContent:"space-between",flexWrap:"nowrap",minHeight:44},children:[o||r.jsxs(f,{sx:{display:"flex",alignItems:"center",gap:1,cursor:"pointer"},onClick:()=>t("/"),children:[r.jsx(f,{component:"img",src:"/logo.svg",alt:"Malloy",sx:{width:28,height:28}}),r.jsx(U,{variant:"h6",sx:{color:"text.primary",fontWeight:700,letterSpacing:"-0.025em",fontSize:{xs:"1.1rem",sm:"1.25rem"}},children:"Malloy Publisher"})]}),n?r.jsxs(r.Fragment,{children:[r.jsx(ao,{color:"inherit",onClick:u,children:r.jsx(Do,{})}),r.jsxs(so,{anchorEl:s,open:p,onClose:d,anchorOrigin:{vertical:"bottom",horizontal:"right"},children:[c.map(l=>r.jsx($,{onClick:()=>{d(),window.location.href=l.link},sx:l.sx,children:l.label},l.label)),e&&r.jsx($,{children:e})]})]}):r.jsxs(no,{direction:"row",spacing:2,alignItems:"center",children:[c.map(l=>r.jsx(lo,{href:l.link,sx:l.sx,children:l.label},l.label)),e]})]}),r.jsx(f,{sx:{borderTop:"1px solid white",paddingLeft:"16px",paddingRight:"16px",marginBottom:"1px",overflowX:"auto"},children:r.jsx(Uo,{})})]})}function Qo({headerProps:o}){return r.jsxs(f,{sx:{display:"flex",flexDirection:"column",minHeight:"100vh"},children:[r.jsx(Lo,{...o}),r.jsx(io,{maxWidth:"xl",component:"main",sx:{flex:1,display:"flex",flexDirection:"column",py:2,gap:2},children:r.jsx(f,{sx:{flex:1},children:r.jsx(g.Suspense,{fallback:r.jsx(po,{}),children:r.jsx(co,{})})})})]})}export{Qo as default};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{t as r,j as e,E as i,F as t,G as m}from"./index-5K9YjIxF.js";function d(){const n=r(),a=n["*"];if(!n.environmentName)return e.jsx("div",{children:e.jsx("h2",{children:"Missing environment name"})});if(!n.packageName)return e.jsx("div",{children:e.jsx("h2",{children:"Missing package name"})});const s=i({environmentName:n.environmentName,packageName:n.packageName,modelPath:a});return a?.endsWith(".malloy")?e.jsx(t,{resourceUri:s,runOnDemand:!0,maxResultSize:512*1024}):a?.endsWith(".malloynb")?e.jsx(m,{resourceUri:s,maxResultSize:1024*1024}):e.jsx("div",{children:e.jsxs("h2",{children:["Unrecognized file type: ",a]})})}export{d as default};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{t as r,d as t,j as e,E as c,H as o}from"./index-5K9YjIxF.js";function m(){const{environmentName:n,packageName:s}=r(),a=t();if(n)if(s){const i=c({environmentName:n,packageName:s});return e.jsx(o,{onClickPackageFile:a,resourceUri:i})}else return e.jsx("div",{children:e.jsx("h2",{children:"Missing package name"})});else return e.jsx("div",{children:e.jsx("h2",{children:"Missing environment name"})})}export{m as default};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{K as o,j as r,A as s,S as n,v as t,T as a}from"./index-5K9YjIxF.js";function x(){const e=o();return console.error(e),r.jsx(s,{maxWidth:"lg",component:"main",sx:{display:"flex",flexDirection:"column",my:2,gap:0},children:r.jsxs(n,{sx:{m:"auto",flexDirection:"column"},children:[r.jsx(t,{sx:{height:"300px"}}),r.jsx("img",{src:"/error.png"}),r.jsx(a,{variant:"subtitle1",children:"An unexpected error occurred"})]})})}export{x as default};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{t as a,j as e,E as t,Z as c}from"./index-5K9YjIxF.js";function d(){const{workspace:r,workbookPath:s,environmentName:i,packageName:n}=a();if(r)if(s)if(i)if(n){const o=t({environmentName:i,packageName:n});return e.jsx(c,{workbookPath:{path:s,workspace:r},resourceUri:o},`${s}`)}else return e.jsx("div",{children:e.jsx("h2",{children:"Missing package name"})});else return e.jsx("div",{children:e.jsx("h2",{children:"Missing environment name"})});else return e.jsx("div",{children:e.jsx("h2",{children:"Missing workbook path"})});else return e.jsx("div",{children:e.jsx("h2",{children:"Missing workspace"})})}export{d as default};
|