Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 14 additions & 14 deletions crates/connect-core/src/registry/tests/operations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -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
Expand Down
19 changes: 18 additions & 1 deletion crates/connect-hosted-provider/src/workspace/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
66 changes: 64 additions & 2 deletions crates/connect-hosted-provider/tests/projection_lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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}");
}
2 changes: 1 addition & 1 deletion deploy/docker/mdbase-rs-revision
Original file line number Diff line number Diff line change
@@ -1 +1 @@
88d4a21ee51d27092cf06380d6a1c977f5b4f978
0d96ad4e4c61d850999741f97a185371f3c45b67
111 changes: 111 additions & 0 deletions scripts/e2e.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
39 changes: 24 additions & 15 deletions scripts/hosted-provider-e2e.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
Loading
Loading