@malloy-publisher/server 0.0.208 → 0.0.209

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 (39) hide show
  1. package/dist/app/api-doc.yaml +84 -66
  2. package/dist/app/assets/{EnvironmentPage-DDRxo015.js → EnvironmentPage-BRMCY9d8.js} +1 -1
  3. package/dist/app/assets/HomePage-DQzgkiMI.js +1 -0
  4. package/dist/app/assets/{MainPage-DHVFRXPc.js → MainPage-sZdUjUcu.js} +2 -2
  5. package/dist/app/assets/MaterializationsPage-C_jQQUCJ.js +1 -0
  6. package/dist/app/assets/{ModelPage-B8tF_hYc.js → ModelPage-Bh62OIEq.js} +1 -1
  7. package/dist/app/assets/{PackagePage-LzaaviPn.js → PackagePage-DJW4xjLp.js} +1 -1
  8. package/dist/app/assets/{RouteError-vAYvRAi3.js → RouteError-Vi5yGs_F.js} +1 -1
  9. package/dist/app/assets/{WorkbookPage-CTjs2hXr.js → WorkbookPage-BvJMi21d.js} +1 -1
  10. package/dist/app/assets/{core-AOmIKwkc.es-B29cca-e.js → core-BbW0t3RQ.es-L-mZcOk3.js} +1 -1
  11. package/dist/app/assets/{index-Dj4uKosi.js → index-CCcHdeew.js} +1 -1
  12. package/dist/app/assets/{index-CVGIZdxd.js → index-CNFX-CGL.js} +1 -1
  13. package/dist/app/assets/{index-Db2wvjL3.js → index-DuqTjxM_.js} +114 -114
  14. package/dist/app/assets/{index.umd-BRRXibWA.js → index.umd-GgEb4WfT.js} +1 -1
  15. package/dist/app/index.html +1 -1
  16. package/dist/server.mjs +5822 -304
  17. package/package.json +2 -1
  18. package/src/controller/materialization.controller.spec.ts +125 -0
  19. package/src/controller/materialization.controller.ts +23 -27
  20. package/src/materialization_metrics.ts +117 -34
  21. package/src/server-old.ts +2 -11
  22. package/src/server.ts +2 -10
  23. package/src/service/build_plan.spec.ts +116 -0
  24. package/src/service/build_plan.ts +238 -0
  25. package/src/service/connection.ts +4 -0
  26. package/src/service/connection_config.spec.ts +182 -1
  27. package/src/service/connection_config.ts +70 -0
  28. package/src/service/db_utils.spec.ts +159 -1
  29. package/src/service/db_utils.ts +131 -0
  30. package/src/service/materialization_service.spec.ts +388 -184
  31. package/src/service/materialization_service.ts +156 -442
  32. package/src/service/materialization_test_fixtures.ts +119 -0
  33. package/src/service/package.ts +41 -1
  34. package/src/storage/DatabaseInterface.ts +5 -13
  35. package/src/storage/duckdb/MaterializationRepository.ts +5 -14
  36. package/src/storage/duckdb/schema.ts +4 -5
  37. package/tests/integration/materialization/materialization_lifecycle.integration.spec.ts +72 -211
  38. package/dist/app/assets/HomePage-BeIoPOVO.js +0 -1
  39. package/dist/app/assets/MaterializationsPage-BYnr56IV.js +0 -1
@@ -594,6 +594,133 @@ async function getSchemasForDuckLake(
594
594
  }
595
595
  }
596
596
 
