From 808e17f326cbf767c6a7c70cc4853c5380172220 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 15 Nov 2025 16:49:53 +0000 Subject: [PATCH 1/2] fix: make frontmatter.tags optional in template execution (#41) The execute_template function was failing when templates lacked a tags field in their YAML frontmatter, returning "frontmatter.tags must be an array (was null)". This change makes the tags field optional in ApiVaultFileResponse, aligning with standard Obsidian template practices where tags are not required. Fixes #41 --- bun.lock | 7 ++++--- packages/shared/src/types/plugin-local-rest-api.ts | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/bun.lock b/bun.lock index 462734b..7e31834 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "dependencies": { @@ -269,7 +270,7 @@ "@types/body-parser": ["@types/body-parser@1.19.5", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg=="], - "@types/bun": ["@types/bun@1.2.18", "", { "dependencies": { "bun-types": "1.2.18" } }, "sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ=="], + "@types/bun": ["@types/bun@1.3.2", "", { "dependencies": { "bun-types": "1.3.2" } }, "sha512-t15P7k5UIgHKkxwnMNkJbWlh/617rkDGEdSsDbu+qNHTaz9SKf7aC8fiIlUdD5RPpH6GEkP0cK7WlvmrEBRtWg=="], "@types/codemirror": ["@types/codemirror@5.60.8", "", { "dependencies": { "@types/tern": "*" } }, "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw=="], @@ -403,7 +404,7 @@ "buffer-crc32": ["buffer-crc32@1.0.0", "", {}, "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w=="], - "bun-types": ["bun-types@1.2.18", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="], + "bun-types": ["bun-types@1.3.2", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-i/Gln4tbzKNuxP70OWhJRZz1MRfvqExowP7U6JKoI8cntFrtxg7RJK3jvz7wQW54UuvNC8tbKHHri5fy74FVqg=="], "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], @@ -861,7 +862,7 @@ "object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="], - "obsidian": ["obsidian@1.8.7", "", { "dependencies": { "@types/codemirror": "5.60.8", "moment": "2.29.4" }, "peerDependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0" } }, "sha512-h4bWwNFAGRXlMlMAzdEiIM2ppTGlrh7uGOJS6w4gClrsjc+ei/3YAtU2VdFUlCiPuTHpY4aBpFJJW75S1Tl/JA=="], + "obsidian": ["obsidian@1.10.3", "", { "dependencies": { "@types/codemirror": "5.60.8", "moment": "2.29.4" }, "peerDependencies": { "@codemirror/state": "6.5.0", "@codemirror/view": "6.38.6" } }, "sha512-VP+ZSxNMG7y6Z+sU9WqLvJAskCfkFrTz2kFHWmmzis+C+4+ELjk/sazwcTHrHXNZlgCeo8YOlM6SOrAFCynNew=="], "obsidian-calendar-ui": ["obsidian-calendar-ui@0.3.12", "", { "dependencies": { "obsidian-daily-notes-interface": "0.8.4", "svelte": "3.35.0", "tslib": "2.1.0" } }, "sha512-hdoRqCPnukfRgCARgArXaqMQZ+Iai0eY7f0ZsFHHfywpv4gKg3Tx5p47UsLvRO5DD+4knlbrL7Gel57MkfcLTw=="], diff --git a/packages/shared/src/types/plugin-local-rest-api.ts b/packages/shared/src/types/plugin-local-rest-api.ts index 3f7a216..c55b2c0 100644 --- a/packages/shared/src/types/plugin-local-rest-api.ts +++ b/packages/shared/src/types/plugin-local-rest-api.ts @@ -181,7 +181,7 @@ export const ApiVaultDirectoryResponse = type({ */ export const ApiVaultFileResponse = type({ frontmatter: { - tags: "string[]", + tags: "string[]?", description: "string?", }, content: "string", From 60b349576d298ba9da29f37a259d15474d857d0e Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 15 Nov 2025 17:26:44 +0000 Subject: [PATCH 2/2] test: add tests for optional frontmatter.tags in ApiVaultFileResponse (#41) - Test vault file response with tags array - Test vault file response without tags (optional) - Test vault file response without description (optional) - Test vault file response with empty frontmatter - Test vault file response with empty tags array - Test rejection of tags as non-array type - Test rejection of tags with non-string elements - Test required fields (content, path, stat) - Test typical Obsidian frontmatter structures - Test Templater templates without tags --- .../src/types/plugin-local-rest-api.test.ts | 256 ++++++++++++++++++ 1 file changed, 256 insertions(+) create mode 100644 packages/shared/src/types/plugin-local-rest-api.test.ts diff --git a/packages/shared/src/types/plugin-local-rest-api.test.ts b/packages/shared/src/types/plugin-local-rest-api.test.ts new file mode 100644 index 0000000..9136ad2 --- /dev/null +++ b/packages/shared/src/types/plugin-local-rest-api.test.ts @@ -0,0 +1,256 @@ +import { describe, expect, test } from "bun:test"; +import { type } from "arktype"; + +/** + * Tests for Local REST API type schemas + * Issue #41: Make frontmatter.tags optional in ApiVaultFileResponse + */ +describe("ApiVaultFileResponse schema", () => { + // Replicate the schema from plugin-local-rest-api.ts + const ApiVaultFileResponse = type({ + frontmatter: { + tags: "string[]?", + description: "string?", + }, + content: "string", + path: "string", + stat: { + ctime: "number", + mtime: "number", + size: "number", + }, + }); + + test("accepts vault file response with tags", () => { + const response = { + frontmatter: { + tags: ["tag1", "tag2"], + description: "A test file", + }, + content: "# Test Content", + path: "/vault/test.md", + stat: { + ctime: 1234567890, + mtime: 1234567890, + size: 100, + }, + }; + + const validated = ApiVaultFileResponse(response); + expect(validated instanceof type.errors).toBe(false); + + if (!(validated instanceof type.errors)) { + expect(validated.frontmatter.tags).toEqual(["tag1", "tag2"]); + expect(validated.frontmatter.description).toBe("A test file"); + } + }); + + test("accepts vault file response without tags", () => { + const response = { + frontmatter: { + description: "A test file", + }, + content: "# Test Content", + path: "/vault/test.md", + stat: { + ctime: 1234567890, + mtime: 1234567890, + size: 100, + }, + }; + + const validated = ApiVaultFileResponse(response); + expect(validated instanceof type.errors).toBe(false); + + if (!(validated instanceof type.errors)) { + expect(validated.frontmatter.tags).toBeUndefined(); + expect(validated.frontmatter.description).toBe("A test file"); + } + }); + + test("accepts vault file response without description", () => { + const response = { + frontmatter: { + tags: ["tag1"], + }, + content: "# Test Content", + path: "/vault/test.md", + stat: { + ctime: 1234567890, + mtime: 1234567890, + size: 100, + }, + }; + + const validated = ApiVaultFileResponse(response); + expect(validated instanceof type.errors).toBe(false); + + if (!(validated instanceof type.errors)) { + expect(validated.frontmatter.tags).toEqual(["tag1"]); + expect(validated.frontmatter.description).toBeUndefined(); + } + }); + + test("accepts vault file response with empty frontmatter", () => { + const response = { + frontmatter: {}, + content: "# Test Content", + path: "/vault/test.md", + stat: { + ctime: 1234567890, + mtime: 1234567890, + size: 100, + }, + }; + + const validated = ApiVaultFileResponse(response); + expect(validated instanceof type.errors).toBe(false); + + if (!(validated instanceof type.errors)) { + expect(validated.frontmatter.tags).toBeUndefined(); + expect(validated.frontmatter.description).toBeUndefined(); + } + }); + + test("accepts vault file response with empty tags array", () => { + const response = { + frontmatter: { + tags: [], + }, + content: "# Test Content", + path: "/vault/test.md", + stat: { + ctime: 1234567890, + mtime: 1234567890, + size: 100, + }, + }; + + const validated = ApiVaultFileResponse(response); + expect(validated instanceof type.errors).toBe(false); + + if (!(validated instanceof type.errors)) { + expect(validated.frontmatter.tags).toEqual([]); + } + }); + + test("rejects vault file response with tags as non-array", () => { + const response = { + frontmatter: { + tags: "not-an-array", + }, + content: "# Test Content", + path: "/vault/test.md", + stat: { + ctime: 1234567890, + mtime: 1234567890, + size: 100, + }, + }; + + const validated = ApiVaultFileResponse(response); + expect(validated instanceof type.errors).toBe(true); + }); + + test("rejects vault file response with tags containing non-strings", () => { + const response = { + frontmatter: { + tags: [123, 456], + }, + content: "# Test Content", + path: "/vault/test.md", + stat: { + ctime: 1234567890, + mtime: 1234567890, + size: 100, + }, + }; + + const validated = ApiVaultFileResponse(response); + expect(validated instanceof type.errors).toBe(true); + }); + + test("requires content field", () => { + const response = { + frontmatter: {}, + path: "/vault/test.md", + stat: { + ctime: 1234567890, + mtime: 1234567890, + size: 100, + }, + }; + + const validated = ApiVaultFileResponse(response); + expect(validated instanceof type.errors).toBe(true); + }); + + test("requires path field", () => { + const response = { + frontmatter: {}, + content: "# Test Content", + stat: { + ctime: 1234567890, + mtime: 1234567890, + size: 100, + }, + }; + + const validated = ApiVaultFileResponse(response); + expect(validated instanceof type.errors).toBe(true); + }); + + test("requires stat field with correct structure", () => { + const response = { + frontmatter: {}, + content: "# Test Content", + path: "/vault/test.md", + stat: { + ctime: 1234567890, + mtime: 1234567890, + // missing size + }, + }; + + const validated = ApiVaultFileResponse(response); + expect(validated instanceof type.errors).toBe(true); + }); + + test("accepts vault file with typical Obsidian frontmatter", () => { + const response = { + frontmatter: { + tags: ["obsidian", "notes", "productivity"], + description: "My daily note from today", + }, + content: "# Daily Note\n\n## Tasks\n- [ ] Task 1", + path: "/vault/Daily/2024-01-15.md", + stat: { + ctime: 1705334400000, + mtime: 1705420800000, + size: 1024, + }, + }; + + const validated = ApiVaultFileResponse(response); + expect(validated instanceof type.errors).toBe(false); + }); + + test("accepts vault file from Templater plugin without tags", () => { + // Templater templates may not have tags in frontmatter + const response = { + frontmatter: { + description: "Template for new notes", + }, + content: "<% tp.date.now() %>", + path: "/vault/Templates/note-template.md", + stat: { + ctime: 1705334400000, + mtime: 1705420800000, + size: 512, + }, + }; + + const validated = ApiVaultFileResponse(response); + expect(validated instanceof type.errors).toBe(false); + }); +});