diff --git a/crates/connect-core/src/registry/tests/operations.rs b/crates/connect-core/src/registry/tests/operations.rs index 0a211bb49..0c6183c4b 100644 --- a/crates/connect-core/src/registry/tests/operations.rs +++ b/crates/connect-core/src/registry/tests/operations.rs @@ -837,12 +837,13 @@ fn full_collection_scope_lists_and_executes_saved_views() { let collection_parent = tempdir().unwrap(); let root = collection_parent.path().join("views"); let registry = CollectionRegistry::open(state.path()).unwrap(); - let collection = registry.create(&root, Some("Views"), "UTC").unwrap(); + // Definitions exist before the runtime opens, as after collection setup. + fs::create_dir_all(root.join("_types")).unwrap(); fs::write( - root.join("_types/view.md"), + root.join("_types/task.md"), r#"--- kind: mdbase.type -name: view +name: task version: 1 schema: dialect: json-schema-2020-12 @@ -851,19 +852,18 @@ schema: "#, ) .unwrap(); + fs::create_dir_all(root.join("_contracts")).unwrap(); fs::write( - root.join("_types/task.md"), - r#"--- -kind: mdbase.type -name: task -version: 1 -schema: - dialect: json-schema-2020-12 - value: { type: object } ---- -"#, + root.join("_contracts/mdbase.view.md"), + "---\nkind: mdbase.contract\ncontract_type: record\nid: mdbase.view\nversion: 1.0.0\nrecord_schema: {dialect: json-schema-2020-12, value: {type: object, properties: {id: {}, version: {}, name: {}, query: {}, views: {}}}}\n---\n", ) .unwrap(); + fs::write( + root.join("_types/view.md"), + "---\nkind: mdbase.type\nname: view\nversion: 1\nmatch: {where: {type: view}}\nschema: {dialect: json-schema-2020-12, value: {type: object, properties: {id: {}, version: {}, name: {}, query: {}, views: {}}}}\nimplements: [{contract: mdbase.view, version: 1.0.0, fields: {id: id, version: version, name: name, query: query, views: views}}]\n---\n", + ) + .unwrap(); + let collection = registry.create(&root, Some("Views"), "UTC").unwrap(); fs::create_dir_all(root.join("tasks")).unwrap(); fs::create_dir_all(root.join("views")).unwrap(); fs::write( @@ -895,7 +895,7 @@ views: .operation(collection.id, "list_views", &json!({})) .unwrap(); assert_eq!(listed["valid"], true, "{listed}"); - assert_eq!(listed["result"]["meta"]["total_count"], 1); + assert_eq!(listed["result"]["meta"]["total_count"], 1, "{listed}"); assert_eq!(listed["result"]["views"][0]["id"], "task.views"); let executed = registry diff --git a/crates/connect-hosted-provider/src/workspace/tests.rs b/crates/connect-hosted-provider/src/workspace/tests.rs index 3a612c86d..f21efdf0a 100644 --- a/crates/connect-hosted-provider/src/workspace/tests.rs +++ b/crates/connect-hosted-provider/src/workspace/tests.rs @@ -445,9 +445,26 @@ fn reads_creates_and_updates_type_resources() { assert!(types.iter().any(|definition| definition.name == "project")); } +/// A minimal `mdbase.view` contract and implementing `view` type. +pub(super) fn view_contract_resources() -> [(String, String); 2] { + [ + ( + "_contracts/mdbase.view.md", + "---\nkind: mdbase.contract\ncontract_type: record\nid: mdbase.view\nversion: 1.0.0\nrecord_schema: {dialect: json-schema-2020-12, value: {type: object, properties: {id: {}, version: {}, name: {}, query: {}, views: {}}}}\n---\n", + ), + ( + "_types/view.md", + "---\nkind: mdbase.type\nname: view\nversion: 1\nmatch: {where: {type: view}}\nschema: {dialect: json-schema-2020-12, value: {type: object, properties: {id: {}, version: {}, name: {}, query: {}, views: {}}}}\nimplements: [{contract: mdbase.view, version: 1.0.0, fields: {id: id, version: version, name: name, query: query, views: views}}]\n---\n", + ), + ] + .map(|(path, document)| (path.to_string(), document.to_string())) +} + #[test] fn reads_creates_updates_and_deletes_saved_view_resources() { - let workspace = AuthorityWorkspace::materialize(resources(), []).unwrap(); + let mut resources = resources(); + resources.extend(view_contract_resources()); + let workspace = AuthorityWorkspace::materialize(resources, []).unwrap(); let document = r#"--- type: view id: task.views diff --git a/crates/connect-hosted-provider/tests/projection_lifecycle.rs b/crates/connect-hosted-provider/tests/projection_lifecycle.rs index 12e9149d5..3c1aac92c 100644 --- a/crates/connect-hosted-provider/tests/projection_lifecycle.rs +++ b/crates/connect-hosted-provider/tests/projection_lifecycle.rs @@ -7036,6 +7036,9 @@ views: assert_eq!(remaining_invocations, 0); Box::pin(assert_live_base_invocation_cursor(&fixture, false)).await; + install_view_contract(&fixture, &writer_token).await; + // The definition change retires the active projection until the worker rebuilds it. + let view_generation = complete_generation(&fixture).await; let stable_view_document = "---\ntype: view\nid: stable.views\nversion: 1\nname: Stable\nquery:\n where: this.id == 'stable.views'\nviews:\n - id: all\n name: All\n---\n"; let stable_view = fixture .provider @@ -7144,7 +7147,7 @@ views: .fetch_one(&fixture.pool) .await .unwrap(); - assert_eq!(active_after_view, Some(second_generation)); + assert_eq!(active_after_view, Some(view_generation)); let query_after_view = fixture .provider .operation( @@ -7195,7 +7198,7 @@ views: tokio::time::sleep(Duration::from_millis(10)).await; } let rebuilt_generation = rebuilt_generation.expect("semantic generation rebuilt"); - assert_ne!(rebuilt_generation, second_generation); + assert_ne!(rebuilt_generation, view_generation); let query_after_rebuild = fixture .provider .operation( @@ -10556,3 +10559,62 @@ async fn delete( other => panic!("delete was not applied: {other:?}"), } } + +/// Install the `mdbase.view` contract and a `view` type implementing it, as +/// collection setup does before saved views exist. +async fn install_view_contract(fixture: &FileLifecycleFixture, token: &str) { + let contract = "---\nkind: mdbase.contract\ncontract_type: record\nid: mdbase.view\nversion: 1.0.0\nrecord_schema: {dialect: json-schema-2020-12, value: {type: object, properties: {id: {}, version: {}, name: {}, query: {}, views: {}}}}\n---\n"; + let view_type = "---\nkind: mdbase.type\nname: view\nversion: 1\nmatch: {where: {type: view}}\nschema: {dialect: json-schema-2020-12, value: {type: object, properties: {id: {}, version: {}, name: {}, query: {}, views: {}}}}\nimplements: [{contract: mdbase.view, version: 1.0.0, fields: {id: id, version: version, name: name, query: query, views: views}}]\n---\n"; + let digest = |document: &str| format!("sha256:{:x}", Sha256::digest(document.as_bytes())); + let mut pack = json!({ + "provision": { + "manifest": { + "kind": "mdbase.type-pack", + "id": "mdbase.view", + "version": "1.0.0", + "resources": [ + {"kind": "contract", "mode": "managed", "source": "contracts/mdbase.view.md", "target": "_contracts/mdbase.view.md", "digest": digest(contract)}, + {"kind": "type", "mode": "managed", "source": "types/view.md", "target": "_types/view.md", "digest": digest(view_type)} + ] + }, + "resources": [ + {"source": "contracts/mdbase.view.md", "document": contract}, + {"source": "types/view.md", "document": view_type} + ], + "provides": [{"id": "mdbase.view", "version": "1.0.0", "digest": digest(contract)}] + }, + "installed_by": "mdbase.view", + "adopt_resources": {}, + "preserve_seed_targets": [], + "target_overrides": {}, + "contract_setups": [] + }); + let assessment = fixture + .provider + .operation( + fixture.collection_id, + token, + "assess_type_pack", + Uuid::new_v4(), + pack.clone(), + None, + ) + .await + .unwrap(); + assert_eq!(assessment["valid"], true, "{assessment}"); + pack["expected_assessment_digest"] = assessment["result"]["assessment_digest"].clone(); + pack["allow_downgrade"] = json!(false); + let applied = fixture + .provider + .operation( + fixture.collection_id, + token, + "apply_type_pack", + Uuid::new_v4(), + pack, + None, + ) + .await + .unwrap(); + assert_eq!(applied["valid"], true, "{applied}"); +} diff --git a/deploy/docker/mdbase-rs-revision b/deploy/docker/mdbase-rs-revision index a7e8095c0..61a262cff 100644 --- a/deploy/docker/mdbase-rs-revision +++ b/deploy/docker/mdbase-rs-revision @@ -1 +1 @@ -88d4a21ee51d27092cf06380d6a1c977f5b4f978 +0d96ad4e4c61d850999741f97a185371f3c45b67 diff --git a/scripts/e2e.mjs b/scripts/e2e.mjs index 1043970a6..a2a63f615 100644 --- a/scripts/e2e.mjs +++ b/scripts/e2e.mjs @@ -861,6 +861,117 @@ implements: } await cliJson(["access", "revoke", taskNotesToken.body.grant_id]); + // An application that saves views declares mdbase.view and provisions its + // published pack; setup installs it and views are ordinary records. + const viewPack = JSON.parse(await readFile( + join(repoRoot, "test", "fixtures", "packs", "mdbase.view-1.0.0.json"), + "utf8" + )); + const viewOperations = [ + ...COLLECTION_READ_OPERATIONS, + ...RECORD_CREATE_OPERATIONS, + ...RECORD_EDIT_OPERATIONS, + ...RECORD_DELETE_OPERATIONS, + ...SETUP_OPERATIONS + ]; + const viewApplication = await request("/v1/apps/register", { + method: "POST", + body: { + manifest: { + manifest_version: 1, + id: "dev.mdbase.saved-views-e2e", + name: "Saved views E2E", + homepage: manifest.origin, + redirect_uris: [manifest.redirectUri], + requirements: { + access: "full_collection", + contracts: viewPack.provides, + capabilities: { + contract_version: 2, + required: ["collection.read", "records.create", "records.edit", "records.delete"] + } + }, + provisions: { type_packs: [viewPack] } + } + } + }); + const viewVerifier = "saved-views-e2e-verifier-with-forty-three-characters"; + const viewAuthorization = await startSignedWebAuthorization({ + application: viewApplication.body.application, + redirectUri: manifest.redirectUri, + verifier: viewVerifier, + state: "saved-views-e2e", + operations: viewOperations, + cookie + }); + const viewRequest = await poll(async () => { + const current = await request( + `/v1/authorization-requests/${viewAuthorization.id}`, + { cookie } + ); + return current.body.collections?.some((candidate) => candidate.id === collection.id) + ? current + : null; + }, "Saved-view setup collection offer did not reach the portal"); + const viewOffer = viewRequest.body.collections.find( + (candidate) => candidate.id === collection.id + ); + await approvePortalAuthorization(viewAuthorization.id, cookie, { + collection_id: viewOffer.id, + offer_id: viewOffer.offer_id, + operations: viewOperations, + contract_setups: [] + }); + const viewCallback = await finishSignedWebAuthorization(viewAuthorization); + const viewToken = await request("/oauth/token", { + method: "POST", + form: { + grant_type: "authorization_code", + code: viewCallback.searchParams.get("code"), + client_id: viewApplication.body.application.id, + redirect_uri: manifest.redirectUri, + code_verifier: viewVerifier + } + }); + const viewOperation = async (operation, input) => { + const response = await signedGrantOperation( + viewAuthorization, viewToken.body, collection.id, operation, input + ); + const body = await response.json(); + if (response.status !== 200 || body.result?.valid !== true) { + throw new Error(`Saved-view ${operation} failed: ${JSON.stringify(body)}`); + } + return body.result.result; + }; + const createdView = await viewOperation("create", { + path: "views/e2e.md", + frontmatter: { + type: "view", + id: "e2e.views", + version: 1, + name: "E2E views", + views: [{ id: "all", name: "All records", select: ["title"] }] + } + }); + const listedViews = await viewOperation("list_views", {}); + if (!listedViews.views.some((view) => view.id === "e2e.views" + && view.source.path === "views/e2e.md")) { + throw new Error(`Created view record was not listed: ${JSON.stringify(listedViews)}`); + } + await viewOperation("execute_view", { path: "views/e2e.md", view: "all" }); + await viewOperation("update", { + path: "views/e2e.md", + if_revision: createdView.revision, + document: "---\ntype: view\nid: e2e.views\nversion: 1\nname: Renamed views\nviews:\n - id: all\n name: All records\n---\n" + }); + const relisted = await viewOperation("list_views", {}); + const replaced = relisted.views.find((view) => view.id === "e2e.views"); + if (replaced?.name !== "Renamed views") { + throw new Error(`Replaced view record was not relisted: ${JSON.stringify(relisted)}`); + } + await viewOperation("delete", { path: "views/e2e.md", if_revision: replaced.source.revision }); + await cliJson(["access", "revoke", viewToken.body.grant_id]); + relayContext = { store: applicationKeyStore, handle: applicationKey.handle, diff --git a/scripts/hosted-provider-e2e.mjs b/scripts/hosted-provider-e2e.mjs index b69eaae85..4c7728e55 100644 --- a/scripts/hosted-provider-e2e.mjs +++ b/scripts/hosted-provider-e2e.mjs @@ -2078,22 +2078,31 @@ schema: path: "Plain.md", ifRevision: sdkBodyOnlyUpdated.revision })).deleted, true); - const viewType = requireConnectSuccess(await hostedConnection.createType({ - document: `--- -kind: mdbase.type -name: view -version: 1 -match: - where: - type: view -schema: - dialect: json-schema-2020-12 - value: - type: object ---- -` + // Saved views are records of a type implementing the installed mdbase.view contract. + const viewContract = "---\nkind: mdbase.contract\ncontract_type: record\nid: mdbase.view\nversion: 1.0.0\nrecord_schema: {dialect: json-schema-2020-12, value: {type: object, properties: {id: {}, version: {}, name: {}, query: {}, properties: {}, views: {}}}}\n---\n"; + const viewType = "---\nkind: mdbase.type\nname: view\nversion: 1\nmatch: {where: {type: view}}\nschema: {dialect: json-schema-2020-12, value: {type: object, properties: {id: {}, version: {}, name: {}, query: {}, properties: {}, views: {}}}}\nimplements: [{contract: mdbase.view, version: 1.0.0, fields: {id: id, version: version, name: name, query: query, properties: properties, views: views}}]\n---\n"; + const sha256 = (document) => `sha256:${createHash("sha256").update(document).digest("hex")}`; + const viewPack = { + provision: { + manifest: { + kind: "mdbase.type-pack", + id: "mdbase.view", + version: "1.0.0", + resources: [ + { kind: "contract", mode: "managed", source: "contracts/mdbase.view.md", target: "_contracts/mdbase.view.md", digest: sha256(viewContract) } + ] + }, + resources: [{ source: "contracts/mdbase.view.md", document: viewContract }], + provides: [{ id: "mdbase.view", version: "1.0.0", digest: sha256(viewContract) }] + }, + installedBy: "mdbase.view" + }; + const viewPackAssessment = requireConnectSuccess(await hostedConnection.assessTypePack(viewPack)); + requireConnectSuccess(await hostedConnection.applyTypePack({ + ...viewPack, + expectedAssessmentDigest: viewPackAssessment.assessmentDigest })); - assert.equal(viewType.name, "view"); + assert.equal(requireConnectSuccess(await hostedConnection.createType({ document: viewType })).name, "view"); const viewRecord = requireConnectSuccess(await hostedConnection.create({ path: "Views/writing.md", frontmatter: { diff --git a/test/fixtures/packs/mdbase.view-1.0.0.json b/test/fixtures/packs/mdbase.view-1.0.0.json new file mode 100644 index 000000000..210fa58ea --- /dev/null +++ b/test/fixtures/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" + } + ] +}