diff --git a/e2e/cloud-connection.spec.ts b/e2e/cloud-connection.spec.ts index 65e92640..6d9ac394 100644 --- a/e2e/cloud-connection.spec.ts +++ b/e2e/cloud-connection.spec.ts @@ -203,6 +203,8 @@ test("reviews a scratchpad selectively and collapses outline branches", async ({ result = defaultViewDocuments(); } else if (operation === "execute_view") { result = defaultViewExecution([]); + } else if (operation === "read" && isViewSourceRead(request.input)) { + result = viewSourceRecord(request.input); } else if (operation === "read") { result = scratchpad; } else result = {}; @@ -347,6 +349,8 @@ test("acknowledges slow relay creates and prefetches revisions before delete", a }; records.push(created); result = valid(created); + } else if (operation === "read" && isViewSourceRead(request.input)) { + result = valid(viewSourceRecord(request.input)); } else if (operation === "read") { readRequests += 1; await readGate.promise; @@ -505,7 +509,9 @@ test("edits a contract-defined task without collapsing custom status or fields", result = defaultViewDocuments(); } else if (operation === "execute_view") { result = defaultViewExecution([record]); - } else if (operation === "read") result = record; + } else if (operation === "read" && isViewSourceRead(request.input)) + result = viewSourceRecord(request.input); + else if (operation === "read") result = record; else if (operation === "update") { updateInput = request.input as { patch?: JsonObject; @@ -643,6 +649,25 @@ function collectionDescription() { }, ], }, + // A set-up collection also carries the saved-view contracts TaskNotes + // requires; their packs are installed during collection setup. + ...bundledManifest.requirements.contracts + .filter((contract) => contract.id !== "tasknotes.task") + .map((contract) => ({ + contract_type: "record" as const, + id: contract.id, + version: contract.version, + digest: contract.digest, + schema: { type: "object" }, + binding_schema: { type: "object" }, + implementations: [] as Array<{ + type_name: string; + type_version: number; + digest: string; + fields: Record; + binding: JsonObject; + }>, + })), ], }; } @@ -865,6 +890,28 @@ async function fulfillOperation( }); } +// Saved views are `.base` records read with the ordinary record operation. +function isViewSourceRead(input: unknown): input is { path: string } { + const path = (input as { path?: unknown }).path; + return typeof path === "string" && path.endsWith(".base"); +} + +function viewSourceRecord({ path }: { path: string }) { + const id = path + .split("/") + .at(-1)! + .replace(/\.base$/u, ""); + const name = `${id[0].toUpperCase()}${id.slice(1)}`; + return { + path, + types: ["obsidian_base"], + revision: "view-r1", + frontmatter: { views: [{ type: "tasknotesTaskList", name }] }, + body: "", + document: `views:\n - type: tasknotesTaskList\n name: ${name}\n`, + }; +} + function defaultViewDocuments() { return { views: ["today", "upcoming", "calendar", "projects", "archive"].map( diff --git a/package.json b/package.json index 2642312b..8435a5c1 100644 --- a/package.json +++ b/package.json @@ -61,8 +61,8 @@ "@fullcalendar/react": "6.1.17", "@fullcalendar/timegrid": "6.1.17", "@lezer/highlight": "^1.2.3", - "@mdbase-dev/connect": "0.1.0-beta.114", - "@mdbase-dev/connect-protocol": "0.1.0-beta.114", + "@mdbase-dev/connect": "0.1.0-beta.117", + "@mdbase-dev/connect-protocol": "0.1.0-beta.117", "@tanstack/react-virtual": "3.14.11", "@tasknotes/model": "file:vendor/tasknotes-model-0.3.0-rc.15.tgz", "codemirror": "^6.0.2", @@ -86,8 +86,8 @@ "@capacitor/cli": "8.4.2", "@capacitor/ios": "8.4.2", "@eslint/js": "10.0.1", - "@mdbase-dev/connect-dev": "0.1.0-beta.114", - "@mdbase-dev/connect-testing": "0.1.0-beta.114", + "@mdbase-dev/connect-dev": "0.1.0-beta.117", + "@mdbase-dev/connect-testing": "0.1.0-beta.117", "@playwright/test": "1.61.1", "@testing-library/jest-dom": "7.0.0", "@testing-library/react": "16.3.2", @@ -112,9 +112,9 @@ }, "pnpm": { "overrides": { - "@mdbase-dev/connect-protocol": "0.1.0-beta.114", - "@mdbase-dev/connect": "0.1.0-beta.114", - "@mdbase-dev/connect-sync": "0.1.0-beta.114" + "@mdbase-dev/connect-protocol": "0.1.0-beta.117", + "@mdbase-dev/connect": "0.1.0-beta.117", + "@mdbase-dev/connect-sync": "0.1.0-beta.117" }, "patchedDependencies": {} } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 62886b62..68f8a0b0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,9 +5,9 @@ settings: excludeLinksFromLockfile: false overrides: - '@mdbase-dev/connect-protocol': 0.1.0-beta.114 - '@mdbase-dev/connect': 0.1.0-beta.114 - '@mdbase-dev/connect-sync': 0.1.0-beta.114 + '@mdbase-dev/connect-protocol': 0.1.0-beta.117 + '@mdbase-dev/connect': 0.1.0-beta.117 + '@mdbase-dev/connect-sync': 0.1.0-beta.117 importers: @@ -77,11 +77,11 @@ importers: specifier: ^1.2.3 version: 1.2.3 '@mdbase-dev/connect': - specifier: 0.1.0-beta.114 - version: 0.1.0-beta.114 + specifier: 0.1.0-beta.117 + version: 0.1.0-beta.117 '@mdbase-dev/connect-protocol': - specifier: 0.1.0-beta.114 - version: 0.1.0-beta.114 + specifier: 0.1.0-beta.117 + version: 0.1.0-beta.117 '@tanstack/react-virtual': specifier: 3.14.11 version: 3.14.11(react-dom@19.2.8(react@19.2.8))(react@19.2.8) @@ -147,11 +147,11 @@ importers: specifier: 10.0.1 version: 10.0.1(eslint@10.7.0) '@mdbase-dev/connect-dev': - specifier: 0.1.0-beta.114 - version: 0.1.0-beta.114 + specifier: 0.1.0-beta.117 + version: 0.1.0-beta.117 '@mdbase-dev/connect-testing': - specifier: 0.1.0-beta.114 - version: 0.1.0-beta.114 + specifier: 0.1.0-beta.117 + version: 0.1.0-beta.117 '@playwright/test': specifier: 1.61.1 version: 1.61.1 @@ -902,18 +902,18 @@ packages: '@marijn/find-cluster-break@1.0.4': resolution: {integrity: sha512-Wy0V7+SGUjnF9/TkiM1hKVDPj7jKXduPNboMVtHTA8dySMURWqfg/JZ9E2Sq8JgSJmkl7k7Qe9FLeMSrSraWmQ==} - '@mdbase-dev/connect-dev@0.1.0-beta.114': - resolution: {integrity: sha512-HlRXwwgPiuKAflaHAMchLOWPBQ+XDTZ9FO///3r9QioSrAQ3CtULHwisAKEGaSJo6S0HxQ3+KflVH8j9Wa3DvA==} + '@mdbase-dev/connect-dev@0.1.0-beta.117': + resolution: {integrity: sha512-liKRrsMkl7fNkJY54wjWKcLDkd0IOWXfYdJAKVmdWbtsfkazg4lXkq6CTi+k4XGTfEuyXT/+6U/qJE4h0FCtqg==} hasBin: true - '@mdbase-dev/connect-protocol@0.1.0-beta.114': - resolution: {integrity: sha512-t9J7wVdoYjEMpQ7osgyEE/b+KRxo3vY1nAwX7x6cYRd41pD7/t69XufddvY18OnmYf1GopXzv3Mgdql4SjO+JQ==} + '@mdbase-dev/connect-protocol@0.1.0-beta.117': + resolution: {integrity: sha512-4LxZvsMTgmrTP8TtVVqydr2oZQpCOi2kwrMPtC/SF4uHeksbbDmxbjBwGG5Npj5DDxBi2Y+cClHf+HdpPB6zVQ==} - '@mdbase-dev/connect-testing@0.1.0-beta.114': - resolution: {integrity: sha512-dWdssxcptoR/mkx4Rv1ZCSZfXKBAfCmfsJJ3zpMGmUIIvpkmJXOJQk/eRIsSsfDl1IKUOOuOx2qXBundXGcb+g==} + '@mdbase-dev/connect-testing@0.1.0-beta.117': + resolution: {integrity: sha512-yqPbYbr3a+25MCOvnXnI/sqDs1f7ZRReqMMa3NtrMsLw8y64+oC8u9NCd964FICQ0hpoz6KQJJLyqfipJKCwRw==} - '@mdbase-dev/connect@0.1.0-beta.114': - resolution: {integrity: sha512-NnES3Y3NC5nJTegp6YmaB+FGHIDB3596nc2SlZqTSzg33q6GJ98USn/ua0JB8qrIbeCJVvqHJrsGxRboBw75BA==} + '@mdbase-dev/connect@0.1.0-beta.117': + resolution: {integrity: sha512-6GyK+6ZWGJQCRsQKg48gU7XS13AHjavJZ7jBj6Q2f9McO/Omo73zWmQlcXQzBNnzz7zhUPhIU3PfTE2z6g21Xg==} '@napi-rs/wasm-runtime@1.1.6': resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} @@ -4795,26 +4795,26 @@ snapshots: '@marijn/find-cluster-break@1.0.4': {} - '@mdbase-dev/connect-dev@0.1.0-beta.114': + '@mdbase-dev/connect-dev@0.1.0-beta.117': dependencies: - '@mdbase-dev/connect': 0.1.0-beta.114 - '@mdbase-dev/connect-protocol': 0.1.0-beta.114 + '@mdbase-dev/connect': 0.1.0-beta.117 + '@mdbase-dev/connect-protocol': 0.1.0-beta.117 ajv: 8.20.0 yaml: 2.9.1 - '@mdbase-dev/connect-protocol@0.1.0-beta.114': + '@mdbase-dev/connect-protocol@0.1.0-beta.117': dependencies: ajv: 8.20.0 ajv-formats: 3.0.1(ajv@8.20.0) - '@mdbase-dev/connect-testing@0.1.0-beta.114': + '@mdbase-dev/connect-testing@0.1.0-beta.117': dependencies: - '@mdbase-dev/connect': 0.1.0-beta.114 - '@mdbase-dev/connect-protocol': 0.1.0-beta.114 + '@mdbase-dev/connect': 0.1.0-beta.117 + '@mdbase-dev/connect-protocol': 0.1.0-beta.117 - '@mdbase-dev/connect@0.1.0-beta.114': + '@mdbase-dev/connect@0.1.0-beta.117': dependencies: - '@mdbase-dev/connect-protocol': 0.1.0-beta.114 + '@mdbase-dev/connect-protocol': 0.1.0-beta.117 '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': dependencies: diff --git a/scripts/tasknotes-manifest.mjs b/scripts/tasknotes-manifest.mjs index 0d45ae3a..cf4495da 100644 --- a/scripts/tasknotes-manifest.mjs +++ b/scripts/tasknotes-manifest.mjs @@ -2,6 +2,8 @@ import { MDBASE_TIMER_FIRED_CONTRACT } from "@mdbase-dev/connect-protocol"; import { loadCanonicalTaskNotesTypePack } from "./canonical-task-pack.mjs"; import { buildScratchpadTypePack } from "./scratchpad-type.mjs"; import { buildScratchImageTypePack } from "./scratch-image-type.mjs"; +import { readFile } from "node:fs/promises"; +import { resolve } from "node:path"; export { TASKNOTES_APP_TYPE_PACK_VERSION } from "./canonical-task-pack.mjs"; @@ -15,6 +17,18 @@ export async function buildTaskNotesManifest({ // authorization should use a public tunnel and TASKNOTES_APP_URL. const identityUrl = appUrl; const typePack = await loadCanonicalTaskNotesTypePack(); + // Saved views are records: Bases through obsidian.base (YAML document + // records) and mdbase views through mdbase.view. The published packs are + // embedded byte for byte. + const viewPack = async (file) => + JSON.parse( + await readFile( + resolve(process.cwd(), "vendor/mdbase-contracts", file), + "utf8", + ), + ); + const basePack = await viewPack("obsidian.base-1.0.0.json"); + const mdbaseViewPack = await viewPack("mdbase.view-1.0.0.json"); const taskContract = typePack.provides.find( (contract) => contract.id === "tasknotes.task", ); @@ -32,7 +46,11 @@ export async function buildTaskNotesManifest({ ], requirements: { people: { version: 1, optional: ["identity", "members"] }, - contracts: [taskContract], + contracts: [ + taskContract, + ...basePack.provides, + ...mdbaseViewPack.provides, + ], capabilities: { contract_version: 2, required: [ @@ -40,7 +58,6 @@ export async function buildTaskNotesManifest({ "records.create", "records.edit", "records.delete", - "views.manage", "definitions.manage", "background.schedule", ], @@ -57,6 +74,20 @@ export async function buildTaskNotesManifest({ predicate: "contains", value: "TaskNotes/Views/**/*.base", }, + // record_extensions is the complete set, so keep Markdown explicitly + // when adding Bases. + { + id: "tasknotes-markdown-records", + path: "/settings/record_extensions", + predicate: "contains", + value: "md", + }, + { + id: "tasknotes-base-records", + path: "/settings/record_extensions", + predicate: "contains", + value: "base", + }, ], }, provisions: { @@ -64,6 +95,8 @@ export async function buildTaskNotesManifest({ typePack, buildScratchpadTypePack(), buildScratchImageTypePack(), + basePack, + mdbaseViewPack, ], configuration: [ { @@ -72,6 +105,18 @@ export async function buildTaskNotesManifest({ path: "/x-obsidian/bases/include", value: "TaskNotes/Views/**/*.base", }, + { + requirement: "tasknotes-markdown-records", + operation: "set_add", + path: "/settings/record_extensions", + value: "md", + }, + { + requirement: "tasknotes-base-records", + operation: "set_add", + path: "/settings/record_extensions", + value: "base", + }, ], }, notifications: { diff --git a/scripts/tasknotes-manifest.test.mjs b/scripts/tasknotes-manifest.test.mjs index 75438298..7e370838 100644 --- a/scripts/tasknotes-manifest.test.mjs +++ b/scripts/tasknotes-manifest.test.mjs @@ -40,6 +40,8 @@ describe("TaskNotes mdbase manifest", () => { version: "0.3.0-rc.5", digest: expect.stringMatching(/^sha256:[0-9a-f]{64}$/), }), + expect.objectContaining({ id: "obsidian.base", version: "1.0.0" }), + expect.objectContaining({ id: "mdbase.view", version: "1.0.0" }), ]); expect(manifest.requirements.files).toEqual({ required: ["list", "read", "add", "replace", "move", "delete"], @@ -54,7 +56,6 @@ describe("TaskNotes mdbase manifest", () => { "records.create", "records.edit", "records.delete", - "views.manage", "definitions.manage", "background.schedule", ], @@ -66,6 +67,18 @@ describe("TaskNotes mdbase manifest", () => { predicate: "contains", value: "TaskNotes/Views/**/*.base", }, + { + id: "tasknotes-markdown-records", + path: "/settings/record_extensions", + predicate: "contains", + value: "md", + }, + { + id: "tasknotes-base-records", + path: "/settings/record_extensions", + predicate: "contains", + value: "base", + }, ]); expect(manifest.provisions.configuration).toEqual([ { @@ -74,6 +87,18 @@ describe("TaskNotes mdbase manifest", () => { path: "/x-obsidian/bases/include", value: "TaskNotes/Views/**/*.base", }, + { + requirement: "tasknotes-markdown-records", + operation: "set_add", + path: "/settings/record_extensions", + value: "md", + }, + { + requirement: "tasknotes-base-records", + operation: "set_add", + path: "/settings/record_extensions", + value: "base", + }, ]); expect(manifest.provisions.type_packs[0].manifest.resources).toHaveLength( 4, diff --git a/src/cloud/connect.test.ts b/src/cloud/connect.test.ts index c8642a83..68418a83 100644 --- a/src/cloud/connect.test.ts +++ b/src/cloud/connect.test.ts @@ -87,7 +87,6 @@ describe("TaskNotes mdbase session", () => { "update", "rename", "delete", - "delete_view_source", "apply_type_pack", "reconcile_timers", "assess_collection_setup", @@ -95,6 +94,8 @@ describe("TaskNotes mdbase session", () => { ]), ); expect(expectedOperations).not.toContain("sync"); + // Saved views are records written with the record operations. + expect(expectedOperations).not.toContain("delete_view_source"); }); it("evaluates required capabilities through the real SDK without throwing", () => { diff --git a/src/domain/default-view-source.test.ts b/src/domain/default-view-source.test.ts index 4c25847d..28c23c9a 100644 --- a/src/domain/default-view-source.test.ts +++ b/src/domain/default-view-source.test.ts @@ -8,7 +8,10 @@ import { taskNotesDefaultBaseDocument, taskNotesDefaultBaseSources, taskNotesDefaultCanonicalDocument, + newViewSourcePath, taskNotesViewSourcePath, + viewSourceFormat, + viewSourceRecord, } from "./default-view-source"; import { ensureTaskNotesDefaultViewSource } from "../application/ensure-default-view-source"; @@ -395,3 +398,27 @@ function legacyStarterDocument(): TaskViewDocument { ), }; } + +describe("saved-view records", () => { + it("stores Bases as YAML document records and mdbase views as Markdown", () => { + const base = newViewSourcePath("obsidian.base", "Focused work"); + expect(base).toBe("TaskNotes/Views/focused-work.base"); + expect(viewSourceFormat(base)).toBe("obsidian.base"); + expect( + viewSourceRecord(base, "# kept by Obsidian\nviews:\n - type: table\n"), + ).toEqual({ frontmatter: { views: [{ type: "table" }] } }); + + const view = newViewSourcePath("mdbase.view", "Focused work"); + expect(view).toBe("views/focused-work.md"); + expect(viewSourceFormat(view)).toBe("mdbase.view"); + expect( + viewSourceRecord(view, "---\ntype: view\n---\nNotes\n"), + ).toMatchObject({ frontmatter: { type: "view" }, body: "Notes\n" }); + }); + + it("rejects a Base that is not a YAML mapping", () => { + expect(() => + viewSourceRecord("TaskNotes/Views/list.base", "- not a mapping\n"), + ).toThrow("A Base must be a YAML mapping."); + }); +}); diff --git a/src/domain/default-view-source.ts b/src/domain/default-view-source.ts index 898fe2bc..f6380c31 100644 --- a/src/domain/default-view-source.ts +++ b/src/domain/default-view-source.ts @@ -1,4 +1,7 @@ -import { serializeMarkdownDocument } from "@tasknotes/model/frontmatter"; +import { + parseFrontmatter, + serializeMarkdownDocument, +} from "@tasknotes/model/frontmatter"; import { parse, stringify } from "yaml"; import type { TaskCollectionConfiguration } from "./task-configuration"; @@ -387,3 +390,35 @@ export function basesProperty(field: string): string { function literal(value: string): string { return JSON.stringify(value); } + +/** Saved-view sources are records: `.base` Bases and `.md` mdbase views. */ +export function viewSourceFormat( + path: string, +): "obsidian.base" | "mdbase.view" { + return path.endsWith(".base") ? "obsidian.base" : "mdbase.view"; +} + +/** Where a new saved view is stored when the caller names no path. */ +export function newViewSourcePath(format: string, name: string): string { + if (format === "obsidian.base") return taskNotesViewSourcePath(name); + return taskNotesViewSourcePath(name) + .replace(/^TaskNotes\/Views\//u, "views/") + .replace(/\.base$/u, ".md"); +} + +/** The record a view source document stores, in the format its path fixes. */ +export function viewSourceRecord( + path: string, + document: string, +): { + frontmatter: Record; + body?: string; +} { + if (viewSourceFormat(path) === "obsidian.base") { + const value: unknown = parse(document); + if (!value || typeof value !== "object" || Array.isArray(value)) + throw new Error("A Base must be a YAML mapping."); + return { frontmatter: value as Record }; + } + return parseFrontmatter(document); +} diff --git a/src/generated/mdbase-app.json b/src/generated/mdbase-app.json index 05c54ae6..7694c41a 100644 --- a/src/generated/mdbase-app.json +++ b/src/generated/mdbase-app.json @@ -15,6 +15,16 @@ "id": "tasknotes.task", "version": "0.3.0-rc.5", "digest": "sha256:dde71d0d776aa0069ee627fe3c10b1a20f87ba36929a16790598ebfb4e8ebf04" + }, + { + "id": "obsidian.base", + "version": "1.0.0", + "digest": "sha256:37e8a0bfb32bb4795c1b3c5dec613c32429aaa765dc33e4adc0c627a48f5c1ff" + }, + { + "id": "mdbase.view", + "version": "1.0.0", + "digest": "sha256:0918acefce08cddd778fab768fe4f2e4615d263a0eec3e5330b6897c686d744e" } ], "capabilities": { @@ -24,7 +34,6 @@ "records.create", "records.edit", "records.delete", - "views.manage", "definitions.manage", "background.schedule" ] @@ -40,6 +49,18 @@ "path": "/x-obsidian/bases/include", "predicate": "contains", "value": "TaskNotes/Views/**/*.base" + }, + { + "id": "tasknotes-markdown-records", + "path": "/settings/record_extensions", + "predicate": "contains", + "value": "md" + }, + { + "id": "tasknotes-base-records", + "path": "/settings/record_extensions", + "predicate": "contains", + "value": "base" } ] }, @@ -162,6 +183,101 @@ } ], "provides": [] + }, + { + "manifest": { + "kind": "mdbase.type-pack", + "id": "obsidian.base", + "version": "1.0.0", + "name": "Obsidian Bases", + "description": "The obsidian.base contract and an editable type that stores Obsidian Bases as records.", + "resources": [ + { + "kind": "contract", + "mode": "managed", + "source": "contracts/obsidian.base/1.0.0.md", + "target": "_contracts/obsidian.base/1.0.0.md", + "digest": "sha256:fca77a9c845223784d4af7f5e13e2f149295621ad1b794ea9bb4d99f604467e8" + }, + { + "kind": "type", + "mode": "seed", + "source": "types/obsidian-base/1.md", + "target": "_types/obsidian_base.md", + "digest": "sha256:bfeb3880200bc1bef7fd50b777d3f927ce931d08b4fb3c30d4cddb55feaf3f92" + } + ] + }, + "resources": [ + { + "source": "contracts/obsidian.base/1.0.0.md", + "document": "---\nkind: mdbase.contract\ncontract_type: record\nid: obsidian.base\nversion: 1.0.0\nname: Obsidian Base\ndescription: An Obsidian Bases saved-view source stored as a YAML document record.\nrecord_schema:\n dialect: json-schema-2020-12\n value:\n $schema: https://json-schema.org/draft/2020-12/schema\n type: object\n required: [views]\n properties:\n filters: {}\n formulas: { type: object }\n properties: { type: object }\n views:\n type: array\n minItems: 1\n items: { type: object, required: [type], properties: { type: { type: string }, name: { type: string } } }\n---\n\n# Obsidian Base\n\nA record exposed through `obsidian.base` is an Obsidian Bases source: global\nfilters, formulas, property metadata, and one or more views. The schema is\ndeliberately permissive. Obsidian owns the format and adds keys over time;\nunknown keys are preserved and ignored.\n\nTools that advertise `obsidian_bases_views` discover and execute these records\nthrough this contract and evaluate them with the Obsidian Bases expression\ndialect described in the [Obsidian Bases adapter](https://mdbase.dev/spec/adapters/obsidian-bases).\n\nThis artifact is passive. Implementing it grants no authority to read, execute,\nor modify any record.\n" + }, + { + "source": "types/obsidian-base/1.md", + "document": "---\nkind: mdbase.type\nname: obsidian_base\nversion: 1\ndescription: Obsidian Bases saved views stored as records\nmatch:\n path_glob: '**/*.base'\nschema:\n dialect: json-schema-2020-12\n value:\n $schema: https://json-schema.org/draft/2020-12/schema\n title: Obsidian Base\n description: An Obsidian Bases file. Obsidian owns the format; unknown keys are kept.\n type: object\n required:\n - views\n properties:\n filters: {}\n formulas:\n type: object\n properties:\n type: object\n views:\n type: array\n minItems: 1\n items:\n type: object\n required:\n - type\n properties:\n type:\n type: string\n name:\n type: string\nimplements:\n - contract: obsidian.base\n version: 1.0.0\n fields:\n filters: filters\n formulas: formulas\n properties: properties\n views: views\n---\n\n# Obsidian Base\n\nMakes `.base` files discoverable as saved views through the `obsidian.base`\ncontract. It takes effect when the collection lists `base` in\n`settings.record_extensions`; narrow the `path_glob` to the folders that hold\nyour Bases. Edit Bases with whole-document replacement so Obsidian's comments\nand layout survive.\n" + } + ], + "provides": [ + { + "id": "obsidian.base", + "version": "1.0.0", + "digest": "sha256:37e8a0bfb32bb4795c1b3c5dec613c32429aaa765dc33e4adc0c627a48f5c1ff" + } + ] + }, + { + "manifest": { + "kind": "mdbase.type-pack", + "id": "mdbase.view", + "version": "1.0.0", + "name": "Saved views", + "description": "The mdbase.view contract and an editable View type for saved, reusable queries.", + "resources": [ + { + "kind": "schema", + "mode": "managed", + "source": "schemas/mdbase.view/1.0.0.schema.json", + "target": "schemas/mdbase.view/1.0.0.schema.json", + "digest": "sha256:d58b8b691c14d59f0575fc1cc93ab5b46363c485e4469bc58039ec0cece7ca36" + }, + { + "kind": "contract", + "mode": "managed", + "source": "contracts/mdbase.view/1.0.0.md", + "target": "_contracts/mdbase.view/1.0.0.md", + "digest": "sha256:91eae42371c2d411b20d3c976aece3071bbd753963b77b6bcdc26137a8fdcded" + }, + { + "kind": "type", + "mode": "seed", + "source": "types/view/1.md", + "target": "_types/view.md", + "digest": "sha256:480919f6f88f91592abb9fe7971f09e874d536c6883981616b181cf14cbe7ff4" + } + ] + }, + "resources": [ + { + "source": "schemas/mdbase.view/1.0.0.schema.json", + "document": "{\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"$id\": \"https://mdbase.dev/schemas/v0.3/view.schema.json\",\n \"title\": \"mdbase.view 1.0.0 record contract view\",\n \"description\": \"The contract view of a saved-view record. A type implements mdbase.view by mapping these fields; the optional type property only lets the canonical view type validate its own membership value.\",\n \"type\": \"object\",\n \"required\": [\"id\", \"version\", \"name\", \"views\"],\n \"properties\": {\n \"type\": { \"type\": \"string\", \"minLength\": 1 },\n \"id\": { \"$ref\": \"#/$defs/identifier\" },\n \"version\": { \"type\": \"integer\", \"minimum\": 1 },\n \"name\": { \"type\": \"string\", \"minLength\": 1 },\n \"description\": { \"type\": \"string\" },\n \"query\": { \"$ref\": \"#/$defs/sharedQuery\" },\n \"properties\": { \"$ref\": \"#/$defs/propertyMetadataSet\" },\n \"summary_functions\": { \"$ref\": \"#/$defs/summaryFunctionSet\" },\n \"views\": {\n \"type\": \"array\",\n \"minItems\": 1,\n \"items\": { \"$ref\": \"#/$defs/view\" }\n }\n },\n \"patternProperties\": {\n \"^x-[A-Za-z][A-Za-z0-9._:-]{0,127}$\": { \"$ref\": \"#/$defs/extension\" }\n },\n \"additionalProperties\": false,\n \"$defs\": {\n \"identifier\": {\n \"type\": \"string\",\n \"pattern\": \"^[A-Za-z][A-Za-z0-9._:-]*$\"\n },\n \"typeName\": {\n \"type\": \"string\",\n \"pattern\": \"^[A-Za-z][A-Za-z0-9_-]{0,127}$\"\n },\n \"fieldName\": {\n \"type\": \"string\",\n \"pattern\": \"^[A-Za-z_][A-Za-z0-9_:-]*$\"\n },\n \"expression\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"extension\": {\n \"type\": \"object\",\n \"additionalProperties\": true\n },\n \"typeList\": {\n \"type\": \"array\",\n \"minItems\": 1,\n \"uniqueItems\": true,\n \"items\": { \"$ref\": \"#/$defs/typeName\" }\n },\n \"sharedQuery\": {\n \"type\": \"object\",\n \"properties\": {\n \"types\": { \"$ref\": \"#/$defs/typeList\" },\n \"where\": { \"$ref\": \"#/$defs/expression\" },\n \"context\": { \"$ref\": \"#/$defs/viewContext\" },\n \"projections\": { \"$ref\": \"#/$defs/projectionSet\" }\n },\n \"patternProperties\": {\n \"^x-[A-Za-z][A-Za-z0-9._:-]{0,127}$\": { \"$ref\": \"#/$defs/extension\" }\n },\n \"additionalProperties\": false\n },\n \"viewContext\": {\n \"type\": \"object\",\n \"required\": [\"this\"],\n \"properties\": {\n \"this\": { \"$ref\": \"#/$defs/thisContext\" }\n },\n \"patternProperties\": {\n \"^x-[A-Za-z][A-Za-z0-9._:-]{0,127}$\": { \"$ref\": \"#/$defs/extension\" }\n },\n \"additionalProperties\": false\n },\n \"thisContext\": {\n \"type\": \"object\",\n \"properties\": {\n \"on_missing\": {\n \"enum\": [\"view\", \"null\", \"error\"],\n \"default\": \"view\"\n },\n \"types\": { \"$ref\": \"#/$defs/typeList\" }\n },\n \"patternProperties\": {\n \"^x-[A-Za-z][A-Za-z0-9._:-]{0,127}$\": { \"$ref\": \"#/$defs/extension\" }\n },\n \"additionalProperties\": false\n },\n \"projectionSet\": {\n \"type\": \"object\",\n \"propertyNames\": { \"$ref\": \"#/$defs/fieldName\" },\n \"additionalProperties\": { \"$ref\": \"#/$defs/projection\" }\n },\n \"projection\": {\n \"type\": \"object\",\n \"required\": [\"expr\"],\n \"properties\": {\n \"expr\": { \"$ref\": \"#/$defs/expression\" },\n \"description\": { \"type\": \"string\" }\n },\n \"patternProperties\": {\n \"^x-[A-Za-z][A-Za-z0-9._:-]{0,127}$\": { \"$ref\": \"#/$defs/extension\" }\n },\n \"additionalProperties\": false\n },\n \"propertyMetadataSet\": {\n \"type\": \"object\",\n \"propertyNames\": { \"type\": \"string\", \"minLength\": 1 },\n \"additionalProperties\": { \"$ref\": \"#/$defs/propertyMetadata\" }\n },\n \"propertyMetadata\": {\n \"type\": \"object\",\n \"properties\": {\n \"label\": { \"type\": \"string\" },\n \"description\": { \"type\": \"string\" },\n \"format\": { \"type\": \"string\" },\n \"hidden\": { \"type\": \"boolean\" }\n },\n \"patternProperties\": {\n \"^x-[A-Za-z][A-Za-z0-9._:-]{0,127}$\": { \"$ref\": \"#/$defs/extension\" }\n },\n \"additionalProperties\": false\n },\n \"summaryFunctionSet\": {\n \"type\": \"object\",\n \"propertyNames\": { \"$ref\": \"#/$defs/fieldName\" },\n \"additionalProperties\": { \"$ref\": \"#/$defs/summaryFunction\" }\n },\n \"summaryFunction\": {\n \"type\": \"object\",\n \"required\": [\"expr\"],\n \"properties\": {\n \"expr\": { \"$ref\": \"#/$defs/expression\" },\n \"description\": { \"type\": \"string\" }\n },\n \"patternProperties\": {\n \"^x-[A-Za-z][A-Za-z0-9._:-]{0,127}$\": { \"$ref\": \"#/$defs/extension\" }\n },\n \"additionalProperties\": false\n },\n \"view\": {\n \"type\": \"object\",\n \"required\": [\"id\", \"name\"],\n \"properties\": {\n \"id\": { \"$ref\": \"#/$defs/identifier\" },\n \"name\": { \"type\": \"string\", \"minLength\": 1 },\n \"description\": { \"type\": \"string\" },\n \"types\": { \"$ref\": \"#/$defs/typeList\" },\n \"where\": { \"$ref\": \"#/$defs/expression\" },\n \"context\": { \"$ref\": \"#/$defs/viewContext\" },\n \"projections\": { \"$ref\": \"#/$defs/projectionSet\" },\n \"select\": { \"$ref\": \"#/$defs/select\" },\n \"order_by\": { \"$ref\": \"#/$defs/orderBy\" },\n \"group_by\": { \"$ref\": \"#/$defs/groupBy\" },\n \"summaries\": { \"$ref\": \"#/$defs/summaries\" },\n \"limit\": { \"type\": \"integer\", \"minimum\": 0 },\n \"offset\": { \"type\": \"integer\", \"minimum\": 0 },\n \"include_body\": { \"type\": \"boolean\", \"default\": false },\n \"frontmatter_mode\": {\n \"enum\": [\"effective\", \"persisted\", \"both\"],\n \"default\": \"effective\"\n },\n \"presentation\": { \"$ref\": \"#/$defs/presentation\" }\n },\n \"patternProperties\": {\n \"^x-[A-Za-z][A-Za-z0-9._:-]{0,127}$\": { \"$ref\": \"#/$defs/extension\" }\n },\n \"additionalProperties\": false\n },\n \"select\": {\n \"type\": \"array\",\n \"minItems\": 1,\n \"items\": {\n \"oneOf\": [\n { \"type\": \"string\", \"minLength\": 1 },\n { \"$ref\": \"#/$defs/selectExpression\" }\n ]\n }\n },\n \"selectExpression\": {\n \"type\": \"object\",\n \"required\": [\"name\", \"expr\"],\n \"properties\": {\n \"name\": { \"$ref\": \"#/$defs/fieldName\" },\n \"expr\": { \"$ref\": \"#/$defs/expression\" },\n \"label\": { \"type\": \"string\" },\n \"description\": { \"type\": \"string\" }\n },\n \"patternProperties\": {\n \"^x-[A-Za-z][A-Za-z0-9._:-]{0,127}$\": { \"$ref\": \"#/$defs/extension\" }\n },\n \"additionalProperties\": false\n },\n \"orderBy\": {\n \"type\": \"array\",\n \"minItems\": 1,\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"field\"],\n \"properties\": {\n \"field\": { \"type\": \"string\", \"minLength\": 1 },\n \"direction\": { \"enum\": [\"asc\", \"desc\"], \"default\": \"asc\" }\n },\n \"additionalProperties\": false\n }\n },\n \"groupBy\": {\n \"type\": \"array\",\n \"minItems\": 1,\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"field\"],\n \"properties\": {\n \"field\": { \"type\": \"string\", \"minLength\": 1 },\n \"direction\": { \"enum\": [\"asc\", \"desc\"], \"default\": \"asc\" }\n },\n \"additionalProperties\": false\n }\n },\n \"summaries\": {\n \"type\": \"array\",\n \"minItems\": 1,\n \"items\": { \"$ref\": \"#/$defs/summary\" }\n },\n \"summary\": {\n \"type\": \"object\",\n \"required\": [\"field\", \"function\"],\n \"properties\": {\n \"field\": { \"type\": \"string\", \"minLength\": 1 },\n \"function\": { \"$ref\": \"#/$defs/identifier\" },\n \"name\": { \"$ref\": \"#/$defs/fieldName\" },\n \"label\": { \"type\": \"string\" }\n },\n \"additionalProperties\": false\n },\n \"presentation\": {\n \"type\": \"object\",\n \"required\": [\"type\"],\n \"properties\": {\n \"type\": { \"$ref\": \"#/$defs/identifier\" },\n \"fallback\": { \"$ref\": \"#/$defs/identifier\" },\n \"mappings\": {\n \"type\": \"object\",\n \"propertyNames\": { \"$ref\": \"#/$defs/fieldName\" },\n \"additionalProperties\": { \"type\": \"string\", \"minLength\": 1 }\n },\n \"options\": {\n \"type\": \"object\",\n \"additionalProperties\": true\n }\n },\n \"patternProperties\": {\n \"^x-[A-Za-z][A-Za-z0-9._:-]{0,127}$\": { \"$ref\": \"#/$defs/extension\" }\n },\n \"additionalProperties\": false\n }\n }\n}\n" + }, + { + "source": "contracts/mdbase.view/1.0.0.md", + "document": "---\nkind: mdbase.contract\ncontract_type: record\nid: mdbase.view\nversion: 1.0.0\nname: Saved view\ndescription: Shared query scope and stable named views executable through the Query profile.\nrecord_schema:\n dialect: json-schema-2020-12\n ref: ../../schemas/mdbase.view/1.0.0.schema.json\n---\n\n# Saved view\n\nA record exposed through `mdbase.view` stores shared query scope and one or more\nstable named views. Each named view resolves to the query model in Chapter 11.\nOptional `presentation` metadata is advisory and does not alter headless query\nresults.\n\nImplementing this contract is how a collection declares which of its records\nare saved views. A tool that advertises `view_records` discovers and executes\nviews through this contract's type implementations, not through a reserved type\nname, path, or frontmatter value.\n\nThis artifact is passive. Implementing it grants no authority to read, execute,\nor modify any record.\n" + }, + { + "source": "types/view/1.md", + "document": "---\nkind: mdbase.type\nname: view\nversion: 1\ndescription: 'Saved views: shared query scope and stable named views'\nmatch:\n where:\n type: view\nschema:\n dialect: json-schema-2020-12\n value:\n $schema: https://json-schema.org/draft/2020-12/schema\n title: View\n description: 'Saved views: shared query scope and stable named views.'\n type: object\n required:\n - type\n - id\n - version\n - name\n - views\n properties:\n type:\n const: view\n id:\n $ref: '#/$defs/identifier'\n version:\n type: integer\n minimum: 1\n name:\n type: string\n minLength: 1\n description:\n type: string\n query:\n $ref: '#/$defs/sharedQuery'\n properties:\n $ref: '#/$defs/propertyMetadataSet'\n summary_functions:\n $ref: '#/$defs/summaryFunctionSet'\n views:\n type: array\n minItems: 1\n items:\n $ref: '#/$defs/view'\n patternProperties:\n ^x-[A-Za-z][A-Za-z0-9._:-]{0,127}$:\n $ref: '#/$defs/extension'\n additionalProperties: false\n $defs:\n identifier:\n type: string\n pattern: ^[A-Za-z][A-Za-z0-9._:-]*$\n typeName:\n type: string\n pattern: ^[A-Za-z][A-Za-z0-9_-]{0,127}$\n fieldName:\n type: string\n pattern: ^[A-Za-z_][A-Za-z0-9_:-]*$\n expression:\n type: string\n minLength: 1\n extension:\n type: object\n additionalProperties: true\n typeList:\n type: array\n minItems: 1\n uniqueItems: true\n items:\n $ref: '#/$defs/typeName'\n sharedQuery:\n type: object\n properties:\n types:\n $ref: '#/$defs/typeList'\n where:\n $ref: '#/$defs/expression'\n context:\n $ref: '#/$defs/viewContext'\n projections:\n $ref: '#/$defs/projectionSet'\n patternProperties:\n ^x-[A-Za-z][A-Za-z0-9._:-]{0,127}$:\n $ref: '#/$defs/extension'\n additionalProperties: false\n viewContext:\n type: object\n required:\n - this\n properties:\n this:\n $ref: '#/$defs/thisContext'\n patternProperties:\n ^x-[A-Za-z][A-Za-z0-9._:-]{0,127}$:\n $ref: '#/$defs/extension'\n additionalProperties: false\n thisContext:\n type: object\n properties:\n on_missing:\n enum:\n - view\n - 'null'\n - error\n default: view\n types:\n $ref: '#/$defs/typeList'\n patternProperties:\n ^x-[A-Za-z][A-Za-z0-9._:-]{0,127}$:\n $ref: '#/$defs/extension'\n additionalProperties: false\n projectionSet:\n type: object\n propertyNames:\n $ref: '#/$defs/fieldName'\n additionalProperties:\n $ref: '#/$defs/projection'\n projection:\n type: object\n required:\n - expr\n properties:\n expr:\n $ref: '#/$defs/expression'\n description:\n type: string\n patternProperties:\n ^x-[A-Za-z][A-Za-z0-9._:-]{0,127}$:\n $ref: '#/$defs/extension'\n additionalProperties: false\n propertyMetadataSet:\n type: object\n propertyNames:\n type: string\n minLength: 1\n additionalProperties:\n $ref: '#/$defs/propertyMetadata'\n propertyMetadata:\n type: object\n properties:\n label:\n type: string\n description:\n type: string\n format:\n type: string\n hidden:\n type: boolean\n patternProperties:\n ^x-[A-Za-z][A-Za-z0-9._:-]{0,127}$:\n $ref: '#/$defs/extension'\n additionalProperties: false\n summaryFunctionSet:\n type: object\n propertyNames:\n $ref: '#/$defs/fieldName'\n additionalProperties:\n $ref: '#/$defs/summaryFunction'\n summaryFunction:\n type: object\n required:\n - expr\n properties:\n expr:\n $ref: '#/$defs/expression'\n description:\n type: string\n patternProperties:\n ^x-[A-Za-z][A-Za-z0-9._:-]{0,127}$:\n $ref: '#/$defs/extension'\n additionalProperties: false\n view:\n type: object\n required:\n - id\n - name\n properties:\n id:\n $ref: '#/$defs/identifier'\n name:\n type: string\n minLength: 1\n description:\n type: string\n types:\n $ref: '#/$defs/typeList'\n where:\n $ref: '#/$defs/expression'\n context:\n $ref: '#/$defs/viewContext'\n projections:\n $ref: '#/$defs/projectionSet'\n select:\n $ref: '#/$defs/select'\n order_by:\n $ref: '#/$defs/orderBy'\n group_by:\n $ref: '#/$defs/groupBy'\n summaries:\n $ref: '#/$defs/summaries'\n limit:\n type: integer\n minimum: 0\n offset:\n type: integer\n minimum: 0\n include_body:\n type: boolean\n default: false\n frontmatter_mode:\n enum:\n - effective\n - persisted\n - both\n default: effective\n presentation:\n $ref: '#/$defs/presentation'\n patternProperties:\n ^x-[A-Za-z][A-Za-z0-9._:-]{0,127}$:\n $ref: '#/$defs/extension'\n additionalProperties: false\n select:\n type: array\n minItems: 1\n items:\n oneOf:\n - type: string\n minLength: 1\n - $ref: '#/$defs/selectExpression'\n selectExpression:\n type: object\n required:\n - name\n - expr\n properties:\n name:\n $ref: '#/$defs/fieldName'\n expr:\n $ref: '#/$defs/expression'\n label:\n type: string\n description:\n type: string\n patternProperties:\n ^x-[A-Za-z][A-Za-z0-9._:-]{0,127}$:\n $ref: '#/$defs/extension'\n additionalProperties: false\n orderBy:\n type: array\n minItems: 1\n items:\n type: object\n required:\n - field\n properties:\n field:\n type: string\n minLength: 1\n direction:\n enum:\n - asc\n - desc\n default: asc\n additionalProperties: false\n groupBy:\n type: array\n minItems: 1\n items:\n type: object\n required:\n - field\n properties:\n field:\n type: string\n minLength: 1\n direction:\n enum:\n - asc\n - desc\n default: asc\n additionalProperties: false\n summaries:\n type: array\n minItems: 1\n items:\n $ref: '#/$defs/summary'\n summary:\n type: object\n required:\n - field\n - function\n properties:\n field:\n type: string\n minLength: 1\n function:\n $ref: '#/$defs/identifier'\n name:\n $ref: '#/$defs/fieldName'\n label:\n type: string\n additionalProperties: false\n presentation:\n type: object\n required:\n - type\n properties:\n type:\n $ref: '#/$defs/identifier'\n fallback:\n $ref: '#/$defs/identifier'\n mappings:\n type: object\n propertyNames:\n $ref: '#/$defs/fieldName'\n additionalProperties:\n type: string\n minLength: 1\n options:\n type: object\n additionalProperties: true\n patternProperties:\n ^x-[A-Za-z][A-Za-z0-9._:-]{0,127}$:\n $ref: '#/$defs/extension'\n additionalProperties: false\nimplements:\n- contract: mdbase.view\n version: 1.0.0\n fields:\n id: id\n version: version\n name: name\n description: description\n query: query\n properties: properties\n summary_functions: summary_functions\n views: views\ncollection:\n display:\n name_field: name\n---\n\n# View\n\nA view record stores shared query scope and one or more stable named views.\nTools that support saved views discover these records through the `mdbase.view`\ncontract and run each named view as an ordinary query.\n\nThis type belongs to your collection once installed. You can rename fields or\nchange how records are matched, as long as the `implements` mapping still\npoints each contract field at the right frontmatter field.\n" + } + ], + "provides": [ + { + "id": "mdbase.view", + "version": "1.0.0", + "digest": "sha256:0918acefce08cddd778fab768fe4f2e4615d263a0eec3e5330b6897c686d744e" + } + ] } ], "configuration": [ @@ -170,6 +286,18 @@ "operation": "set_add", "path": "/x-obsidian/bases/include", "value": "TaskNotes/Views/**/*.base" + }, + { + "requirement": "tasknotes-markdown-records", + "operation": "set_add", + "path": "/settings/record_extensions", + "value": "md" + }, + { + "requirement": "tasknotes-base-records", + "operation": "set_add", + "path": "/settings/record_extensions", + "value": "base" } ] }, diff --git a/src/storage/mdbase-repository.test.ts b/src/storage/mdbase-repository.test.ts index 4a3af2cd..b1d44805 100644 --- a/src/storage/mdbase-repository.test.ts +++ b/src/storage/mdbase-repository.test.ts @@ -1375,7 +1375,7 @@ describe("mdbase task repository", () => { const fixture = mdbaseFixture([]); const connect = { ...fixture.connect, - readViewSource: undefined, + read: undefined, } as unknown as MdbaseConnection; const repository = new MdbaseTaskRepository(connect); await repository.initialize(); @@ -1418,13 +1418,13 @@ describe("mdbase task repository", () => { ifRevision: created.revision, }); expect(updated).toMatchObject({ revision: "view-r3" }); - expect(fixture.updateViewSource).toHaveBeenCalledWith( + expect(fixture.update).toHaveBeenCalledWith( expect.objectContaining({ ifRevision: "view-r2" }), expect.objectContaining({ signal: expect.anything() }), ); await repository.deleteViewSource(updated.path, updated.revision); - expect(fixture.deleteViewSource).toHaveBeenCalledWith( + expect(fixture.remove).toHaveBeenCalledWith( { path: updated.path, ifRevision: "view-r3", diff --git a/src/storage/mdbase-repository.ts b/src/storage/mdbase-repository.ts index c8441b3a..a0db8eb2 100644 --- a/src/storage/mdbase-repository.ts +++ b/src/storage/mdbase-repository.ts @@ -41,6 +41,11 @@ import { runMdbaseMutation, } from "./mdbase-mutation-coordinator"; import { MdbaseCollectionFileStore } from "./mdbase-files"; +import { + newViewSourcePath, + viewSourceFormat, + viewSourceRecord, +} from "../domain/default-view-source"; import { readPeopleDirectory } from "./mdbase-people"; import { activeScratchpad, @@ -1233,8 +1238,13 @@ export class MdbaseTaskRepository implements TaskRepository { async readViewSource(path: string): Promise { try { - return validResult( - await this.connect.readViewSource({ path }, this.requestOptions()), + return viewSourceDocument( + validResult( + await this.connect.read( + { path, includeDocument: true }, + this.requestOptions(), + ), + ), ); } catch (reason) { this.noteOperationFailure(reason); @@ -1242,11 +1252,19 @@ export class MdbaseTaskRepository implements TaskRepository { } } + /** Saved views are ordinary records: Bases through obsidian.base, others through mdbase.view. */ async createViewSource( input: CreateTaskViewSourceInput, ): Promise { + const path = + input.path ?? + newViewSourcePath(input.format ?? "obsidian.base", input.name ?? "view"); + const operationInput = { + path, + ...viewSourceRecord(path, input.document), + includeDocument: true, + }; try { - const operationInput = { ...input }; const applyCreated = (created: TaskViewSourceDocument) => { this.invalidateViewsAfterMutation(); return created; @@ -1255,10 +1273,12 @@ export class MdbaseTaskRepository implements TaskRepository { this.connect, async () => applyCreated( - validResult( - await this.connect.createViewSource( - operationInput, - this.requestOptions(), + viewSourceDocument( + validResult( + await this.connect.create( + operationInput, + this.requestOptions(), + ), ), ), ), @@ -1274,6 +1294,7 @@ export class MdbaseTaskRepository implements TaskRepository { } } + /** A whole-document replacement keeps Obsidian's comments and layout. */ async updateViewSource( input: UpdateTaskViewSourceInput, ): Promise { @@ -1291,10 +1312,12 @@ export class MdbaseTaskRepository implements TaskRepository { this.connect, async () => applyUpdated( - validResult( - await this.connect.updateViewSource( - operationInput, - this.requestOptions(), + viewSourceDocument( + validResult( + await this.connect.update( + operationInput, + this.requestOptions(), + ), ), ), ), @@ -1320,10 +1343,7 @@ export class MdbaseTaskRepository implements TaskRepository { this.connect, async () => { validResult( - await this.connect.deleteViewSource( - operationInput, - this.requestOptions(), - ), + await this.connect.delete(operationInput, this.requestOptions()), ); applyDeleted(); }, @@ -2603,3 +2623,16 @@ function connectionErrorMessage(reason: unknown): string { if (reason instanceof Error && reason.message) return reason.message; return "This collection is not reachable right now."; } + +function viewSourceDocument(record: { + path: string; + revision: string; + document?: string; +}): TaskViewSourceDocument { + return { + path: record.path, + format: viewSourceFormat(record.path), + revision: record.revision, + document: record.document ?? "", + }; +} diff --git a/src/storage/mdbase-scratchpad-stream.test.ts b/src/storage/mdbase-scratchpad-stream.test.ts index df2af8e1..56699968 100644 --- a/src/storage/mdbase-scratchpad-stream.test.ts +++ b/src/storage/mdbase-scratchpad-stream.test.ts @@ -371,7 +371,7 @@ describe("mdbase scratchpad stream", () => { if ( !demotionFailed && input.path === current.path && - input.patch.state === "converted" + input.patch?.state === "converted" ) { demotionFailed = true; throw new Error("Second write failed"); diff --git a/src/test/mdbase-fixture.ts b/src/test/mdbase-fixture.ts index 74907c7e..8c9bfb6d 100644 --- a/src/test/mdbase-fixture.ts +++ b/src/test/mdbase-fixture.ts @@ -19,6 +19,7 @@ import { serializeMarkdownDocument, } from "@tasknotes/model/frontmatter"; import { vi } from "vitest"; +import { stringify } from "yaml"; import { TaskNotesTaskModel } from "../domain/tasknotes-model"; import { MdbaseTaskRepository } from "../storage/mdbase-repository"; @@ -156,7 +157,28 @@ export function mdbaseFixture( ); })(), ); + // Saved views are records; their documents are kept so reads with + // includeDocument and whole-document updates round-trip. + const viewSources = new Map([ + [ + "views/tasks.base", + { + path: "views/tasks.base", + revision: "view-r1", + document: "views:\n - name: Kanban\n type: tasknotesKanban\n", + }, + ], + ]); + let viewRevision = 2; + const isViewSource = (path: string) => + path.endsWith(".base") || path.startsWith("views/"); + const viewSource = (path: string) => { + const source = viewSources.get(path); + if (!source) throw new Error("View source not found."); + return source; + }; const read = vi.fn(async ({ path }: { path: string }) => { + if (isViewSource(path)) return valid(structuredClone(viewSource(path))); const record = records.get(path); if (!record) throw new Error("Task not found."); return valid(record); @@ -169,6 +191,17 @@ export function mdbaseFixture( body?: string; }) => { const path = input.path ?? `tasks/${crypto.randomUUID()}.md`; + if (isViewSource(path)) { + const source = { + path, + revision: `view-r${viewRevision++}`, + document: path.endsWith(".base") + ? stringify(input.frontmatter) + : serializeMarkdownDocument(input.frontmatter, input.body ?? ""), + }; + viewSources.set(path, source); + return valid(structuredClone(source)); + } if (records.has(path)) throw new Error(`Path already exists: ${path}`); const record: TestRecord = { path, @@ -184,16 +217,29 @@ export function mdbaseFixture( const update = vi.fn( async (input: { path: string; - patch: JsonObject; + patch?: JsonObject; + document?: string; body?: string; ifRevision?: string; }) => { + if (isViewSource(input.path)) { + const current = viewSource(input.path); + if (input.ifRevision !== current.revision) + throw new Error("Revision conflict."); + const source = { + ...current, + revision: `view-r${viewRevision++}`, + document: input.document ?? current.document, + }; + viewSources.set(input.path, source); + return valid(structuredClone(source)); + } const current = records.get(input.path); if (!current) throw new Error("Task not found."); if (input.ifRevision !== current.revision) throw new Error("Revision conflict."); const frontmatter = structuredClone(current.frontmatter); - for (const [key, value] of Object.entries(input.patch)) { + for (const [key, value] of Object.entries(input.patch ?? {})) { if (value === null) delete frontmatter[key]; else frontmatter[key] = structuredClone(value); } @@ -208,6 +254,12 @@ export function mdbaseFixture( }, ); const remove = vi.fn(async (input: { path: string; ifRevision?: string }) => { + if (isViewSource(input.path)) { + if (input.ifRevision !== viewSource(input.path).revision) + throw new Error("Revision conflict."); + viewSources.delete(input.path); + return valid({ path: input.path, deleted: true }); + } const current = records.get(input.path); if (!current) throw new Error("Task not found."); if (input.ifRevision !== current.revision) @@ -297,77 +349,6 @@ export function mdbaseFixture( ); })(), ); - const viewSources = new Map< - string, - { - path: string; - format: "obsidian.base" | "mdbase.view"; - revision: string; - document: string; - } - >([ - [ - "views/tasks.base", - { - path: "views/tasks.base", - format: "obsidian.base" as const, - revision: "view-r1", - document: "views:\n - name: Kanban\n type: tasknotesKanban\n", - }, - ], - ]); - let viewRevision = 2; - const readViewSource = vi.fn(async ({ path }: { path: string }) => { - const source = viewSources.get(path); - if (!source) throw new Error("View source not found."); - return valid(structuredClone(source)); - }); - const createViewSource = vi.fn( - async (input: { - path?: string; - format: "obsidian.base" | "mdbase.view"; - name: string; - document: string; - }) => { - const extension = input.format === "obsidian.base" ? "base" : "md"; - const path = - input.path ?? - `views/${input.name.toLowerCase().replaceAll(" ", "-")}.${extension}`; - const source = { - path, - format: input.format, - revision: `view-r${viewRevision++}`, - document: input.document, - }; - viewSources.set(path, source); - return valid(structuredClone(source)); - }, - ); - const updateViewSource = vi.fn( - async (input: { path: string; document: string; ifRevision?: string }) => { - const current = viewSources.get(input.path); - if (!current) throw new Error("View source not found."); - if (input.ifRevision !== current.revision) - throw new Error("Revision conflict."); - const source = { - ...current, - revision: `view-r${viewRevision++}`, - document: input.document, - }; - viewSources.set(input.path, source); - return valid(structuredClone(source)); - }, - ); - const deleteViewSource = vi.fn( - async (input: { path: string; ifRevision?: string }) => { - const current = viewSources.get(input.path); - if (!current) throw new Error("View source not found."); - if (input.ifRevision !== current.revision) - throw new Error("Revision conflict."); - viewSources.delete(input.path); - return valid({ path: input.path, deleted: true }); - }, - ); const readType = vi.fn( async ({ name, path }: { name?: string; path?: string }) => { if (name !== typeDocument.name && path !== typeDocument.path) @@ -436,10 +417,6 @@ export function mdbaseFixture( listViews, executeView, executeViewPages, - readViewSource, - createViewSource, - updateViewSource, - deleteViewSource, readType, updateType, } as unknown as MdbaseConnection; @@ -477,10 +454,6 @@ export function mdbaseFixture( listViews, executeView, executeViewPages, - readViewSource, - createViewSource, - updateViewSource, - deleteViewSource, readType, updateType, stagePendingMutation, diff --git a/vendor/mdbase-contracts/mdbase.view-1.0.0.json b/vendor/mdbase-contracts/mdbase.view-1.0.0.json new file mode 100644 index 00000000..210fa58e --- /dev/null +++ b/vendor/mdbase-contracts/mdbase.view-1.0.0.json @@ -0,0 +1,53 @@ +{ + "manifest": { + "kind": "mdbase.type-pack", + "id": "mdbase.view", + "version": "1.0.0", + "name": "Saved views", + "description": "The mdbase.view contract and an editable View type for saved, reusable queries.", + "resources": [ + { + "kind": "schema", + "mode": "managed", + "source": "schemas/mdbase.view/1.0.0.schema.json", + "target": "schemas/mdbase.view/1.0.0.schema.json", + "digest": "sha256:d58b8b691c14d59f0575fc1cc93ab5b46363c485e4469bc58039ec0cece7ca36" + }, + { + "kind": "contract", + "mode": "managed", + "source": "contracts/mdbase.view/1.0.0.md", + "target": "_contracts/mdbase.view/1.0.0.md", + "digest": "sha256:91eae42371c2d411b20d3c976aece3071bbd753963b77b6bcdc26137a8fdcded" + }, + { + "kind": "type", + "mode": "seed", + "source": "types/view/1.md", + "target": "_types/view.md", + "digest": "sha256:480919f6f88f91592abb9fe7971f09e874d536c6883981616b181cf14cbe7ff4" + } + ] + }, + "resources": [ + { + "source": "schemas/mdbase.view/1.0.0.schema.json", + "document": "{\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"$id\": \"https://mdbase.dev/schemas/v0.3/view.schema.json\",\n \"title\": \"mdbase.view 1.0.0 record contract view\",\n \"description\": \"The contract view of a saved-view record. A type implements mdbase.view by mapping these fields; the optional type property only lets the canonical view type validate its own membership value.\",\n \"type\": \"object\",\n \"required\": [\"id\", \"version\", \"name\", \"views\"],\n \"properties\": {\n \"type\": { \"type\": \"string\", \"minLength\": 1 },\n \"id\": { \"$ref\": \"#/$defs/identifier\" },\n \"version\": { \"type\": \"integer\", \"minimum\": 1 },\n \"name\": { \"type\": \"string\", \"minLength\": 1 },\n \"description\": { \"type\": \"string\" },\n \"query\": { \"$ref\": \"#/$defs/sharedQuery\" },\n \"properties\": { \"$ref\": \"#/$defs/propertyMetadataSet\" },\n \"summary_functions\": { \"$ref\": \"#/$defs/summaryFunctionSet\" },\n \"views\": {\n \"type\": \"array\",\n \"minItems\": 1,\n \"items\": { \"$ref\": \"#/$defs/view\" }\n }\n },\n \"patternProperties\": {\n \"^x-[A-Za-z][A-Za-z0-9._:-]{0,127}$\": { \"$ref\": \"#/$defs/extension\" }\n },\n \"additionalProperties\": false,\n \"$defs\": {\n \"identifier\": {\n \"type\": \"string\",\n \"pattern\": \"^[A-Za-z][A-Za-z0-9._:-]*$\"\n },\n \"typeName\": {\n \"type\": \"string\",\n \"pattern\": \"^[A-Za-z][A-Za-z0-9_-]{0,127}$\"\n },\n \"fieldName\": {\n \"type\": \"string\",\n \"pattern\": \"^[A-Za-z_][A-Za-z0-9_:-]*$\"\n },\n \"expression\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"extension\": {\n \"type\": \"object\",\n \"additionalProperties\": true\n },\n \"typeList\": {\n \"type\": \"array\",\n \"minItems\": 1,\n \"uniqueItems\": true,\n \"items\": { \"$ref\": \"#/$defs/typeName\" }\n },\n \"sharedQuery\": {\n \"type\": \"object\",\n \"properties\": {\n \"types\": { \"$ref\": \"#/$defs/typeList\" },\n \"where\": { \"$ref\": \"#/$defs/expression\" },\n \"context\": { \"$ref\": \"#/$defs/viewContext\" },\n \"projections\": { \"$ref\": \"#/$defs/projectionSet\" }\n },\n \"patternProperties\": {\n \"^x-[A-Za-z][A-Za-z0-9._:-]{0,127}$\": { \"$ref\": \"#/$defs/extension\" }\n },\n \"additionalProperties\": false\n },\n \"viewContext\": {\n \"type\": \"object\",\n \"required\": [\"this\"],\n \"properties\": {\n \"this\": { \"$ref\": \"#/$defs/thisContext\" }\n },\n \"patternProperties\": {\n \"^x-[A-Za-z][A-Za-z0-9._:-]{0,127}$\": { \"$ref\": \"#/$defs/extension\" }\n },\n \"additionalProperties\": false\n },\n \"thisContext\": {\n \"type\": \"object\",\n \"properties\": {\n \"on_missing\": {\n \"enum\": [\"view\", \"null\", \"error\"],\n \"default\": \"view\"\n },\n \"types\": { \"$ref\": \"#/$defs/typeList\" }\n },\n \"patternProperties\": {\n \"^x-[A-Za-z][A-Za-z0-9._:-]{0,127}$\": { \"$ref\": \"#/$defs/extension\" }\n },\n \"additionalProperties\": false\n },\n \"projectionSet\": {\n \"type\": \"object\",\n \"propertyNames\": { \"$ref\": \"#/$defs/fieldName\" },\n \"additionalProperties\": { \"$ref\": \"#/$defs/projection\" }\n },\n \"projection\": {\n \"type\": \"object\",\n \"required\": [\"expr\"],\n \"properties\": {\n \"expr\": { \"$ref\": \"#/$defs/expression\" },\n \"description\": { \"type\": \"string\" }\n },\n \"patternProperties\": {\n \"^x-[A-Za-z][A-Za-z0-9._:-]{0,127}$\": { \"$ref\": \"#/$defs/extension\" }\n },\n \"additionalProperties\": false\n },\n \"propertyMetadataSet\": {\n \"type\": \"object\",\n \"propertyNames\": { \"type\": \"string\", \"minLength\": 1 },\n \"additionalProperties\": { \"$ref\": \"#/$defs/propertyMetadata\" }\n },\n \"propertyMetadata\": {\n \"type\": \"object\",\n \"properties\": {\n \"label\": { \"type\": \"string\" },\n \"description\": { \"type\": \"string\" },\n \"format\": { \"type\": \"string\" },\n \"hidden\": { \"type\": \"boolean\" }\n },\n \"patternProperties\": {\n \"^x-[A-Za-z][A-Za-z0-9._:-]{0,127}$\": { \"$ref\": \"#/$defs/extension\" }\n },\n \"additionalProperties\": false\n },\n \"summaryFunctionSet\": {\n \"type\": \"object\",\n \"propertyNames\": { \"$ref\": \"#/$defs/fieldName\" },\n \"additionalProperties\": { \"$ref\": \"#/$defs/summaryFunction\" }\n },\n \"summaryFunction\": {\n \"type\": \"object\",\n \"required\": [\"expr\"],\n \"properties\": {\n \"expr\": { \"$ref\": \"#/$defs/expression\" },\n \"description\": { \"type\": \"string\" }\n },\n \"patternProperties\": {\n \"^x-[A-Za-z][A-Za-z0-9._:-]{0,127}$\": { \"$ref\": \"#/$defs/extension\" }\n },\n \"additionalProperties\": false\n },\n \"view\": {\n \"type\": \"object\",\n \"required\": [\"id\", \"name\"],\n \"properties\": {\n \"id\": { \"$ref\": \"#/$defs/identifier\" },\n \"name\": { \"type\": \"string\", \"minLength\": 1 },\n \"description\": { \"type\": \"string\" },\n \"types\": { \"$ref\": \"#/$defs/typeList\" },\n \"where\": { \"$ref\": \"#/$defs/expression\" },\n \"context\": { \"$ref\": \"#/$defs/viewContext\" },\n \"projections\": { \"$ref\": \"#/$defs/projectionSet\" },\n \"select\": { \"$ref\": \"#/$defs/select\" },\n \"order_by\": { \"$ref\": \"#/$defs/orderBy\" },\n \"group_by\": { \"$ref\": \"#/$defs/groupBy\" },\n \"summaries\": { \"$ref\": \"#/$defs/summaries\" },\n \"limit\": { \"type\": \"integer\", \"minimum\": 0 },\n \"offset\": { \"type\": \"integer\", \"minimum\": 0 },\n \"include_body\": { \"type\": \"boolean\", \"default\": false },\n \"frontmatter_mode\": {\n \"enum\": [\"effective\", \"persisted\", \"both\"],\n \"default\": \"effective\"\n },\n \"presentation\": { \"$ref\": \"#/$defs/presentation\" }\n },\n \"patternProperties\": {\n \"^x-[A-Za-z][A-Za-z0-9._:-]{0,127}$\": { \"$ref\": \"#/$defs/extension\" }\n },\n \"additionalProperties\": false\n },\n \"select\": {\n \"type\": \"array\",\n \"minItems\": 1,\n \"items\": {\n \"oneOf\": [\n { \"type\": \"string\", \"minLength\": 1 },\n { \"$ref\": \"#/$defs/selectExpression\" }\n ]\n }\n },\n \"selectExpression\": {\n \"type\": \"object\",\n \"required\": [\"name\", \"expr\"],\n \"properties\": {\n \"name\": { \"$ref\": \"#/$defs/fieldName\" },\n \"expr\": { \"$ref\": \"#/$defs/expression\" },\n \"label\": { \"type\": \"string\" },\n \"description\": { \"type\": \"string\" }\n },\n \"patternProperties\": {\n \"^x-[A-Za-z][A-Za-z0-9._:-]{0,127}$\": { \"$ref\": \"#/$defs/extension\" }\n },\n \"additionalProperties\": false\n },\n \"orderBy\": {\n \"type\": \"array\",\n \"minItems\": 1,\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"field\"],\n \"properties\": {\n \"field\": { \"type\": \"string\", \"minLength\": 1 },\n \"direction\": { \"enum\": [\"asc\", \"desc\"], \"default\": \"asc\" }\n },\n \"additionalProperties\": false\n }\n },\n \"groupBy\": {\n \"type\": \"array\",\n \"minItems\": 1,\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"field\"],\n \"properties\": {\n \"field\": { \"type\": \"string\", \"minLength\": 1 },\n \"direction\": { \"enum\": [\"asc\", \"desc\"], \"default\": \"asc\" }\n },\n \"additionalProperties\": false\n }\n },\n \"summaries\": {\n \"type\": \"array\",\n \"minItems\": 1,\n \"items\": { \"$ref\": \"#/$defs/summary\" }\n },\n \"summary\": {\n \"type\": \"object\",\n \"required\": [\"field\", \"function\"],\n \"properties\": {\n \"field\": { \"type\": \"string\", \"minLength\": 1 },\n \"function\": { \"$ref\": \"#/$defs/identifier\" },\n \"name\": { \"$ref\": \"#/$defs/fieldName\" },\n \"label\": { \"type\": \"string\" }\n },\n \"additionalProperties\": false\n },\n \"presentation\": {\n \"type\": \"object\",\n \"required\": [\"type\"],\n \"properties\": {\n \"type\": { \"$ref\": \"#/$defs/identifier\" },\n \"fallback\": { \"$ref\": \"#/$defs/identifier\" },\n \"mappings\": {\n \"type\": \"object\",\n \"propertyNames\": { \"$ref\": \"#/$defs/fieldName\" },\n \"additionalProperties\": { \"type\": \"string\", \"minLength\": 1 }\n },\n \"options\": {\n \"type\": \"object\",\n \"additionalProperties\": true\n }\n },\n \"patternProperties\": {\n \"^x-[A-Za-z][A-Za-z0-9._:-]{0,127}$\": { \"$ref\": \"#/$defs/extension\" }\n },\n \"additionalProperties\": false\n }\n }\n}\n" + }, + { + "source": "contracts/mdbase.view/1.0.0.md", + "document": "---\nkind: mdbase.contract\ncontract_type: record\nid: mdbase.view\nversion: 1.0.0\nname: Saved view\ndescription: Shared query scope and stable named views executable through the Query profile.\nrecord_schema:\n dialect: json-schema-2020-12\n ref: ../../schemas/mdbase.view/1.0.0.schema.json\n---\n\n# Saved view\n\nA record exposed through `mdbase.view` stores shared query scope and one or more\nstable named views. Each named view resolves to the query model in Chapter 11.\nOptional `presentation` metadata is advisory and does not alter headless query\nresults.\n\nImplementing this contract is how a collection declares which of its records\nare saved views. A tool that advertises `view_records` discovers and executes\nviews through this contract's type implementations, not through a reserved type\nname, path, or frontmatter value.\n\nThis artifact is passive. Implementing it grants no authority to read, execute,\nor modify any record.\n" + }, + { + "source": "types/view/1.md", + "document": "---\nkind: mdbase.type\nname: view\nversion: 1\ndescription: 'Saved views: shared query scope and stable named views'\nmatch:\n where:\n type: view\nschema:\n dialect: json-schema-2020-12\n value:\n $schema: https://json-schema.org/draft/2020-12/schema\n title: View\n description: 'Saved views: shared query scope and stable named views.'\n type: object\n required:\n - type\n - id\n - version\n - name\n - views\n properties:\n type:\n const: view\n id:\n $ref: '#/$defs/identifier'\n version:\n type: integer\n minimum: 1\n name:\n type: string\n minLength: 1\n description:\n type: string\n query:\n $ref: '#/$defs/sharedQuery'\n properties:\n $ref: '#/$defs/propertyMetadataSet'\n summary_functions:\n $ref: '#/$defs/summaryFunctionSet'\n views:\n type: array\n minItems: 1\n items:\n $ref: '#/$defs/view'\n patternProperties:\n ^x-[A-Za-z][A-Za-z0-9._:-]{0,127}$:\n $ref: '#/$defs/extension'\n additionalProperties: false\n $defs:\n identifier:\n type: string\n pattern: ^[A-Za-z][A-Za-z0-9._:-]*$\n typeName:\n type: string\n pattern: ^[A-Za-z][A-Za-z0-9_-]{0,127}$\n fieldName:\n type: string\n pattern: ^[A-Za-z_][A-Za-z0-9_:-]*$\n expression:\n type: string\n minLength: 1\n extension:\n type: object\n additionalProperties: true\n typeList:\n type: array\n minItems: 1\n uniqueItems: true\n items:\n $ref: '#/$defs/typeName'\n sharedQuery:\n type: object\n properties:\n types:\n $ref: '#/$defs/typeList'\n where:\n $ref: '#/$defs/expression'\n context:\n $ref: '#/$defs/viewContext'\n projections:\n $ref: '#/$defs/projectionSet'\n patternProperties:\n ^x-[A-Za-z][A-Za-z0-9._:-]{0,127}$:\n $ref: '#/$defs/extension'\n additionalProperties: false\n viewContext:\n type: object\n required:\n - this\n properties:\n this:\n $ref: '#/$defs/thisContext'\n patternProperties:\n ^x-[A-Za-z][A-Za-z0-9._:-]{0,127}$:\n $ref: '#/$defs/extension'\n additionalProperties: false\n thisContext:\n type: object\n properties:\n on_missing:\n enum:\n - view\n - 'null'\n - error\n default: view\n types:\n $ref: '#/$defs/typeList'\n patternProperties:\n ^x-[A-Za-z][A-Za-z0-9._:-]{0,127}$:\n $ref: '#/$defs/extension'\n additionalProperties: false\n projectionSet:\n type: object\n propertyNames:\n $ref: '#/$defs/fieldName'\n additionalProperties:\n $ref: '#/$defs/projection'\n projection:\n type: object\n required:\n - expr\n properties:\n expr:\n $ref: '#/$defs/expression'\n description:\n type: string\n patternProperties:\n ^x-[A-Za-z][A-Za-z0-9._:-]{0,127}$:\n $ref: '#/$defs/extension'\n additionalProperties: false\n propertyMetadataSet:\n type: object\n propertyNames:\n type: string\n minLength: 1\n additionalProperties:\n $ref: '#/$defs/propertyMetadata'\n propertyMetadata:\n type: object\n properties:\n label:\n type: string\n description:\n type: string\n format:\n type: string\n hidden:\n type: boolean\n patternProperties:\n ^x-[A-Za-z][A-Za-z0-9._:-]{0,127}$:\n $ref: '#/$defs/extension'\n additionalProperties: false\n summaryFunctionSet:\n type: object\n propertyNames:\n $ref: '#/$defs/fieldName'\n additionalProperties:\n $ref: '#/$defs/summaryFunction'\n summaryFunction:\n type: object\n required:\n - expr\n properties:\n expr:\n $ref: '#/$defs/expression'\n description:\n type: string\n patternProperties:\n ^x-[A-Za-z][A-Za-z0-9._:-]{0,127}$:\n $ref: '#/$defs/extension'\n additionalProperties: false\n view:\n type: object\n required:\n - id\n - name\n properties:\n id:\n $ref: '#/$defs/identifier'\n name:\n type: string\n minLength: 1\n description:\n type: string\n types:\n $ref: '#/$defs/typeList'\n where:\n $ref: '#/$defs/expression'\n context:\n $ref: '#/$defs/viewContext'\n projections:\n $ref: '#/$defs/projectionSet'\n select:\n $ref: '#/$defs/select'\n order_by:\n $ref: '#/$defs/orderBy'\n group_by:\n $ref: '#/$defs/groupBy'\n summaries:\n $ref: '#/$defs/summaries'\n limit:\n type: integer\n minimum: 0\n offset:\n type: integer\n minimum: 0\n include_body:\n type: boolean\n default: false\n frontmatter_mode:\n enum:\n - effective\n - persisted\n - both\n default: effective\n presentation:\n $ref: '#/$defs/presentation'\n patternProperties:\n ^x-[A-Za-z][A-Za-z0-9._:-]{0,127}$:\n $ref: '#/$defs/extension'\n additionalProperties: false\n select:\n type: array\n minItems: 1\n items:\n oneOf:\n - type: string\n minLength: 1\n - $ref: '#/$defs/selectExpression'\n selectExpression:\n type: object\n required:\n - name\n - expr\n properties:\n name:\n $ref: '#/$defs/fieldName'\n expr:\n $ref: '#/$defs/expression'\n label:\n type: string\n description:\n type: string\n patternProperties:\n ^x-[A-Za-z][A-Za-z0-9._:-]{0,127}$:\n $ref: '#/$defs/extension'\n additionalProperties: false\n orderBy:\n type: array\n minItems: 1\n items:\n type: object\n required:\n - field\n properties:\n field:\n type: string\n minLength: 1\n direction:\n enum:\n - asc\n - desc\n default: asc\n additionalProperties: false\n groupBy:\n type: array\n minItems: 1\n items:\n type: object\n required:\n - field\n properties:\n field:\n type: string\n minLength: 1\n direction:\n enum:\n - asc\n - desc\n default: asc\n additionalProperties: false\n summaries:\n type: array\n minItems: 1\n items:\n $ref: '#/$defs/summary'\n summary:\n type: object\n required:\n - field\n - function\n properties:\n field:\n type: string\n minLength: 1\n function:\n $ref: '#/$defs/identifier'\n name:\n $ref: '#/$defs/fieldName'\n label:\n type: string\n additionalProperties: false\n presentation:\n type: object\n required:\n - type\n properties:\n type:\n $ref: '#/$defs/identifier'\n fallback:\n $ref: '#/$defs/identifier'\n mappings:\n type: object\n propertyNames:\n $ref: '#/$defs/fieldName'\n additionalProperties:\n type: string\n minLength: 1\n options:\n type: object\n additionalProperties: true\n patternProperties:\n ^x-[A-Za-z][A-Za-z0-9._:-]{0,127}$:\n $ref: '#/$defs/extension'\n additionalProperties: false\nimplements:\n- contract: mdbase.view\n version: 1.0.0\n fields:\n id: id\n version: version\n name: name\n description: description\n query: query\n properties: properties\n summary_functions: summary_functions\n views: views\ncollection:\n display:\n name_field: name\n---\n\n# View\n\nA view record stores shared query scope and one or more stable named views.\nTools that support saved views discover these records through the `mdbase.view`\ncontract and run each named view as an ordinary query.\n\nThis type belongs to your collection once installed. You can rename fields or\nchange how records are matched, as long as the `implements` mapping still\npoints each contract field at the right frontmatter field.\n" + } + ], + "provides": [ + { + "id": "mdbase.view", + "version": "1.0.0", + "digest": "sha256:0918acefce08cddd778fab768fe4f2e4615d263a0eec3e5330b6897c686d744e" + } + ] +} diff --git a/vendor/mdbase-contracts/obsidian.base-1.0.0.json b/vendor/mdbase-contracts/obsidian.base-1.0.0.json new file mode 100644 index 00000000..d2648ab9 --- /dev/null +++ b/vendor/mdbase-contracts/obsidian.base-1.0.0.json @@ -0,0 +1,42 @@ +{ + "manifest": { + "kind": "mdbase.type-pack", + "id": "obsidian.base", + "version": "1.0.0", + "name": "Obsidian Bases", + "description": "The obsidian.base contract and an editable type that stores Obsidian Bases as records.", + "resources": [ + { + "kind": "contract", + "mode": "managed", + "source": "contracts/obsidian.base/1.0.0.md", + "target": "_contracts/obsidian.base/1.0.0.md", + "digest": "sha256:fca77a9c845223784d4af7f5e13e2f149295621ad1b794ea9bb4d99f604467e8" + }, + { + "kind": "type", + "mode": "seed", + "source": "types/obsidian-base/1.md", + "target": "_types/obsidian_base.md", + "digest": "sha256:bfeb3880200bc1bef7fd50b777d3f927ce931d08b4fb3c30d4cddb55feaf3f92" + } + ] + }, + "resources": [ + { + "source": "contracts/obsidian.base/1.0.0.md", + "document": "---\nkind: mdbase.contract\ncontract_type: record\nid: obsidian.base\nversion: 1.0.0\nname: Obsidian Base\ndescription: An Obsidian Bases saved-view source stored as a YAML document record.\nrecord_schema:\n dialect: json-schema-2020-12\n value:\n $schema: https://json-schema.org/draft/2020-12/schema\n type: object\n required: [views]\n properties:\n filters: {}\n formulas: { type: object }\n properties: { type: object }\n views:\n type: array\n minItems: 1\n items: { type: object, required: [type], properties: { type: { type: string }, name: { type: string } } }\n---\n\n# Obsidian Base\n\nA record exposed through `obsidian.base` is an Obsidian Bases source: global\nfilters, formulas, property metadata, and one or more views. The schema is\ndeliberately permissive. Obsidian owns the format and adds keys over time;\nunknown keys are preserved and ignored.\n\nTools that advertise `obsidian_bases_views` discover and execute these records\nthrough this contract and evaluate them with the Obsidian Bases expression\ndialect described in the [Obsidian Bases adapter](https://mdbase.dev/spec/adapters/obsidian-bases).\n\nThis artifact is passive. Implementing it grants no authority to read, execute,\nor modify any record.\n" + }, + { + "source": "types/obsidian-base/1.md", + "document": "---\nkind: mdbase.type\nname: obsidian_base\nversion: 1\ndescription: Obsidian Bases saved views stored as records\nmatch:\n path_glob: '**/*.base'\nschema:\n dialect: json-schema-2020-12\n value:\n $schema: https://json-schema.org/draft/2020-12/schema\n title: Obsidian Base\n description: An Obsidian Bases file. Obsidian owns the format; unknown keys are kept.\n type: object\n required:\n - views\n properties:\n filters: {}\n formulas:\n type: object\n properties:\n type: object\n views:\n type: array\n minItems: 1\n items:\n type: object\n required:\n - type\n properties:\n type:\n type: string\n name:\n type: string\nimplements:\n - contract: obsidian.base\n version: 1.0.0\n fields:\n filters: filters\n formulas: formulas\n properties: properties\n views: views\n---\n\n# Obsidian Base\n\nMakes `.base` files discoverable as saved views through the `obsidian.base`\ncontract. It takes effect when the collection lists `base` in\n`settings.record_extensions`; narrow the `path_glob` to the folders that hold\nyour Bases. Edit Bases with whole-document replacement so Obsidian's comments\nand layout survive.\n" + } + ], + "provides": [ + { + "id": "obsidian.base", + "version": "1.0.0", + "digest": "sha256:37e8a0bfb32bb4795c1b3c5dec613c32429aaa765dc33e4adc0c627a48f5c1ff" + } + ] +}