Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ obsidian-help/

# Test artifacts
.mdbase-test-*/
test/fixtures/strict-collection/.mdbase/
15 changes: 1 addition & 14 deletions src/commands/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import chalk from "chalk";
import { Collection } from "@callumalpass/mdbase";
import type { MdbaseError } from "@callumalpass/mdbase";
import yaml from "js-yaml";
import { finishCommand, parseFieldValue } from "../utils.js";
import { finishCommand, formatIssue, parseFieldValue } from "../utils.js";

function parseFields(fieldArgs: string[]): Record<string, unknown> | null {
const frontmatter: Record<string, unknown> = {};
Expand Down Expand Up @@ -95,19 +95,6 @@ export function registerCreate(program: Command): void {
});
}

function formatIssue(issue: MdbaseError): string {
const severity = issue.severity ?? "error";
const tag =
severity === "error"
? chalk.red("error")
: severity === "warning"
? chalk.yellow("warn")
: chalk.blue("info");

const field = issue.field ? ` field ${chalk.bold(issue.field)}` : "";
return ` ${tag}${field}: ${issue.message} ${chalk.dim(`[${issue.code}]`)}`;
}

function outputResult(
result: { valid?: boolean; frontmatter?: Record<string, unknown>; body?: string; path?: string; error?: { code: string; message: string }; issues?: MdbaseError[] },
requestedPath: string | undefined,
Expand Down
15 changes: 13 additions & 2 deletions src/commands/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { Command } from "commander";
import path from "node:path";
import chalk from "chalk";
import { Collection } from "@callumalpass/mdbase";
import type { MdbaseError } from "@callumalpass/mdbase";
import yaml from "js-yaml";
import { finishCommand } from "../utils.js";
import { finishCommand, formatIssue } from "../utils.js";

export function registerDelete(program: Command): void {
program
Expand Down Expand Up @@ -38,10 +39,20 @@ export function registerDelete(program: Command): void {
: result.error.code === "permission_denied" ? 5
: 1;

const issues = (result as { issues?: MdbaseError[] }).issues;
if (opts.format === "json") {
console.log(JSON.stringify({ error: result.error }, null, 2));
const output: Record<string, unknown> = { error: result.error };
if (issues) {
output.issues = issues;
}
console.log(JSON.stringify(output, null, 2));
} else {
console.error(chalk.red(`error: ${result.error.message}`));
if (issues) {
for (const issue of issues) {
console.error(formatIssue(issue));
}
}
}
await finishCommand(collection, exitCode);
return;
Expand Down
15 changes: 13 additions & 2 deletions src/commands/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { Command } from "commander";
import path from "node:path";
import chalk from "chalk";
import { Collection } from "@callumalpass/mdbase";
import type { MdbaseError } from "@callumalpass/mdbase";
import yaml from "js-yaml";
import { finishCommand, parseFieldValue } from "../utils.js";
import { finishCommand, formatIssue, parseFieldValue } from "../utils.js";

function parseFields(fieldArgs: string[]): Record<string, unknown> | null {
const fields: Record<string, unknown> = {};
Expand Down Expand Up @@ -97,10 +98,20 @@ export function registerUpdate(program: Command): void {
: result.error.code === "permission_denied" ? 5
: 1;

const issues = (result as { issues?: MdbaseError[] }).issues;
if (opts.format === "json") {
console.log(JSON.stringify({ error: result.error }, null, 2));
const output: Record<string, unknown> = { error: result.error };
if (issues) {
output.issues = issues;
}
console.log(JSON.stringify(output, null, 2));
} else {
console.error(chalk.red(`error: ${result.error.message}`));
if (issues) {
for (const issue of issues) {
console.error(formatIssue(issue));
}
}
}
await finishCommand(collection, exitCode);
return;
Expand Down
16 changes: 16 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,23 @@
import chalk from "chalk";
import type { MdbaseError } from "@callumalpass/mdbase";

type ClosableCollection = {
close(): Promise<void>;
};

export function formatIssue(issue: MdbaseError): string {
const severity = issue.severity ?? "error";
const tag =
severity === "error"
? chalk.red("error")
: severity === "warning"
? chalk.yellow("warn")
: chalk.blue("info");

const field = issue.field ? ` field ${chalk.bold(issue.field)}` : "";
return ` ${tag}${field}: ${issue.message} ${chalk.dim(`[${issue.code}]`)}`;
}

export async function finishCommand(
collection: ClosableCollection | null | undefined,
code: number,
Expand Down
29 changes: 29 additions & 0 deletions test/create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,35 @@ describe("create command", () => {
});
});

describe("validation issues", () => {
it("includes issues in JSON output when validation fails", () => {
const file = trackFile("create-invalid.md");
const { stdout, exitCode } = run(
["create", file, "-t", "note", "-f", "rating=10", "--format", "json"],
VALID,
);
expect(exitCode).toBe(2);
const parsed = JSON.parse(stdout);
expect(parsed.error.code).toBe("validation_failed");
expect(parsed.issues).toBeInstanceOf(Array);
const codes = parsed.issues.map((i: { code: string }) => i.code);
expect(codes).toContain("missing_required");
expect(codes).toContain("number_too_large");
});

it("prints issues in text output when validation fails", () => {
const file = trackFile("create-invalid.md");
const { exitCode, stderr } = run(
["create", file, "-t", "note", "-f", "rating=10"],
VALID,
);
expect(exitCode).toBe(2);
expect(stderr).toContain("Validation failed on create");
expect(stderr).toContain("missing_required");
expect(stderr).toContain("number_too_large");
});
});

describe("error handling", () => {
it("exits 1 for path conflict", () => {
const { exitCode, stderr } = run(
Expand Down
11 changes: 11 additions & 0 deletions test/fixtures/strict-collection/_types/note.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
name: note
fields:
title:
type: string
required: true
rating:
type: integer
min: 1
max: 5
---
7 changes: 7 additions & 0 deletions test/fixtures/strict-collection/hello.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
type: note
title: Hello World
rating: 5
---

This is a valid note.
4 changes: 4 additions & 0 deletions test/fixtures/strict-collection/mdbase.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
spec_version: "0.2.0"
name: strict-collection
settings:
default_validation: error
28 changes: 28 additions & 0 deletions test/update.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import path from "node:path";
const CLI = path.resolve(__dirname, "../src/cli.ts");
const TSX_CLI = path.resolve(__dirname, "../node_modules/tsx/dist/cli.mjs");
const VALID = path.resolve(__dirname, "fixtures/valid-collection");
const STRICT = path.resolve(__dirname, "fixtures/strict-collection");

function run(args: string[], cwd: string): { stdout: string; stderr: string; exitCode: number } {
try {
Expand Down Expand Up @@ -196,6 +197,33 @@ describe("update command", () => {
});
});

describe("validation issues", () => {
it("includes issues in JSON output when validation fails", () => {
const { stdout, exitCode } = run(
["update", "hello.md", "-f", "rating=10", "--format", "json"],
STRICT,
);
expect(exitCode).toBe(2);
const parsed = JSON.parse(stdout);
expect(parsed.error.code).toBe("validation_failed");
expect(parsed.issues).toBeInstanceOf(Array);
expect(parsed.issues.length).toBeGreaterThan(0);
expect(parsed.issues[0].field).toBe("rating");
expect(parsed.issues[0].code).toBe("number_too_large");
});

it("prints issues in text output when validation fails", () => {
const { exitCode, stderr } = run(
["update", "hello.md", "-f", "rating=10"],
STRICT,
);
expect(exitCode).toBe(2);
expect(stderr).toContain("Validation failed on update");
expect(stderr).toContain("rating");
expect(stderr).toContain("number_too_large");
});
});

describe("file persistence", () => {
it("actually writes changes to disk", () => {
backupFile("hello.md");
Expand Down