Improve error handling in data extension editor

This improves the error handling in the data extension editor by showing
more errors to the user and adding validation to data extension model
files.
This commit is contained in:
Koen Vlaswinkel
2023-04-13 14:57:19 +02:00
parent 807eb92c92
commit 3a761d080b
6 changed files with 110 additions and 31 deletions

View File

@@ -14,8 +14,8 @@ import {
import { ProgressUpdate } from "../progress"; import { ProgressUpdate } from "../progress";
import { QueryRunner } from "../queryRunner"; import { QueryRunner } from "../queryRunner";
import { import {
showAndLogErrorMessage,
showAndLogExceptionWithTelemetry, showAndLogExceptionWithTelemetry,
showAndLogWarningMessage,
} from "../helpers"; } from "../helpers";
import { extLogger } from "../common"; import { extLogger } from "../common";
import { readFile, writeFile } from "fs-extra"; import { readFile, writeFile } from "fs-extra";
@@ -166,7 +166,9 @@ export class DataExtensionsEditorView extends AbstractWebview<
const existingModeledMethods = loadDataExtensionYaml(data); const existingModeledMethods = loadDataExtensionYaml(data);
if (!existingModeledMethods) { if (!existingModeledMethods) {
void showAndLogWarningMessage("Failed to parse data extension YAML."); void showAndLogErrorMessage(
`Failed to parse data extension YAML ${this.modelFilename}.`,
);
return; return;
} }
@@ -175,7 +177,11 @@ export class DataExtensionsEditorView extends AbstractWebview<
modeledMethods: existingModeledMethods, modeledMethods: existingModeledMethods,
}); });
} catch (e: unknown) { } catch (e: unknown) {
void extLogger.log(`Unable to read data extension YAML: ${e}`); void showAndLogErrorMessage(
`Unable to read data extension YAML ${
this.modelFilename
}: ${getErrorMessage(e)}`,
);
} }
} }
@@ -208,7 +214,6 @@ export class DataExtensionsEditorView extends AbstractWebview<
const bqrsChunk = await readQueryResults({ const bqrsChunk = await readQueryResults({
cliServer: this.cliServer, cliServer: this.cliServer,
bqrsPath: queryResult.outputDir.bqrsPath, bqrsPath: queryResult.outputDir.bqrsPath,
logger: extLogger,
}); });
if (!bqrsChunk) { if (!bqrsChunk) {
await this.clearProgress(); await this.clearProgress();
@@ -233,7 +238,7 @@ export class DataExtensionsEditorView extends AbstractWebview<
void showAndLogExceptionWithTelemetry( void showAndLogExceptionWithTelemetry(
redactableError( redactableError(
asError(err), asError(err),
)`Failed to load external APi usages: ${getErrorMessage(err)}`, )`Failed to load external API usages: ${getErrorMessage(err)}`,
); );
} }
} }

View File

@@ -0,0 +1,45 @@
{
"type": "object",
"properties": {
"extensions": {
"type": "array",
"items": {
"type": "object",
"required": ["addsTo", "data"],
"properties": {
"addsTo": {
"type": "object",
"required": ["pack", "extensible"],
"properties": {
"pack": {
"type": "string"
},
"extensible": {
"type": "string"
}
}
},
"data": {
"type": "array",
"items": {
"type": "array",
"items": {
"oneOf": [
{
"type": "string"
},
{
"type": "boolean"
},
{
"type": "number"
}
]
}
}
}
}
}
}
}
}

View File

