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.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); + }); +}); 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",