@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.
Files changed (55) hide show
  1. package/README.docker.md +88 -20
  2. package/README.md +15 -0
  3. package/build.ts +16 -0
  4. package/dist/app/api-doc.yaml +20 -3
  5. package/dist/app/assets/EnvironmentPage-BVkQH_xQ.js +1 -0
  6. package/dist/app/assets/HomePage-BgH9UkjK.js +1 -0
  7. package/dist/app/assets/MainPage-DiBxABem.js +2 -0
  8. package/dist/app/assets/ModelPage-oS70fj83.js +1 -0
  9. package/dist/app/assets/PackagePage-F_qLDAdv.js +1 -0
  10. package/dist/app/assets/RouteError-WqpffppN.js +1 -0
  11. package/dist/app/assets/WorkbookPage-_YmC-ebR.js +1 -0
  12. package/dist/app/assets/{core-w79IMXAG.es-Bd0UlzOL.js → core-B8L9xCYT.es-BcRLJTnC.js} +14 -14
  13. package/dist/app/assets/index-BMViiwtJ.js +451 -0
  14. package/dist/app/assets/{index-C513UodQ.js → index-C3XPaTaS.js} +15 -15
  15. package/dist/app/assets/index-rg8Ok8nl.js +1803 -0
  16. package/dist/app/assets/{index.umd-BMeMPq_9.js → index.umd-CCAfKkxY.js} +1 -1
  17. package/dist/app/index.html +2 -3
  18. package/dist/default-publisher.config.json +23 -0
  19. package/dist/instrumentation.mjs +1 -3
  20. package/dist/server.mjs +334 -165
  21. package/package.json +11 -12
  22. package/publisher.config.example.bigquery.json +33 -0
  23. package/publisher.config.example.duckdb.json +23 -0
  24. package/publisher.config.json +1 -11
  25. package/src/config.spec.ts +118 -0
  26. package/src/config.ts +78 -2
  27. package/src/controller/connection.controller.ts +1 -1
  28. package/src/default-publisher.config.json +23 -0
  29. package/src/errors.spec.ts +42 -0
  30. package/src/errors.ts +8 -0
  31. package/src/health.ts +26 -0
  32. package/src/logger.ts +1 -3
  33. package/src/pg_helpers.spec.ts +226 -0
  34. package/src/pg_helpers.ts +129 -0
  35. package/src/server.ts +20 -0
  36. package/src/service/connection.spec.ts +6 -4
  37. package/src/service/connection.ts +8 -3
  38. package/src/service/connection_config.ts +2 -2
  39. package/src/service/environment.ts +53 -25
  40. package/src/service/environment_store.spec.ts +19 -0
  41. package/src/service/environment_store.ts +21 -2
  42. package/src/service/package.ts +4 -3
  43. package/src/storage/StorageManager.ts +71 -11
  44. package/src/utils.ts +11 -0
  45. package/tests/unit/duckdb/attached_databases.test.ts +5 -5
  46. package/tests/unit/storage/StorageManager.test.ts +166 -0
  47. package/dist/app/assets/EnvironmentPage-1j6QDWAy.js +0 -1
  48. package/dist/app/assets/HomePage-DMop21VG.js +0 -1
  49. package/dist/app/assets/MainPage-BbE8ETz1.js +0 -2
  50. package/dist/app/assets/ModelPage-D2jvfe3t.js +0 -1
  51. package/dist/app/assets/PackagePage-BbnhGoD3.js +0 -1
  52. package/dist/app/assets/RouteError-D3LGEZ3i.js +0 -1
  53. package/dist/app/assets/WorkbookPage-DttVIj4u.js +0 -1
  54. package/dist/app/assets/index-5K9YjIxF.js +0 -456
  55. 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 { getOperationalState, markNotReady, markReady } from "../health";
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
- markReady();
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);
@@ -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
- let files = undefined;
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
- let catalogName = this.attachedCatalogs.get(key);
145
- if (!catalogName) {
146
- // Catalog name derived from the config so multiple configs can coexist as
147
- // separate ATTACHments without colliding on the name.
148
- catalogName = catalogNameForConfig(config);
149
- await this.attachDuckLakeCatalog(config, catalogName);
150
- this.attachedCatalogs.set(key, catalogName);
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
- const escapedCatalogUrl = escapeSQL(config.catalogUrl);
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(`Attaching DuckLake manifest catalog: ${attachCmd}`);
202
- await connection.run(attachCmd);
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("DuckDB connection name cannot be 'duckdb'");
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("DuckDB connection name cannot be 'duckdb'");
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};