diff --git a/apps/extension/scripts/write-mdbase-manifest.mjs b/apps/extension/scripts/write-mdbase-manifest.mjs index e983327..0444e85 100644 --- a/apps/extension/scripts/write-mdbase-manifest.mjs +++ b/apps/extension/scripts/write-mdbase-manifest.mjs @@ -1,7 +1,7 @@ import { mkdir, writeFile } from "node:fs/promises"; import { resolve } from "node:path"; -import { format } from "prettier"; +import { format, resolveConfig } from "prettier"; import { extensionEnvironment } from "./extension-manifest.mjs"; import { buildReaderManifest } from "../../reader/scripts/reader-manifest.mjs"; @@ -21,4 +21,7 @@ const manifest = { }; const target = resolve(root, "src/generated/mdbase-app.json"); await mkdir(resolve(target, ".."), { recursive: true }); -await writeFile(target, await format(JSON.stringify(manifest), { parser: "json" })); +await writeFile( + target, + await format(JSON.stringify(manifest), { ...(await resolveConfig(target)), parser: "json" }), +); diff --git a/apps/extension/src/generated/mdbase-app.json b/apps/extension/src/generated/mdbase-app.json index d783f5d..8b8d969 100644 --- a/apps/extension/src/generated/mdbase-app.json +++ b/apps/extension/src/generated/mdbase-app.json @@ -14,22 +14,18 @@ "id": "dev.mdbase.reader.annotation", "version": "1.0.0-beta.1", "digest": "sha256:9593376fed19903b07013ce0167b1d25ff0517364c826ddb8ecf3b4b7d83ef7c" + }, + { + "id": "mdbase.view", + "version": "1.0.0", + "digest": "sha256:0918acefce08cddd778fab768fe4f2e4615d263a0eec3e5330b6897c686d744e" } ], "capabilities": { "contract_version": 2, - "required": [ - "collection.read", - "records.create", - "records.edit", - "records.delete", - "views.manage" - ] + "required": ["collection.read", "records.create", "records.edit", "records.delete"] }, - "files": { - "required": ["list", "read", "add"], - "scope": { "kind": "collection" } - } + "files": { "required": ["list", "read", "add"], "scope": { "kind": "collection" } } }, "provisions": { "type_packs": [ @@ -101,6 +97,59 @@ "document": "---\nkind: mdbase.type\nname: reader-annotation\nversion: 1\ndescription: An independently addressable mdbase Reader annotation.\nschema:\n dialect: json-schema-2020-12\n value:\n $schema: https://json-schema.org/draft/2020-12/schema\n type: object\n additionalProperties: true\n required: [type, id, source, annotation_type, created_at]\n properties:\n type: { const: reader-annotation }\n id: { type: string, minLength: 1 }\n source: { type: string, minLength: 1 }\n document:\n type: object\n additionalProperties: false\n required: [file_id, file, revision]\n properties:\n file_id: { type: string, minLength: 1 }\n file: { type: string, minLength: 1 }\n revision: { type: string, pattern: \"^sha256:[0-9a-f]{64}$\" }\n annotation_type: { type: string, minLength: 1 }\n motivation: { type: string }\n color: { type: string }\n locator:\n type: object\n additionalProperties: true\n target:\n type: object\n additionalProperties: true\n tags:\n type: array\n items: { type: string }\n created_at: { type: string, format: date-time }\n modified_at: { type: string, format: date-time }\n created_by: { type: string }\ncollection:\n unique:\n - field: id\n scope: type\n links:\n source:\n target_type: reader-source\n validate_exists: true\n document.file:\n target_type: any\n validate_exists: true\nimplements:\n - contract: dev.mdbase.reader.annotation\n version: 1.0.0-beta.1\n fields:\n id: id\n source: source\n document: document\n annotation_type: annotation_type\n motivation: motivation\n color: color\n locator: locator\n target: target\n tags: tags\n created_at: created_at\n modified_at: modified_at\n created_by: created_by\n---\n\n# Reader annotation\n\nThe body contains a readable quotation, an optional note, or both.\n" } ] + }, + { + "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/apps/reader/mdbase/packs/mdbase.view-1.0.0.json b/apps/reader/mdbase/packs/mdbase.view-1.0.0.json new file mode 100644 index 0000000..210fa58 --- /dev/null +++ b/apps/reader/mdbase/packs/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/apps/reader/public/.well-known/mdbase-app.json b/apps/reader/public/.well-known/mdbase-app.json index 44954b7..871d31e 100644 --- a/apps/reader/public/.well-known/mdbase-app.json +++ b/apps/reader/public/.well-known/mdbase-app.json @@ -17,22 +17,18 @@ "id": "dev.mdbase.reader.annotation", "version": "1.0.0-beta.1", "digest": "sha256:9593376fed19903b07013ce0167b1d25ff0517364c826ddb8ecf3b4b7d83ef7c" + }, + { + "id": "mdbase.view", + "version": "1.0.0", + "digest": "sha256:0918acefce08cddd778fab768fe4f2e4615d263a0eec3e5330b6897c686d744e" } ], "capabilities": { "contract_version": 2, - "required": [ - "collection.read", - "records.create", - "records.edit", - "records.delete", - "views.manage" - ] + "required": ["collection.read", "records.create", "records.edit", "records.delete"] }, - "files": { - "required": ["list", "read", "add"], - "scope": { "kind": "collection" } - } + "files": { "required": ["list", "read", "add"], "scope": { "kind": "collection" } } }, "provisions": { "type_packs": [ @@ -104,6 +100,59 @@ "document": "---\nkind: mdbase.type\nname: reader-annotation\nversion: 1\ndescription: An independently addressable mdbase Reader annotation.\nschema:\n dialect: json-schema-2020-12\n value:\n $schema: https://json-schema.org/draft/2020-12/schema\n type: object\n additionalProperties: true\n required: [type, id, source, annotation_type, created_at]\n properties:\n type: { const: reader-annotation }\n id: { type: string, minLength: 1 }\n source: { type: string, minLength: 1 }\n document:\n type: object\n additionalProperties: false\n required: [file_id, file, revision]\n properties:\n file_id: { type: string, minLength: 1 }\n file: { type: string, minLength: 1 }\n revision: { type: string, pattern: \"^sha256:[0-9a-f]{64}$\" }\n annotation_type: { type: string, minLength: 1 }\n motivation: { type: string }\n color: { type: string }\n locator:\n type: object\n additionalProperties: true\n target:\n type: object\n additionalProperties: true\n tags:\n type: array\n items: { type: string }\n created_at: { type: string, format: date-time }\n modified_at: { type: string, format: date-time }\n created_by: { type: string }\ncollection:\n unique:\n - field: id\n scope: type\n links:\n source:\n target_type: reader-source\n validate_exists: true\n document.file:\n target_type: any\n validate_exists: true\nimplements:\n - contract: dev.mdbase.reader.annotation\n version: 1.0.0-beta.1\n fields:\n id: id\n source: source\n document: document\n annotation_type: annotation_type\n motivation: motivation\n color: color\n locator: locator\n target: target\n tags: tags\n created_at: created_at\n modified_at: modified_at\n created_by: created_by\n---\n\n# Reader annotation\n\nThe body contains a readable quotation, an optional note, or both.\n" } ] + }, + { + "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/apps/reader/scripts/reader-manifest.mjs b/apps/reader/scripts/reader-manifest.mjs index 4c7f763..a094415 100644 --- a/apps/reader/scripts/reader-manifest.mjs +++ b/apps/reader/scripts/reader-manifest.mjs @@ -8,6 +8,9 @@ import { parse as parseYaml } from "yaml"; export const READER_TYPE_PACK_VERSION = "1.0.0-beta.1"; const projectRoot = resolve(import.meta.dirname, ".."); +// Reader saves library views as mdbase.view records; this is the published +// mdbase-contracts provision, embedded byte-for-byte. +const viewPackPath = resolve(projectRoot, "mdbase", "packs", "mdbase.view-1.0.0.json"); const resources = [ { kind: "contract", @@ -65,6 +68,8 @@ export async function buildReaderManifest({ }; }); + const viewPack = JSON.parse(await readFile(viewPackPath, "utf8")); + return { manifest_version: 1, id: "dev.mdbase.reader", @@ -74,18 +79,12 @@ export async function buildReaderManifest({ redirect_uris: [appUrl], requirements: { access: "full_collection", - contracts, + contracts: [...contracts, ...viewPack.provides], // Contract readiness and collection setup are derived from `contracts` and // `provisions`; version 2 has no capability identifiers for them. capabilities: { contract_version: 2, - required: [ - "collection.read", - "records.create", - "records.edit", - "records.delete", - "views.manage", - ], + required: ["collection.read", "records.create", "records.edit", "records.delete"], }, files: { required: ["list", "read", "add"], @@ -106,6 +105,7 @@ export async function buildReaderManifest({ }, resources: packResources.map(({ source, document }) => ({ source, document })), }, + viewPack, ], }, }; diff --git a/apps/reader/scripts/verify-manifest.mjs b/apps/reader/scripts/verify-manifest.mjs index d4413c1..d866bb4 100644 --- a/apps/reader/scripts/verify-manifest.mjs +++ b/apps/reader/scripts/verify-manifest.mjs @@ -13,36 +13,49 @@ const validation = validateAppManifest(manifest); if (!validation.valid) { fail(`Reader manifest is invalid: ${formatValidationIssues(validation.issues)}`); } -const [pack] = manifest.provisions?.type_packs ?? []; -if (!pack || manifest.requirements.contracts.length !== 2) { - fail("Reader must provide one type pack containing its two required contracts."); +const packs = manifest.provisions?.type_packs ?? []; +const provided = packs.flatMap((pack) => + pack.provides.map(({ id, version }) => `${id}@${version}`), +); +const required = manifest.requirements.contracts.map(({ id, version }) => `${id}@${version}`); +if (provided.sort().join() !== required.sort().join()) { + fail("Reader's type packs must provide exactly its required contracts."); } -const documents = new Map(pack.resources.map((resource) => [resource.source, resource.document])); -for (const resource of pack.manifest.resources) { - const document = documents.get(resource.source); - if (!document) { - fail(`Type-pack source '${resource.source}' is missing.`); - } - if (digest(document) !== resource.digest) { - fail(`Type-pack source '${resource.source}' does not match its digest.`); +for (const pack of packs) { + const documents = new Map(pack.resources.map((resource) => [resource.source, resource.document])); + for (const resource of pack.manifest.resources) { + const document = documents.get(resource.source); + if (!document) { + fail(`Type-pack source '${resource.source}' is missing.`); + } + if (digest(document) !== resource.digest) { + fail(`Type-pack source '${resource.source}' does not match its digest.`); + } } } const collectionRoot = await mkdtemp(join(tmpdir(), "mdbase-reader-pack-")); try { await writeFile(join(collectionRoot, "mdbase.yaml"), "spec_version: 0.3.0\n"); - const provision = { manifest: pack.manifest, resources: pack.resources }; - const assessment = await assessTypePack(collectionRoot, provision, { - installedBy: manifest.id, - }); - assertValid(assessment, "assessment"); - const installed = await applyTypePack(collectionRoot, provision, { - installedBy: manifest.id, - expectedAssessmentDigest: assessment.result.assessment_digest, - }); - assertValid(installed, "installation"); + for (const pack of packs) { + const provision = { manifest: pack.manifest, resources: pack.resources }; + const assessment = await assessTypePack(collectionRoot, provision, { + installedBy: manifest.id, + }); + assertValid(assessment, "assessment"); + const installed = await applyTypePack(collectionRoot, provision, { + installedBy: manifest.id, + expectedAssessmentDigest: assessment.result.assessment_digest, + }); + assertValid(installed, "installation"); + } + await mkdir(join(collectionRoot, "views")); + await writeFile( + join(collectionRoot, "views", "example.md"), + "---\ntype: view\nid: reader.library.example\nversion: 1\nname: Example\nviews:\n - id: all\n name: All\n---\n", + ); await mkdir(join(collectionRoot, "sources")); await writeFile( join(collectionRoot, "sources", "example.md"), @@ -81,6 +94,14 @@ try { if (!source.valid || source.view.title !== "Example source") { fail("Reader source contract did not project a valid source view."); } + const view = await opened.collection.getContractView( + "views/example.md", + "mdbase.view", + "1.0.0", + ); + if (!view.valid || view.view.name !== "Example") { + fail("A Reader view record did not project a valid mdbase.view contract view."); + } } finally { await opened.collection.close(); } diff --git a/apps/reader/scripts/write-manifest.mjs b/apps/reader/scripts/write-manifest.mjs index 0d1b973..96bdaa6 100644 --- a/apps/reader/scripts/write-manifest.mjs +++ b/apps/reader/scripts/write-manifest.mjs @@ -1,7 +1,7 @@ import { mkdir, writeFile } from "node:fs/promises"; import { resolve } from "node:path"; -import { format } from "prettier"; +import { format, resolveConfig } from "prettier"; import { buildReaderManifest } from "./reader-manifest.mjs"; @@ -10,7 +10,10 @@ const manifest = await buildReaderManifest({ origin: process.env.MDBASE_READER_ORIGIN ?? "https://reader.mdbase.dev", basePath: process.env.MDBASE_READER_BASE_PATH ?? "/", }); -const document = await format(JSON.stringify(manifest), { parser: "json" }); +const document = await format(JSON.stringify(manifest), { + ...(await resolveConfig(projectRoot)), + parser: "json", +}); const targets = [ resolve(projectRoot, "public", ".well-known", "mdbase-app.json"), resolve(projectRoot, "src", "generated", "mdbase-app.json"), diff --git a/apps/reader/src/connect-workspace-views.test.ts b/apps/reader/src/connect-workspace-views.test.ts index a24d0ff..95f1718 100644 --- a/apps/reader/src/connect-workspace-views.test.ts +++ b/apps/reader/src/connect-workspace-views.test.ts @@ -23,7 +23,7 @@ function document(id: string, views: NamedView[]): ViewDocument { id, name: id, source: { - path: `views/${id}.mdbase.view`, + path: `views/${id}.md`, format: "mdbase.view", revision: "rev-1", writable: true, @@ -81,8 +81,8 @@ describe("Reader saved-view discovery", () => { expect(result.map(({ name }) => name)).toEqual(["All sources", "Reading queue", "Finished"]); expect(result[0]).toEqual(defaultLibraryView); expect(result[1]).toMatchObject({ - key: "views/shared.mdbase.view::Reading queue", - path: "views/shared.mdbase.view", + key: "views/shared.md::Reading queue", + path: "views/shared.md", revision: "rev-1", writable: true, owned: true, @@ -116,29 +116,24 @@ describe("Reader saved-view discovery", () => { }); it("can still reopen a newly saved Reader-marked view", async () => { - const path = "views/saved.mdbase.view"; + const path = "views/reader.library.reading-queue.md"; const views = repository([ - document("saved", [ + document("reader.library.reading-queue", [ namedView("Reading queue", { type: "table", options: { readerViewVersion: 1 }, }), ]), ]); - vi.mocked(views.save).mockResolvedValue({ - path, - format: "mdbase.view", - revision: "rev-1", - document: "", - }); + vi.mocked(views.save).mockResolvedValue({ path, revision: "rev-1" }); const result = await gateway(views).saveLibraryView({ name: "Reading queue", configuration: defaultLibraryViewConfiguration, }); expect(result).toMatchObject({ path, name: "Reading queue", owned: true }); expect(views.save).toHaveBeenCalledWith({ - name: "Reading queue", - document: expect.stringContaining("readerViewVersion: 1"), + path, + frontmatter: expect.objectContaining({ type: "view", name: "Reading queue" }), }); }); }); diff --git a/apps/reader/src/connect-workspace.ts b/apps/reader/src/connect-workspace.ts index c6cdb62..d545726 100644 --- a/apps/reader/src/connect-workspace.ts +++ b/apps/reader/src/connect-workspace.ts @@ -13,13 +13,12 @@ import { import { annotationViewConfiguration, - buildAnnotationViewDocument, - buildSourceAnnotationsViewDocument, + buildAnnotationView, + buildSourceAnnotationsView, readerViewKind, - sourceAnnotationsViewName, } from "./mdbase-annotation-views.js"; import { - buildLibraryViewDocument, + buildLibraryView, defaultLibraryView, libraryViewConfiguration, libraryViewKey, @@ -34,7 +33,7 @@ import type { ReaderLibrarySnapshot, ReaderWorkspaceGateway, } from "./workspace-model.js"; -import type { LibraryViewRepository } from "@mdbase-reader/connect"; +import type { JsonObject, LibraryViewRepository } from "@mdbase-reader/connect"; import type { Annotation, AnnotationDeletionPlan, @@ -200,17 +199,22 @@ export class ConnectWorkspaceGateway implements ReaderWorkspaceGateway { if (!this.libraryViewRepository) { throw new Error("Saved mdbase views are unavailable for this collection."); } - const saved = await this.libraryViewRepository.save({ - document: await (request.annotations - ? buildAnnotationViewDocument({ - name: request.name, - configuration: request.annotations, - ...(request.fieldShapes ? { fieldShapes: request.fieldShapes } : {}), - }) - : buildLibraryViewDocument(request)), - ...(request.existing?.path ? { path: request.existing.path } : { name: request.name }), - ...(request.existing?.revision ? { revision: request.existing.revision } : {}), - }); + const frontmatter = request.annotations + ? buildAnnotationView({ + name: request.name, + configuration: request.annotations, + ...(request.fieldShapes ? { fieldShapes: request.fieldShapes } : {}), + }) + : buildLibraryView(request); + const saved = await this.libraryViewRepository.save( + request.existing?.path + ? { + path: request.existing.path, + frontmatter, + replace: request.existing.revision ? { revision: request.existing.revision } : {}, + } + : { path: viewPath(frontmatter), frontmatter }, + ); const views = await this.listLibraryViews(); const match = views.find( (view) => @@ -251,9 +255,10 @@ export class ConnectWorkspaceGateway implements ReaderWorkspaceGateway { if (existing) { return { path: existing.source.path, created: false }; } + const frontmatter = buildSourceAnnotationsView(); const saved = await this.libraryViewRepository.save({ - document: await buildSourceAnnotationsViewDocument(), - name: sourceAnnotationsViewName, + path: viewPath(frontmatter), + frontmatter, }); return { path: saved.path, created: true }; } @@ -596,3 +601,8 @@ function replaceInOrder( ): readonly SourceSummary[] { return sources.map((source) => (source.id === replacement.id ? replacement : source)); } + +/** New views live beside each other under `views/`, named by their stable view ID. */ +function viewPath(frontmatter: JsonObject): string { + return `views/${String(frontmatter["id"])}.md`; +} diff --git a/apps/reader/src/generated/mdbase-app.json b/apps/reader/src/generated/mdbase-app.json index 44954b7..871d31e 100644 --- a/apps/reader/src/generated/mdbase-app.json +++ b/apps/reader/src/generated/mdbase-app.json @@ -17,22 +17,18 @@ "id": "dev.mdbase.reader.annotation", "version": "1.0.0-beta.1", "digest": "sha256:9593376fed19903b07013ce0167b1d25ff0517364c826ddb8ecf3b4b7d83ef7c" + }, + { + "id": "mdbase.view", + "version": "1.0.0", + "digest": "sha256:0918acefce08cddd778fab768fe4f2e4615d263a0eec3e5330b6897c686d744e" } ], "capabilities": { "contract_version": 2, - "required": [ - "collection.read", - "records.create", - "records.edit", - "records.delete", - "views.manage" - ] + "required": ["collection.read", "records.create", "records.edit", "records.delete"] }, - "files": { - "required": ["list", "read", "add"], - "scope": { "kind": "collection" } - } + "files": { "required": ["list", "read", "add"], "scope": { "kind": "collection" } } }, "provisions": { "type_packs": [ @@ -104,6 +100,59 @@ "document": "---\nkind: mdbase.type\nname: reader-annotation\nversion: 1\ndescription: An independently addressable mdbase Reader annotation.\nschema:\n dialect: json-schema-2020-12\n value:\n $schema: https://json-schema.org/draft/2020-12/schema\n type: object\n additionalProperties: true\n required: [type, id, source, annotation_type, created_at]\n properties:\n type: { const: reader-annotation }\n id: { type: string, minLength: 1 }\n source: { type: string, minLength: 1 }\n document:\n type: object\n additionalProperties: false\n required: [file_id, file, revision]\n properties:\n file_id: { type: string, minLength: 1 }\n file: { type: string, minLength: 1 }\n revision: { type: string, pattern: \"^sha256:[0-9a-f]{64}$\" }\n annotation_type: { type: string, minLength: 1 }\n motivation: { type: string }\n color: { type: string }\n locator:\n type: object\n additionalProperties: true\n target:\n type: object\n additionalProperties: true\n tags:\n type: array\n items: { type: string }\n created_at: { type: string, format: date-time }\n modified_at: { type: string, format: date-time }\n created_by: { type: string }\ncollection:\n unique:\n - field: id\n scope: type\n links:\n source:\n target_type: reader-source\n validate_exists: true\n document.file:\n target_type: any\n validate_exists: true\nimplements:\n - contract: dev.mdbase.reader.annotation\n version: 1.0.0-beta.1\n fields:\n id: id\n source: source\n document: document\n annotation_type: annotation_type\n motivation: motivation\n color: color\n locator: locator\n target: target\n tags: tags\n created_at: created_at\n modified_at: modified_at\n created_by: created_by\n---\n\n# Reader annotation\n\nThe body contains a readable quotation, an optional note, or both.\n" } ] + }, + { + "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/apps/reader/src/library-columns.test.ts b/apps/reader/src/library-columns.test.ts index 520acbd..618b1cb 100644 --- a/apps/reader/src/library-columns.test.ts +++ b/apps/reader/src/library-columns.test.ts @@ -1,6 +1,5 @@ import { collectionId, sourceId, type SourceSummary } from "@mdbase-reader/core"; import { describe, expect, it } from "vitest"; -import { parse as parseYaml } from "yaml"; import { columnFieldKey, @@ -12,7 +11,7 @@ import { } from "./library-columns.js"; import { applyLibraryViewConfiguration, - buildLibraryViewDocument, + buildLibraryView, defaultLibraryViewConfiguration, libraryViewConfiguration, } from "./mdbase-library-views.js"; @@ -82,7 +81,7 @@ describe("property columns", () => { }); describe("saved view layout", () => { - it("round-trips property columns, widths and property sorting through the view file", async () => { + it("round-trips property columns, widths and property sorting through the view file", () => { const configuration = { ...defaultLibraryViewConfiguration, columns: ["title", "property:course", "status"] as const, @@ -90,8 +89,7 @@ describe("saved view layout", () => { sortField: "property:course" as const, sortDirection: "asc" as const, }; - const document = await buildLibraryViewDocument({ name: "Courses", configuration }); - const frontmatter = parseYaml(document.split("---")[1] ?? "") as { + const frontmatter = buildLibraryView({ name: "Courses", configuration }) as unknown as { views: [ { select: string[]; order_by: [{ field: string }]; presentation: Record }, ]; diff --git a/apps/reader/src/mdbase-annotation-views.test.ts b/apps/reader/src/mdbase-annotation-views.test.ts index 9365bb4..bbcb71a 100644 --- a/apps/reader/src/mdbase-annotation-views.test.ts +++ b/apps/reader/src/mdbase-annotation-views.test.ts @@ -1,20 +1,15 @@ import { describe, expect, it } from "vitest"; -import { parse as parseYaml } from "yaml"; import { annotationViewConfiguration, annotationViewWhere, - buildAnnotationViewDocument, - buildSourceAnnotationsViewDocument, + buildAnnotationView, + buildSourceAnnotationsView, defaultAnnotationViewConfiguration, readerViewKind, type AnnotationViewConfiguration, } from "./mdbase-annotation-views.js"; -function frontmatter(document: string): Record { - return parseYaml(document.split("---")[1] ?? "") as Record; -} - const configuration: AnnotationViewConfiguration = { columns: ["passage", "source", "created"], columnWidths: { passage: 500 }, @@ -29,9 +24,8 @@ const configuration: AnnotationViewConfiguration = { }; describe("saved annotation views", () => { - it("writes an mdbase view of reader-annotation records that reads back the same", async () => { - const document = await buildAnnotationViewDocument({ name: "Ethics quotes", configuration }); - const view = frontmatter(document); + it("writes an mdbase view of reader-annotation records that reads back the same", () => { + const view = buildAnnotationView({ name: "Ethics quotes", configuration }); expect(view["query"]).toEqual({ types: ["reader-annotation"], projections: { @@ -87,11 +81,8 @@ describe("saved annotation views", () => { expect(readerViewKind(undefined)).toBeNull(); }); - it("writes one view that lists the annotations of the source it runs against", async () => { - const [named] = frontmatter(await buildSourceAnnotationsViewDocument())["views"] as Record< - string, - unknown - >[]; + it("writes one view that lists the annotations of the source it runs against", () => { + const [named] = buildSourceAnnotationsView()["views"] as Record[]; expect(named?.["context"]).toEqual({ this: { on_missing: "error", types: ["reader-source"] }, }); diff --git a/apps/reader/src/mdbase-annotation-views.ts b/apps/reader/src/mdbase-annotation-views.ts index 8c8ad0b..48d86d5 100644 --- a/apps/reader/src/mdbase-annotation-views.ts +++ b/apps/reader/src/mdbase-annotation-views.ts @@ -9,7 +9,8 @@ import { import { emptyAnnotationFilter, type AnnotationFilter } from "./annotation-overview.js"; import { maximumColumnWidth, minimumColumnWidth } from "./library-columns.js"; import { conditionToCel, parseConditions, type FieldShape } from "./library-conditions.js"; -import { viewDocument } from "./mdbase-library-views.js"; + +import type { JsonObject } from "@mdbase-reader/connect"; /** An annotations view as the reader arranged it: columns, sort and filters. */ export interface AnnotationViewConfiguration { @@ -118,7 +119,7 @@ export function annotationViewWhere( return terms.length > 0 ? terms.join(" && ") : null; } -export function buildAnnotationViewDocument(request: AnnotationViewSaveRequest): Promise { +export function buildAnnotationView(request: AnnotationViewSaveRequest): JsonObject { const identifier = viewIdentifier(request.name); const { configuration } = request; const where = annotationViewWhere(configuration.filter, request.fieldShapes); @@ -126,7 +127,7 @@ export function buildAnnotationViewDocument(request: AnnotationViewSaveRequest): const field = columnSelection(column); return field ? [{ column, field }] : []; }); - const frontmatter = { + return { type: "view", id: `reader.annotations.${identifier}`, version: 1, @@ -164,7 +165,6 @@ export function buildAnnotationViewDocument(request: AnnotationViewSaveRequest): }, ], }; - return viewDocument(frontmatter); } export const sourceAnnotationsViewName = "Annotations for this source"; @@ -173,8 +173,8 @@ export const sourceAnnotationsViewName = "Annotations for this source"; * One view listing the annotations of whichever source it runs against: embed it in a source * note, or run it with a source as its context from any mdbase tool. */ -export function buildSourceAnnotationsViewDocument(): Promise { - const frontmatter = { +export function buildSourceAnnotationsView(): JsonObject { + return { type: "view", id: "reader.annotations.for-source", version: 1, @@ -205,7 +205,6 @@ export function buildSourceAnnotationsViewDocument(): Promise { }, ], }; - return viewDocument(frontmatter); } function columnSelection(column: AnnotationColumn): string | null { diff --git a/apps/reader/src/mdbase-library-views.test.ts b/apps/reader/src/mdbase-library-views.test.ts index 98dd5d9..eb86a77 100644 --- a/apps/reader/src/mdbase-library-views.test.ts +++ b/apps/reader/src/mdbase-library-views.test.ts @@ -1,9 +1,8 @@ import { describe, expect, it } from "vitest"; -import { parse as parseYaml } from "yaml"; import { applyLibraryViewConfiguration, - buildLibraryViewDocument, + buildLibraryView, defaultLibraryViewConfiguration, libraryViewConfiguration, } from "./mdbase-library-views.js"; @@ -11,8 +10,8 @@ import { import type { SourceSummary } from "@mdbase-reader/core"; describe("mdbase library views", () => { - it("serializes Reader configuration as an ordinary canonical mdbase view", async () => { - const document = await buildLibraryViewDocument({ + it("describes Reader configuration as an ordinary canonical mdbase view", () => { + const frontmatter = buildLibraryView({ name: "Reading queue", configuration: { ...defaultLibraryViewConfiguration, @@ -28,10 +27,6 @@ describe("mdbase library views", () => { }, }, }); - const frontmatter = parseYaml(/^---\n([\s\S]*?)\n---/u.exec(document)?.[1] ?? "") as Record< - string, - unknown - >; expect(frontmatter).toMatchObject({ type: "view", id: "reader.library.reading-queue", diff --git a/apps/reader/src/mdbase-library-views.ts b/apps/reader/src/mdbase-library-views.ts index 64ca215..c094100 100644 --- a/apps/reader/src/mdbase-library-views.ts +++ b/apps/reader/src/mdbase-library-views.ts @@ -21,6 +21,7 @@ import { } from "./library-conditions.js"; import type { AnnotationViewConfiguration } from "./mdbase-annotation-views.js"; +import type { JsonObject } from "@mdbase-reader/connect"; export { columnLabel }; export type { LibraryColumn }; @@ -140,11 +141,11 @@ export function libraryViewConfiguration( }; } -export function buildLibraryViewDocument(request: LibraryViewSaveRequest): Promise { +export function buildLibraryView(request: LibraryViewSaveRequest): JsonObject { const identifier = viewIdentifier(request.name); const configuration = request.configuration; const where = durableWhere(configuration.filter, request.fieldShapes ?? {}); - const frontmatter = { + return { type: "view", id: `reader.library.${identifier}`, version: 1, @@ -189,15 +190,6 @@ export function buildLibraryViewDocument(request: LibraryViewSaveRequest): Promi }, ], }; - return viewDocument(frontmatter); -} - -/** Serializes a view document; YAML is only needed when saving, so it loads on demand. */ -export async function viewDocument( - frontmatter: Readonly>, -): Promise { - const { stringify } = await import("yaml"); - return `---\n${stringify(frontmatter).trimEnd()}\n---\n\n`; } export function applyLibraryViewConfiguration( diff --git a/docs/architecture/source-views.md b/docs/architecture/source-views.md index f9426f1..8854f35 100644 --- a/docs/architecture/source-views.md +++ b/docs/architecture/source-views.md @@ -20,8 +20,12 @@ interpreted through their exact Reader contracts. Executed view rows are resolved back to known Reader source paths; arbitrary collection rows never become source records merely because a view returned them. -Reader-created views are ordinary `type: view` Markdown records. Their query -targets the seeded `reader-source` type, while presentation metadata records: +Reader-created views are ordinary Markdown records of a type implementing the +`mdbase.view` contract. Reader provisions the published `mdbase.view` pack +(contract plus a seed `view` type) during collection setup and saves views with +ordinary record operations under its record grants; it requests no +view-specific capability. Their query targets the seeded `reader-source` type, +while presentation metadata records: - table or card presentation; - visible bibliographic columns; @@ -35,11 +39,12 @@ can always be adapted with **Save as view**, preserving its original source. ## Runtime model -Connect owns saved-view discovery, execution, and source mutation: +Connect owns saved-view discovery and execution; views are persisted as records: - `listViews()` discovers view records and named views; - `executeView()` returns ordered rows and selected values; -- `createViewSource()` and `updateViewSource()` persist explicit user saves. +- `create()` writes a new view at `views/.md`, and `update()` replaces + an existing Reader view's document on explicit user saves. The default **All sources** working view is not written to the collection. It uses the already-loaded contract projection so the first library surface is diff --git a/packages/connect/package.json b/packages/connect/package.json index 5b792f6..56456e4 100644 --- a/packages/connect/package.json +++ b/packages/connect/package.json @@ -13,6 +13,7 @@ "dependencies": { "@mdbase-dev/connect": "0.1.0-beta.112", "@mdbase-reader/core": "workspace:*", - "@mdbase-reader/migration": "workspace:*" + "@mdbase-reader/migration": "workspace:*", + "yaml": "^2.9.0" } } diff --git a/packages/connect/src/index.ts b/packages/connect/src/index.ts index aa2b0ac..a399aaf 100644 --- a/packages/connect/src/index.ts +++ b/packages/connect/src/index.ts @@ -12,4 +12,4 @@ export * from "./portable-application-session.js"; export * from "./record-session.js"; export * from "./repositories.js"; export * from "./source-imports.js"; -export type { MdbaseAppManifest } from "@mdbase-dev/connect"; +export type { JsonObject, MdbaseAppManifest } from "@mdbase-dev/connect"; diff --git a/packages/connect/src/library-views.ts b/packages/connect/src/library-views.ts index 06e0f9b..d353c35 100644 --- a/packages/connect/src/library-views.ts +++ b/packages/connect/src/library-views.ts @@ -2,24 +2,30 @@ import { connectOptions, outcomeValue } from "./repository-client.js"; import type { ExecuteViewInput, + JsonObject, MdbaseConnection, SavedViewExecution, SavedViewList, - SavedViewSourceDocument, } from "@mdbase-dev/connect"; import type { ReaderRequestOptions } from "@mdbase-reader/core"; +/** A saved view is an ordinary record whose type implements `mdbase.view`. */ export interface LibraryViewSaveInput { - readonly document: string; - readonly path?: string; - readonly revision?: string; - readonly name?: string; + readonly path: string; + readonly frontmatter: JsonObject; + /** Present when replacing the view at `path`; its absence creates one there. */ + readonly replace?: { readonly revision?: string }; +} + +export interface SavedLibraryView { + readonly path: string; + readonly revision: string; } export interface LibraryViewRepository { list(options?: ReaderRequestOptions): Promise; execute(input: ExecuteViewInput, options?: ReaderRequestOptions): Promise; - save(input: LibraryViewSaveInput): Promise; + save(input: LibraryViewSaveInput): Promise; } export function connectLibraryViewRepository(connection: MdbaseConnection): LibraryViewRepository { @@ -36,25 +42,25 @@ export function connectLibraryViewRepository(connection: MdbaseConnection): Libr "execute library view", ); }, - async save(input) { - if (input.path) { - return outcomeValue( - await connection.updateViewSource({ - path: input.path, - document: input.document, - ...(input.revision ? { ifRevision: input.revision } : {}), - }), - "save library view", - ); - } - return outcomeValue( - await connection.createViewSource({ - document: input.document, - ...(input.name ? { name: input.name } : {}), - format: "mdbase.view", - }), - "create library view", - ); + async save({ path, frontmatter, replace }) { + const saved = replace + ? outcomeValue( + await connection.update({ + path, + // A view is replaced whole, so fields the new definition drops are removed. + document: await viewDocument(frontmatter), + ...(replace.revision ? { ifRevision: replace.revision } : {}), + }), + "save library view", + ) + : outcomeValue(await connection.create({ path, frontmatter }), "create library view"); + return { path: saved.path, revision: saved.revision }; }, }; } + +/** YAML is only needed when replacing a view, so it loads on demand. */ +async function viewDocument(frontmatter: JsonObject): Promise { + const { stringify } = await import("yaml"); + return `---\n${stringify(frontmatter).trimEnd()}\n---\n\n`; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 967250e..d968388 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -223,6 +223,9 @@ importers: '@mdbase-reader/migration': specifier: workspace:* version: link:../migration + yaml: + specifier: ^2.9.0 + version: 2.9.1 packages/core: {}