@@ -3,12 +3,16 @@ import { qlpackOfDatabase } from "../contextual/queryResolver";
import { file } from "tmp-promise"; import { file } from "tmp-promise";
import { writeFile } from "fs-extra"; import { writeFile } from "fs-extra";
import { dump as dumpYaml } from "js-yaml"; import { dump as dumpYaml } from "js-yaml";
import { getOnDiskWorkspaceFolders } from "../helpers"; import {
getOnDiskWorkspaceFolders,
showAndLogExceptionWithTelemetry,
} from "../helpers";
import { Logger, TeeLogger } from "../common"; import { Logger, TeeLogger } from "../common";
import { CancellationToken } from "vscode"; import { CancellationToken } from "vscode";
import { CodeQLCliServer } from "../cli"; import { CodeQLCliServer } from "../cli";
import { DatabaseItem } from "../local-databases"; import { DatabaseItem } from "../local-databases";
import { ProgressCallback } from "../progress"; import { ProgressCallback } from "../progress";
import { redactableError } from "../pure/errors";
export type RunQueryOptions = { export type RunQueryOptions = {
cliServer: Pick<CodeQLCliServer, "resolveQlpacks" | "resolveQueriesInSuite">; cliServer: Pick<CodeQLCliServer, "resolveQlpacks" | "resolveQueriesInSuite">;
@@ -92,18 +96,16 @@ export async function runQuery({
export type GetResultsOptions = { export type GetResultsOptions = {
cliServer: Pick<CodeQLCliServer, "bqrsInfo" | "bqrsDecode">; cliServer: Pick<CodeQLCliServer, "bqrsInfo" | "bqrsDecode">;
bqrsPath: string; bqrsPath: string;
logger: Logger;
}; };
export async function readQueryResults({ export async function readQueryResults({
cliServer, cliServer,
bqrsPath, bqrsPath,
logger,
}: GetResultsOptions) { }: GetResultsOptions) {
const bqrsInfo = await cliServer.bqrsInfo(bqrsPath); const bqrsInfo = await cliServer.bqrsInfo(bqrsPath);
if (bqrsInfo["result-sets"].length !== 1) { if (bqrsInfo["result-sets"].length !== 1) {
void logger.log( void showAndLogExceptionWithTelemetry(
`Expected exactly one result set, got ${bqrsInfo["result-sets"].length}`, redactableError`Expected exactly one result set, got ${bqrsInfo["result-sets"].length}`,
); );
return undefined; return undefined;
} }

View File

@@ -6,11 +6,16 @@ import { CodeQLCliServer } from "../cli";
import { TeeLogger } from "../common"; import { TeeLogger } from "../common";
import { extensiblePredicateDefinitions } from "./yaml"; import { extensiblePredicateDefinitions } from "./yaml";
import { ProgressCallback } from "../progress"; import { ProgressCallback } from "../progress";
import { getOnDiskWorkspaceFolders } from "../helpers"; import {
getOnDiskWorkspaceFolders,
showAndLogExceptionWithTelemetry,
} from "../helpers";
import { import {
ModeledMethodType, ModeledMethodType,
ModeledMethodWithSignature, ModeledMethodWithSignature,
} from "./modeled-method"; } from "./modeled-method";
import { redactableError } from "../pure/errors";
import { QueryResultType } from "../pure/new-messages";
type FlowModelOptions = { type FlowModelOptions = {
cliServer: CodeQLCliServer; cliServer: CodeQLCliServer;
@@ -67,13 +72,21 @@ async function getModeledMethodsFromFlow(
token, token,
new TeeLogger(queryRunner.logger, queryRun.outputDir.logPath), new TeeLogger(queryRunner.logger, queryRun.outputDir.logPath),
); );
if (queryResult.resultType !== QueryResultType.SUCCESS) {
void showAndLogExceptionWithTelemetry(
redactableError`Failed to run ${queryName} query: ${
queryResult.message ?? "No message"
}`,
);
return [];
}
const bqrsPath = queryResult.outputDir.bqrsPath; const bqrsPath = queryResult.outputDir.bqrsPath;
const bqrsInfo = await cliServer.bqrsInfo(bqrsPath); const bqrsInfo = await cliServer.bqrsInfo(bqrsPath);
if (bqrsInfo["result-sets"].length !== 1) { if (bqrsInfo["result-sets"].length !== 1) {
throw new Error( void showAndLogExceptionWithTelemetry(
`Expected exactly one result set, got ${bqrsInfo["result-sets"].length}`, redactableError`Expected exactly one result set, got ${bqrsInfo["result-sets"].length} for ${queryName}`,
); );
} }

View File

@@ -1,3 +1,5 @@
import Ajv from "ajv";
import { ExternalApiUsage } from "./external-api-usage"; import { ExternalApiUsage } from "./external-api-usage";
import { import {
ModeledMethod, ModeledMethod,
@@ -5,6 +7,11 @@ import {
ModeledMethodWithSignature, ModeledMethodWithSignature,
} from "./modeled-method"; } from "./modeled-method";
import * as dataSchemaJson from "./data-schema.json";
const ajv = new Ajv({ allErrors: true });
const dataSchemaValidate = ajv.compile(dataSchemaJson);
type ExternalApiUsageByType = { type ExternalApiUsageByType = {
externalApiUsage: ExternalApiUsage; externalApiUsage: ExternalApiUsage;
modeledMethod: ModeledMethod; modeledMethod: ModeledMethod;
@@ -191,8 +198,14 @@ ${extensions.join("\n")}`;
export function loadDataExtensionYaml( export function loadDataExtensionYaml(
data: any, data: any,
): Record<string, ModeledMethod> | undefined { ): Record<string, ModeledMethod> | undefined {
if (typeof data !== "object") { dataSchemaValidate(data);
return undefined;
if (dataSchemaValidate.errors) {
throw new Error(
`Invalid data extension YAML: ${dataSchemaValidate.errors
.map((error) => `${error.instancePath} ${error.message}`)
.join(", ")}`,
);
} }
const extensions = data.extensions; const extensions = data.extensions;
@@ -204,19 +217,8 @@ export function loadDataExtensionYaml(
for (const extension of extensions) { for (const extension of extensions) {
const addsTo = extension.addsTo; const addsTo = extension.addsTo;
if (typeof addsTo !== "object") {
continue;
}
const extensible = addsTo.extensible; const extensible = addsTo.extensible;
if (typeof extensible !== "string") {
continue;
}
const data = extension.data; const data = extension.data;
if (!Array.isArray(data)) {
continue;
}
const definition = Object.values(extensiblePredicateDefinitions).find( const definition = Object.values(extensiblePredicateDefinitions).find(
(definition) => definition.extensiblePredicate === extensible, (definition) => definition.extensiblePredicate === extensible,

View File

@@ -10,6 +10,8 @@ import { file } from "tmp-promise";
import { QueryResultType } from "../../../../src/pure/new-messages"; import { QueryResultType } from "../../../../src/pure/new-messages";
import { readFile } from "fs-extra"; import { readFile } from "fs-extra";
import { load } from "js-yaml"; import { load } from "js-yaml";
import * as helpers from "../../../../src/helpers";
import { RedactableError } from "../../../../src/pure/errors";
function createMockUri(path = "/a/b/c/foo"): Uri { function createMockUri(path = "/a/b/c/foo"): Uri {
return { return {
@@ -127,17 +129,27 @@ describe("readQueryResults", () => {
bqrsDecode: jest.fn(), bqrsDecode: jest.fn(),
}, },
bqrsPath: "/tmp/results.bqrs", bqrsPath: "/tmp/results.bqrs",
logger: createMockLogger(),
}; };
let showAndLogExceptionWithTelemetrySpy: jest.SpiedFunction<
typeof helpers.showAndLogExceptionWithTelemetry
>;
beforeEach(() => {
showAndLogExceptionWithTelemetrySpy = jest.spyOn(
helpers,
"showAndLogExceptionWithTelemetry",
);
});
it("returns undefined when there are no results", async () => { it("returns undefined when there are no results", async () => {
options.cliServer.bqrsInfo.mockResolvedValue({ options.cliServer.bqrsInfo.mockResolvedValue({
"result-sets": [], "result-sets": [],
}); });
expect(await readQueryResults(options)).toBeUndefined(); expect(await readQueryResults(options)).toBeUndefined();
expect(options.logger.log).toHaveBeenCalledWith( expect(showAndLogExceptionWithTelemetrySpy).toHaveBeenCalledWith(
expect.stringMatching(/Expected exactly one result set/), expect.any(RedactableError),
); );
}); });
@@ -166,8 +178,8 @@ describe("readQueryResults", () => {
}); });
expect(await readQueryResults(options)).toBeUndefined(); expect(await readQueryResults(options)).toBeUndefined();
expect(options.logger.log).toHaveBeenCalledWith( expect(showAndLogExceptionWithTelemetrySpy).toHaveBeenCalledWith(
expect.stringMatching(/Expected exactly one result set/), expect.any(RedactableError),
); );
}); });