@malloy-publisher/server 0.0.208 → 0.0.210
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 +84 -66
- package/dist/app/assets/{EnvironmentPage-DDRxo015.js → EnvironmentPage-BRMCY9d8.js} +1 -1
- package/dist/app/assets/HomePage-DQzgkiMI.js +1 -0
- package/dist/app/assets/{MainPage-DHVFRXPc.js → MainPage-sZdUjUcu.js} +2 -2
- package/dist/app/assets/MaterializationsPage-C_jQQUCJ.js +1 -0
- package/dist/app/assets/{ModelPage-B8tF_hYc.js → ModelPage-Bh62OIEq.js} +1 -1
- package/dist/app/assets/{PackagePage-LzaaviPn.js → PackagePage-DJW4xjLp.js} +1 -1
- package/dist/app/assets/{RouteError-vAYvRAi3.js → RouteError-Vi5yGs_F.js} +1 -1
- package/dist/app/assets/{WorkbookPage-CTjs2hXr.js → WorkbookPage-BvJMi21d.js} +1 -1
- package/dist/app/assets/{core-AOmIKwkc.es-B29cca-e.js → core-BbW0t3RQ.es-L-mZcOk3.js} +1 -1
- package/dist/app/assets/{index-Dj4uKosi.js → index-CCcHdeew.js} +1 -1
- package/dist/app/assets/{index-CVGIZdxd.js → index-CNFX-CGL.js} +1 -1
- package/dist/app/assets/{index-Db2wvjL3.js → index-DuqTjxM_.js} +114 -114
- package/dist/app/assets/{index.umd-BRRXibWA.js → index.umd-GgEb4WfT.js} +1 -1
- package/dist/app/index.html +1 -1
- package/dist/server.mjs +5986 -470
- package/package.json +2 -1
- package/src/controller/materialization.controller.spec.ts +125 -0
- package/src/controller/materialization.controller.ts +23 -27
- package/src/materialization_metrics.ts +117 -34
- package/src/server-old.ts +2 -11
- package/src/server.ts +2 -10
- package/src/service/build_plan.spec.ts +116 -0
- package/src/service/build_plan.ts +238 -0
- package/src/service/connection.ts +4 -0
- package/src/service/connection_config.spec.ts +182 -1
- package/src/service/connection_config.ts +70 -0
- package/src/service/db_utils.spec.ts +159 -1
- package/src/service/db_utils.ts +131 -0
- package/src/service/materialization_service.spec.ts +388 -184
- package/src/service/materialization_service.ts +156 -442
- package/src/service/materialization_test_fixtures.ts +119 -0
- package/src/service/model.ts +25 -25
- package/src/service/package.ts +46 -6
- package/src/service/package_worker_path.spec.ts +65 -55
- package/src/storage/DatabaseInterface.ts +5 -13
- package/src/storage/duckdb/MaterializationRepository.ts +5 -14
- package/src/storage/duckdb/schema.ts +4 -5
- package/tests/integration/materialization/materialization_lifecycle.integration.spec.ts +72 -211
- package/dist/app/assets/HomePage-BeIoPOVO.js +0 -1
- package/dist/app/assets/MaterializationsPage-BYnr56IV.js +0 -1
package/src/service/db_utils.ts
CHANGED
|
@@ -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
|
}
|