diff --git a/docs/cmis-1-1-capability-scorecard.md b/docs/cmis-1-1-capability-scorecard.md index 094941b..d7d29a2 100644 --- a/docs/cmis-1-1-capability-scorecard.md +++ b/docs/cmis-1-1-capability-scorecard.md @@ -13,6 +13,11 @@ The score below remains a product-depth estimate against mature CMIS products. The selected OpenCMIS baseline is now stable preparation evidence for repository/type and object/content services, not a full CMIS certification. +Read-side contract update: `KONT-WP-0016` adds a documented bounded query +subset, common CMIS `ORDER BY`, target/either relationship filters, enriched +relationship and ACL projections, and explicit `notSupported` diagnostics for +unsupported navigation selectors. + Status: baseline scorecard for the current Browser Binding subset. ## Purpose @@ -72,9 +77,9 @@ CMIS interoperability importance rather than engine-internal importance. | Metric | Score | | --- | ---: | | OpenCMIS selected-baseline infrastructure score | 99.1% | -| Weighted CMIS 1.1 depth vs Hyland Alfresco benchmark | 60% | -| Controlled-client Browser Binding usefulness | 82% | -| Broad commodity CMIS client compatibility | 55% | +| Weighted CMIS 1.1 depth vs Hyland Alfresco benchmark | 63% | +| Controlled-client Browser Binding usefulness | 84% | +| Broad commodity CMIS client compatibility | 57% | Interpretation: the OpenCMIS infrastructure score measures the selected `repository-type` and `object-content` harness baseline only. The current CMIS @@ -94,9 +99,9 @@ broad ECM/CMIS replacement surface. | Object write service | 8 | 72% | Hyland Alfresco ACS | - `createDocument`, `createFolder`, scoped `moveObject`, folder rename, selected standard property updates, custom metadata updates, content stream set/append/delete, `bulkUpdateProperties`, and `createDocumentFromSource` exist.
- No broad filing mutation, raw physical delete, checkout/checkin, or policy/item creation.
- Delete remains intentionally governed, not raw repository removal. | | Content stream read/write | 8 | 86% | Hyland Alfresco ACS | - Byte streaming, explicit content headers, multipart Browser Binding create, deduplicating `setContentStream`, whole-object `appendContentStream`, no-content compatibility streams, content tombstones, partial body slicing, and offset-zero full-stream classification exist.
- Digest verification and governed access exist.
- Chunk-level blob composition remains a later optimization for very large append workloads. | | Versioning service | 8 | 25% | Hyland Alfresco ACS | - Version properties can be projected from engine versions.
- No checkout/checkin/cancelCheckout/PWC services.
- No version history route or all-versions query behavior. | -| Discovery/query | 8 | 25% | Hyland Alfresco ACS | - Narrow document select subset exists.
- Unsupported joins/order-by return diagnostics.
- Missing CMIS SQL predicates, type joins, full-text, ordering, and rich projection rules. | -| Relationships | 5 | 60% | Hyland Alfresco ACS | - Relationship object projection and source filtering exist.
- Visibility gates prevent protected relationship leakage.
- Missing full relationship service filters, relationship creation through CMIS, and type hierarchy maturity. | -| ACL service | 6 | 35% | Hyland Alfresco ACS | - Discover-only ACL projection exists.
- `applyACL` is blocked as not implemented.
- Missing inherited/direct ACL fidelity, propagation, ACL mutation, and repository principal model. | +| Discovery/query | 8 | 42% | Hyland Alfresco ACS | - Bounded `SELECT *` document queries support equality, `LIKE`, `IN`, `AND`, paging, and common CMIS property ordering.
- Capability flags now advertise `capabilityOrderBy=common` rather than overclaiming custom ordering.
- Missing joins, full text, nested predicates, arbitrary projections, and broad CMIS SQL coverage. | +| Relationships | 5 | 70% | Hyland Alfresco ACS | - Relationship object projection, source filters, target filters, either-direction filters, confidence, direction, provenance, and visibility gates exist.
- Protected relationship leakage is covered by profile gates.
- Missing relationship creation through CMIS and deeper relationship type hierarchy maturity. | +| ACL service | 6 | 48% | Hyland Alfresco ACS | - Discover-only ACL projection has stable principal ids, principal kinds, permission mapping, direct/inherited markers, and policy authority metadata.
- `applyACL` is blocked as not implemented.
- Missing propagation, ACL mutation, and repository-wide principal/group enumeration. | | Policy service | 3 | 10% | Hyland Alfresco ACS | - Native policy decisions govern exposure.
- No CMIS policy objects, `applyPolicy`, `removePolicy`, or `getAppliedPolicies` service surface.
- Explicitly unsupported. | | Change log | 5 | 60% | Hyland Alfresco ACS | - Audit-backed object-id change entries and paging exist.
- CMIS change-token conflicts are now enforced for property/content mutations.
- Missing richer change event typing and broader token semantics across optional services. | | Multi-filing and unfiling | 4 | 25% | Hyland Alfresco ACS | - Projection-only parent maps exist and are useful for navigation.
- Standard CMIS `capabilityMultifiling` is correctly false.
- No add/remove filing mutations or canonical folder membership model. | @@ -108,7 +113,7 @@ broad ECM/CMIS replacement surface. | Web Services binding | 2 | 0% | Hyland Alfresco ACS | - No SOAP/WSDL stack.
- Intentionally deferred until monetized need. | | External conformance evidence | 3 | 86% | OpenCMIS TCK against Alfresco-like server behavior | - OpenCMIS Browser Binding session creation succeeds against `compat-tck`.
- Selected `repository-type` and `object-content` baselines complete with one local transport warning and no object/content warnings.
- Evidence still covers a selected baseline, not the full OpenCMIS TCK surface. | -Weighted result from this table: **60%**. +Weighted result from this table: **63%**. ## Most Important Gaps @@ -126,9 +131,10 @@ Weighted result from this table: **60%**. optional services are added. 3. **Query depth** - - Add a real CMIS SQL subset parser instead of a two-query allowlist. - - Support basic `WHERE`, equality predicates, paging, ordering where claimed, - and diagnostics for everything outside the subset. + - Expand only where it stays natural: additional indexed metadata fields, + richer comparator support, and selected client-requested predicates. + - Keep joins, full text, and arbitrary CMIS SQL unsupported unless a real + integration need appears. 4. **Navigation depth** - Decide whether `getDescendants` and `getFolderTree` are worth implementing diff --git a/docs/cmis-compliance-assessment.md b/docs/cmis-compliance-assessment.md index b36d520..761846e 100644 --- a/docs/cmis-compliance-assessment.md +++ b/docs/cmis-compliance-assessment.md @@ -50,15 +50,15 @@ Practical strategy: | Object service write | Governed subset. | `createDocument`, custom metadata updates, `setContentStream`, and delete-request lifecycle transition are supported by authoring profiles. Unsupported standard property updates now fail with diagnostics. | Medium | | Content streams | Implemented subset. | Descriptor and byte-stream routes exist; `setContentStream` and whole-object `appendContentStream` write through deduplicating blob storage, while `deleteContentStream` tombstones the CMIS projection. Chunk-level append composition remains deferred. | Low | | Versioning | Projection only. | Latest-version properties can be projected from engine versions, but CMIS checkout/PWC/all-versions services are not advertised. | Low if unsupported remains acceptable | -| Discovery/query | Implemented narrow subset. | `SELECT * FROM cmis:document` and `SELECT * FROM kontextual:document` are supported. Joins, order-by, full CMIS SQL predicates, and full-text are flagged unsupported. | Medium | -| Relationships | Implemented subset. | Relationship object projections and source filters are covered and profile-gated. | Low | -| ACL service | Discover only. | ACL projection is supported; `applyACL` is not authorized even for authoring profiles and returns an unimplemented diagnostic. | Low | +| Discovery/query | Implemented bounded subset. | `SELECT *` document queries support equality, `LIKE`, `IN`, `AND`, paging, and common CMIS property ordering. Joins, full text, nested predicates, arbitrary projection lists, and custom-property ordering are flagged unsupported. | Medium | +| Relationships | Implemented subset. | Relationship object projections, source filters, target filters, either-direction filters, provenance, confidence, and profile-gated visibility are covered. | Low | +| ACL service | Discover only. | ACL projection is supported with stable principal/permission vocabulary, direct/inherited markers, and policy authority metadata; `applyACL` returns an unimplemented diagnostic. | Low | | Policy service | Unsupported. | `applyPolicy`/`removePolicy` are explicitly unsupported; engine policy remains native, not CMIS policy objects. | Low | | Change log | Implemented subset. | Audit-backed object-id change entries and paging are supported; full property-level change details are not advertised. | Low | | Multi-filing/unfiling | Projection only. | Multiple virtual parents are exposed as a Kontextual repository feature, while CMIS `capabilityMultifiling` and unfiling stay false. | Low | | Renditions | Unsupported. | Capability is `none`; derived representations are not exposed as CMIS rendition streams. | Low | | Retention and hold | Unsupported. | Not advertised; left as native governance metadata until a real integration requires CMIS legal-hold semantics. | Low | -| Bulk update | Unsupported. | `bulkUpdateProperties` is explicitly unsupported. | Low | +| Bulk update | Profile-scoped subset. | `bulkUpdateProperties` is available for the TCK compatibility profile through existing governed property updates and change-token handling; it remains narrow and is not enabled on normal authoring profiles. | Low | | Browser JSON binding | FastAPI JSON service already exists. | Need CMIS Browser Binding routes, selectors/actions, multipart/content stream behavior. | High | | AtomPub binding | No AtomPub/XML binding. | Need XML/Atom feed generation and protocol semantics. | Very High | | Web Services binding | No SOAP stack. | Need WSDL/SOAP implementation. | Very High | @@ -72,7 +72,8 @@ Maintain a constrained CMIS 1.1 Browser Binding profile: projection. - Explicitly unsupported or read-only: AtomPub, Web Services, descendants/tree, full ACL mutation, retention/hold, mutating multifiling/unfiling, PWC/versioning - services, renditions, bulk updates, order-by, and full CMIS SQL joins. + services, renditions, custom-property ordering, broad bulk-update exposure, + and full CMIS SQL joins. Then expand by profile: diff --git a/docs/cmis-read-side-contract.md b/docs/cmis-read-side-contract.md new file mode 100644 index 0000000..6eb29de --- /dev/null +++ b/docs/cmis-read-side-contract.md @@ -0,0 +1,94 @@ +# CMIS Read-Side Contract + +Date: 2026-05-14 +Workplan: `KONT-WP-0016` +Status: release-stable Browser Binding subset + +## Boundary + +The CMIS layer is a connector projection over native `kontextual-engine` +services. Native assets, classifications, metadata records, relationships, +audit events, policy decisions, and blob representations remain authoritative. +CMIS does not own a second object store, a separate ACL model, or an independent +filing graph. + +## Release-Stable Capabilities + +| Capability | Release posture | Contract | +| --- | --- | --- | +| Repository and type discovery | Supported | Browser Binding service document, repository info, base type definitions, and property definitions are stable. Optional unsupported features are declared in `unsupported_features`. | +| Navigation | Bounded supported subset | `children`, `parent`, `parents`, `object`, `properties`, `allowableActions`, `policies`, and `content` are supported through profile-gated folder/path projections. | +| Descendants and folder tree | Unsupported | `descendants` and `folderTree` return CMIS `notSupported`; repository flags remain false. This avoids creating a broad ECM tree contract before there is a native need. | +| Query | Bounded supported subset | `SELECT * FROM cmis:document` and `SELECT * FROM kontextual:document` support simple `WHERE` predicates joined by `AND`, deterministic paging, and common-property ordering. | +| Relationships | Supported read projection | Source, target, and either-direction filters map to the native relationship repository. Relationship objects expose source/target ids, predicate, confidence, direction, provenance, actor, and a stable relationship change token. | +| ACL discovery | Supported projection | `getACL` exposes direct actor permissions plus synthetic/public inheritance markers. The native policy gateway remains the authority; `applyACL` stays unsupported. | +| Change tokens | Supported for governed mutations | Asset `cmis:changeToken` is the current version id. Folder workspace tokens and relationship tokens are projection tokens. Stale asset mutation tokens map to CMIS `updateConflict`. | + +## Query Subset + +Accepted grammar: + +```sql +SELECT * FROM cmis:document +SELECT * FROM kontextual:document +SELECT * FROM cmis:document WHERE = '' [AND ...] +SELECT * FROM cmis:document WHERE LIKE '' [AND ...] +SELECT * FROM cmis:document WHERE IN ('', ...) [AND ...] +SELECT * FROM cmis:document ... ORDER BY [ASC|DESC] +``` + +Filterable fields: + +`cmis:objectId`, `cmis:name`, `cmis:objectTypeId`, `cmis:baseTypeId`, +`cmis:description`, `kontextual:assetId`, `kontextual:assetType`, +`kontextual:sensitivity`, `kontextual:lifecycle`, `kontextual:owner`, +`kontextual:topics`, and `kontextual:reviewState`. + +Orderable fields: + +`cmis:objectId`, `cmis:name`, `cmis:creationDate`, and +`cmis:lastModificationDate`. + +Unsupported joins, `OR`, nested expressions, full text, arbitrary projection +lists, and custom-property ordering return CMIS `notSupported` diagnostics with +the supported grammar and field sets included. + +## Compatibility Notes + +- `capabilityQuery` remains `metadataonly`. +- `capabilityOrderBy` is now `common`, not `none`, because common CMIS property + ordering is implemented and covered by tests. +- `capabilityGetDescendants` and `capabilityGetFolderTree` remain false. +- Multifiling remains projection-only. Mutation semantics are deliberately out + of scope for this read-side contract. +- The ACL vocabulary is intentionally small: `cmis:read`, `cmis:write`, and + `cmis:delete`, with `direct`, `inherited`, `principal_kind`, and `source` + markers. + +## Evidence + +Focused tests run on 2026-05-14: + +```text +python3 -m pytest \ + tests/cmis/test_cmis_runtime_browser_binding.py \ + tests/cmis/test_cmis_browser_binding_api.py \ + tests/cmis/test_cmis_compliance_flags.py \ + tests/cmis/test_cmis_contract_examples.py + +Result: 21 passed, 16 skipped in 5.19s +``` + +Browser Binding API verification with optional service extras: + +```text +.venv/bin/python -m pytest tests/cmis/test_cmis_browser_binding_api.py -q + +Result: 16 passed in 39.09s +``` + +The default system-Python run skips Browser Binding API tests when FastAPI/HTTPX +are unavailable and skips capacity probes unless `KONTEXTUAL_RUN_CAPACITY=1` is +set. The capacity probe exercises 400 documents and 250 relationships over +query and target-filter paths, while relying on the shared performance history +monitor for drift tracking. diff --git a/docs/first-release-readiness.md b/docs/first-release-readiness.md index 6b0d39d..3d2a3f6 100644 --- a/docs/first-release-readiness.md +++ b/docs/first-release-readiness.md @@ -29,11 +29,11 @@ Out of scope for `0.1.0`: | Area | Gate | Current state | | --- | --- | --- | -| CMIS read-side contract | Query, navigation, relationship, ACL, and change-token contracts are release-stable or explicitly waived. | `KONT-WP-0016` created as a pre-release dependency. | -| Tests | Full suite passes in the project venv. | `.venv/bin/python -m pytest -q` passed: `166 passed`, `14 skipped`; advisory performance drift warnings recorded. | +| CMIS read-side contract | Query, navigation, relationship, ACL, and change-token contracts are release-stable or explicitly waived. | `KONT-WP-0016` implemented locally; full release verification still required. | +| Tests | Full suite passes in the project venv. | `.venv/bin/python -m pytest -q` passed: `166 passed`, `15 skipped`; advisory performance drift warnings recorded. | | CMIS evidence | OpenCMIS selected baseline completes with no unexpected findings. | `run-20260513T223537Z` completed; only local HTTP warning remains. | | Transport | Released CMIS access points are served behind HTTPS. | Required deployment gate; local loopback warning is accepted only for harness runs. | -| Capability honesty | Scorecard, unsupported catalog, and examples match behavior. | Updated for `appendContentStream`; final doc review required. | +| Capability honesty | Scorecard, unsupported catalog, and examples match behavior. | Updated for `appendContentStream` and WP-0016 read-side contract; final doc review required. | | Packaging | Version, dependencies, optional extras, and install smoke are checked. | `pyproject.toml` is already `0.1.0`; build/install smoke still required. | | Configuration | Storage backend, S3 settings, local blob path, and environment defaults are documented. | Existing docs cover backends; release runbook should point to them. | | Data safety | Blob cleanup, backups, restore path, and migration posture are documented. | Cleanup exists; backup/restore release notes still needed. | @@ -54,23 +54,21 @@ Out of scope for `0.1.0`: ## Release Procedure -1. Complete or explicitly waive `KONT-WP-0016`. -2. Run `.venv/bin/python -m pytest -q`. -3. Run the OpenCMIS selected baseline through `guide-board` and persist the +1. Run `.venv/bin/python -m pytest -q`. +2. Run the OpenCMIS selected baseline through `guide-board` and persist the evidence document. -4. Run State Hub consistency check and ensure workplans are registered. -5. Run packaging smoke: build wheel/sdist and import `kontextual_engine` from a +3. Run State Hub consistency check and ensure workplans are registered. +4. Run packaging smoke: build wheel/sdist and import `kontextual_engine` from a clean install. -6. Review security/configuration: HTTPS termination, profile exposure, secrets, +5. Review security/configuration: HTTPS termination, profile exposure, secrets, local/S3 blob backend settings, and dependency licenses. -7. Update `CHANGELOG.md` or release notes with capabilities, known limitations, +6. Update `CHANGELOG.md` or release notes with capabilities, known limitations, and accepted warnings. -8. Tag the release only after the gates above are green or explicitly waived. +7. Tag the release only after the gates above are green or explicitly waived. ## Release Decision -The current foundation is close to a controlled `0.1.0` preview, but CMIS -read-side contracts should be settled before release if external projects will -build on the engine. After `KONT-WP-0016`, the remaining release work is mainly -discipline around repeatable verification, packaging, deployment posture, and -clearly documented limits. +The current foundation is close to a controlled `0.1.0` preview. With the +`KONT-WP-0016` read-side contract settled locally, the remaining release work is +mainly discipline around repeatable verification, packaging, deployment +posture, and clearly documented limits. diff --git a/examples/cmis/capability-fixtures.json b/examples/cmis/capability-fixtures.json index 4def144..f5a82df 100644 --- a/examples/cmis/capability-fixtures.json +++ b/examples/cmis/capability-fixtures.json @@ -125,10 +125,10 @@ { "id": "discovery-query", "service": "discovery", - "examples": ["lexical-query", "metadata-filter", "relationship-scoped-query", "unsupported-join"], + "examples": ["lexical-query", "metadata-filter", "relationship-scoped-query", "bounded-order-by", "unsupported-join"], "supported_profiles": ["readonly-browser", "governed-authoring", "admin-export", "compat-tck"], - "must_validate": ["query_capabilities", "paging", "unsupported_grammar_diagnostics"], - "unsupported": ["full_cmis_sql_joins", "order_by"] + "must_validate": ["query_capabilities", "paging", "bounded_order_by", "unsupported_grammar_diagnostics"], + "unsupported": ["full_cmis_sql_joins", "custom_order_by"] }, { "id": "relationships", @@ -174,7 +174,7 @@ "private_working_copy": "capability_not_supported", "all_versions_search": "capability_not_supported", "full_cmis_sql_joins": "query_not_supported", - "order_by": "query_not_supported", + "custom_order_by": "custom_order_by_not_supported", "apply_acl": "operation_not_implemented", "apply_policy": "capability_not_supported", "remove_policy": "capability_not_supported", diff --git a/src/kontextual_engine/api/app.py b/src/kontextual_engine/api/app.py index 0cca7b4..37c0bde 100644 --- a/src/kontextual_engine/api/app.py +++ b/src/kontextual_engine/api/app.py @@ -7,6 +7,7 @@ requests into service/runtime contracts and must not own domain behavior. from __future__ import annotations import json +import re from dataclasses import dataclass, field, replace from datetime import datetime from email import policy @@ -90,6 +91,43 @@ from kontextual_engine.services import ( API_VERSION = "v1" OPENAPI_VERSION = "1.0.0" CMIS_APPEND_MAX_COMPOSED_BYTES = 64 * 1024 * 1024 +CMIS_QUERY_SUPPORTED = [ + "SELECT * FROM cmis:document", + "SELECT * FROM kontextual:document", + "SELECT * FROM cmis:document WHERE = '' [AND ...]", + "SELECT * FROM cmis:document WHERE LIKE '' [AND ...]", + "SELECT * FROM cmis:document WHERE IN ('', ...) [AND ...]", + "SELECT * FROM cmis:document ... ORDER BY [ASC|DESC]", +] +CMIS_QUERY_FILTERABLE_FIELDS = { + "cmis:objectId", + "cmis:name", + "cmis:objectTypeId", + "cmis:baseTypeId", + "cmis:description", + "kontextual:assetId", + "kontextual:assetType", + "kontextual:sensitivity", + "kontextual:lifecycle", + "kontextual:owner", + "kontextual:topics", + "kontextual:reviewState", +} +CMIS_QUERY_ORDERABLE_FIELDS = { + "cmis:objectId", + "cmis:name", + "cmis:creationDate", + "cmis:lastModificationDate", +} +CMIS_QUERY_LIKE_FIELDS = { + "cmis:name", + "cmis:description", + "kontextual:assetId", + "kontextual:assetType", + "kontextual:owner", + "kontextual:topics", + "kontextual:reviewState", +} AGENT_OPERATION_CATALOG: tuple[dict[str, Any], ...] = ( @@ -1593,19 +1631,13 @@ class ServiceRuntime: decision = mapper.access_point.decide_action(CMISAction.QUERY, context) if not decision.allowed: raise _cmis_authorization_error(decision, "query") - normalized = query.strip().lower() - if normalized not in {"select * from cmis:document", "select * from kontextual:document"}: - raise ValidationError( - "Unsupported CMIS query subset", - details={ - "query": query, - "supported": ["SELECT * FROM cmis:document", "SELECT * FROM kontextual:document"], - }, - ) + query_spec = _parse_cmis_query(query) projections = self._cmis_document_projections(mapper, context) + projections = _apply_cmis_query_spec(projections, query_spec) paged = projections[max(skip_count, 0) : max(skip_count, 0) + max(max_items, 0)] return { "query": query, + "query_spec": query_spec, "results": paged, "num_items": len(paged), "has_more_items": len(projections) > max(skip_count, 0) + len(paged), @@ -1618,19 +1650,55 @@ class ServiceRuntime: context: OperationContext, *, object_id: str | None = None, + target_id: str | None = None, + relationship_direction: str | None = None, ) -> dict[str, Any]: mapper = self._cmis_mapper(access_point_id) decision = mapper.access_point.decide_action(CMISAction.GET_RELATIONSHIPS, context) if not decision.allowed: raise _cmis_authorization_error(decision, "getRelationships") - source_id = _cmis_asset_id(object_id) if object_id else None + direction = (relationship_direction or "source").strip().lower() + if direction not in {"source", "target", "either"}: + raise ValidationError( + "Unsupported CMIS relationship direction", + details={ + "code": "cmis.relationship_direction_unsupported", + "operation": "getObjectRelationships", + "direction": relationship_direction, + "supported": ["source", "target", "either"], + }, + ) + source_filter = _cmis_asset_id(object_id) if object_id and direction == "source" else None + target_filter = _cmis_asset_id(object_id) if object_id and direction == "target" else None + if target_id: + target_filter = _cmis_asset_id(target_id) + relationships = self.repository.list_relationships(source_id=source_filter, target_id=target_filter) + if object_id and direction == "either": + asset_id = _cmis_asset_id(object_id) + relationships = [ + relationship + for relationship in relationships + if relationship.source_id == asset_id + or ( + relationship.target_kind == RelationshipTargetKind.ASSET + and relationship.target_id == asset_id + ) + ] projections = [ projection.to_dict() - for relationship in self.repository.list_relationships(source_id=source_id) + for relationship in relationships if self._cmis_relationship_visible(mapper, relationship, context) if (projection := mapper.map_relationship(relationship, context)) ] - return {"items": projections, "count": len(projections)} + return { + "items": projections, + "count": len(projections), + "filters": { + "object_id": object_id, + "target_id": target_id, + "relationship_direction": direction, + }, + } def cmis_change_log( self, @@ -3629,26 +3697,38 @@ def create_app(runtime: ServiceRuntime | None = None): ) def unsupported_browser_selector(selector: str | None) -> dict[str, Any]: + unsupported_details: dict[str, Any] = { + "cmisselector": selector, + "supported": [ + "repositoryInfo", + "typeChildren", + "typeDescendants", + "typeDefinition", + "query", + "object", + "children", + "parent", + "parents", + "properties", + "allowableActions", + "policies", + "content", + ], + } + if selector in {"descendants", "folderTree"}: + unsupported_details.update( + { + "code": "cmis.not_supported", + "cmis_exception": "notSupported", + "unsupported_feature": "get_descendants" + if selector == "descendants" + else "get_folder_tree", + "release_contract": "Navigation tree selectors remain unsupported for the first release.", + } + ) raise ValidationError( "Unsupported CMIS Browser Binding selector", - details={ - "cmisselector": selector, - "supported": [ - "repositoryInfo", - "typeChildren", - "typeDescendants", - "typeDefinition", - "query", - "object", - "children", - "parent", - "parents", - "properties", - "allowableActions", - "policies", - "content", - ], - }, + details=unsupported_details, ) async def browser_action_payload(request: Request) -> dict[str, Any]: @@ -4230,9 +4310,18 @@ def create_app(runtime: ServiceRuntime | None = None): def cmis_relationships( access_point_id: str, object_id: str | None = Query(None), + target_id: str | None = Query(None, alias="targetId"), + relationship_direction: str | None = Query(None, alias="relationshipDirection"), context: OperationContext = Depends(context_from_headers), ) -> dict[str, Any]: - return response(runtime.cmis_relationships, access_point_id, context, object_id=object_id) + return response( + runtime.cmis_relationships, + access_point_id, + context, + object_id=object_id, + target_id=target_id, + relationship_direction=relationship_direction, + ) @app.get("/cmis/{access_point_id}/browser/changes", tags=["cmis"]) def cmis_changes( @@ -4702,6 +4791,212 @@ def _normalize_cmis_path(path: str) -> str: return "/" + "/".join(parts) +_CMIS_QUERY_RE = re.compile( + r"^\s*SELECT\s+(?P