Skip to content

Commit bba8d41

Browse files
authored
feat: add YAML documentation parsing for dbt models (#1686)
1 parent 052e34e commit bba8d41

File tree

11 files changed

+361
-51
lines changed

11 files changed

+361
-51
lines changed

package-lock.json

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1345,6 +1345,7 @@
13451345
"@types/glob": "^8.1.0",
13461346
"@types/istanbul-lib-coverage": "^2.0.6",
13471347
"@types/jest": "^29.5.0",
1348+
"@types/js-yaml": "^4.0.9",
13481349
"@types/node": "^20.14.11",
13491350
"@types/vscode": "^1.95.0",
13501351
"@types/which": "^3.0.4",

src/manifest/parsers/docParser.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { createFullPathForNode } from ".";
99
export class DocParser {
1010
constructor(private terminal: DBTTerminal) {}
1111

12-
createDocMetaMap(docs: any[], project: DBTProject): Promise<DocMetaMap> {
12+
createDocMetaMap(docs: any, project: DBTProject): Promise<DocMetaMap> {
1313
return new Promise(async (resolve) => {
1414
this.terminal.debug(
1515
"DocParser",
@@ -22,12 +22,8 @@ export class DocParser {
2222
resolve(docMetaMap);
2323
return;
2424
}
25-
if (typeof docs[Symbol.iterator] !== "function") {
26-
resolve(docMetaMap);
27-
return;
28-
}
29-
for (const doc of docs) {
30-
const { package_name, name, original_file_path } = doc;
25+
for (const doc of Object.values(docs)) {
26+
const { package_name, name, original_file_path } = doc as any;
3127
const packageName = package_name;
3228
// TODO: these things can change so we should recreate them if project config changes
3329
const projectName = project.getProjectName();
@@ -47,6 +43,7 @@ export class DocParser {
4743
original_file_path,
4844
);
4945
if (!fullPath) {
46+
resolve(docMetaMap);
5047
return;
5148
}
5249
try {

src/services/docGenService.ts

Lines changed: 103 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import path = require("path");
2+
import { promises as fs } from "fs";
3+
import * as yaml from "js-yaml";
24
import {
35
ProgressLocation,
46
Uri,
@@ -13,7 +15,11 @@ import { RateLimitException } from "../exceptions";
1315
import { DBTProject } from "../manifest/dbtProject";
1416
import { DBTProjectContainer } from "../manifest/dbtProjectContainer";
1517
import { TelemetryService } from "../telemetry";
16-
import { extendErrorWithSupportLinks, provideSingleton } from "../utils";
18+
import {
19+
extendErrorWithSupportLinks,
20+
provideSingleton,
21+
removeProtocol,
22+
} from "../utils";
1723
import {
1824
AIColumnDescription,
1925
DBTDocumentation,
@@ -33,7 +39,7 @@ interface DocumentationSchemaModel {
3339
name: string;
3440
description: string;
3541
tests: any;
36-
columns: [];
42+
columns: { name: string; description?: string; data_type?: string }[];
3743
}
3844
export interface DocumentationSchema {
3945
version: number;
@@ -77,6 +83,85 @@ export class DocGenService {
7783
private dbtTerminal: DBTTerminal,
7884
) {}
7985

86+
private async getDocumentationFromYaml(
87+
projectRoot: string,
88+
filePath: string,
89+
modelName: string,
90+
): Promise<DBTDocumentation | undefined> {
91+
const eventResult = this.queryManifestService.getEventByCurrentProject();
92+
if (!eventResult || !eventResult.event) {
93+
return undefined;
94+
}
95+
96+
const { event } = eventResult;
97+
const currentNode = event.nodeMetaMap.lookupByBaseName(modelName);
98+
if (!currentNode?.patch_path) {
99+
return undefined;
100+
}
101+
102+
try {
103+
// Read and parse the YAML file
104+
const yamlPath = path.join(
105+
projectRoot,
106+
removeProtocol(currentNode.patch_path),
107+
);
108+
const content = await fs.readFile(yamlPath, "utf8");
109+
const parsedDoc = yaml.load(content) as DocumentationSchema;
110+
111+
// Find matching model definition
112+
const modelDef = parsedDoc.models?.find((m) => m.name === modelName);
113+
if (!modelDef) {
114+
return undefined;
115+
}
116+
117+
// Map to DBTDocumentation format
118+
return {
119+
aiEnabled: this.altimateRequest.enabled(),
120+
name: modelName,
121+
patchPath: currentNode.patch_path,
122+
description: modelDef.description || "",
123+
generated: false,
124+
resource_type: currentNode.resource_type,
125+
uniqueId: currentNode.uniqueId,
126+
filePath,
127+
columns: (modelDef.columns || []).map((column) => ({
128+
name: column.name,
129+
description: column.description || "",
130+
generated: false,
131+
source: Source.YAML,
132+
type: column.data_type?.toLowerCase(),
133+
})),
134+
} as DBTDocumentation;
135+
} catch (error) {
136+
this.dbtTerminal.error(
137+
"docGenService:getDocumentationYamlError",
138+
`Error reading YAML documentation: ${error}`,
139+
error,
140+
);
141+
}
142+
// falling back on original implementation
143+
const docColumns = currentNode.columns;
144+
return {
145+
aiEnabled: this.altimateRequest.enabled(),
146+
name: modelName,
147+
patchPath: currentNode.patch_path,
148+
description: currentNode.description,
149+
generated: false,
150+
resource_type: currentNode.resource_type,
151+
uniqueId: currentNode.uniqueId,
152+
filePath,
153+
columns: Object.values(docColumns).map((column) => {
154+
return {
155+
name: column.name,
156+
description: column.description,
157+
generated: false,
158+
source: Source.YAML,
159+
type: column.data_type?.toLowerCase(),
160+
};
161+
}),
162+
} as DBTDocumentation;
163+
}
164+
80165
private async generateDocsForColumn(
81166
documentation: DBTDocumentation | undefined,
82167
compiledSql: string | undefined,
@@ -221,53 +306,33 @@ export class DocGenService {
221306
documentation: DBTDocumentation | undefined;
222307
message?: { message: string; type: string };
223308
}> {
224-
const eventResult = this.queryManifestService.getEventByCurrentProject();
225-
if (!eventResult) {
226-
return {
227-
documentation: undefined,
228-
message: this.getMissingDocumentationMessage(filePath),
229-
};
309+
const messageObj = this.getMissingDocumentationMessage(filePath);
310+
if (messageObj.type === "error") {
311+
return { documentation: undefined, message: messageObj };
230312
}
231-
const { event } = eventResult;
232313

233-
if (!event || !filePath) {
234-
return {
235-
documentation: undefined,
236-
message: this.getMissingDocumentationMessage(filePath),
237-
};
314+
if (!filePath) {
315+
return { documentation: undefined, message: messageObj };
238316
}
239317

240318
const modelName = path.basename(filePath, ".sql");
241-
const currentNode = event.nodeMetaMap.lookupByBaseName(modelName);
242-
if (currentNode === undefined) {
319+
const project = this.dbtProjectContainer.findDBTProject(Uri.file(filePath));
320+
321+
if (!project || !project.projectRoot) {
243322
return {
244323
documentation: undefined,
245324
message: this.getMissingDocumentationMessage(filePath),
246325
};
247326
}
248327

249-
const docColumns = currentNode.columns;
250-
return {
251-
documentation: {
252-
aiEnabled: this.altimateRequest.enabled(),
253-
name: modelName,
254-
patchPath: currentNode.patch_path,
255-
description: currentNode.description,
256-
generated: false,
257-
resource_type: currentNode.resource_type,
258-
uniqueId: currentNode.uniqueId,
259-
filePath,
260-
columns: Object.values(docColumns).map((column) => {
261-
return {
262-
name: column.name,
263-
description: column.description,
264-
generated: false,
265-
source: Source.YAML,
266-
type: column.data_type?.toLowerCase(),
267-
};
268-
}),
269-
} as DBTDocumentation,
270-
};
328+
// Get model directly from YAML
329+
const documentation = await this.getDocumentationFromYaml(
330+
project.projectRoot.fsPath,
331+
filePath,
332+
modelName,
333+
);
334+
335+
return { documentation };
271336
}
272337

273338
private chunk(a: string[], n: number) {

src/webview_provider/docsEditPanel.ts

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,7 @@ import { DbtTestService } from "../services/dbtTestService";
5454
import { gte } from "semver";
5555
import { TelemetryEvents } from "../telemetry/events";
5656
import { SendMessageProps } from "./altimateWebviewProvider";
57-
import {
58-
CllEvents,
59-
DbtLineageService,
60-
Table,
61-
} from "../services/dbtLineageService";
62-
import { Model } from "@lib";
57+
import { DbtLineageService, Table } from "../services/dbtLineageService";
6358

6459
export enum Source {
6560
YAML = "YAML",
@@ -181,10 +176,33 @@ export class DocsEditViewPanel implements WebviewViewProvider {
181176
collaborationEnabled: workspace
182177
.getConfiguration("dbt")
183178
.get<boolean>("enableCollaboration", false),
179+
docBlocks: this.getDocBlocksForCurrentProject(),
184180
});
185181
}
186182
}
187183

184+
private getDocBlocksForCurrentProject(): Array<{
185+
name: string;
186+
path: string;
187+
}> {
188+
const project = this.getProject();
189+
if (!project) {
190+
return [];
191+
}
192+
193+
const manifestEvent = this.eventMap.get(project.projectRoot.fsPath);
194+
if (!manifestEvent?.docMetaMap) {
195+
return [];
196+
}
197+
198+
return Array.from(manifestEvent.docMetaMap.entries()).map(
199+
([name, metaData]) => ({
200+
name,
201+
path: metaData.path,
202+
}),
203+
);
204+
}
205+
188206
private async transmitColumns(columns: MetadataColumn[]) {
189207
if (this._panel) {
190208
await this._panel.webview.postMessage({

webview_panels/src/modules/documentationEditor/DocumentationProvider.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,14 @@ import documentationSlice, {
3030
updateSelectedConversationGroup,
3131
updateSingleDocsPropRightPanel,
3232
updateUserInstructions,
33+
setDocBlocks,
3334
} from "./state/documentationSlice";
3435
import {
3536
DBTDocumentation,
3637
DBTModelTest,
3738
DocsGenerateUserInstructions,
3839
MetadataColumn,
40+
DocBlock,
3941
} from "./state/types";
4042
import { ContextProps } from "./types";
4143
import { getGenerationsInModel, isStateDirty } from "./utils";
@@ -57,6 +59,7 @@ type IncomingMessageEvent = MessageEvent<
5759
project?: string;
5860
columns?: DBTDocumentation["columns"];
5961
model?: string;
62+
docBlocks?: DocBlock[];
6063
name?: string;
6164
description?: string;
6265
collaborationEnabled?: boolean;
@@ -132,6 +135,7 @@ const DocumentationProvider = (): JSX.Element => {
132135
dispatch(
133136
setMissingDocumentationMessage(event.data.missingDocumentationMessage),
134137
);
138+
dispatch(setDocBlocks(event.data.docBlocks ?? []));
135139
};
136140

137141
const onMessage = useCallback((event: IncomingMessageEvent) => {
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
.docBlockItem {
2+
padding: 8px 12px;
3+
cursor: pointer;
4+
border-radius: 4px;
5+
margin-bottom: 4px;
6+
border: 1px solid transparent;
7+
transition: background-color 0.15s ease, border-color 0.15s ease;
8+
9+
&:hover {
10+
background-color: var(--background--01);
11+
border-color: var(--background--02);
12+
}
13+
}
14+
15+
.popoverContent {
16+
min-width: 320px;
17+
padding: 8px;
18+
}
19+
20+
.searchContainer {
21+
margin-bottom: 12px;
22+
}
23+
24+
.itemsList {
25+
max-height: 240px;
26+
overflow-y: auto;
27+
}
28+
29+
.itemContent {
30+
gap: 2px;
31+
}
32+
33+
.itemName {
34+
font-size: 14px;
35+
}
36+
37+
.emptyState {
38+
padding: 16px;
39+
}

0 commit comments

Comments
 (0)