597
+ /** Abort introspection requests that hang instead of holding the slot open. */
598
+ const PUBLISHER_INTROSPECTION_TIMEOUT_MS = 60_000;
599
+
600
+ /**
601
+ * Strip any userinfo (`user:pass@`) from a URL before it reaches a log or error
602
+ * message. `connectionUri` comes from a config file and may legitimately carry
603
+ * embedded credentials; the bearer token always travels in the header, never
604
+ * the URL, but this keeps any URL-embedded secret out of thrown messages too.
605
+ */
606
+ function redactUrlCredentials(url: string): string {
607
+ try {
608
+ const parsed = new URL(url);
609
+ parsed.username = "";
610
+ parsed.password = "";
611
+ return parsed.toString();
612
+ } catch {
613
+ // Non-absolute / unparseable: nothing to redact, return as-is.
614
+ return url;
615
+ }
616
+ }
617
+
618
+ /**
619
+ * Publisher proxy connections introspect against the remote dataplane, which
620
+ * exposes the same connection API as this server. Schema/table discovery is a
621
+ * GET passthrough — the remote owns the dialect-specific introspection and
622
+ * returns the same Schema/Table shapes, so we forward the response verbatim
623
+ * (no Malloy connection involved).
624
+ *
625
+ * URL contract: the request is the configured `connectionUri` with the
626
+ * introspection `pathSuffix` appended verbatim. `connectionUri` must point at
627
+ * the connection resource itself (`.../connections/<name>`), so this path and
628
+ * the data path (db-publisher's `PublisherConnection`) resolve against the same
629
+ * remote connection. Note the two halves derive their URL differently: the
630
+ * db-publisher client re-parses `connectionUri` and rebuilds it through its
631
+ * generated client, while this introspection path appends to it verbatim — the
632
+ * remote must serve both forms for a given `connectionUri`. Keep them pointed at
633
+ * the same connection resource so they stay in agreement.
634
+ */
635
+ async function fetchFromPublisherDataplane<T>(
636
+ connection: ApiConnection,
637
+ pathSuffix: string,
638
+ ): Promise<T> {
639
+ const publisher = connection.publisherConnection;
640
+ // Type-narrowing guard only — the user-facing validation (with the
641
+ // actionable `Fix:` message) lives in validateConnectionShape, which every
642
+ // connection passes before it can be assembled and introspected here.
643
+ if (!publisher?.connectionUri) {
644
+ throw new Error(
645
+ `Publisher connection "${connection.name}" is missing connectionUri`,
646
+ );
647
+ }
648
+ const url = `${publisher.connectionUri.replace(/\/+$/, "")}${pathSuffix}`;
649
+ const headers: Record<string, string> = {};
650
+ if (publisher.accessToken) {
651
+ headers["Authorization"] = `Bearer ${publisher.accessToken}`;
652
+ }
653
+
654
+ let response: Response;
655
+ try {
656
+ response = await fetch(url, {
657
+ headers,
658
+ signal: AbortSignal.timeout(PUBLISHER_INTROSPECTION_TIMEOUT_MS),
659
+ });
660
+ } catch (error) {
661
+ // Network-level failure (DNS, connection refused, timeout) — no HTTP
662
+ // status. Surface it with the connection name, like the other
663
+ // introspectors, and never echo the token-bearing header.
664
+ const reason =
665
+ (error as Error)?.name === "TimeoutError"
666
+ ? `timed out after ${PUBLISHER_INTROSPECTION_TIMEOUT_MS}ms`
667
+ : (error as Error).message;
668
+ throw new Error(
669
+ `Publisher dataplane request to ${redactUrlCredentials(url)} failed ` +
670
+ `for connection "${connection.name}": ${reason}`,
671
+ );
672
+ }
673
+
674
+ if (!response.ok) {
675
+ const body = await response.text().catch(() => "");
676
+ throw new Error(
677
+ `Publisher dataplane request to ${redactUrlCredentials(url)} failed ` +
678
+ `(${response.status}): ${body.slice(0, 200)}`,
679
+ );
680
+ }
681
+ return (await response.json()) as T;
682
+ }
683
+
684
+ async function getSchemasForPublisher(
685
+ connection: ApiConnection,
686
+ ): Promise<ApiSchema[]> {
687
+ return fetchFromPublisherDataplane<ApiSchema[]>(connection, "/schemas");
688
+ }
689
+
690
+ async function listTablesForPublisher(
691
+ connection: ApiConnection,
692
+ schemaName: string,
693
+ tableNames?: string[],
694
+ ): Promise<ApiTable[]> {
695
+ const tables = await fetchFromPublisherDataplane<ApiTable[]>(
696
+ connection,
697
+ `/schemas/${encodeURIComponent(schemaName)}/tables`,
698
+ );
699
+ if (!tableNames) {
700
+ return tables;
701
+ }
702
+ // `tableNames` is a bare-table-name filter (see listTablesForBigQuery). The
703
+ // remote's `resource` is dotted and its qualification depends on the remote
704
+ // dialect ("<schema>.<table>" or "<catalog>.<schema>.<table>"), so match the
705
+ // bare name (last segment) as well as the schema-stripped and full forms. An
706
+ // unrecognized shape still matches on the full resource rather than being
707
+ // silently dropped.
708
+ const allowed = new Set(tableNames);
709
+ const prefix = `${schemaName}.`;
710
+ return tables.filter((table) => {
711
+ const resource = table.resource ?? "";
712
+ const schemaStripped = resource.startsWith(prefix)
713
+ ? resource.slice(prefix.length)
714
+ : resource;
715
+ const bareName = resource.slice(resource.lastIndexOf(".") + 1);
716
+ return (
717
+ allowed.has(bareName) ||
718
+ allowed.has(schemaStripped) ||
719
+ allowed.has(resource)
720
+ );
721
+ });
722
+ }
723
+
597
724
  export async function getSchemasForConnection(
598
725
  connection: ApiConnection,
599
726
  malloyConnection: Connection,
@@ -617,6 +744,8 @@ export async function getSchemasForConnection(
617
744
  return getSchemasForMotherDuck(connection, malloyConnection);
618
745
  case "ducklake":
619
746
  return getSchemasForDuckLake(connection, malloyConnection);
747
+ case "publisher":
748
+ return getSchemasForPublisher(connection);
620
749
  default:
621
750
  throw new Error(`Unsupported connection type: ${connection.type}`);
622
751
  }
@@ -929,6 +1058,8 @@ export async function listTablesForSchema(
929
1058
  malloyConnection,
930
1059
  tableNames,
931
1060
  );
1061
+ case "publisher":
1062
+ return listTablesForPublisher(connection, schemaName, tableNames);
932
1063
  default:
933
1064
  throw new Error(`Unsupported connection type: ${connection.type}`);
934
1065
  }