generated from coulomb/repo-seed
projection-only multifiling
This commit is contained in:
@@ -28,6 +28,7 @@ GET /cmis/{access_point_id}/browser/children
|
||||
GET /cmis/{access_point_id}/browser/object/{object_id}
|
||||
GET /cmis/{access_point_id}/browser/content/{object_id}
|
||||
GET /cmis/{access_point_id}/browser/acl/{object_id}
|
||||
GET /cmis/{access_point_id}/browser/parents/{object_id}
|
||||
GET /cmis/{access_point_id}/browser/query
|
||||
GET /cmis/{access_point_id}/browser/relationships
|
||||
GET /cmis/{access_point_id}/browser/changes
|
||||
@@ -61,6 +62,7 @@ Actor context is passed through the existing service headers, especially:
|
||||
| Browser Binding repository info | yes | yes | yes | yes |
|
||||
| Type definitions | yes | yes | yes | yes |
|
||||
| Synthetic navigation | yes | yes | yes | yes |
|
||||
| Projection-only multifiling | yes | yes | yes | yes |
|
||||
| Object reads | yes | yes | yes | yes |
|
||||
| Content stream descriptors | yes | yes | yes | yes |
|
||||
| ACL projection | discover | discover | discover | discover |
|
||||
@@ -103,7 +105,7 @@ It is not yet suitable for clients that require:
|
||||
- AtomPub,
|
||||
- SOAP/Web Services,
|
||||
- full CMIS SQL,
|
||||
- multifiling/unfiling,
|
||||
- mutating multifiling/unfiling,
|
||||
- private-working-copy semantics,
|
||||
- retention/hold mutation,
|
||||
- rendition streams,
|
||||
@@ -132,6 +134,8 @@ capability groups before treating them as implementation bugs.
|
||||
## Operational Notes
|
||||
|
||||
- Hidden objects should be treated as not found by CMIS clients.
|
||||
- Multifiling is projection-only: assets may appear under multiple derived
|
||||
folder paths without changing canonical asset identity.
|
||||
- Relationship and change-log responses are filtered through the same visibility
|
||||
gates as object reads.
|
||||
- Mutations always pass through engine services and produce normal engine audit
|
||||
@@ -140,4 +144,3 @@ capability groups before treating them as implementation bugs.
|
||||
not physical removal.
|
||||
- Compatibility should be discussed per profile and per client rather than as a
|
||||
repo-wide binary property.
|
||||
|
||||
|
||||
@@ -120,6 +120,17 @@ Relationship listings and change logs now apply the same asset visibility gates
|
||||
as object reads. This prevents indirect leakage of confidential or restricted
|
||||
asset IDs through relationship targets or audit-backed change entries.
|
||||
|
||||
## Projection-Only Multifiling
|
||||
|
||||
CMIS navigation now supports projection-only multifiling. The same asset can be
|
||||
listed under several derived folder paths, including source system, topics,
|
||||
owner, lifecycle, and asset type. These folders are navigation projections; they
|
||||
do not duplicate assets and do not become canonical storage locations.
|
||||
|
||||
`GET /cmis/{access_point_id}/browser/parents/{object_id}` returns the projected
|
||||
parent folders for one asset. `GET /cmis/{access_point_id}/browser/children`
|
||||
supports folder-scoped navigation through those projected paths.
|
||||
|
||||
## Fixture And Optional TCK Integration
|
||||
|
||||
CMIS fixtures now act as active compatibility contracts:
|
||||
|
||||
@@ -104,7 +104,7 @@
|
||||
"examples": ["root-folder", "topic-folder", "source-system-folder", "unfiled-assets"],
|
||||
"supported_profiles": ["readonly-browser", "governed-authoring", "admin-export", "compat-tck"],
|
||||
"must_validate": ["get_children", "get_descendants", "get_folder_tree", "get_object_by_path"],
|
||||
"unsupported": ["multifiling", "unfiling"]
|
||||
"unsupported": ["unfiling"]
|
||||
},
|
||||
{
|
||||
"id": "object-content",
|
||||
@@ -166,7 +166,7 @@
|
||||
"unsupported_diagnostics": {
|
||||
"atompub": "binding_not_supported",
|
||||
"web_services": "binding_not_supported",
|
||||
"multifiling": "capability_not_supported",
|
||||
"multifiling": "projection_only",
|
||||
"unfiling": "capability_not_supported",
|
||||
"append_content_stream": "capability_not_supported",
|
||||
"private_working_copy": "capability_not_supported",
|
||||
|
||||
@@ -346,26 +346,12 @@ class ServiceRuntime:
|
||||
decision = mapper.access_point.decide_action(CMISAction.GET_CHILDREN, context)
|
||||
if not decision.allowed:
|
||||
raise _cmis_authorization_error(decision, "getChildren")
|
||||
projections = [
|
||||
projection.to_dict()
|
||||
for asset in self.repository.list_assets()
|
||||
if (
|
||||
projection := mapper.map_asset(
|
||||
asset,
|
||||
context,
|
||||
representations=self.repository.list_representations(asset_id=asset.id),
|
||||
versions=self.repository.list_versions(asset.id),
|
||||
relationship_ids=[
|
||||
f"cmis:relationship:{relationship.relationship_id}"
|
||||
for relationship in self.repository.list_relationships(source_id=asset.id)
|
||||
],
|
||||
metadata_records=self.repository.list_metadata_records(asset.id),
|
||||
)
|
||||
)
|
||||
]
|
||||
folder_path = _cmis_folder_path(folder_id)
|
||||
projections = self._cmis_children_for_folder(mapper, context, folder_path=folder_path)
|
||||
paged = projections[max(skip_count, 0) : max(skip_count, 0) + max(max_items, 0)]
|
||||
return {
|
||||
"folder_id": folder_id or mapper.access_point.root_folder_id,
|
||||
"folder_path": folder_path or "/",
|
||||
"objects": paged,
|
||||
"num_items": len(paged),
|
||||
"has_more_items": len(projections) > max(skip_count, 0) + len(paged),
|
||||
@@ -441,6 +427,26 @@ class ServiceRuntime:
|
||||
)
|
||||
return acl
|
||||
|
||||
def cmis_object_parents(
|
||||
self,
|
||||
access_point_id: str,
|
||||
object_id: str,
|
||||
context: OperationContext,
|
||||
) -> dict[str, Any]:
|
||||
mapper = self._cmis_mapper(access_point_id)
|
||||
decision = mapper.access_point.decide_action(CMISAction.GET_OBJECT_PARENTS, context, resource=object_id)
|
||||
if not decision.allowed:
|
||||
raise _cmis_authorization_error(decision, "getObjectParents")
|
||||
asset_id = _cmis_asset_id(object_id)
|
||||
asset = self.repository.get_asset(asset_id)
|
||||
if not mapper.access_point.exposes_asset(asset, context):
|
||||
raise NotFoundError(
|
||||
"CMIS object not found",
|
||||
details={"object_id": object_id, "access_point_id": access_point_id},
|
||||
)
|
||||
parents = list(mapper.parent_folders_for_asset(asset))
|
||||
return {"object_id": mapper.asset_object_id(asset.id), "parents": parents, "count": len(parents)}
|
||||
|
||||
def cmis_create_document(
|
||||
self,
|
||||
access_point_id: str,
|
||||
@@ -584,18 +590,14 @@ class ServiceRuntime:
|
||||
"supported": ["SELECT * FROM cmis:document", "SELECT * FROM kontextual:document"],
|
||||
},
|
||||
)
|
||||
children = self.cmis_children(
|
||||
access_point_id,
|
||||
context,
|
||||
skip_count=skip_count,
|
||||
max_items=max_items,
|
||||
)
|
||||
projections = self._cmis_document_projections(mapper, context)
|
||||
paged = projections[max(skip_count, 0) : max(skip_count, 0) + max(max_items, 0)]
|
||||
return {
|
||||
"query": query,
|
||||
"results": children["objects"],
|
||||
"num_items": children["num_items"],
|
||||
"has_more_items": children["has_more_items"],
|
||||
"total_num_items": children["total_num_items"],
|
||||
"results": paged,
|
||||
"num_items": len(paged),
|
||||
"has_more_items": len(projections) > max(skip_count, 0) + len(paged),
|
||||
"total_num_items": len(projections),
|
||||
}
|
||||
|
||||
def cmis_relationships(
|
||||
@@ -685,6 +687,74 @@ class ServiceRuntime:
|
||||
return self._cmis_asset_visible(mapper, relationship.target_id, context)
|
||||
return True
|
||||
|
||||
def _cmis_children_for_folder(
|
||||
self,
|
||||
mapper: CMISDomainMapper,
|
||||
context: OperationContext,
|
||||
*,
|
||||
folder_path: str | None,
|
||||
) -> list[dict[str, Any]]:
|
||||
assets = [
|
||||
asset
|
||||
for asset in self.repository.list_assets()
|
||||
if mapper.access_point.exposes_asset(asset, context)
|
||||
]
|
||||
if folder_path in (None, "/"):
|
||||
child_folder_paths = set()
|
||||
for asset in assets:
|
||||
for path in mapper.asset_paths(asset):
|
||||
first = path.strip("/").split("/")[0]
|
||||
if first:
|
||||
child_folder_paths.add("/" + first)
|
||||
return [mapper.folder_projection(path) for path in sorted(child_folder_paths)]
|
||||
children: list[dict[str, Any]] = []
|
||||
folder_path = _normalize_cmis_path(folder_path)
|
||||
child_folder_paths: set[str] = set()
|
||||
for asset in assets:
|
||||
for path in mapper.asset_paths(asset):
|
||||
parent = _path_parent(path)
|
||||
if parent == folder_path:
|
||||
projection = mapper.map_asset(
|
||||
asset,
|
||||
context,
|
||||
representations=self.repository.list_representations(asset_id=asset.id),
|
||||
versions=self.repository.list_versions(asset.id),
|
||||
relationship_ids=[
|
||||
f"cmis:relationship:{relationship.relationship_id}"
|
||||
for relationship in self.repository.list_relationships(source_id=asset.id)
|
||||
if self._cmis_relationship_visible(mapper, relationship, context)
|
||||
],
|
||||
metadata_records=self.repository.list_metadata_records(asset.id),
|
||||
)
|
||||
if projection is not None:
|
||||
children.append(projection.to_dict())
|
||||
elif _path_parent(parent) == folder_path:
|
||||
child_folder_paths.add(parent)
|
||||
return [mapper.folder_projection(path) for path in sorted(child_folder_paths)] + children
|
||||
|
||||
def _cmis_document_projections(
|
||||
self,
|
||||
mapper: CMISDomainMapper,
|
||||
context: OperationContext,
|
||||
) -> list[dict[str, Any]]:
|
||||
projections = []
|
||||
for asset in self.repository.list_assets():
|
||||
projection = mapper.map_asset(
|
||||
asset,
|
||||
context,
|
||||
representations=self.repository.list_representations(asset_id=asset.id),
|
||||
versions=self.repository.list_versions(asset.id),
|
||||
relationship_ids=[
|
||||
f"cmis:relationship:{relationship.relationship_id}"
|
||||
for relationship in self.repository.list_relationships(source_id=asset.id)
|
||||
if self._cmis_relationship_visible(mapper, relationship, context)
|
||||
],
|
||||
metadata_records=self.repository.list_metadata_records(asset.id),
|
||||
)
|
||||
if projection is not None:
|
||||
projections.append(projection.to_dict())
|
||||
return projections
|
||||
|
||||
def create_asset(self, payload: dict[str, Any], context: OperationContext) -> dict[str, Any]:
|
||||
classification = Classification.from_dict(payload["classification"])
|
||||
result = self.asset_service().create_asset(
|
||||
@@ -2140,6 +2210,14 @@ def create_app(runtime: ServiceRuntime | None = None):
|
||||
) -> dict[str, Any]:
|
||||
return response(runtime.cmis_acl, access_point_id, object_id, context)
|
||||
|
||||
@app.get("/cmis/{access_point_id}/browser/parents/{object_id:path}", tags=["cmis"])
|
||||
def cmis_object_parents(
|
||||
access_point_id: str,
|
||||
object_id: str,
|
||||
context: OperationContext = Depends(context_from_headers),
|
||||
) -> dict[str, Any]:
|
||||
return response(runtime.cmis_object_parents, access_point_id, object_id, context)
|
||||
|
||||
@app.post("/cmis/{access_point_id}/browser/document", tags=["cmis"])
|
||||
def cmis_create_document(
|
||||
access_point_id: str,
|
||||
@@ -2632,6 +2710,29 @@ def _cmis_asset_id(object_id: str | None) -> str:
|
||||
return normalized
|
||||
|
||||
|
||||
def _cmis_folder_path(folder_id: str | None) -> str | None:
|
||||
if not folder_id:
|
||||
return None
|
||||
normalized = folder_id.strip()
|
||||
if normalized in {"cmis-root", "root", "/"}:
|
||||
return "/"
|
||||
if normalized.startswith("cmis:folder:"):
|
||||
return "/" + normalized.removeprefix("cmis:folder:").replace("::", "/")
|
||||
return _normalize_cmis_path(normalized)
|
||||
|
||||
|
||||
def _normalize_cmis_path(path: str) -> str:
|
||||
parts = [part.strip().strip("/") for part in path.replace("\\", "/").split("/") if part.strip("/")]
|
||||
return "/" + "/".join(parts)
|
||||
|
||||
|
||||
def _path_parent(path: str) -> str:
|
||||
parts = _normalize_cmis_path(path).strip("/").split("/")
|
||||
if len(parts) <= 1:
|
||||
return "/"
|
||||
return "/" + "/".join(parts[:-1])
|
||||
|
||||
|
||||
def _cmis_authorization_error(decision: PolicyDecision, operation: str) -> AuthorizationError:
|
||||
return AuthorizationError(
|
||||
"CMIS operation denied by access-point profile",
|
||||
|
||||
@@ -48,6 +48,7 @@ class CMISAction(str, Enum):
|
||||
GET_REPOSITORY_INFO = "get_repository_info"
|
||||
GET_TYPE_DEFINITION = "get_type_definition"
|
||||
GET_CHILDREN = "get_children"
|
||||
GET_OBJECT_PARENTS = "get_object_parents"
|
||||
GET_OBJECT = "get_object"
|
||||
GET_CONTENT_STREAM = "get_content_stream"
|
||||
GET_ACL = "get_acl"
|
||||
@@ -76,6 +77,7 @@ ACTION_CAPABILITIES: dict[CMISAction, CMISCapability] = {
|
||||
CMISAction.GET_REPOSITORY_INFO: CMISCapability.REPOSITORY,
|
||||
CMISAction.GET_TYPE_DEFINITION: CMISCapability.TYPE_DEFINITIONS,
|
||||
CMISAction.GET_CHILDREN: CMISCapability.NAVIGATION,
|
||||
CMISAction.GET_OBJECT_PARENTS: CMISCapability.NAVIGATION,
|
||||
CMISAction.GET_OBJECT: CMISCapability.OBJECT_READ,
|
||||
CMISAction.GET_CONTENT_STREAM: CMISCapability.CONTENT_STREAM_READ,
|
||||
CMISAction.GET_ACL: CMISCapability.ACL,
|
||||
@@ -486,7 +488,7 @@ class CMISDomainMapper:
|
||||
"capability_renditions": "read" if profile.has_capability(CMISCapability.RENDITIONS) else "none",
|
||||
"capability_get_descendants": profile.has_capability(CMISCapability.NAVIGATION),
|
||||
"capability_get_folder_tree": profile.has_capability(CMISCapability.NAVIGATION),
|
||||
"capability_multifiling": False,
|
||||
"capability_multifiling": profile.has_capability(CMISCapability.NAVIGATION),
|
||||
"capability_unfiling": False,
|
||||
"capability_version_specific_filing": False,
|
||||
"capability_pwc_searchable": False,
|
||||
@@ -613,21 +615,67 @@ class CMISDomainMapper:
|
||||
def asset_object_id(self, asset_id: str) -> str:
|
||||
return f"cmis:asset:{asset_id}"
|
||||
|
||||
def folder_object_id(self, path: str) -> str:
|
||||
return "cmis:folder:" + _normalize_path(path).strip("/").replace("/", "::")
|
||||
|
||||
def asset_path(self, asset: KnowledgeAsset) -> str:
|
||||
return self.asset_paths(asset)[0]
|
||||
|
||||
def asset_paths(self, asset: KnowledgeAsset) -> tuple[str, ...]:
|
||||
paths: list[str] = []
|
||||
explicit = asset.metadata.get("cmis_path")
|
||||
if explicit:
|
||||
return _normalize_path(str(explicit))
|
||||
paths.append(_normalize_path(str(explicit)))
|
||||
for value in asset.metadata.get("cmis_paths", ()):
|
||||
paths.append(_normalize_path(str(value)))
|
||||
if asset.source_refs:
|
||||
source_ref = asset.source_refs[0]
|
||||
source_root = _safe_path_segment(source_ref.source_system)
|
||||
if source_ref.path:
|
||||
return _normalize_path(f"/sources/{source_root}/{source_ref.path}")
|
||||
paths.append(_normalize_path(f"/sources/{source_root}/{source_ref.path}"))
|
||||
if source_ref.external_id:
|
||||
return _normalize_path(f"/sources/{source_root}/{source_ref.external_id}")
|
||||
topics = asset.classification.topics
|
||||
if topics:
|
||||
return _normalize_path(f"/topics/{topics[0]}/{asset.id}")
|
||||
return _normalize_path(f"/assets/{asset.classification.asset_type}/{asset.id}")
|
||||
paths.append(_normalize_path(f"/sources/{source_root}/{source_ref.external_id}"))
|
||||
for topic in asset.classification.topics:
|
||||
paths.append(_normalize_path(f"/topics/{topic}/{asset.id}"))
|
||||
if asset.classification.owner:
|
||||
paths.append(_normalize_path(f"/owners/{asset.classification.owner}/{asset.id}"))
|
||||
paths.append(_normalize_path(f"/lifecycle/{_enum_value(asset.lifecycle)}/{asset.id}"))
|
||||
paths.append(_normalize_path(f"/assets/{asset.classification.asset_type}/{asset.id}"))
|
||||
return tuple(dict.fromkeys(paths))
|
||||
|
||||
def parent_folders_for_asset(self, asset: KnowledgeAsset) -> tuple[dict[str, Any], ...]:
|
||||
folders = []
|
||||
for path in self.asset_paths(asset):
|
||||
parent = _parent_path(path)
|
||||
folders.append(
|
||||
{
|
||||
"object_id": self.folder_object_id(parent),
|
||||
"path": parent,
|
||||
"name": _path_name(parent),
|
||||
"base_type_id": CMISBaseType.FOLDER.value,
|
||||
"type_id": "kontextual:folder",
|
||||
"filing_source": _filing_source(parent),
|
||||
}
|
||||
)
|
||||
return tuple({folder["path"]: folder for folder in folders}.values())
|
||||
|
||||
def folder_projection(self, path: str) -> dict[str, Any]:
|
||||
normalized = _normalize_path(path)
|
||||
return {
|
||||
"object_id": self.folder_object_id(normalized),
|
||||
"base_type_id": CMISBaseType.FOLDER.value,
|
||||
"type_id": "kontextual:folder",
|
||||
"name": _path_name(normalized),
|
||||
"path": normalized,
|
||||
"properties": {
|
||||
"cmis:objectId": self.folder_object_id(normalized),
|
||||
"cmis:name": _path_name(normalized),
|
||||
"cmis:baseTypeId": CMISBaseType.FOLDER.value,
|
||||
"cmis:objectTypeId": "kontextual:folder",
|
||||
"kontextual:filingSource": _filing_source(normalized),
|
||||
},
|
||||
"allowable_actions": [CMISAction.GET_CHILDREN.value],
|
||||
}
|
||||
|
||||
def asset_properties(
|
||||
self,
|
||||
@@ -716,6 +764,7 @@ class CMISDomainMapper:
|
||||
CMISAction.GET_CONTENT_STREAM,
|
||||
CMISAction.GET_ACL,
|
||||
CMISAction.GET_RELATIONSHIPS,
|
||||
CMISAction.GET_OBJECT_PARENTS,
|
||||
CMISAction.UPDATE_PROPERTIES,
|
||||
CMISAction.DELETE_OBJECT,
|
||||
CMISAction.SET_CONTENT_STREAM,
|
||||
@@ -819,3 +868,22 @@ def _normalize_path(path: str) -> str:
|
||||
|
||||
def _safe_path_segment(value: str) -> str:
|
||||
return str(value).strip().strip("/") or "_"
|
||||
|
||||
|
||||
def _parent_path(path: str) -> str:
|
||||
parts = _normalize_path(path).strip("/").split("/")
|
||||
if len(parts) <= 1:
|
||||
return "/"
|
||||
return "/" + "/".join(parts[:-1])
|
||||
|
||||
|
||||
def _path_name(path: str) -> str:
|
||||
normalized = _normalize_path(path)
|
||||
if normalized == "/":
|
||||
return "root"
|
||||
return normalized.rsplit("/", 1)[-1]
|
||||
|
||||
|
||||
def _filing_source(path: str) -> str:
|
||||
parts = _normalize_path(path).strip("/").split("/")
|
||||
return parts[0] if parts and parts[0] else "root"
|
||||
|
||||
@@ -84,6 +84,7 @@ def test_cmis_browser_binding_routes_are_advertised_in_openapi(cmis_client) -> N
|
||||
assert "/cmis/{access_point_id}/browser/object/{object_id}" in paths
|
||||
assert "/cmis/{access_point_id}/browser/content/{object_id}" in paths
|
||||
assert "/cmis/{access_point_id}/browser/acl/{object_id}" in paths
|
||||
assert "/cmis/{access_point_id}/browser/parents/{object_id}" in paths
|
||||
assert "/cmis/{access_point_id}/browser/query" in paths
|
||||
assert "/cmis/{access_point_id}/browser/relationships" in paths
|
||||
assert "/cmis/{access_point_id}/browser/changes" in paths
|
||||
|
||||
@@ -73,7 +73,7 @@ def test_mapper_exposes_repository_info_capabilities_and_base_type_definitions()
|
||||
assert repository["cmis_version_supported"] == "1.1"
|
||||
assert repository["binding"] == "browser"
|
||||
assert repository["capabilities"]["capability_query"] == "metadataonly"
|
||||
assert repository["capabilities"]["capability_multifiling"] is False
|
||||
assert repository["capabilities"]["capability_multifiling"] is True
|
||||
|
||||
assert set(types) == {
|
||||
"cmis:document",
|
||||
@@ -128,6 +128,21 @@ def test_mapper_projects_asset_to_cmis_document_envelope() -> None:
|
||||
assert CMISAction.UPDATE_PROPERTIES.value not in serialized["allowable_actions"]
|
||||
|
||||
|
||||
def test_mapper_projects_multiple_parent_folders_without_duplicate_assets() -> None:
|
||||
mapper = _mapper()
|
||||
asset = _asset()
|
||||
|
||||
parents = mapper.parent_folders_for_asset(asset)
|
||||
parent_paths = {parent["path"] for parent in parents}
|
||||
|
||||
assert "/sources/sharepoint/Architecture" in parent_paths
|
||||
assert "/topics/architecture" in parent_paths
|
||||
assert "/topics/cmis" in parent_paths
|
||||
assert "/owners/Platform Knowledge" in parent_paths
|
||||
assert "/lifecycle/active" in parent_paths
|
||||
assert len(parent_paths) == len(parents)
|
||||
|
||||
|
||||
def test_governed_authoring_projection_includes_write_allowable_actions() -> None:
|
||||
mapper = _mapper(CMISAccessProfile.governed_authoring())
|
||||
asset = _asset()
|
||||
@@ -175,4 +190,3 @@ def test_mapper_projects_relationship_objects() -> None:
|
||||
assert serialized["properties"]["cmis:sourceId"] == "cmis:asset:asset-source"
|
||||
assert serialized["properties"]["cmis:targetId"] == "cmis:asset:asset-target"
|
||||
assert serialized["properties"]["kontextual:predicate"] == "derived_from"
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ def cmis_runtime() -> tuple[ServiceRuntime, object]:
|
||||
asset_type="document",
|
||||
sensitivity=Sensitivity.INTERNAL,
|
||||
owner="Platform Knowledge",
|
||||
topics=("cmis",),
|
||||
topics=("cmis", "integration"),
|
||||
),
|
||||
context,
|
||||
asset_id="asset-runtime-source",
|
||||
@@ -85,16 +85,21 @@ def test_runtime_cmis_browser_repository_types_children_and_object(cmis_runtime)
|
||||
repository = runtime.cmis_repository_info("readonly-browser")
|
||||
types = runtime.cmis_type_definitions("readonly-browser")
|
||||
children = runtime.cmis_children("readonly-browser", context)
|
||||
topic_children = runtime.cmis_children("readonly-browser", context, folder_id="/topics/cmis")
|
||||
obj = runtime.cmis_object("readonly-browser", "cmis:asset:asset-runtime-source", context)
|
||||
parents = runtime.cmis_object_parents("readonly-browser", "cmis:asset:asset-runtime-source", context)
|
||||
|
||||
assert access_points["count"] == 4
|
||||
assert repository["repository_id"] == "kontextual-readonly-browser"
|
||||
assert repository["capabilities"]["capability_get_descendants"] is True
|
||||
assert {item["base_type_id"] for item in types["items"]} >= {"cmis:document", "cmis:folder"}
|
||||
object_ids = {item["object_id"] for item in children["objects"]}
|
||||
assert "cmis:asset:asset-runtime-source" in object_ids
|
||||
assert "cmis:asset:asset-runtime-public" in object_ids
|
||||
assert "cmis:asset:asset-runtime-confidential" not in object_ids
|
||||
root_paths = {item["path"] for item in children["objects"]}
|
||||
topic_object_ids = {item["object_id"] for item in topic_children["objects"]}
|
||||
parent_paths = {item["path"] for item in parents["parents"]}
|
||||
assert "/topics" in root_paths
|
||||
assert "cmis:asset:asset-runtime-source" in topic_object_ids
|
||||
assert "cmis:asset:asset-runtime-confidential" not in topic_object_ids
|
||||
assert {"/topics/cmis", "/topics/integration"} <= parent_paths
|
||||
assert obj["properties"]["kontextual:assetId"] == "asset-runtime-source"
|
||||
|
||||
|
||||
|
||||
@@ -658,6 +658,7 @@ def test_service_health_readiness_version_and_openapi_contracts(client) -> None:
|
||||
assert "/cmis/{access_point_id}/browser" in paths
|
||||
assert "/cmis/{access_point_id}/browser/children" in paths
|
||||
assert "/cmis/{access_point_id}/browser/acl/{object_id}" in paths
|
||||
assert "/cmis/{access_point_id}/browser/parents/{object_id}" in paths
|
||||
assert "/cmis/{access_point_id}/browser/document" in paths
|
||||
assert "/cmis/{access_point_id}/browser/object/{object_id}/properties" in paths
|
||||
assert "/api/v1/assets" in paths
|
||||
|
||||
Reference in New Issue
Block a user