projection-only multifiling

This commit is contained in:
2026-05-07 02:52:49 +02:00
parent 801d2f4851
commit 81e132b33b
9 changed files with 250 additions and 46 deletions

View File

@@ -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/object/{object_id}
GET /cmis/{access_point_id}/browser/content/{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/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/query
GET /cmis/{access_point_id}/browser/relationships GET /cmis/{access_point_id}/browser/relationships
GET /cmis/{access_point_id}/browser/changes 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 | | Browser Binding repository info | yes | yes | yes | yes |
| Type definitions | yes | yes | yes | yes | | Type definitions | yes | yes | yes | yes |
| Synthetic navigation | yes | yes | yes | yes | | Synthetic navigation | yes | yes | yes | yes |
| Projection-only multifiling | yes | yes | yes | yes |
| Object reads | yes | yes | yes | yes | | Object reads | yes | yes | yes | yes |
| Content stream descriptors | yes | yes | yes | yes | | Content stream descriptors | yes | yes | yes | yes |
| ACL projection | discover | discover | discover | discover | | ACL projection | discover | discover | discover | discover |
@@ -103,7 +105,7 @@ It is not yet suitable for clients that require:
- AtomPub, - AtomPub,
- SOAP/Web Services, - SOAP/Web Services,
- full CMIS SQL, - full CMIS SQL,
- multifiling/unfiling, - mutating multifiling/unfiling,
- private-working-copy semantics, - private-working-copy semantics,
- retention/hold mutation, - retention/hold mutation,
- rendition streams, - rendition streams,
@@ -132,6 +134,8 @@ capability groups before treating them as implementation bugs.
## Operational Notes ## Operational Notes
- Hidden objects should be treated as not found by CMIS clients. - 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 - Relationship and change-log responses are filtered through the same visibility
gates as object reads. gates as object reads.
- Mutations always pass through engine services and produce normal engine audit - 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. not physical removal.
- Compatibility should be discussed per profile and per client rather than as a - Compatibility should be discussed per profile and per client rather than as a
repo-wide binary property. repo-wide binary property.

View File

@@ -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 as object reads. This prevents indirect leakage of confidential or restricted
asset IDs through relationship targets or audit-backed change entries. 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 ## Fixture And Optional TCK Integration
CMIS fixtures now act as active compatibility contracts: CMIS fixtures now act as active compatibility contracts:

View File

@@ -104,7 +104,7 @@
"examples": ["root-folder", "topic-folder", "source-system-folder", "unfiled-assets"], "examples": ["root-folder", "topic-folder", "source-system-folder", "unfiled-assets"],
"supported_profiles": ["readonly-browser", "governed-authoring", "admin-export", "compat-tck"], "supported_profiles": ["readonly-browser", "governed-authoring", "admin-export", "compat-tck"],
"must_validate": ["get_children", "get_descendants", "get_folder_tree", "get_object_by_path"], "must_validate": ["get_children", "get_descendants", "get_folder_tree", "get_object_by_path"],
"unsupported": ["multifiling", "unfiling"] "unsupported": ["unfiling"]
}, },
{ {
"id": "object-content", "id": "object-content",
@@ -166,7 +166,7 @@
"unsupported_diagnostics": { "unsupported_diagnostics": {
"atompub": "binding_not_supported", "atompub": "binding_not_supported",
"web_services": "binding_not_supported", "web_services": "binding_not_supported",
"multifiling": "capability_not_supported", "multifiling": "projection_only",
"unfiling": "capability_not_supported", "unfiling": "capability_not_supported",
"append_content_stream": "capability_not_supported", "append_content_stream": "capability_not_supported",
"private_working_copy": "capability_not_supported", "private_working_copy": "capability_not_supported",

View File

@@ -346,26 +346,12 @@ class ServiceRuntime:
decision = mapper.access_point.decide_action(CMISAction.GET_CHILDREN, context) decision = mapper.access_point.decide_action(CMISAction.GET_CHILDREN, context)
if not decision.allowed: if not decision.allowed:
raise _cmis_authorization_error(decision, "getChildren") raise _cmis_authorization_error(decision, "getChildren")
projections = [ folder_path = _cmis_folder_path(folder_id)
projection.to_dict() projections = self._cmis_children_for_folder(mapper, context, folder_path=folder_path)
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),
)
)
]
paged = projections[max(skip_count, 0) : max(skip_count, 0) + max(max_items, 0)] paged = projections[max(skip_count, 0) : max(skip_count, 0) + max(max_items, 0)]
return { return {
"folder_id": folder_id or mapper.access_point.root_folder_id, "folder_id": folder_id or mapper.access_point.root_folder_id,
"folder_path": folder_path or "/",
"objects": paged, "objects": paged,
"num_items": len(paged), "num_items": len(paged),
"has_more_items": len(projections) > max(skip_count, 0) + len(paged), "has_more_items": len(projections) > max(skip_count, 0) + len(paged),
@@ -441,6 +427,26 @@ class ServiceRuntime:
) )
return acl 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( def cmis_create_document(
self, self,
access_point_id: str, access_point_id: str,
@@ -584,18 +590,14 @@ class ServiceRuntime:
"supported": ["SELECT * FROM cmis:document", "SELECT * FROM kontextual:document"], "supported": ["SELECT * FROM cmis:document", "SELECT * FROM kontextual:document"],
}, },
) )
children = self.cmis_children( projections = self._cmis_document_projections(mapper, context)
access_point_id, paged = projections[max(skip_count, 0) : max(skip_count, 0) + max(max_items, 0)]
context,
skip_count=skip_count,
max_items=max_items,
)
return { return {
"query": query, "query": query,
"results": children["objects"], "results": paged,
"num_items": children["num_items"], "num_items": len(paged),
"has_more_items": children["has_more_items"], "has_more_items": len(projections) > max(skip_count, 0) + len(paged),
"total_num_items": children["total_num_items"], "total_num_items": len(projections),
} }
def cmis_relationships( def cmis_relationships(
@@ -685,6 +687,74 @@ class ServiceRuntime:
return self._cmis_asset_visible(mapper, relationship.target_id, context) return self._cmis_asset_visible(mapper, relationship.target_id, context)
return True 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]: def create_asset(self, payload: dict[str, Any], context: OperationContext) -> dict[str, Any]:
classification = Classification.from_dict(payload["classification"]) classification = Classification.from_dict(payload["classification"])
result = self.asset_service().create_asset( result = self.asset_service().create_asset(
@@ -2140,6 +2210,14 @@ def create_app(runtime: ServiceRuntime | None = None):
) -> dict[str, Any]: ) -> dict[str, Any]:
return response(runtime.cmis_acl, access_point_id, object_id, context) 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"]) @app.post("/cmis/{access_point_id}/browser/document", tags=["cmis"])
def cmis_create_document( def cmis_create_document(
access_point_id: str, access_point_id: str,
@@ -2632,6 +2710,29 @@ def _cmis_asset_id(object_id: str | None) -> str:
return normalized 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: def _cmis_authorization_error(decision: PolicyDecision, operation: str) -> AuthorizationError:
return AuthorizationError( return AuthorizationError(
"CMIS operation denied by access-point profile", "CMIS operation denied by access-point profile",

View File

@@ -48,6 +48,7 @@ class CMISAction(str, Enum):
GET_REPOSITORY_INFO = "get_repository_info" GET_REPOSITORY_INFO = "get_repository_info"
GET_TYPE_DEFINITION = "get_type_definition" GET_TYPE_DEFINITION = "get_type_definition"
GET_CHILDREN = "get_children" GET_CHILDREN = "get_children"
GET_OBJECT_PARENTS = "get_object_parents"
GET_OBJECT = "get_object" GET_OBJECT = "get_object"
GET_CONTENT_STREAM = "get_content_stream" GET_CONTENT_STREAM = "get_content_stream"
GET_ACL = "get_acl" GET_ACL = "get_acl"
@@ -76,6 +77,7 @@ ACTION_CAPABILITIES: dict[CMISAction, CMISCapability] = {
CMISAction.GET_REPOSITORY_INFO: CMISCapability.REPOSITORY, CMISAction.GET_REPOSITORY_INFO: CMISCapability.REPOSITORY,
CMISAction.GET_TYPE_DEFINITION: CMISCapability.TYPE_DEFINITIONS, CMISAction.GET_TYPE_DEFINITION: CMISCapability.TYPE_DEFINITIONS,
CMISAction.GET_CHILDREN: CMISCapability.NAVIGATION, CMISAction.GET_CHILDREN: CMISCapability.NAVIGATION,
CMISAction.GET_OBJECT_PARENTS: CMISCapability.NAVIGATION,
CMISAction.GET_OBJECT: CMISCapability.OBJECT_READ, CMISAction.GET_OBJECT: CMISCapability.OBJECT_READ,
CMISAction.GET_CONTENT_STREAM: CMISCapability.CONTENT_STREAM_READ, CMISAction.GET_CONTENT_STREAM: CMISCapability.CONTENT_STREAM_READ,
CMISAction.GET_ACL: CMISCapability.ACL, CMISAction.GET_ACL: CMISCapability.ACL,
@@ -486,7 +488,7 @@ class CMISDomainMapper:
"capability_renditions": "read" if profile.has_capability(CMISCapability.RENDITIONS) else "none", "capability_renditions": "read" if profile.has_capability(CMISCapability.RENDITIONS) else "none",
"capability_get_descendants": profile.has_capability(CMISCapability.NAVIGATION), "capability_get_descendants": profile.has_capability(CMISCapability.NAVIGATION),
"capability_get_folder_tree": 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_unfiling": False,
"capability_version_specific_filing": False, "capability_version_specific_filing": False,
"capability_pwc_searchable": False, "capability_pwc_searchable": False,
@@ -613,21 +615,67 @@ class CMISDomainMapper:
def asset_object_id(self, asset_id: str) -> str: def asset_object_id(self, asset_id: str) -> str:
return f"cmis:asset:{asset_id}" 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: 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") explicit = asset.metadata.get("cmis_path")
if explicit: 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: if asset.source_refs:
source_ref = asset.source_refs[0] source_ref = asset.source_refs[0]
source_root = _safe_path_segment(source_ref.source_system) source_root = _safe_path_segment(source_ref.source_system)
if source_ref.path: 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: if source_ref.external_id:
return _normalize_path(f"/sources/{source_root}/{source_ref.external_id}") paths.append(_normalize_path(f"/sources/{source_root}/{source_ref.external_id}"))
topics = asset.classification.topics for topic in asset.classification.topics:
if topics: paths.append(_normalize_path(f"/topics/{topic}/{asset.id}"))
return _normalize_path(f"/topics/{topics[0]}/{asset.id}") if asset.classification.owner:
return _normalize_path(f"/assets/{asset.classification.asset_type}/{asset.id}") 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( def asset_properties(
self, self,
@@ -716,6 +764,7 @@ class CMISDomainMapper:
CMISAction.GET_CONTENT_STREAM, CMISAction.GET_CONTENT_STREAM,
CMISAction.GET_ACL, CMISAction.GET_ACL,
CMISAction.GET_RELATIONSHIPS, CMISAction.GET_RELATIONSHIPS,
CMISAction.GET_OBJECT_PARENTS,
CMISAction.UPDATE_PROPERTIES, CMISAction.UPDATE_PROPERTIES,
CMISAction.DELETE_OBJECT, CMISAction.DELETE_OBJECT,
CMISAction.SET_CONTENT_STREAM, CMISAction.SET_CONTENT_STREAM,
@@ -819,3 +868,22 @@ def _normalize_path(path: str) -> str:
def _safe_path_segment(value: str) -> str: def _safe_path_segment(value: str) -> str:
return str(value).strip().strip("/") or "_" 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"

View File

@@ -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/object/{object_id}" in paths
assert "/cmis/{access_point_id}/browser/content/{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/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/query" in paths
assert "/cmis/{access_point_id}/browser/relationships" in paths assert "/cmis/{access_point_id}/browser/relationships" in paths
assert "/cmis/{access_point_id}/browser/changes" in paths assert "/cmis/{access_point_id}/browser/changes" in paths

View File

@@ -73,7 +73,7 @@ def test_mapper_exposes_repository_info_capabilities_and_base_type_definitions()
assert repository["cmis_version_supported"] == "1.1" assert repository["cmis_version_supported"] == "1.1"
assert repository["binding"] == "browser" assert repository["binding"] == "browser"
assert repository["capabilities"]["capability_query"] == "metadataonly" assert repository["capabilities"]["capability_query"] == "metadataonly"
assert repository["capabilities"]["capability_multifiling"] is False assert repository["capabilities"]["capability_multifiling"] is True
assert set(types) == { assert set(types) == {
"cmis:document", "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"] 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: def test_governed_authoring_projection_includes_write_allowable_actions() -> None:
mapper = _mapper(CMISAccessProfile.governed_authoring()) mapper = _mapper(CMISAccessProfile.governed_authoring())
asset = _asset() 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:sourceId"] == "cmis:asset:asset-source"
assert serialized["properties"]["cmis:targetId"] == "cmis:asset:asset-target" assert serialized["properties"]["cmis:targetId"] == "cmis:asset:asset-target"
assert serialized["properties"]["kontextual:predicate"] == "derived_from" assert serialized["properties"]["kontextual:predicate"] == "derived_from"

View File

@@ -25,7 +25,7 @@ def cmis_runtime() -> tuple[ServiceRuntime, object]:
asset_type="document", asset_type="document",
sensitivity=Sensitivity.INTERNAL, sensitivity=Sensitivity.INTERNAL,
owner="Platform Knowledge", owner="Platform Knowledge",
topics=("cmis",), topics=("cmis", "integration"),
), ),
context, context,
asset_id="asset-runtime-source", 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") repository = runtime.cmis_repository_info("readonly-browser")
types = runtime.cmis_type_definitions("readonly-browser") types = runtime.cmis_type_definitions("readonly-browser")
children = runtime.cmis_children("readonly-browser", context) 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) 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 access_points["count"] == 4
assert repository["repository_id"] == "kontextual-readonly-browser" assert repository["repository_id"] == "kontextual-readonly-browser"
assert repository["capabilities"]["capability_get_descendants"] is True assert repository["capabilities"]["capability_get_descendants"] is True
assert {item["base_type_id"] for item in types["items"]} >= {"cmis:document", "cmis:folder"} assert {item["base_type_id"] for item in types["items"]} >= {"cmis:document", "cmis:folder"}
object_ids = {item["object_id"] for item in children["objects"]} root_paths = {item["path"] for item in children["objects"]}
assert "cmis:asset:asset-runtime-source" in object_ids topic_object_ids = {item["object_id"] for item in topic_children["objects"]}
assert "cmis:asset:asset-runtime-public" in object_ids parent_paths = {item["path"] for item in parents["parents"]}
assert "cmis:asset:asset-runtime-confidential" not in object_ids 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" assert obj["properties"]["kontextual:assetId"] == "asset-runtime-source"

View File

@@ -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" in paths
assert "/cmis/{access_point_id}/browser/children" 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/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/document" in paths
assert "/cmis/{access_point_id}/browser/object/{object_id}/properties" in paths assert "/cmis/{access_point_id}/browser/object/{object_id}/properties" in paths
assert "/api/v1/assets" in paths assert "/api/v1/assets" in paths