Scoped CMIS workspace folders with create, list, parent, path lookup, delete, and delete-tree behavior

This commit is contained in:
2026-05-08 16:47:30 +02:00
parent efb6152487
commit 06e3654aaa
6 changed files with 1103 additions and 23 deletions

View File

@@ -55,9 +55,11 @@ class CMISAction(str, Enum):
QUERY = "query"
GET_RELATIONSHIPS = "get_relationships"
GET_CHANGE_LOG = "get_change_log"
CREATE_FOLDER = "create_folder"
CREATE_DOCUMENT = "create_document"
UPDATE_PROPERTIES = "update_properties"
DELETE_OBJECT = "delete_object"
DELETE_TREE = "delete_tree"
SET_CONTENT_STREAM = "set_content_stream"
APPLY_ACL = "apply_acl"
APPLY_POLICY = "apply_policy"
@@ -84,9 +86,11 @@ ACTION_CAPABILITIES: dict[CMISAction, CMISCapability] = {
CMISAction.QUERY: CMISCapability.DISCOVERY_QUERY,
CMISAction.GET_RELATIONSHIPS: CMISCapability.RELATIONSHIPS,
CMISAction.GET_CHANGE_LOG: CMISCapability.CHANGE_LOG,
CMISAction.CREATE_FOLDER: CMISCapability.OBJECT_WRITE,
CMISAction.CREATE_DOCUMENT: CMISCapability.OBJECT_WRITE,
CMISAction.UPDATE_PROPERTIES: CMISCapability.OBJECT_WRITE,
CMISAction.DELETE_OBJECT: CMISCapability.OBJECT_WRITE,
CMISAction.DELETE_TREE: CMISCapability.OBJECT_WRITE,
CMISAction.SET_CONTENT_STREAM: CMISCapability.CONTENT_STREAM_WRITE,
CMISAction.APPLY_ACL: CMISCapability.ACL,
CMISAction.APPLY_POLICY: CMISCapability.POLICY,
@@ -104,16 +108,20 @@ IMPLEMENTED_CMIS_ACTIONS: frozenset[CMISAction] = frozenset(
CMISAction.QUERY,
CMISAction.GET_RELATIONSHIPS,
CMISAction.GET_CHANGE_LOG,
CMISAction.CREATE_FOLDER,
CMISAction.CREATE_DOCUMENT,
CMISAction.UPDATE_PROPERTIES,
CMISAction.DELETE_OBJECT,
CMISAction.DELETE_TREE,
CMISAction.SET_CONTENT_STREAM,
}
)
MUTATION_ACTIONS = {
CMISAction.CREATE_DOCUMENT,
CMISAction.CREATE_FOLDER,
CMISAction.UPDATE_PROPERTIES,
CMISAction.DELETE_OBJECT,
CMISAction.DELETE_TREE,
CMISAction.SET_CONTENT_STREAM,
CMISAction.APPLY_ACL,
CMISAction.APPLY_POLICY,
@@ -670,7 +678,7 @@ class CMISDomainMapper:
can_write = self.access_point.profile.allow_mutations
return [
_type_definition(CMISBaseType.DOCUMENT, "kontextual:document", "Kontextual Document", can_write),
_type_definition(CMISBaseType.FOLDER, "kontextual:folder", "Kontextual Folder", False),
_type_definition(CMISBaseType.FOLDER, "kontextual:folder", "Kontextual Folder", can_write),
_type_definition(
CMISBaseType.RELATIONSHIP,
"kontextual:relationship",
@@ -874,6 +882,9 @@ class CMISDomainMapper:
"cmis:creationDate": asset.created_at,
"cmis:lastModificationDate": asset.updated_at,
"cmis:changeToken": asset.current_version_id,
"cmis:description": asset.metadata.get("description", asset.title),
"cmis:secondaryObjectTypeIds": list(asset.metadata.get("cmis_secondary_object_type_ids", ())),
"cmis:path": self.asset_path(asset),
"cmis:contentStreamLength": content_stream.get("length") if content_stream else None,
"cmis:contentStreamMimeType": content_stream.get("mime_type") if content_stream else None,
"cmis:contentStreamFileName": content_stream.get("file_name") if content_stream else None,
@@ -890,7 +901,14 @@ class CMISDomainMapper:
key = getattr(record, "key", None)
if key:
properties[f"kontextual:metadata:{key}"] = getattr(record, "value", None)
return compact_dict(properties)
compacted = compact_dict(properties)
compacted.setdefault("cmis:createdBy", "system")
compacted.setdefault("cmis:lastModifiedBy", "system")
compacted.setdefault("cmis:secondaryObjectTypeIds", [])
compacted.setdefault("kontextual:owner", "")
compacted.setdefault("kontextual:topics", [])
compacted.setdefault("kontextual:reviewState", "")
return compacted
def map_content_stream(
self,
@@ -925,6 +943,12 @@ class CMISDomainMapper:
"cmis:isLatestMajorVersion": True,
"cmis:versionSeriesId": f"cmis:version-series:{asset.id}",
"cmis:versionLabel": "1",
"cmis:isVersionSeriesCheckedOut": False,
"cmis:isPrivateWorkingCopy": False,
"cmis:versionSeriesCheckedOutBy": None,
"cmis:versionSeriesCheckedOutId": None,
"cmis:checkinComment": "",
"cmis:isImmutable": False,
}
latest_sequence = max((version.sequence for version in versions), default=current_version.sequence)
return {
@@ -933,6 +957,12 @@ class CMISDomainMapper:
"cmis:isLatestMajorVersion": current_version.sequence == latest_sequence,
"cmis:versionSeriesId": f"cmis:version-series:{asset.id}",
"cmis:versionLabel": str(current_version.sequence),
"cmis:isVersionSeriesCheckedOut": False,
"cmis:isPrivateWorkingCopy": False,
"cmis:versionSeriesCheckedOutBy": None,
"cmis:versionSeriesCheckedOutId": None,
"cmis:checkinComment": "",
"cmis:isImmutable": False,
"kontextual:versionId": current_version.version_id,
"kontextual:versionChangeType": current_version.change_type.value,
}
@@ -1187,7 +1217,10 @@ def cmis_browser_object(projection: dict[str, Any]) -> dict[str, Any]:
for key, value in properties.items()
},
"succinctProperties": properties,
"allowableActions": cmis_browser_allowable_actions(projection.get("allowable_actions", [])),
"allowableActions": cmis_browser_allowable_actions(
projection.get("allowable_actions", []),
base_type_id=properties.get("cmis:baseTypeId"),
),
}
acl = projection.get("acl")
if isinstance(acl, dict) and acl:
@@ -1292,8 +1325,13 @@ def cmis_browser_property_value(property_id: str, value: Any) -> dict[str, Any]:
}
def cmis_browser_allowable_actions(actions: list[str] | tuple[str, ...]) -> dict[str, bool]:
def cmis_browser_allowable_actions(
actions: list[str] | tuple[str, ...],
*,
base_type_id: str | None = None,
) -> dict[str, bool]:
native = set(actions)
is_folder = base_type_id == CMISBaseType.FOLDER.value
return {
"canGetObjectParents": CMISAction.GET_OBJECT_PARENTS.value in native,
"canGetProperties": CMISAction.GET_OBJECT.value in native,
@@ -1305,13 +1343,13 @@ def cmis_browser_allowable_actions(actions: list[str] | tuple[str, ...]) -> dict
"canSetContentStream": CMISAction.SET_CONTENT_STREAM.value in native,
"canGetChildren": CMISAction.GET_CHILDREN.value in native,
"canCreateDocument": CMISAction.CREATE_DOCUMENT.value in native,
"canCreateFolder": False,
"canCreateFolder": CMISAction.CREATE_FOLDER.value in native,
"canCreateRelationship": False,
"canCreateItem": False,
"canDeleteTree": False,
"canDeleteTree": CMISAction.DELETE_TREE.value in native,
"canGetDescendants": False,
"canGetFolderTree": False,
"canGetFolderParent": False,
"canGetFolderParent": is_folder and CMISAction.GET_OBJECT_PARENTS.value in native,
"canGetRenditions": False,
"canMoveObject": False,
"canAddObjectToFolder": False,
@@ -1329,6 +1367,9 @@ def cmis_browser_allowable_actions(actions: list[str] | tuple[str, ...]) -> dict
def cmis_browser_root_folder(access_point: CMISAccessPoint) -> dict[str, Any]:
allowable_actions = [CMISAction.GET_OBJECT.value, CMISAction.GET_CHILDREN.value]
if access_point.profile.allow_mutations:
allowable_actions.extend([CMISAction.CREATE_DOCUMENT.value, CMISAction.CREATE_FOLDER.value])
return {
"object_id": access_point.root_folder_id,
"base_type_id": CMISBaseType.FOLDER.value,
@@ -1352,8 +1393,10 @@ def cmis_browser_root_folder(access_point: CMISAccessPoint) -> dict[str, Any]:
"cmis:allowedChildObjectTypeIds": [CMISBaseType.DOCUMENT.value, CMISBaseType.FOLDER.value],
"kontextual:sensitivity": "internal",
"kontextual:lifecycle": LifecycleState.ACTIVE.value,
"kontextual:filingSource": "root",
"kontextual:workspaceFolder": False,
},
"allowable_actions": [CMISAction.GET_OBJECT.value, CMISAction.GET_CHILDREN.value],
"allowable_actions": allowable_actions,
}
@@ -1545,6 +1588,7 @@ def _browser_propdef(
def _browser_object_properties(projection: dict[str, Any]) -> dict[str, Any]:
properties = dict(projection.get("properties", {}))
properties.update(projection.get("version", {}))
base_type = projection.get("base_type_id") or properties.get("cmis:baseTypeId")
properties["cmis:objectId"] = projection.get("object_id", properties.get("cmis:objectId"))
properties["cmis:name"] = projection.get("name", properties.get("cmis:name"))
@@ -1612,8 +1656,8 @@ def _type_definition(
"queryable": is_document,
"controllable_acl": is_document,
"controllable_policy": False,
"creatable": can_write and is_document,
"fileable": is_document,
"creatable": can_write and base_type_id in {CMISBaseType.DOCUMENT, CMISBaseType.FOLDER},
"fileable": base_type_id in {CMISBaseType.DOCUMENT, CMISBaseType.FOLDER},
"fulltext_indexed": False,
"included_in_supertype_query": is_document,
"versionable": False,
@@ -1627,6 +1671,14 @@ def _property_definitions(base_type_id: CMISBaseType) -> dict[str, dict[str, Any
"cmis:name": {"property_type": "string", "cardinality": "single", "required": True},
"cmis:baseTypeId": {"property_type": "id", "cardinality": "single", "required": True},
"cmis:objectTypeId": {"property_type": "id", "cardinality": "single", "required": True},
"cmis:createdBy": {"property_type": "string", "cardinality": "single", "required": False},
"cmis:lastModifiedBy": {"property_type": "string", "cardinality": "single", "required": False},
"cmis:creationDate": {"property_type": "datetime", "cardinality": "single", "required": False},
"cmis:lastModificationDate": {"property_type": "datetime", "cardinality": "single", "required": False},
"cmis:changeToken": {"property_type": "string", "cardinality": "single", "required": False},
"cmis:secondaryObjectTypeIds": {"property_type": "id", "cardinality": "multi", "required": False},
"cmis:path": {"property_type": "string", "cardinality": "single", "required": False},
"cmis:description": {"property_type": "string", "cardinality": "single", "required": False},
"kontextual:sensitivity": {"property_type": "string", "cardinality": "single", "required": False},
"kontextual:lifecycle": {"property_type": "string", "cardinality": "single", "required": False},
}
@@ -1653,8 +1705,109 @@ def _property_definitions(base_type_id: CMISBaseType) -> dict[str, dict[str, Any
"cardinality": "single",
"required": False,
},
"kontextual:assetId": {
"property_type": "id",
"cardinality": "single",
"required": False,
},
"kontextual:assetType": {
"property_type": "string",
"cardinality": "single",
"required": False,
},
"kontextual:owner": {
"property_type": "string",
"cardinality": "single",
"required": False,
},
"kontextual:topics": {
"property_type": "string",
"cardinality": "multi",
"required": False,
},
"kontextual:reviewState": {
"property_type": "string",
"cardinality": "single",
"required": False,
},
"cmis:isImmutable": {
"property_type": "boolean",
"cardinality": "single",
"required": False,
},
"cmis:isLatestVersion": {
"property_type": "boolean",
"cardinality": "single",
"required": False,
},
"cmis:isMajorVersion": {
"property_type": "boolean",
"cardinality": "single",
"required": False,
},
"cmis:isLatestMajorVersion": {
"property_type": "boolean",
"cardinality": "single",
"required": False,
},
"cmis:versionLabel": {
"property_type": "string",
"cardinality": "single",
"required": False,
},
"cmis:versionSeriesId": {
"property_type": "id",
"cardinality": "single",
"required": False,
},
"cmis:isVersionSeriesCheckedOut": {
"property_type": "boolean",
"cardinality": "single",
"required": False,
},
"cmis:isPrivateWorkingCopy": {
"property_type": "boolean",
"cardinality": "single",
"required": False,
},
"cmis:versionSeriesCheckedOutBy": {
"property_type": "string",
"cardinality": "single",
"required": False,
},
"cmis:versionSeriesCheckedOutId": {
"property_type": "id",
"cardinality": "single",
"required": False,
},
"cmis:checkinComment": {
"property_type": "string",
"cardinality": "single",
"required": False,
},
"kontextual:versionId": {
"property_type": "id",
"cardinality": "single",
"required": False,
},
"kontextual:versionChangeType": {
"property_type": "string",
"cardinality": "single",
"required": False,
},
}
)
if base_type_id == CMISBaseType.FOLDER:
definitions["kontextual:filingSource"] = {
"property_type": "string",
"cardinality": "single",
"required": False,
}
definitions["kontextual:workspaceFolder"] = {
"property_type": "boolean",
"cardinality": "single",
"required": False,
}
if base_type_id == CMISBaseType.RELATIONSHIP:
definitions["cmis:sourceId"] = {"property_type": "id", "cardinality": "single", "required": True}
definitions["cmis:targetId"] = {"property_type": "id", "cardinality": "single", "required": True}