dbdocs 0.0.0__py3-none-any.whl

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.
dbdocs/site/builder.py ADDED
@@ -0,0 +1,132 @@
1
+ """Assemble the report data dict and write the self-contained site.
2
+
3
+ ``ReportBuilder.generate`` is the whole ``dbdocs generate`` pipeline: load
4
+ artifacts → build the one data dict (metadata, nodes, node-level lineage,
5
+ column-level lineage, ERDs, nav tree) → stage the bundled SPA → base64-inject the
6
+ data → write ``index.html`` + a debug ``dbdocs-data.json``.
7
+ """
8
+
9
+ import json
10
+ from datetime import datetime
11
+ from pathlib import Path
12
+ from shutil import copytree
13
+ from typing import Any
14
+
15
+ from dbdocs.core.artifacts import adapter_type, load_artifacts
16
+ from dbdocs.core.config import DbDocsConfig
17
+ from dbdocs.core.log import logger
18
+ from dbdocs.extract.column_lineage import ColumnLineageExtractor
19
+ from dbdocs.extract.erd import build_erd, build_erd_data
20
+ from dbdocs.extract.graph import LineageGraph
21
+ from dbdocs.extract.nodes import build_nodes, build_tree
22
+ from dbdocs.site.inject import inject
23
+
24
+ #: The bundled SPA (shell + assets), shipped in the wheel.
25
+ BUNDLE_DIR = Path(__file__).resolve().parent / "bundle"
26
+
27
+
28
+ class ReportBuilder:
29
+ """Builds the dbdocs site for a project from its config."""
30
+
31
+ def __init__(self, config: DbDocsConfig) -> None:
32
+ self.config = config
33
+
34
+ def build_data(self) -> dict:
35
+ """Build the full data dict injected into / dumped alongside the SPA."""
36
+ target_path = self.config.target_path
37
+ manifest, catalog = load_artifacts(target_path)
38
+ adapter = adapter_type(target_path)
39
+
40
+ nodes = build_nodes(manifest, catalog)
41
+ tree = build_tree(nodes)
42
+ graph = LineageGraph(manifest, node_ids=set(nodes)).build()
43
+
44
+ erd = build_erd(self.config.dbterd, artifacts_dir=target_path)
45
+ erd_data = build_erd_data(erd)
46
+
47
+ dialect = self.config.dialect or adapter
48
+ column_lineage = ColumnLineageExtractor(manifest, catalog, dialect=dialect).extract()
49
+
50
+ return {
51
+ "metadata": {
52
+ **self.config.render_context(),
53
+ "generated_at": datetime.now().isoformat(sep=" ", timespec="seconds"),
54
+ "adapter_type": adapter,
55
+ "dialect": dialect,
56
+ "counts": self._counts(manifest),
57
+ },
58
+ "nodes": nodes,
59
+ "lineage": graph,
60
+ "columnLineage": column_lineage,
61
+ "erd": erd_data,
62
+ "tree": {"byDatabase": tree},
63
+ "readme": self._read_readme(),
64
+ }
65
+
66
+ def _read_readme(self) -> str:
67
+ """The project README markdown (rendered on the overview), or ``""``.
68
+
69
+ ``config.readme`` is a path relative to the cwd; a missing file or an
70
+ empty config value just yields no README section.
71
+ """
72
+ if not self.config.readme:
73
+ return ""
74
+ path = Path(self.config.readme)
75
+ try:
76
+ return path.read_text(encoding="utf-8")
77
+ except OSError:
78
+ return ""
79
+
80
+ #: Manifest collections that aren't keyed by a ``<type>.`` unique_id prefix,
81
+ #: mapped to the resource-type label to report them under.
82
+ _MANIFEST_COLLECTIONS = {
83
+ "sources": "source",
84
+ "exposures": "exposure",
85
+ "metrics": "metric",
86
+ "semantic_models": "semantic_model",
87
+ "saved_queries": "saved_query",
88
+ "unit_tests": "unit_test",
89
+ }
90
+
91
+ @classmethod
92
+ def _counts(cls, manifest: Any) -> dict:
93
+ """Count every dbt resource type present, not just the headline four.
94
+
95
+ ``manifest.nodes`` is keyed ``<resource_type>.<pkg>.<name>`` (model, seed,
96
+ snapshot, test, operation, …); the remaining resource types live in their
97
+ own top-level collections (sources, exposures, metrics, …).
98
+ """
99
+ counts: dict = {}
100
+ for unique_id in getattr(manifest, "nodes", {}) or {}:
101
+ rtype = str(unique_id).split(".")[0]
102
+ counts[rtype] = counts.get(rtype, 0) + 1
103
+ for attr, label in cls._MANIFEST_COLLECTIONS.items():
104
+ collection = getattr(manifest, attr, None)
105
+ if collection:
106
+ counts[label] = counts.get(label, 0) + len(collection)
107
+ return counts
108
+
109
+ def generate(self, output_dir: "str | None" = None) -> str:
110
+ """Render the site into ``output_dir`` (or config's). Returns its path."""
111
+ out = Path(output_dir) if output_dir else Path(self.config.output_path)
112
+ out.mkdir(parents=True, exist_ok=True)
113
+ copytree(src=BUNDLE_DIR, dst=out, dirs_exist_ok=True)
114
+
115
+ data = self.build_data()
116
+ index = out / "index.html"
117
+ index.write_text(inject(index.read_text(encoding="utf-8"), data), encoding="utf-8")
118
+ (out / "dbdocs-data.json").write_text(
119
+ json.dumps(data, indent=2, default=self._json_default), encoding="utf-8"
120
+ )
121
+
122
+ logger.info(
123
+ "Generated site at %s (%s nodes, %s column-lineage edges).",
124
+ out,
125
+ len(data["nodes"]),
126
+ len(data["columnLineage"]),
127
+ )
128
+ return str(out)
129
+
130
+ @staticmethod
131
+ def _json_default(value: Any) -> str:
132
+ return str(value)
@@ -0,0 +1,500 @@
1
+ /* dbdocs SPA — reads window.dbdocsData (base64-injected at generate time),
2
+ renders the catalog and per-node pages. Graphs (lineage DAG + ERDs) are
3
+ rendered by the React Flow bundle exposed as window.dbdocsGraph. No build step
4
+ for this file. */
5
+ (function () {
6
+ "use strict";
7
+
8
+ var DATA = window.dbdocsData || { metadata: {}, nodes: {}, lineage: {}, columnLineage: {}, erd: {}, tree: { byDatabase: {} } };
9
+ var NODES = DATA.nodes || {};
10
+ var META = DATA.metadata || {};
11
+ var COLLIN = DATA.columnLineage || {};
12
+ var TREE = (DATA.tree && DATA.tree.byDatabase) || {};
13
+ var README = DATA.readme || "";
14
+
15
+ var app = document.getElementById("app");
16
+ var sidebar = document.getElementById("sidebar");
17
+ var mountedGraph = null;
18
+
19
+ function el(tag, attrs, children) {
20
+ var node = document.createElement(tag);
21
+ if (attrs) Object.keys(attrs).forEach(function (k) {
22
+ if (k === "class") node.className = attrs[k];
23
+ else if (k === "html") node.innerHTML = attrs[k];
24
+ else if (k === "text") node.textContent = attrs[k];
25
+ else if (k.slice(0, 2) === "on" && typeof attrs[k] === "function") node.addEventListener(k.slice(2), attrs[k]);
26
+ else if (attrs[k] != null) node.setAttribute(k, attrs[k]);
27
+ });
28
+ (children || []).forEach(function (c) { if (c != null) node.appendChild(typeof c === "string" ? document.createTextNode(c) : c); });
29
+ return node;
30
+ }
31
+ function clear(node) { while (node.firstChild) node.removeChild(node.firstChild); }
32
+ function shortName(id) { return String(id).split(".").pop(); }
33
+
34
+ /* Minimal, XSS-safe inline markdown for author-controlled config text:
35
+ escapes HTML first, then renders links, **bold**, _italic_/*italic*, `code`.
36
+ Returns an HTML string (use with the el() "html" attr). */
37
+ function mdInline(text) {
38
+ var s = String(text == null ? "" : text).replace(/[&<>"]/g, function (c) {
39
+ return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c];
40
+ });
41
+ s = s.replace(/`([^`]+)`/g, "<code>$1</code>");
42
+ s = s.replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g,
43
+ '<a href="$2" target="_blank" rel="noopener">$1</a>');
44
+ s = s.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
45
+ s = s.replace(/(^|[\s(])[*_]([^*_]+)[*_]/g, "$1<em>$2</em>");
46
+ return s;
47
+ }
48
+
49
+ var ICONS = {
50
+ catalog: '<path d="M3 5h18v2H3zM3 11h18v2H3zM3 17h12v2H3z"/>',
51
+ dag: '<path d="M4 4h5v5H4zM15 15h5v5h-5zM9 6h6v2H9zM16 9v6h-2V9zM6 9v6h2v-6z"/>',
52
+ database: '<path d="M12 3c-4 0-7 1.3-7 3v12c0 1.7 3 3 7 3s7-1.3 7-3V6c0-1.7-3-3-7-3zm5 15c0 .5-1.9 1.5-5 1.5S7 18.5 7 18v-2.3c1.3.7 3.1 1.1 5 1.1s3.7-.4 5-1.1V18zm0-5c0 .5-1.9 1.5-5 1.5S7 13.5 7 13v-2.3c1.3.7 3.1 1.1 5 1.1s3.7-.4 5-1.1V13zM12 9C8.9 9 7 8 7 7.5S8.9 6 12 6s5 1 5 1.5S15.1 9 12 9z"/>',
53
+ schema: '<path d="M10 6L8.6 7.4 13.2 12l-4.6 4.6L10 18l6-6z"/>',
54
+ graph: '<path d="M10 20H4v-6h2v2.6l3.3-3.3 1.4 1.4L7.4 18H10zM20 10h-2V7.4l-3.3 3.3-1.4-1.4L16.6 6H14V4h6z"/>',
55
+ model: '<path d="M3 5h18v4H3zM3 10h18v4H3zM3 15h18v4H3z" opacity=".25"/><path d="M3 5h18v4H3z"/>',
56
+ source: '<path d="M12 3c-4 0-7 1.3-7 3v12c0 1.7 3 3 7 3s7-1.3 7-3V6c0-1.7-3-3-7-3zm5 15c0 .5-1.9 1.5-5 1.5S7 18.5 7 18v-2.3c1.3.7 3.1 1.1 5 1.1s3.7-.4 5-1.1zm0-5c0 .5-1.9 1.5-5 1.5S7 13.5 7 13v-2.3c1.3.7 3.1 1.1 5 1.1s3.7-.4 5-1.1zM12 9C8.9 9 7 8 7 7.5S8.9 6 12 6s5 1 5 1.5S15.1 9 12 9z"/>',
57
+ seed: '<path d="M12 2C7 6 5 10 5 14a7 7 0 0014 0c0-4-2-8-7-12zm0 17a5 5 0 01-5-5c0-2.6 1.3-5.5 5-9 3.7 3.5 5 6.4 5 9a5 5 0 01-5 5z"/>',
58
+ snapshot: '<path d="M12 8a4 4 0 104 4 4 4 0 00-4-4zm8.94 3A9 9 0 0013 3.06V1h-2v2.06A9 9 0 003.06 11H1v2h2.06A9 9 0 0011 20.94V23h2v-2.06A9 9 0 0020.94 13H23v-2zM12 19a7 7 0 117-7 7 7 0 01-7 7z"/>',
59
+ test: '<path d="M12 1L3 5v6c0 5 3.8 9.7 9 11 5.2-1.3 9-6 9-11V5l-9-4zm-1.2 14.2l-3.5-3.5 1.4-1.4 2.1 2.1 4.6-4.6 1.4 1.4-6 6z"/>',
60
+ unit_test: '<path d="M9 2v2h1v6.2L4.3 19A2 2 0 006 22h12a2 2 0 001.7-3L14 10.2V4h1V2H9zm3 11l3.3 5H8.7L12 13z"/>',
61
+ metric: '<path d="M12 4a9 9 0 00-9 9 8.9 8.9 0 002 5.6l1.5-1.3A7 7 0 0112 6a7 7 0 015.5 11.3L19 18.6A8.9 8.9 0 0021 13a9 9 0 00-9-9zm-1 5v5a1.5 1.5 0 103 0c0-.6-.3-1-.7-1.3L11 9z"/>',
62
+ semantic_model: '<path d="M12 2L2 7l10 5 10-5-10-5zm0 7.5L4.2 6 12 2.5 19.8 6 12 9.5zM2 12l10 5 10-5-2.3-1.2L12 14.5 4.3 10.8 2 12zm0 5l10 5 10-5-2.3-1.2L12 19.5 4.3 15.8 2 17z"/>',
63
+ exposure: '<path d="M12 5C6.5 5 2 9 1 12c1 3 5.5 7 11 7s10-4 11-7c-1-3-5.5-7-11-7zm0 11a4 4 0 110-8 4 4 0 010 8zm0-6a2 2 0 100 4 2 2 0 000-4z"/>',
64
+ saved_query: '<path d="M6 2a2 2 0 00-2 2v18l8-4 8 4V4a2 2 0 00-2-2H6zm6 4a4 4 0 013.2 6.4l2.2 2.2-1.4 1.4-2.2-2.2A4 4 0 1112 6zm0 2a2 2 0 100 4 2 2 0 000-4z"/>',
65
+ operation: '<path d="M3 4h18v16H3V4zm2 4l4 3-4 3 1.3 1L12 11 6.3 7 5 8zm7 6h6v-1.5h-6V14z"/>',
66
+ fullscreen: '<path d="M4 9V4h5v2H6v3H4zm11-5h5v5h-2V6h-3V4zM4 15h2v3h3v2H4v-5zm14 0h2v5h-5v-2h3v-3z"/>',
67
+ };
68
+ function icon(name, size) {
69
+ var span = el("span", { class: "ic" });
70
+ var s = size || 16;
71
+ span.innerHTML = '<svg viewBox="0 0 24 24" width="' + s + '" height="' + s + '" fill="currentColor">' + (ICONS[name] || "") + "</svg>";
72
+ return span;
73
+ }
74
+
75
+ function parseHash() {
76
+ var raw = location.hash.replace(/^#\/?/, "");
77
+ var qIndex = raw.indexOf("?");
78
+ var path = qIndex >= 0 ? raw.slice(0, qIndex) : raw;
79
+ var query = {};
80
+ if (qIndex >= 0) raw.slice(qIndex + 1).split("&").forEach(function (p) {
81
+ var kv = p.split("="); query[decodeURIComponent(kv[0])] = decodeURIComponent(kv[1] || "");
82
+ });
83
+ var parts = path.split("/").filter(Boolean);
84
+ return { view: parts[0] || "overview", id: parts[1] ? decodeURIComponent(parts[1]) : null, query: query };
85
+ }
86
+
87
+ function unmountGraph() {
88
+ if (mountedGraph && window.dbdocsGraph) {
89
+ try { window.dbdocsGraph.unmount(mountedGraph); } catch (e) { /* ignore */ }
90
+ }
91
+ mountedGraph = null;
92
+ }
93
+
94
+ function graphMount(mode, focus) {
95
+ var root = el("div", { class: "graph-host", id: "graph-root", "data-mode": mode });
96
+ if (focus) root.setAttribute("data-focus", focus);
97
+ setTimeout(function () {
98
+ if (window.dbdocsGraph) { mountedGraph = root; window.dbdocsGraph.mount(root); }
99
+ else root.appendChild(el("p", { class: "empty" }, ["Graph bundle not loaded."]));
100
+ }, 0);
101
+ return root;
102
+ }
103
+
104
+ function route() {
105
+ unmountGraph();
106
+ var r = parseHash();
107
+ if (r.view === "node" && r.id && NODES[r.id]) renderNode(NODES[r.id]);
108
+ else if (r.view === "dag") renderDag(r.query.focus);
109
+ else renderOverview();
110
+ if (r.view !== "dag") app.appendChild(contentFooter());
111
+ highlightNav(r);
112
+ app.focus();
113
+ app.scrollTop = 0;
114
+ }
115
+
116
+ function contentFooter() {
117
+ var foot = el("div", { class: "content-foot" });
118
+ var left = el("span", { class: "cf-left" }, [
119
+ icon("catalog", 14),
120
+ el("span", {}, ["Generated by "]),
121
+ el("a", { href: "https://github.com/datnguye/dbt-docs", target: "_blank", rel: "noopener" }, ["dbdocs"]),
122
+ ]);
123
+ var right = el("a", { class: "cf-top", href: "#", onclick: function (e) { e.preventDefault(); app.scrollTop = 0; } }, ["↑ Back to top"]);
124
+ foot.appendChild(left);
125
+ foot.appendChild(right);
126
+ return foot;
127
+ }
128
+
129
+ var RTYPE_ORDER = { model: 0, snapshot: 1, seed: 2, source: 3 };
130
+
131
+ function buildNav() {
132
+ clear(sidebar);
133
+ sidebar.appendChild(el("a", { class: "nav-cta", href: "#/overview", "data-nav": "overview" }, [icon("catalog"), "Catalog overview"]));
134
+ sidebar.appendChild(el("a", { class: "nav-cta", href: "#/dag", "data-nav": "dag" }, [icon("dag"), "Lineage / DAG"]));
135
+
136
+ Object.keys(TREE).forEach(function (db) {
137
+ var dbDetails = el("details", { class: "nav-section nav-db", open: "" }, [
138
+ el("summary", {}, [icon("database"), el("span", {}, [db])]),
139
+ ]);
140
+ Object.keys(TREE[db]).forEach(function (schema) {
141
+ var ids = TREE[db][schema].slice().sort(function (a, b) {
142
+ var ra = RTYPE_ORDER[NODES[a].resource_type], rb = RTYPE_ORDER[NODES[b].resource_type];
143
+ if (ra !== rb) return ra - rb;
144
+ return NODES[a].label.localeCompare(NODES[b].label);
145
+ });
146
+ var items = el("ul", { class: "nav-items" }, ids.map(function (id) {
147
+ var n = NODES[id];
148
+ return el("li", {}, [el("a", { href: "#/node/" + encodeURIComponent(id), "data-node": id }, [
149
+ el("span", { class: "dot " + n.resource_type }), n.label,
150
+ ])]);
151
+ }));
152
+ var schemaDetails = el("details", { class: "nav-section nav-schema", open: "" }, [
153
+ el("summary", {}, [icon("schema"), el("span", {}, [schema])]), items,
154
+ ]);
155
+ dbDetails.appendChild(schemaDetails);
156
+ });
157
+ sidebar.appendChild(dbDetails);
158
+ });
159
+ }
160
+
161
+ function highlightNav(r) {
162
+ sidebar.querySelectorAll("[data-node], [data-nav]").forEach(function (a) { a.classList.remove("active"); });
163
+ if (r.view === "node" && r.id) {
164
+ var a = sidebar.querySelector('[data-node="' + (window.CSS && CSS.escape ? CSS.escape(r.id) : r.id) + '"]');
165
+ if (a) a.classList.add("active");
166
+ } else {
167
+ var nav = sidebar.querySelector('[data-nav="' + (r.view === "dag" ? "dag" : "overview") + '"]');
168
+ if (nav) nav.classList.add("active");
169
+ }
170
+ }
171
+
172
+ function renderOverview() {
173
+ clear(app);
174
+ var counts = META.counts || {};
175
+ app.appendChild(el("h1", {}, [META.project_name || "dbt docs"]));
176
+ if (META.site_description) app.appendChild(el("p", { class: "description", html: mdInline(META.site_description) }, []));
177
+
178
+ app.appendChild(el("div", { class: "cards" }, resourceCards(counts)));
179
+
180
+ app.appendChild(el("h2", {}, ["Entity-relationship diagram"]));
181
+ app.appendChild(graphMount("erd", null));
182
+
183
+ if (README) app.appendChild(renderReadme(README));
184
+ }
185
+
186
+ function renderReadme(md) {
187
+ var box = el("section", { class: "readme" });
188
+ var body = el("div", { class: "markdown-body" });
189
+ try {
190
+ body.innerHTML = marked.parse(md, { breaks: false, gfm: true });
191
+ } catch (e) {
192
+ body.appendChild(el("pre", { class: "code" }, [md]));
193
+ }
194
+ // README paths are relative to the repo, not the docs site. Rewrite relative
195
+ // image/link URLs to absolute GitHub URLs (raw for images, blob for links)
196
+ // so they don't 404, and open external links in a new tab.
197
+ body.querySelectorAll("img[src]").forEach(function (img) {
198
+ img.src = repoUrl(img.getAttribute("src"), "raw") || img.src;
199
+ });
200
+ body.querySelectorAll("a[href]").forEach(function (a) {
201
+ var fixed = repoUrl(a.getAttribute("href"), "blob");
202
+ if (fixed) a.setAttribute("href", fixed);
203
+ if (/^https?:/.test(a.getAttribute("href") || "")) { a.target = "_blank"; a.rel = "noopener"; }
204
+ });
205
+ box.appendChild(body);
206
+ return box;
207
+ }
208
+
209
+ /* Resolve a README-relative path to an absolute GitHub URL; absolute/anchor
210
+ URLs are returned unchanged (null = leave as-is). ``kind`` is raw|blob. */
211
+ function repoUrl(href, kind) {
212
+ if (!href) return null;
213
+ if (/^(https?:|mailto:|#|data:)/i.test(href)) return null;
214
+ var base = (META.repo_url || "").replace(/\/$/, "");
215
+ if (!/^https:\/\/github\.com\//.test(base)) return null;
216
+ var path = href.replace(/^\.?\//, ""); // drop leading ./ or /
217
+ return base + "/" + kind + "/HEAD/" + path;
218
+ }
219
+ var RTYPE_ORDER_CARDS = [
220
+ "model", "source", "seed", "snapshot", "test", "unit_test",
221
+ "metric", "semantic_model", "exposure", "saved_query", "operation", "macro",
222
+ ];
223
+ function resourceCards(counts) {
224
+ var types = Object.keys(counts).filter(function (t) { return counts[t] > 0; });
225
+ types.sort(function (a, b) {
226
+ var ia = RTYPE_ORDER_CARDS.indexOf(a), ib = RTYPE_ORDER_CARDS.indexOf(b);
227
+ if (ia < 0) ia = 999; if (ib < 0) ib = 999;
228
+ return ia - ib || a.localeCompare(b);
229
+ });
230
+ return types.map(function (t) { return card(counts[t], pluralize(t), t); });
231
+ }
232
+ function pluralize(rtype) {
233
+ var label = String(rtype).replace(/_/g, " ");
234
+ if (/[^aeiou]y$/.test(label)) return label.slice(0, -1) + "ies"; // query → queries
235
+ if (/(s|x|z|ch|sh)$/.test(label)) return label + "es";
236
+ return label + "s";
237
+ }
238
+ function card(num, lbl, rtype) {
239
+ var ic = icon(ICONS[rtype] ? rtype : "graph", 18);
240
+ ic.classList.add("card-ic", rtype);
241
+ var head = el("div", { class: "card-head" }, [ic, el("div", { class: "num" }, [String(num)])]);
242
+ return el("div", { class: "card" }, [head, el("div", { class: "lbl" }, [lbl])]);
243
+ }
244
+
245
+ function renderNode(n) {
246
+ clear(app);
247
+ app.appendChild(el("h1", {}, [n.label, " ", el("span", { class: "page-id" }, ["`" + n.resource_type + "`"])]));
248
+ app.appendChild(el("div", { class: "page-id" }, [n.id]));
249
+
250
+ var badges = el("div", { class: "badges" }, [el("span", { class: "badge rtype " + n.resource_type }, [n.resource_type])]);
251
+ if (n.relation_name) badges.appendChild(el("span", { class: "badge" }, ["⌗ " + n.relation_name]));
252
+ (n.tags || []).forEach(function (t) { badges.appendChild(el("span", { class: "badge tag" }, ["#" + t])); });
253
+ var viewDag = el("a", { class: "badge", href: "#/dag?focus=" + encodeURIComponent(n.id) });
254
+ viewDag.appendChild(icon("graph")); viewDag.appendChild(document.createTextNode(" View in DAG"));
255
+ badges.appendChild(viewDag);
256
+ app.appendChild(badges);
257
+
258
+ app.appendChild(n.description
259
+ ? el("p", { class: "description", html: mdInline(n.description) }, [])
260
+ : el("p", { class: "description muted" }, ["No description provided."]));
261
+
262
+ /* Columns — with an inline upstream-lineage column. */
263
+ var colLin = columnLineageMap(n.id);
264
+ app.appendChild(el("h2", {}, ["Columns"]));
265
+ if (n.columns && n.columns.length) {
266
+ var rows = n.columns.map(function (c) {
267
+ return el("tr", {}, [
268
+ el("td", {}, [el("code", {}, [c.name])]),
269
+ el("td", { class: "muted" }, [c.type || ""]),
270
+ el("td", {}, (c.tags || []).map(function (t) { return el("span", { class: "badge tag" }, ["#" + t]); })),
271
+ el("td", { html: String(c.description || "") }, []),
272
+ el("td", {}, upstreamChips(colLin[String(c.name).toLowerCase()])),
273
+ ]);
274
+ });
275
+ app.appendChild(el("table", {}, [
276
+ el("thead", {}, [el("tr", {}, [th("Column"), th("Type"), th("Tags"), th("Description"), th("Upstream lineage")])]),
277
+ el("tbody", {}, rows),
278
+ ]));
279
+ } else {
280
+ app.appendChild(el("p", { class: "empty" }, ["No columns found in the catalog for this entity."]));
281
+ }
282
+
283
+ /* Related ERD — before the transformation logic. */
284
+ app.appendChild(el("h2", {}, ["Related ERD"]));
285
+ app.appendChild(graphMount("erd-node", n.id));
286
+
287
+ /* Transformation logic. */
288
+ if (n.compiled_code || n.raw_code) {
289
+ app.appendChild(el("h2", {}, ["Transformation logic"]));
290
+ app.appendChild(codeTabs(n));
291
+ if (n.macros && n.macros.length) {
292
+ app.appendChild(el("h2", {}, ["Macros used"]));
293
+ n.macros.forEach(function (m) {
294
+ app.appendChild(el("details", { class: "macro" }, [
295
+ el("summary", {}, [m.name + (m.package ? " (" + m.package + ")" : "")]),
296
+ el("pre", { class: "code" }, [m.sql || ""]),
297
+ ]));
298
+ });
299
+ }
300
+ }
301
+ }
302
+ function th(t) { return el("th", {}, [t]); }
303
+
304
+ /* { lowercased columnName: [{node, column}, …] } for one node.
305
+ Keyed lowercase because sqlglot normalizes identifiers to lower case while
306
+ the catalog may report them uppercase (Snowflake/BigQuery). */
307
+ function columnLineageMap(nodeId) {
308
+ var prefix = nodeId + ".";
309
+ var out = {};
310
+ Object.keys(COLLIN).forEach(function (k) {
311
+ if (k.indexOf(prefix) === 0) out[k.slice(prefix.length).toLowerCase()] = COLLIN[k];
312
+ });
313
+ return out;
314
+ }
315
+ function upstreamChips(upstream) {
316
+ if (!upstream || !upstream.length) return [el("span", { class: "muted" }, ["—"])];
317
+ return upstream.map(function (u) {
318
+ var label = NODES[u.node] ? NODES[u.node].label : shortName(u.node);
319
+ // Link the column (the lineage target the user cares about); the model
320
+ // name is plain context. The href still deep-links to the upstream node.
321
+ return el("span", { class: "up-chip" }, [
322
+ el("span", { class: "up-model" }, [label + "."]),
323
+ el("a", { href: "#/node/" + encodeURIComponent(u.node), title: u.node }, [u.column]),
324
+ ]);
325
+ });
326
+ }
327
+
328
+ function codeTabs(n) {
329
+ var wrap = el("div", {});
330
+ var tabs = el("div", { class: "tabs" });
331
+ var panels = el("div", {});
332
+ var defs = [];
333
+ if (n.compiled_code) defs.push({ label: "Compiled SQL", code: n.compiled_code });
334
+ if (n.raw_code) defs.push({ label: "Source", code: n.raw_code });
335
+ defs.forEach(function (d, i) {
336
+ var panel = el("div", { class: "tab-panel" + (i === 0 ? " active" : "") }, [el("pre", { class: "code" }, [d.code])]);
337
+ var tab = el("div", { class: "tab" + (i === 0 ? " active" : ""), onclick: function () {
338
+ tabs.querySelectorAll(".tab").forEach(function (t) { t.classList.remove("active"); });
339
+ panels.querySelectorAll(".tab-panel").forEach(function (p) { p.classList.remove("active"); });
340
+ tab.classList.add("active"); panel.classList.add("active");
341
+ } }, [d.label]);
342
+ tabs.appendChild(tab); panels.appendChild(panel);
343
+ });
344
+ wrap.appendChild(tabs); wrap.appendChild(panels);
345
+ return wrap;
346
+ }
347
+
348
+ function renderDag(focusId) {
349
+ clear(app);
350
+ var host = graphMount("dag", focusId && NODES[focusId] ? focusId : null);
351
+ var fsBtn = el("button", {
352
+ class: "fs-btn", type: "button", title: "Toggle full screen",
353
+ onclick: function () { toggleFullscreen(host); },
354
+ }, [icon("fullscreen", 15), " Full screen"]);
355
+ var header = el("div", { class: "page-head" }, [el("h1", {}, ["Lineage / DAG"]), fsBtn]);
356
+ app.appendChild(header);
357
+ app.appendChild(host);
358
+ }
359
+
360
+ function toggleFullscreen(host) {
361
+ if (document.fullscreenElement) {
362
+ document.exitFullscreen();
363
+ } else if (host.requestFullscreen) {
364
+ host.requestFullscreen().catch(function () { /* denied — ignore */ });
365
+ }
366
+ }
367
+
368
+ function buildSearch() {
369
+ var input = document.getElementById("search");
370
+ var results = document.getElementById("search-results");
371
+ if (typeof MiniSearch === "undefined") return;
372
+ var docs = Object.keys(NODES).map(function (id) {
373
+ var n = NODES[id];
374
+ return { id: id, label: n.label, resource_type: n.resource_type, schema: n.schema,
375
+ description: n.description, columns: (n.columns || []).map(function (c) { return c.name; }).join(" ") };
376
+ });
377
+ var mini = new MiniSearch({ fields: ["label", "description", "columns"], storeFields: ["label", "resource_type", "schema"], searchOptions: { prefix: true, fuzzy: 0.2, boost: { label: 3 } } });
378
+ mini.addAll(docs);
379
+
380
+ function render(hits) {
381
+ clear(results);
382
+ if (!hits.length) { results.hidden = true; return; }
383
+ hits.slice(0, 12).forEach(function (h) {
384
+ results.appendChild(el("a", { href: "#/node/" + encodeURIComponent(h.id), onclick: function () { results.hidden = true; input.value = ""; } }, [
385
+ el("span", { class: "dot " + h.resource_type }), " " + h.label,
386
+ el("span", { class: "sr-meta" }, [" " + h.resource_type + " · " + h.schema]),
387
+ ]));
388
+ });
389
+ results.hidden = false;
390
+ }
391
+ input.addEventListener("input", function () {
392
+ var q = input.value.trim();
393
+ if (!q) { results.hidden = true; return; }
394
+ render(mini.search(q));
395
+ });
396
+ input.addEventListener("focus", function () { if (input.value.trim()) render(mini.search(input.value.trim())); });
397
+ document.addEventListener("click", function (e) { if (!results.contains(e.target) && e.target !== input) results.hidden = true; });
398
+ }
399
+
400
+ function initTheme() {
401
+ var saved = null;
402
+ try { saved = localStorage.getItem("dbdocs-theme"); } catch (e) { /* private mode */ }
403
+ if (saved) document.documentElement.setAttribute("data-theme", saved);
404
+ document.getElementById("theme-toggle").addEventListener("click", function () {
405
+ var cur = document.documentElement.getAttribute("data-theme") === "dark" ? "light" : "dark";
406
+ document.documentElement.setAttribute("data-theme", cur);
407
+ try { localStorage.setItem("dbdocs-theme", cur); } catch (e) { /* ignore */ }
408
+ route();
409
+ });
410
+ }
411
+
412
+ /* The version directory the site is served from, e.g. ".../latest/index.html"
413
+ → "latest". Drops a trailing file segment (index.html) when present. */
414
+ function currentVersionDir() {
415
+ var parts = location.pathname.split("/").filter(Boolean);
416
+ if (parts.length && /\.[a-z]+$/i.test(parts[parts.length - 1])) parts.pop();
417
+ return parts.length ? parts[parts.length - 1] : "";
418
+ }
419
+
420
+ function initVersions() {
421
+ var sel = document.getElementById("version-switcher");
422
+ fetch("../versions.json").then(function (r) { return r.ok ? r.json() : null; }).then(function (versions) {
423
+ if (!versions || !versions.length) return;
424
+ versions.forEach(function (v) {
425
+ var label = v.title || v.version + (v.aliases && v.aliases.length ? " (" + v.aliases.join(", ") + ")" : "");
426
+ sel.appendChild(el("option", { value: v.version }, [label]));
427
+ });
428
+ sel.hidden = false;
429
+ var current = currentVersionDir();
430
+ versions.forEach(function (v) { if (v.version === current || (v.aliases || []).indexOf(current) >= 0) sel.value = v.version; });
431
+ sel.addEventListener("change", function () { location.href = "../" + sel.value + "/index.html"; });
432
+ maybeWarnNotLatest(versions, current);
433
+ }).catch(function () { /* unversioned build: no switcher */ });
434
+ }
435
+
436
+ function maybeWarnNotLatest(versions, current) {
437
+ var latest = versions[0]; // versions.json is sorted newest-first
438
+ if (!latest) return;
439
+ var isLatest = current === latest.version || (latest.aliases || []).indexOf(current) >= 0;
440
+ var defaultAlias = (META.default_version || "latest");
441
+ if (isLatest || current === defaultAlias) return;
442
+ var target = "../" + ((latest.aliases || []).indexOf(defaultAlias) >= 0 ? defaultAlias : latest.version) + "/index.html";
443
+ var banner = el("div", { class: "version-warning" }, [
444
+ el("span", {}, ["You're viewing version "]), el("strong", {}, [current || "?"]),
445
+ el("span", {}, [" — not the latest ("]), el("strong", {}, [latest.version]), el("span", {}, [")."]),
446
+ el("a", { href: target }, ["Go to latest →"]),
447
+ ]);
448
+ document.body.insertBefore(banner, document.body.firstChild);
449
+ }
450
+
451
+ function initRepo() {
452
+ if (!META.repo_url) return;
453
+ var link = document.getElementById("repo-link");
454
+ link.href = META.repo_url;
455
+ document.getElementById("repo-name").textContent = META.repo_name || META.repo_url;
456
+ link.hidden = false;
457
+ }
458
+
459
+ function initTopbarMeta() {
460
+ var box = document.getElementById("topbar-meta");
461
+ if (!box) return;
462
+ var parts = [];
463
+ if (META.generated_at) parts.push("Generated " + META.generated_at);
464
+ if (META.adapter_type) parts.push(META.adapter_type);
465
+ parts.push(Object.keys(COLLIN).length + " column-lineage edges");
466
+ box.textContent = parts.join(" · ");
467
+ box.hidden = false;
468
+ }
469
+
470
+ function initFooter() {
471
+ var footer = document.getElementById("site-footer");
472
+ if (!footer) return;
473
+ clear(footer);
474
+ footer.appendChild(el("div", { class: "footer-copy" }, [
475
+ "2026 © ",
476
+ el("a", { href: "https://github.com/datnguye", target: "_blank", rel: "noopener" }, ["@Dat Nguyen"]),
477
+ ]));
478
+ if (META.show_buy_me_a_coffee !== false) {
479
+ footer.appendChild(el("a", {
480
+ class: "bmc", href: "https://www.buymeacoffee.com/datnguye",
481
+ target: "_blank", rel: "noopener", title: "Buy me a coffee",
482
+ }, ["☕ Buy me a coffee"]));
483
+ }
484
+ }
485
+
486
+ function boot() {
487
+ document.getElementById("brand-name").textContent = META.site_name || "dbt docs";
488
+ document.title = META.site_name || "dbt docs";
489
+ initRepo();
490
+ initTopbarMeta();
491
+ initFooter();
492
+ initTheme();
493
+ buildNav();
494
+ buildSearch();
495
+ initVersions();
496
+ window.addEventListener("hashchange", route);
497
+ route();
498
+ }
499
+ if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", boot); else boot();
500
+ })();
@@ -0,0 +1,12 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32" role="img" aria-label="dbt docs">
2
+ <!-- dbt-docs mark: a rounded badge (document) with the dbt hexagon pinwheel. -->
3
+ <rect x="1" y="1" width="30" height="30" rx="7" fill="#262A38"/>
4
+ <rect x="1.5" y="1.5" width="29" height="29" rx="6.5" fill="none" stroke="#3a4250" stroke-width="1"/>
5
+ <!-- dbt pinwheel: four orange blades meeting at center -->
6
+ <g fill="#FF694A">
7
+ <path d="M16 6.4c.5 0 .92.28 1.15.72l1.86 3.6a4.7 4.7 0 0 0-2.29 2.29l-3.6-1.86A1.3 1.3 0 0 1 12.4 9.9l1.95-2.93c.24-.36.64-.57 1.07-.57z"/>
8
+ <path d="M25.6 16c0 .5-.28.92-.72 1.15l-3.6 1.86a4.7 4.7 0 0 0-2.29-2.29l1.86-3.6A1.3 1.3 0 0 1 22.1 12.4l2.93 1.95c.36.24.57.64.57 1.07z"/>
9
+ <path d="M16 25.6c-.5 0-.92-.28-1.15-.72l-1.86-3.6a4.7 4.7 0 0 0 2.29-2.29l3.6 1.86A1.3 1.3 0 0 1 19.6 22.1l-1.95 2.93c-.24.36-.64.57-1.07.57z"/>
10
+ <path d="M6.4 16c0-.5.28-.92.72-1.15l3.6-1.86a4.7 4.7 0 0 0 2.29 2.29l-1.86 3.6A1.3 1.3 0 0 1 9.9 19.6L6.97 17.65A1.27 1.27 0 0 1 6.4 16z"/>
11
+ </g>
12
+ </svg>