generated from coulomb/repo-seed
CMIS Browser Binding fixes
This commit is contained in:
@@ -2,16 +2,19 @@
|
||||
|
||||
Date: 2026-05-07
|
||||
|
||||
Evidence update: the 2026-05-08 OpenCMIS TCK compatibility implementation
|
||||
resolved the initial Browser Binding session blocker. The latest run,
|
||||
`run-20260508T092113Z`, completed the selected baseline with `38` passing
|
||||
repository/type cases, one local HTTP transport warning, and `22` object/content
|
||||
skips caused by the current non-creatable folder profile. See
|
||||
`docs/cmis-opencmis-tck-implementation-evidence-2026-05-08T092113Z.md`.
|
||||
Evidence update: the 2026-05-08 WP-0014 OpenCMIS pass moved the selected
|
||||
baseline beyond the original folder-creatable skip boundary and through several
|
||||
object/content maturity issues. The latest run, `run-20260508T164334Z`, reports
|
||||
`repository-type` as warning-only and reduces `object-content` to specific
|
||||
semantic gaps: invalid-type exception mapping, bulk update, content deletion,
|
||||
change-token conflict handling, copy/create-from-source, and one range
|
||||
classification warning. See
|
||||
`docs/cmis-opencmis-tck-wp0014-evidence-2026-05-08T164334Z.md`.
|
||||
|
||||
The score below remains a product-depth estimate against mature CMIS products;
|
||||
the evidence-backed TCK preparation score for the selected baseline is `23.81`
|
||||
with `2/9` capability groups covered.
|
||||
The score below remains a product-depth estimate against mature CMIS products.
|
||||
The selected OpenCMIS baseline still exits as `infrastructure_error`, but the
|
||||
failure frontier is now narrow and capability-specific rather than basic
|
||||
Browser Binding shape or navigation.
|
||||
|
||||
Status: baseline scorecard for the current Browser Binding subset.
|
||||
|
||||
@@ -71,9 +74,9 @@ CMIS interoperability importance rather than engine-internal importance.
|
||||
|
||||
| Metric | Score |
|
||||
| --- | ---: |
|
||||
| Weighted CMIS 1.1 depth vs Hyland Alfresco benchmark | 42% |
|
||||
| Controlled-client Browser Binding usefulness | 58% |
|
||||
| Broad commodity CMIS client compatibility | 35% |
|
||||
| Weighted CMIS 1.1 depth vs Hyland Alfresco benchmark | 51% |
|
||||
| Controlled-client Browser Binding usefulness | 70% |
|
||||
| Broad commodity CMIS client compatibility | 46% |
|
||||
|
||||
Interpretation: the current CMIS layer is a credible Browser Binding subset for
|
||||
known clients and profile-specific integrations. It is not yet a broad ECM/CMIS
|
||||
@@ -83,41 +86,46 @@ replacement surface.
|
||||
|
||||
| CMIS capability area | Weight | Current depth | Most worthy contender | Gap basis behind the percentage |
|
||||
| --- | ---: | ---: | --- | --- |
|
||||
| Repository service and repository info | 5 | 75% | Hyland Alfresco ACS | - Repository info and conservative capability flags exist.<br>- Unsupported feature catalog exists.<br>- Missing exact service-document parity and external TCK evidence. |
|
||||
| Type definitions | 6 | 45% | Hyland Alfresco ACS | - Base types and content stream properties exist.<br>- No mutable types or custom schema/type management.<br>- No broad property definition model beyond current projected fields. |
|
||||
| Navigation service | 8 | 40% | Hyland Alfresco ACS | - Root and folder-scoped children exist.<br>- Projection-only parents exist.<br>- Missing `getDescendants`, `getFolderTree`, object-by-path parity, and real filing mutations. |
|
||||
| Object read service | 10 | 70% | Hyland Alfresco ACS | - Object envelopes, properties, content descriptors, ACL projection, relationships, and allowable actions exist.<br>- Missing selector/property-filter fidelity and full Browser Binding response parity.<br>- Deleted/hidden objects are now correctly not exposed. |
|
||||
| Object write service | 8 | 35% | Hyland Alfresco ACS | - `createDocument`, custom metadata updates, content stream set, and delete-request lifecycle exist.<br>- No createFolder, moveObject, standard `cmis:*` property mutation, or physical delete semantics.<br>- Delete is intentionally governed, not raw repository removal. |
|
||||
| Content stream read/write | 8 | 65% | Hyland Alfresco ACS | - Byte streaming and deduplicating `setContentStream` exist.<br>- Digest verification and governed access exist.<br>- Missing append/delete stream, multipart Browser Binding parity, range handling, and client-tested large stream workflows. |
|
||||
| Repository service and repository info | 5 | 82% | Hyland Alfresco ACS | - Repository info and conservative capability flags exist.<br>- Unsupported feature catalog exists.<br>- OpenCMIS `repository-type` is warning-only, with the remaining warning caused by local HTTP. |
|
||||
| Type definitions | 6 | 52% | Hyland Alfresco ACS | - Base types and nullable content stream properties exist.<br>- No mutable types or custom schema/type management.<br>- Property definition depth remains intentionally narrow. |
|
||||
| Navigation service | 8 | 58% | Hyland Alfresco ACS | - Root and folder-scoped children, path lookup, folder parent lookup, and parent path segments work.<br>- Projection-only parents exist.<br>- Missing `getDescendants`, `getFolderTree`, and real filing mutations. |
|
||||
| Object read service | 10 | 78% | Hyland Alfresco ACS | - Object envelopes, properties, content descriptors, ACL projection, relationships, allowable actions, property filters, and path-addressed Browser Binding reads exist.<br>- Deleted/hidden objects are now correctly not exposed.<br>- Remaining read-side gaps are mostly around optional services and exception shape. |
|
||||
| Object write service | 8 | 58% | Hyland Alfresco ACS | - `createDocument`, `createFolder`, scoped `moveObject`, folder rename, selected standard property updates, custom metadata updates, content stream set, and delete-request lifecycle exist.<br>- No bulk update, copy/create-from-source, broad filing mutation, or physical delete semantics.<br>- Delete is intentionally governed, not raw repository removal. |
|
||||
| Content stream read/write | 8 | 74% | Hyland Alfresco ACS | - Byte streaming, explicit content headers, multipart Browser Binding create, deduplicating `setContentStream`, no-content compatibility streams, and partial body slicing exist.<br>- Digest verification and governed access exist.<br>- Remaining gaps are delete-content semantics, append semantics, change-token conflicts, and one range classification warning. |
|
||||
| Versioning service | 8 | 25% | Hyland Alfresco ACS | - Version properties can be projected from engine versions.<br>- No checkout/checkin/cancelCheckout/PWC services.<br>- No version history route or all-versions query behavior. |
|
||||
| Discovery/query | 8 | 25% | Hyland Alfresco ACS | - Narrow document select subset exists.<br>- Unsupported joins/order-by return diagnostics.<br>- 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.<br>- Visibility gates prevent protected relationship leakage.<br>- 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.<br>- `applyACL` is blocked as not implemented.<br>- Missing inherited/direct ACL fidelity, propagation, ACL mutation, and repository principal model. |
|
||||
| Policy service | 3 | 10% | Hyland Alfresco ACS | - Native policy decisions govern exposure.<br>- No CMIS policy objects, `applyPolicy`, `removePolicy`, or `getAppliedPolicies` service surface.<br>- Explicitly unsupported. |
|
||||
| Change log | 5 | 55% | Hyland Alfresco ACS | - Audit-backed object-id change entries and paging exist.<br>- Missing full change token durability semantics and richer change event typing.<br>- Not yet proven against external CMIS clients. |
|
||||
| Change log | 5 | 55% | Hyland Alfresco ACS | - Audit-backed object-id change entries and paging exist.<br>- Missing CMIS update-conflict behavior for reused change tokens and richer change event typing.<br>- Change-token maturity is now directly visible in OpenCMIS object/content. |
|
||||
| Multi-filing and unfiling | 4 | 25% | Hyland Alfresco ACS | - Projection-only parent maps exist and are useful for navigation.<br>- Standard CMIS `capabilityMultifiling` is correctly false.<br>- No add/remove filing mutations or canonical folder membership model. |
|
||||
| Renditions | 3 | 15% | Hyland Alfresco ACS | - Native representations could become rendition candidates later.<br>- CMIS rendition capability is currently `none`.<br>- No rendition taxonomy or rendition stream routes. |
|
||||
| Retention and hold | 2 | 5% | OpenText / Hyland governance stacks | - Native governance metadata can represent intent later.<br>- No CMIS retention/hold model or mutation services.<br>- Explicitly unsupported. |
|
||||
| Bulk update | 2 | 5% | Hyland Alfresco ACS | - Native batch/error envelopes exist elsewhere in the engine.<br>- No CMIS `bulkUpdateProperties` behavior.<br>- Explicitly unsupported. |
|
||||
| Browser Binding protocol fidelity | 7 | 45% | Hyland Alfresco ACS | - Browser-style routes and JSON envelopes exist.<br>- FastAPI route shapes are pragmatic, not complete CMIS Browser Binding selector/action parity.<br>- Route-level tests skip without optional service dependencies. |
|
||||
| Browser Binding protocol fidelity | 7 | 66% | Hyland Alfresco ACS | - Browser-style routes, JSON envelopes, action aliases, multipart forms, path-addressed root routes, property filters, path segments, and range responses exist.<br>- Optional actions and CMIS exception mapping remain incomplete.<br>- Route-level CMIS tests run under the service extras and OpenCMIS now exercises object/content deeply. |
|
||||
| AtomPub binding | 2 | 0% | Hyland Alfresco ACS | - No AtomPub/XML service document or feeds.<br>- Intentionally deferred until monetized need. |
|
||||
| Web Services binding | 2 | 0% | Hyland Alfresco ACS | - No SOAP/WSDL stack.<br>- Intentionally deferred until monetized need. |
|
||||
| External conformance evidence | 3 | 35% | OpenCMIS TCK against Alfresco-like server behavior | - OpenCMIS Browser Binding session creation now succeeds against `compat-tck`.<br>- Selected `repository-type` baseline completes with no failures and one local HTTP warning.<br>- `object-content` reaches parsed cases but skips because `cmis:folder` is not creatable; broader groups and third-party client matrix are still missing. |
|
||||
| External conformance evidence | 3 | 58% | OpenCMIS TCK against Alfresco-like server behavior | - OpenCMIS Browser Binding session creation succeeds against `compat-tck`.<br>- Selected `repository-type` baseline is warning-only.<br>- `object-content` executes concrete CRUD/content cases and is now blocked by a short list of semantic gaps rather than startup, path, paging, or basic content-read failures. |
|
||||
|
||||
Weighted result from this table: **42%**.
|
||||
Weighted result from this table: **51%**.
|
||||
|
||||
## Most Important Gaps
|
||||
|
||||
1. **External conformance expansion**
|
||||
- Keep the selected OpenCMIS TCK baseline running against `compat-tck`.
|
||||
- Decide whether to add TCK-only `createFolder` support or keep CRUD/content
|
||||
skips as a deliberate profile boundary.
|
||||
- Expand selected groups after the supported capability boundary is agreed.
|
||||
- Close or explicitly waive the remaining object/content gaps: exception
|
||||
mapping, bulk update, delete content, change-token conflicts, copy, and
|
||||
offset-zero range classification.
|
||||
- Expand selected groups after the supported object/content baseline is
|
||||
stable.
|
||||
|
||||
2. **Browser Binding fidelity**
|
||||
- Align route/action/selector shapes more closely with CMIS Browser Binding.
|
||||
- Add non-skipped FastAPI route tests in CI with service extras installed.
|
||||
- Add client smoke tests with Apache Chemistry/OpenCMIS where feasible.
|
||||
- Return CMIS-specific exception classes/statuses instead of generic runtime
|
||||
exceptions where OpenCMIS distinguishes invalid argument, constraint, and
|
||||
update conflict.
|
||||
|
||||
3. **Query depth**
|
||||
- Add a real CMIS SQL subset parser instead of a two-query allowlist.
|
||||
|
||||
99
docs/cmis-opencmis-tck-wp0014-evidence-2026-05-08T153316Z.md
Normal file
99
docs/cmis-opencmis-tck-wp0014-evidence-2026-05-08T153316Z.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# CMIS WP-0014 OpenCMIS Evidence - 2026-05-08T15:33:16Z
|
||||
|
||||
## Scope
|
||||
|
||||
This note records the second WP-0014 maturity pass for the CMIS 1.1 Browser
|
||||
Binding adapter. The pass followed the earlier folder/action work and focused
|
||||
on OpenCMIS compatibility issues that fit the engine architecture naturally.
|
||||
|
||||
## Implemented Since Prior Evidence
|
||||
|
||||
- Added Browser Binding `cmisaction=update` as an alias for
|
||||
`updateProperties`.
|
||||
- Added scoped `cmisaction=move` support for CMIS-authored workspace documents
|
||||
and adapter-managed workspace folders.
|
||||
- Added URL-path Browser Binding routes under `/browser/root/{path}` for object,
|
||||
children, parent, parents, properties, allowable actions, content, and POST
|
||||
actions against path-addressed objects.
|
||||
- Normalized stored CMIS content stream media types and emitted explicit
|
||||
`Content-Type` headers so text streams do not acquire an unwanted charset.
|
||||
- Accepted metadata-backed standard property updates for `cmis:name`,
|
||||
`cmis:description`, and `cmis:secondaryObjectTypeIds`.
|
||||
- Honored `cmis:secondaryObjectTypeIds` during document creation.
|
||||
- Rejected invalid `createDocument` type ids, such as `cmis:folder`.
|
||||
- Removed non-standard `cmis:path` from document property definitions and
|
||||
document object projections while retaining folder `cmis:path`.
|
||||
|
||||
## Local Verification
|
||||
|
||||
Focused CMIS suite:
|
||||
|
||||
```bash
|
||||
.venv/bin/python -m pytest tests/cmis -q
|
||||
```
|
||||
|
||||
Result:
|
||||
|
||||
- `48 passed`
|
||||
- Performance monitor warnings appeared on two Browser Binding tests; these
|
||||
should be watched across additional runs before treating them as a regression.
|
||||
|
||||
## OpenCMIS Run
|
||||
|
||||
The final run used an isolated temporary target on port `8010` to avoid
|
||||
colliding with other local services:
|
||||
|
||||
```bash
|
||||
cd /home/worsch/guide-board
|
||||
source /home/worsch/open-cmis-tck/.local/toolchains/env.sh
|
||||
PYTHONPATH=src python3 -m guide_board \
|
||||
--extension-dir ../open-cmis-tck \
|
||||
run \
|
||||
--target /tmp/kontextual-cmis-compat-8010.json \
|
||||
--assessment ../open-cmis-tck/profiles/assessments/cmis-browser-baseline.json \
|
||||
--output-dir /tmp/open-cmis-tck-kontextual-wp14-20260508T153146Z
|
||||
```
|
||||
|
||||
Result:
|
||||
|
||||
- Run ID: `run-20260508T153316Z`
|
||||
- Run directory: `/tmp/open-cmis-tck-kontextual-wp14-20260508T153146Z`
|
||||
- Harness status: `infrastructure_error`
|
||||
- Maturity scorecard:
|
||||
`/tmp/open-cmis-tck-kontextual-wp14-20260508T153146Z/reports/cmis-maturity-scorecard.md`
|
||||
- Maturity score: `19.05`
|
||||
- Coverage: `2/9` groups
|
||||
|
||||
## Capability Interpretation
|
||||
|
||||
- `repository-type`: improved from failing to `warning` / partial. Repository
|
||||
info and type metadata are now usable enough for the selected baseline, with
|
||||
warnings instead of hard failures.
|
||||
- `object-content`: still infrastructure-blocked in guide-board classification,
|
||||
but the blockers are now concrete object/content edge cases rather than
|
||||
Browser Binding startup, folder creation, MIME, or secondary-type basics.
|
||||
|
||||
## Remaining Frontier
|
||||
|
||||
- `getObjectByPath` still fails in several OpenCMIS child checks for paths such
|
||||
as `/test-folder/test-folder`; this likely needs tighter folder/document
|
||||
child path-segment semantics and cleanup behavior investigation.
|
||||
- Creating a document without content still leads OpenCMIS into a missing
|
||||
content-stream exception. We need decide whether CMIS-created no-content
|
||||
documents should expose an empty stream or clearer no-content semantics.
|
||||
- Invalid-type errors are now correctly rejected, but OpenCMIS still classifies
|
||||
our HTTP 422 as a runtime warning rather than a clean CMIS constraint
|
||||
response.
|
||||
- Folder `cmis:path` still appears when property filters request narrower
|
||||
property sets. Operation-context filtering for properties, ACLs, allowable
|
||||
actions, and path segments remains a compatibility gap.
|
||||
- `bulkUpdate`, `deleteContent`, and some change-token tests still receive
|
||||
unsupported-action or validation responses. These should be handled either by
|
||||
natural implementation or sharper unsupported behavior.
|
||||
|
||||
## Conclusion
|
||||
|
||||
The CMIS adapter foundation is sounder and more standards-shaped after this
|
||||
pass. We should keep WP-0014 active for the remaining object/content frontier,
|
||||
but the work has moved from "basic Browser Binding compatibility" into specific
|
||||
CMIS behavior polish and optional-service boundary decisions.
|
||||
109
docs/cmis-opencmis-tck-wp0014-evidence-2026-05-08T164334Z.md
Normal file
109
docs/cmis-opencmis-tck-wp0014-evidence-2026-05-08T164334Z.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# CMIS OpenCMIS TCK Evidence - WP-0014 - 2026-05-08T16:43:34Z
|
||||
|
||||
## Run
|
||||
|
||||
- Run ID: `run-20260508T164334Z`
|
||||
- Harness: `guide-board` with external extension `/home/worsch/open-cmis-tck`
|
||||
- Target: `kontextual-cmis-compat`
|
||||
- Endpoint under test: `http://127.0.0.1:8010/cmis/compat-tck/browser`
|
||||
- Output directory:
|
||||
`/tmp/open-cmis-tck-kontextual-wp14-20260508T1643Z`
|
||||
- Assessment package:
|
||||
`/tmp/open-cmis-tck-kontextual-wp14-20260508T1643Z/reports/assessment-package.json`
|
||||
- Report:
|
||||
`/tmp/open-cmis-tck-kontextual-wp14-20260508T1643Z/reports/report.md`
|
||||
|
||||
Command shape:
|
||||
|
||||
```sh
|
||||
cd /home/worsch/guide-board
|
||||
PYTHONPATH=src python3 -m guide_board \
|
||||
--extension-dir ../open-cmis-tck \
|
||||
run \
|
||||
--target /tmp/kontextual-cmis-compat-8010.json \
|
||||
--assessment ../open-cmis-tck/profiles/assessments/cmis-browser-baseline.json \
|
||||
--output-dir /tmp/open-cmis-tck-kontextual-wp14-20260508T1643Z
|
||||
```
|
||||
|
||||
The local Java/Maven toolchain from `/home/worsch/open-cmis-tck/.local/toolchains`
|
||||
was supplied in the command environment.
|
||||
|
||||
## Internal Verification
|
||||
|
||||
- Focused CMIS tests:
|
||||
`.venv/bin/python -m pytest tests/cmis/test_cmis_runtime_browser_binding.py tests/cmis/test_cmis_browser_binding_api.py -q`
|
||||
-> `20 passed`.
|
||||
- Full suite:
|
||||
`.venv/bin/python -m pytest -q`
|
||||
-> `160 passed, 14 skipped`.
|
||||
|
||||
## OpenCMIS Result Summary
|
||||
|
||||
Overall Guide Board status remains `infrastructure_error` because the
|
||||
`object-content` group still contains unsupported or incomplete CMIS services.
|
||||
|
||||
Normalized group counts:
|
||||
|
||||
- `repository-type`: `38 pass`, `2 info`, `1 skipped`, `1 warning`.
|
||||
- `object-content`: `12 info`, `5 skipped`, `3 warning`, `3 fail`,
|
||||
`3 infrastructure_error`.
|
||||
|
||||
The sole `repository-type` warning is local HTTP rather than HTTPS:
|
||||
`HTTPS is not used. Credentials might be transferred as plain text!`
|
||||
|
||||
## Improvements Since `run-20260508T153316Z`
|
||||
|
||||
- Fixed Browser Binding parent `relativePathSegment`; OpenCMIS no longer builds
|
||||
invalid paths such as `/folder/folder` for documents.
|
||||
- Added Browser Binding property filtering and optional envelope trimming for
|
||||
`filter`, `includeAllowableActions`, `includeACL`, and `includePathSegment`.
|
||||
- Changed Browser Binding children `numItems` to report the total child count
|
||||
instead of page length.
|
||||
- Added range-aware content responses with sliced bodies, `Content-Length`,
|
||||
`Content-Range`, and `206` for partial requests.
|
||||
- Added no-content document compatibility streams while keeping document content
|
||||
stream properties nullable.
|
||||
- Added stable object-id behavior for adapter-managed folder rename/update.
|
||||
- Added `setContent` and `deleteContent` Browser Binding action aliases over the
|
||||
existing content-stream service boundary.
|
||||
- Preserved existing blob deduplication and digest verification paths.
|
||||
|
||||
Removed or reduced OpenCMIS frontier items:
|
||||
|
||||
- `getObjectByPath` path-segment failures are gone.
|
||||
- Create/delete document paging `numItems` failures are gone.
|
||||
- Update Smoke Test folder rename/object-id failures are gone.
|
||||
- Operation Context ACL delivery warning is gone.
|
||||
- Most content range warnings are gone.
|
||||
- No-content document retrieval no longer aborts with `Representation not found`.
|
||||
|
||||
## Remaining Frontier
|
||||
|
||||
These are now the concrete maturity gaps visible in the selected OpenCMIS
|
||||
baseline:
|
||||
|
||||
- **Invalid type exception mapping**: invalid document/folder type creation
|
||||
returns HTTP `422`, which OpenCMIS reports as `CmisRuntimeException` instead
|
||||
of a narrower CMIS invalid-argument/constraint exception.
|
||||
- **Bulk update**: `cmisaction=bulkUpdate` is still unsupported and reports
|
||||
`Unprocessable Entity`.
|
||||
- **Delete content semantics**: `deleteContentStream` is accepted through the
|
||||
alias layer, but OpenCMIS still observes content after delete. The adapter
|
||||
needs a stronger natural representation-removal or tombstone model.
|
||||
- **Change tokens**: repeated property/content updates with the same change token
|
||||
do not produce CMIS update-conflict behavior yet.
|
||||
- **Copy/create-from-source**: `createDocumentFromSource` remains unsupported.
|
||||
- **Range classification**: the remaining range warning is for an offset-zero
|
||||
full-stream request being marked as partial.
|
||||
|
||||
## Interpretation
|
||||
|
||||
The CMIS layer is now past the early Browser Binding and basic object/content
|
||||
shape problems. The remaining failures are concentrated in specific CMIS
|
||||
service semantics: exception mapping, content deletion, change token conflict
|
||||
handling, copy support, and bulk update.
|
||||
|
||||
This is a healthy architectural signal: the adapter continues to map naturally
|
||||
onto native asset, representation, blob, metadata, policy, and audit services.
|
||||
The remaining work should be handled as explicit compatibility choices rather
|
||||
than broad ECM reimplementation.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -58,6 +58,7 @@ class CMISAction(str, Enum):
|
||||
CREATE_FOLDER = "create_folder"
|
||||
CREATE_DOCUMENT = "create_document"
|
||||
UPDATE_PROPERTIES = "update_properties"
|
||||
MOVE_OBJECT = "move_object"
|
||||
DELETE_OBJECT = "delete_object"
|
||||
DELETE_TREE = "delete_tree"
|
||||
SET_CONTENT_STREAM = "set_content_stream"
|
||||
@@ -89,6 +90,7 @@ ACTION_CAPABILITIES: dict[CMISAction, CMISCapability] = {
|
||||
CMISAction.CREATE_FOLDER: CMISCapability.OBJECT_WRITE,
|
||||
CMISAction.CREATE_DOCUMENT: CMISCapability.OBJECT_WRITE,
|
||||
CMISAction.UPDATE_PROPERTIES: CMISCapability.OBJECT_WRITE,
|
||||
CMISAction.MOVE_OBJECT: CMISCapability.OBJECT_WRITE,
|
||||
CMISAction.DELETE_OBJECT: CMISCapability.OBJECT_WRITE,
|
||||
CMISAction.DELETE_TREE: CMISCapability.OBJECT_WRITE,
|
||||
CMISAction.SET_CONTENT_STREAM: CMISCapability.CONTENT_STREAM_WRITE,
|
||||
@@ -111,6 +113,7 @@ IMPLEMENTED_CMIS_ACTIONS: frozenset[CMISAction] = frozenset(
|
||||
CMISAction.CREATE_FOLDER,
|
||||
CMISAction.CREATE_DOCUMENT,
|
||||
CMISAction.UPDATE_PROPERTIES,
|
||||
CMISAction.MOVE_OBJECT,
|
||||
CMISAction.DELETE_OBJECT,
|
||||
CMISAction.DELETE_TREE,
|
||||
CMISAction.SET_CONTENT_STREAM,
|
||||
@@ -120,6 +123,7 @@ MUTATION_ACTIONS = {
|
||||
CMISAction.CREATE_DOCUMENT,
|
||||
CMISAction.CREATE_FOLDER,
|
||||
CMISAction.UPDATE_PROPERTIES,
|
||||
CMISAction.MOVE_OBJECT,
|
||||
CMISAction.DELETE_OBJECT,
|
||||
CMISAction.DELETE_TREE,
|
||||
CMISAction.SET_CONTENT_STREAM,
|
||||
@@ -884,7 +888,6 @@ class CMISDomainMapper:
|
||||
"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,
|
||||
@@ -905,6 +908,10 @@ class CMISDomainMapper:
|
||||
compacted.setdefault("cmis:createdBy", "system")
|
||||
compacted.setdefault("cmis:lastModifiedBy", "system")
|
||||
compacted.setdefault("cmis:secondaryObjectTypeIds", [])
|
||||
compacted.setdefault("cmis:contentStreamLength", None)
|
||||
compacted.setdefault("cmis:contentStreamMimeType", None)
|
||||
compacted.setdefault("cmis:contentStreamFileName", None)
|
||||
compacted.setdefault("cmis:contentStreamId", None)
|
||||
compacted.setdefault("kontextual:owner", "")
|
||||
compacted.setdefault("kontextual:topics", [])
|
||||
compacted.setdefault("kontextual:reviewState", "")
|
||||
@@ -980,6 +987,7 @@ class CMISDomainMapper:
|
||||
CMISAction.GET_RELATIONSHIPS,
|
||||
CMISAction.GET_OBJECT_PARENTS,
|
||||
CMISAction.UPDATE_PROPERTIES,
|
||||
CMISAction.MOVE_OBJECT,
|
||||
CMISAction.DELETE_OBJECT,
|
||||
CMISAction.SET_CONTENT_STREAM,
|
||||
]
|
||||
@@ -1209,39 +1217,61 @@ def cmis_browser_type_definition_by_id(
|
||||
raise KeyError(type_id)
|
||||
|
||||
|
||||
def cmis_browser_object(projection: dict[str, Any]) -> dict[str, Any]:
|
||||
properties = _browser_object_properties(projection)
|
||||
def cmis_browser_object(
|
||||
projection: dict[str, Any],
|
||||
*,
|
||||
property_filter: str | None = None,
|
||||
include_allowable_actions: bool = True,
|
||||
include_acl: bool = True,
|
||||
) -> dict[str, Any]:
|
||||
unfiltered_properties = _browser_object_properties(projection)
|
||||
properties = _filter_browser_properties(unfiltered_properties, property_filter)
|
||||
result = {
|
||||
"properties": {
|
||||
key: cmis_browser_property_value(key, value)
|
||||
for key, value in properties.items()
|
||||
},
|
||||
"succinctProperties": properties,
|
||||
"allowableActions": cmis_browser_allowable_actions(
|
||||
projection.get("allowable_actions", []),
|
||||
base_type_id=properties.get("cmis:baseTypeId"),
|
||||
),
|
||||
}
|
||||
acl = projection.get("acl")
|
||||
if include_allowable_actions:
|
||||
result["allowableActions"] = cmis_browser_allowable_actions(
|
||||
projection.get("allowable_actions", []),
|
||||
base_type_id=unfiltered_properties.get("cmis:baseTypeId"),
|
||||
)
|
||||
acl = projection.get("acl") if include_acl else None
|
||||
if isinstance(acl, dict) and acl:
|
||||
result["acl"] = cmis_browser_acl(acl)
|
||||
result["exactACL"] = bool(acl.get("is_exact", True))
|
||||
return compact_dict(result)
|
||||
|
||||
|
||||
def cmis_browser_object_in_folder_list(children: dict[str, Any]) -> dict[str, Any]:
|
||||
def cmis_browser_object_in_folder_list(
|
||||
children: dict[str, Any],
|
||||
*,
|
||||
property_filter: str | None = None,
|
||||
include_allowable_actions: bool = True,
|
||||
include_acl: bool = True,
|
||||
include_path_segment: bool = True,
|
||||
) -> dict[str, Any]:
|
||||
objects = []
|
||||
for item in children.get("objects", []):
|
||||
entry = {
|
||||
"object": cmis_browser_object(
|
||||
item,
|
||||
property_filter=property_filter,
|
||||
include_allowable_actions=include_allowable_actions,
|
||||
include_acl=include_acl,
|
||||
),
|
||||
}
|
||||
if include_path_segment:
|
||||
entry["pathSegment"] = item.get("name") or item.get("object_id")
|
||||
objects.append(
|
||||
{
|
||||
"object": cmis_browser_object(item),
|
||||
"pathSegment": item.get("name") or item.get("object_id"),
|
||||
}
|
||||
entry
|
||||
)
|
||||
return {
|
||||
"objects": objects,
|
||||
"hasMoreItems": bool(children.get("has_more_items", False)),
|
||||
"numItems": int(children.get("num_items", len(objects))),
|
||||
"numItems": int(children.get("total_num_items", children.get("num_items", len(objects)))),
|
||||
}
|
||||
|
||||
|
||||
@@ -1253,15 +1283,17 @@ def cmis_browser_object_list(objects: list[dict[str, Any]], *, has_more_items: b
|
||||
}
|
||||
|
||||
|
||||
def cmis_browser_parent_list(parents: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
def cmis_browser_parent_list(
|
||||
parents: dict[str, Any],
|
||||
*,
|
||||
include_relative_path_segment: bool = True,
|
||||
) -> list[dict[str, Any]]:
|
||||
items = []
|
||||
for parent in parents.get("parents", []):
|
||||
items.append(
|
||||
{
|
||||
"object": cmis_browser_object(parent),
|
||||
"relativePathSegment": parent.get("name"),
|
||||
}
|
||||
)
|
||||
item = {"object": cmis_browser_object(parent)}
|
||||
if include_relative_path_segment:
|
||||
item["relativePathSegment"] = parent.get("relative_path_segment") or parent.get("name")
|
||||
items.append(item)
|
||||
return items
|
||||
|
||||
|
||||
@@ -1270,7 +1302,7 @@ def cmis_browser_query_result(query_result: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
"results": results,
|
||||
"hasMoreItems": bool(query_result.get("has_more_items", False)),
|
||||
"numItems": int(query_result.get("num_items", len(results))),
|
||||
"numItems": int(query_result.get("total_num_items", query_result.get("num_items", len(results)))),
|
||||
}
|
||||
|
||||
|
||||
@@ -1351,7 +1383,7 @@ def cmis_browser_allowable_actions(
|
||||
"canGetFolderTree": False,
|
||||
"canGetFolderParent": is_folder and CMISAction.GET_OBJECT_PARENTS.value in native,
|
||||
"canGetRenditions": False,
|
||||
"canMoveObject": False,
|
||||
"canMoveObject": CMISAction.MOVE_OBJECT.value in native,
|
||||
"canAddObjectToFolder": False,
|
||||
"canRemoveObjectFromFolder": False,
|
||||
"canCheckOut": False,
|
||||
@@ -1362,7 +1394,7 @@ def cmis_browser_allowable_actions(
|
||||
"canRemovePolicy": False,
|
||||
"canGetAppliedPolicies": False,
|
||||
"canApplyACL": False,
|
||||
"canDeleteContentStream": False,
|
||||
"canDeleteContentStream": CMISAction.SET_CONTENT_STREAM.value in native,
|
||||
}
|
||||
|
||||
|
||||
@@ -1600,6 +1632,23 @@ def _browser_object_properties(projection: dict[str, Any]) -> dict[str, Any]:
|
||||
return properties
|
||||
|
||||
|
||||
def _filter_browser_properties(properties: dict[str, Any], property_filter: str | None) -> dict[str, Any]:
|
||||
if property_filter is None:
|
||||
return properties
|
||||
requested = {
|
||||
part.strip()
|
||||
for part in property_filter.split(",")
|
||||
if part.strip()
|
||||
}
|
||||
if not requested or "*" in requested:
|
||||
return properties
|
||||
return {
|
||||
key: value
|
||||
for key, value in properties.items()
|
||||
if key in requested or any(pattern.endswith(":*") and key.startswith(pattern[:-1]) for pattern in requested)
|
||||
}
|
||||
|
||||
|
||||
def _browser_property_type(property_id: str, value: Any) -> str:
|
||||
if property_id in {
|
||||
"cmis:objectId",
|
||||
@@ -1677,11 +1726,12 @@ def _property_definitions(base_type_id: CMISBaseType) -> dict[str, dict[str, Any
|
||||
"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},
|
||||
}
|
||||
if base_type_id == CMISBaseType.FOLDER:
|
||||
definitions["cmis:path"] = {"property_type": "string", "cardinality": "single", "required": False}
|
||||
if base_type_id == CMISBaseType.DOCUMENT:
|
||||
definitions.update(
|
||||
{
|
||||
|
||||
@@ -142,7 +142,7 @@ def test_cmis_repository_info_and_type_definitions(cmis_client) -> None:
|
||||
assert root_object["properties"]["kontextual:workspaceFolder"]["value"] is False
|
||||
assert "kontextual:assetId" in browser_type_definition["propertyDefinitions"]
|
||||
assert browser_type_definition["propertyDefinitions"]["kontextual:topics"]["cardinality"] == "multi"
|
||||
assert "cmis:path" in browser_type_definition["propertyDefinitions"]
|
||||
assert "cmis:path" not in browser_type_definition["propertyDefinitions"]
|
||||
assert {item["base_type_id"] for item in types["items"]} >= {
|
||||
"cmis:document",
|
||||
"cmis:folder",
|
||||
@@ -236,11 +236,15 @@ def test_cmis_governed_authoring_routes_allow_selected_mutations(cmis_client) ->
|
||||
)
|
||||
streamed = cmis_client.post(
|
||||
"/cmis/governed-authoring/browser/object/cmis:asset:asset-api-authored/content",
|
||||
json={"content": "# Updated", "media_type": "text/markdown"},
|
||||
json={"content": "# Updated", "media_type": "text/plain; charset=utf-8"},
|
||||
)
|
||||
byte_stream = cmis_client.get(
|
||||
"/cmis/governed-authoring/browser/content-bytes/cmis:asset:asset-api-authored",
|
||||
)
|
||||
byte_range = cmis_client.get(
|
||||
"/cmis/governed-authoring/browser/content-bytes/cmis:asset:asset-api-authored",
|
||||
params={"offset": 2, "length": 4},
|
||||
)
|
||||
deleted = cmis_client.post(
|
||||
"/cmis/governed-authoring/browser/object/cmis:asset:asset-api-authored/delete",
|
||||
json={},
|
||||
@@ -248,12 +252,80 @@ def test_cmis_governed_authoring_routes_allow_selected_mutations(cmis_client) ->
|
||||
|
||||
assert created.status_code == 200
|
||||
assert updated.json()["properties"]["kontextual:metadata:status"] == "draft"
|
||||
assert streamed.json()["content_stream"]["mime_type"] == "text/markdown"
|
||||
assert streamed.json()["content_stream"]["mime_type"] == "text/plain"
|
||||
assert byte_stream.content == b"# Updated"
|
||||
assert byte_stream.headers["content-type"] == "text/plain"
|
||||
assert byte_stream.headers["etag"].startswith("sha256:")
|
||||
assert byte_range.content == b"Upda"
|
||||
assert byte_range.headers["content-length"] == "4"
|
||||
assert deleted.json()["lifecycle"] == "delete_requested"
|
||||
|
||||
|
||||
def test_cmis_browser_binding_create_document_validates_type_and_secondary_ids(cmis_client) -> None:
|
||||
invalid = cmis_client.post(
|
||||
"/cmis/compat-tck/browser/root",
|
||||
data={
|
||||
"cmisaction": "createDocument",
|
||||
"propertyId[0]": "cmis:objectTypeId",
|
||||
"propertyValue[0]": "cmis:folder",
|
||||
"propertyId[1]": "cmis:name",
|
||||
"propertyValue[1]": "Invalid Document",
|
||||
},
|
||||
)
|
||||
created = cmis_client.post(
|
||||
"/cmis/compat-tck/browser/root",
|
||||
data={
|
||||
"cmisaction": "createDocument",
|
||||
"propertyId[0]": "cmis:objectTypeId",
|
||||
"propertyValue[0]": "cmis:document",
|
||||
"propertyId[1]": "cmis:name",
|
||||
"propertyValue[1]": "Secondary Document",
|
||||
"propertyId[2]": "cmis:secondaryObjectTypeIds",
|
||||
"propertyValue[2][0]": "kontextual:secondary",
|
||||
},
|
||||
files={"content": ("secondary.txt", b"Secondary content", "text/plain")},
|
||||
)
|
||||
|
||||
assert invalid.status_code == 422
|
||||
assert invalid.json()["detail"]["details"]["type_id"] == "cmis:folder"
|
||||
assert created.status_code == 200
|
||||
assert created.json()["properties"]["cmis:secondaryObjectTypeIds"]["value"] == ["kontextual:secondary"]
|
||||
|
||||
|
||||
def test_cmis_browser_binding_document_without_content_streams_empty_compatibility_body(cmis_client) -> None:
|
||||
folder = cmis_client.post(
|
||||
"/cmis/compat-tck/browser/root",
|
||||
data={
|
||||
"cmisaction": "createFolder",
|
||||
"propertyId[0]": "cmis:objectTypeId",
|
||||
"propertyValue[0]": "cmis:folder",
|
||||
"propertyId[1]": "cmis:name",
|
||||
"propertyValue[1]": "No Content Folder",
|
||||
},
|
||||
).json()
|
||||
document = cmis_client.post(
|
||||
"/cmis/compat-tck/browser/root",
|
||||
data={
|
||||
"cmisaction": "createDocument",
|
||||
"objectId": folder["properties"]["cmis:objectId"]["value"],
|
||||
"propertyId[0]": "cmis:objectTypeId",
|
||||
"propertyValue[0]": "cmis:document",
|
||||
"propertyId[1]": "cmis:name",
|
||||
"propertyValue[1]": "No Content Document",
|
||||
},
|
||||
).json()
|
||||
object_id = document["properties"]["cmis:objectId"]["value"]
|
||||
content = cmis_client.get(
|
||||
"/cmis/compat-tck/browser/root",
|
||||
params={"cmisselector": "content", "objectId": object_id},
|
||||
)
|
||||
|
||||
assert document["properties"]["cmis:contentStreamMimeType"]["value"] is None
|
||||
assert content.status_code == 200
|
||||
assert content.content == b""
|
||||
assert content.headers["content-length"] == "0"
|
||||
|
||||
|
||||
def test_cmis_browser_binding_create_folder_action_creates_workspace_folder(cmis_client) -> None:
|
||||
created = cmis_client.post(
|
||||
"/cmis/compat-tck/browser/root",
|
||||
@@ -291,14 +363,77 @@ def test_cmis_browser_binding_create_folder_action_creates_workspace_folder(cmis
|
||||
},
|
||||
files={"content": ("multipart.txt", b"Multipart content", "text/plain")},
|
||||
)
|
||||
document_path = document.json()["properties"]["cmis:path"]["value"]
|
||||
document_id = document.json()["properties"]["cmis:objectId"]["value"]
|
||||
document_path = "/Action Workspace/Multipart Document"
|
||||
fetched_document_by_path = cmis_client.get(
|
||||
"/cmis/compat-tck/browser/root",
|
||||
params={"cmisselector": "object", "path": document_path},
|
||||
).json()
|
||||
fetched_document_by_url_path = cmis_client.get(
|
||||
"/cmis/compat-tck/browser/root/Action%20Workspace/Multipart%20Document",
|
||||
params={"cmisselector": "object"},
|
||||
).json()
|
||||
updated_document_by_alias = cmis_client.post(
|
||||
"/cmis/compat-tck/browser/root/Action%20Workspace/Multipart%20Document",
|
||||
data={
|
||||
"cmisaction": "update",
|
||||
"propertyId[0]": "cmis:secondaryObjectTypeIds",
|
||||
"propertyValue[0][0]": "kontextual:secondary",
|
||||
},
|
||||
)
|
||||
destination = cmis_client.post(
|
||||
"/cmis/compat-tck/browser/root",
|
||||
data={
|
||||
"cmisaction": "createFolder",
|
||||
"propertyId[0]": "cmis:objectTypeId",
|
||||
"propertyValue[0]": "cmis:folder",
|
||||
"propertyId[1]": "cmis:name",
|
||||
"propertyValue[1]": "Destination",
|
||||
},
|
||||
)
|
||||
destination_id = destination.json()["properties"]["cmis:objectId"]["value"]
|
||||
moved_document = cmis_client.post(
|
||||
"/cmis/compat-tck/browser/root",
|
||||
data={
|
||||
"cmisaction": "move",
|
||||
"objectId": document_id,
|
||||
"sourceFolderId": folder_id,
|
||||
"targetFolderId": destination_id,
|
||||
},
|
||||
)
|
||||
moved_path = "/Destination/Multipart Document"
|
||||
fetched_old_document_path_after_move = cmis_client.get(
|
||||
"/cmis/compat-tck/browser/root",
|
||||
params={"cmisselector": "object", "path": document_path},
|
||||
)
|
||||
fetched_moved_document_by_path = cmis_client.get(
|
||||
"/cmis/compat-tck/browser/root",
|
||||
params={"cmisselector": "object", "path": moved_path},
|
||||
).json()
|
||||
document_parents = cmis_client.get(
|
||||
"/cmis/compat-tck/browser/root",
|
||||
params={"cmisselector": "parents", "objectId": document.json()["properties"]["cmis:objectId"]["value"]},
|
||||
params={"cmisselector": "parents", "objectId": document_id},
|
||||
).json()
|
||||
filtered_document = cmis_client.get(
|
||||
"/cmis/compat-tck/browser/root",
|
||||
params={
|
||||
"cmisselector": "object",
|
||||
"path": moved_path,
|
||||
"filter": "cmis:objectId,cmis:name",
|
||||
"includeAllowableActions": False,
|
||||
"includeACL": False,
|
||||
},
|
||||
).json()
|
||||
filtered_children = cmis_client.get(
|
||||
"/cmis/compat-tck/browser/root",
|
||||
params={
|
||||
"cmisselector": "children",
|
||||
"objectId": destination_id,
|
||||
"filter": "cmis:objectId,cmis:name",
|
||||
"includeAllowableActions": False,
|
||||
"includeACL": False,
|
||||
"includePathSegment": False,
|
||||
},
|
||||
).json()
|
||||
fetched_folder_by_path = cmis_client.get(
|
||||
"/cmis/compat-tck/browser/root",
|
||||
@@ -334,9 +469,26 @@ def test_cmis_browser_binding_create_folder_action_creates_workspace_folder(cmis
|
||||
assert document.json()["properties"]["cmis:isLatestVersion"]["value"] is True
|
||||
assert document.json()["properties"]["cmis:secondaryObjectTypeIds"]["value"] == []
|
||||
assert document.json()["allowableActions"]["canGetFolderParent"] is False
|
||||
assert document.json()["allowableActions"]["canMoveObject"] is True
|
||||
assert document_path == "/Action Workspace/Multipart Document"
|
||||
assert fetched_document_by_path["properties"]["cmis:name"]["value"] == "Multipart Document"
|
||||
assert document_parents[0]["object"]["properties"]["cmis:path"]["value"] == "/Action Workspace"
|
||||
assert fetched_document_by_url_path["properties"]["cmis:name"]["value"] == "Multipart Document"
|
||||
assert updated_document_by_alias.status_code == 200
|
||||
assert updated_document_by_alias.json()["properties"]["cmis:secondaryObjectTypeIds"]["value"] == [
|
||||
"kontextual:secondary"
|
||||
]
|
||||
assert destination.status_code == 200
|
||||
assert moved_document.status_code == 200
|
||||
assert moved_path == "/Destination/Multipart Document"
|
||||
assert fetched_old_document_path_after_move.status_code == 404
|
||||
assert fetched_moved_document_by_path["properties"]["cmis:name"]["value"] == "Multipart Document"
|
||||
assert document_parents[0]["object"]["properties"]["cmis:path"]["value"] == "/Destination"
|
||||
assert document_parents[0]["relativePathSegment"] == "Multipart Document"
|
||||
assert set(filtered_document["properties"]) == {"cmis:objectId", "cmis:name"}
|
||||
assert "allowableActions" not in filtered_document
|
||||
assert "pathSegment" not in filtered_children["objects"][0]
|
||||
assert set(filtered_children["objects"][0]["object"]["properties"]) == {"cmis:objectId", "cmis:name"}
|
||||
assert "allowableActions" not in filtered_children["objects"][0]["object"]
|
||||
assert fetched_folder_by_path["properties"]["cmis:objectId"]["value"] == folder_id
|
||||
assert deleted_tree.json()["failedToDelete"] == []
|
||||
assert all(
|
||||
@@ -368,9 +520,14 @@ def test_cmis_readonly_route_rejects_mutation(cmis_client) -> None:
|
||||
def test_cmis_rejects_unsupported_standard_property_update_with_diagnostics(cmis_client) -> None:
|
||||
response = cmis_client.post(
|
||||
"/cmis/governed-authoring/browser/object/cmis:asset:asset-source/properties",
|
||||
json={"properties": {"cmis:name": "Renamed"}},
|
||||
json={"properties": {"cmis:objectTypeId": "cmis:folder"}},
|
||||
)
|
||||
|
||||
assert response.status_code == 422
|
||||
assert response.json()["detail"]["details"]["property"] == "cmis:name"
|
||||
assert response.json()["detail"]["details"]["supported"] == ["kontextual:metadata:<key>"]
|
||||
assert response.json()["detail"]["details"]["property"] == "cmis:objectTypeId"
|
||||
assert response.json()["detail"]["details"]["supported"] == [
|
||||
"cmis:name",
|
||||
"cmis:description",
|
||||
"cmis:secondaryObjectTypeIds",
|
||||
"kontextual:metadata:<key>",
|
||||
]
|
||||
|
||||
@@ -215,6 +215,24 @@ def test_runtime_cmis_compat_profile_supports_workspace_folder_lifecycle(cmis_ru
|
||||
folder_children = runtime.cmis_children("compat-tck", context, folder_id=folder_object_id)
|
||||
document_by_path = runtime.cmis_object_by_path("compat-tck", "/TCK Workspace/Workspace Document", context)
|
||||
document_parents = runtime.cmis_object_parents("compat-tck", document["object_id"], context)
|
||||
browser_document_parents = runtime.cmis_browser_parents("compat-tck", document["object_id"], context)
|
||||
filtered_document = runtime.cmis_browser_object(
|
||||
"compat-tck",
|
||||
document["object_id"],
|
||||
context,
|
||||
property_filter="cmis:objectId,cmis:name",
|
||||
include_allowable_actions=False,
|
||||
include_acl=False,
|
||||
)
|
||||
filtered_children = runtime.cmis_browser_children(
|
||||
"compat-tck",
|
||||
context,
|
||||
object_id=folder_object_id,
|
||||
property_filter="cmis:objectId,cmis:name",
|
||||
include_allowable_actions=False,
|
||||
include_acl=False,
|
||||
include_path_segment=False,
|
||||
)
|
||||
|
||||
assert folder["path"] == "/TCK Workspace"
|
||||
assert folder["properties"]["kontextual:workspaceFolder"] is True
|
||||
@@ -222,11 +240,17 @@ def test_runtime_cmis_compat_profile_supports_workspace_folder_lifecycle(cmis_ru
|
||||
assert fetched["properties"]["cmis:path"] == "/TCK Workspace"
|
||||
assert parents["parents"][0]["object_id"] == "cmis-root"
|
||||
assert document["path"] == "/TCK Workspace/Workspace Document"
|
||||
assert document["properties"]["cmis:path"] == "/TCK Workspace/Workspace Document"
|
||||
assert "cmis:path" not in document["properties"]
|
||||
assert document_by_path["object_id"] == document["object_id"]
|
||||
assert document_parents["count"] == 1
|
||||
assert document_parents["parents"][0]["properties"]["cmis:path"] == "/TCK Workspace"
|
||||
assert browser_document_parents[0]["relativePathSegment"] == "Workspace Document"
|
||||
assert document["object_id"] in {item["object_id"] for item in folder_children["objects"]}
|
||||
assert set(filtered_document["properties"]) == {"cmis:objectId", "cmis:name"}
|
||||
assert "allowableActions" not in filtered_document
|
||||
assert "pathSegment" not in filtered_children["objects"][0]
|
||||
assert set(filtered_children["objects"][0]["object"]["properties"]) == {"cmis:objectId", "cmis:name"}
|
||||
assert "allowableActions" not in filtered_children["objects"][0]["object"]
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
runtime.cmis_delete_object("compat-tck", folder_object_id, {}, context)
|
||||
@@ -242,6 +266,31 @@ def test_runtime_cmis_compat_profile_supports_workspace_folder_lifecycle(cmis_ru
|
||||
assert "CMIS folder not found" in str(exc_info.value)
|
||||
|
||||
|
||||
def test_runtime_cmis_workspace_folder_rename_keeps_object_id_stable(cmis_runtime) -> None:
|
||||
runtime, context = cmis_runtime
|
||||
|
||||
folder = runtime.cmis_create_folder(
|
||||
"compat-tck",
|
||||
{"name": "Rename Source", "properties": {"cmis:objectTypeId": "cmis:folder"}},
|
||||
context,
|
||||
)
|
||||
renamed = runtime.cmis_update_properties(
|
||||
"compat-tck",
|
||||
folder["object_id"],
|
||||
{"properties": {"cmis:name": "Rename Target"}},
|
||||
context,
|
||||
)
|
||||
fetched_by_old_id = runtime.cmis_object("compat-tck", folder["object_id"], context)
|
||||
fetched_by_new_path = runtime.cmis_object_by_path("compat-tck", "/Rename Target", context)
|
||||
deleted = runtime.cmis_delete_object("compat-tck", folder["object_id"], {}, context)
|
||||
|
||||
assert renamed["object_id"] == folder["object_id"]
|
||||
assert renamed["path"] == "/Rename Target"
|
||||
assert fetched_by_old_id["properties"]["cmis:name"] == "Rename Target"
|
||||
assert fetched_by_new_path["object_id"] == folder["object_id"]
|
||||
assert deleted["deleted"] is True
|
||||
|
||||
|
||||
def test_runtime_cmis_rejects_unsupported_standard_property_updates(cmis_runtime) -> None:
|
||||
runtime, context = cmis_runtime
|
||||
|
||||
@@ -249,7 +298,7 @@ def test_runtime_cmis_rejects_unsupported_standard_property_updates(cmis_runtime
|
||||
runtime.cmis_update_properties(
|
||||
"governed-authoring",
|
||||
"cmis:asset:asset-runtime-source",
|
||||
{"properties": {"cmis:name": "Renamed"}},
|
||||
{"properties": {"cmis:objectTypeId": "cmis:folder"}},
|
||||
context,
|
||||
)
|
||||
|
||||
|
||||
@@ -127,6 +127,75 @@ Current external frontier:
|
||||
These are follow-up maturity items rather than the original folder-creatable
|
||||
blocker.
|
||||
|
||||
## Implementation Evidence - 2026-05-08T15:33:16Z
|
||||
|
||||
Evidence file:
|
||||
|
||||
- `docs/cmis-opencmis-tck-wp0014-evidence-2026-05-08T153316Z.md`
|
||||
|
||||
Implemented in this pass:
|
||||
|
||||
- Browser Binding action aliases and path-addressed routes:
|
||||
`cmisaction=update`, `cmisaction=move`, and `/browser/root/{path}`.
|
||||
- MIME normalization and explicit content stream `Content-Type` headers.
|
||||
- Metadata-backed standard property support for `cmis:name`,
|
||||
`cmis:description`, and `cmis:secondaryObjectTypeIds`.
|
||||
- Create-time secondary type id projection and invalid document type rejection.
|
||||
- Removal of non-standard document `cmis:path` while preserving folder paths.
|
||||
|
||||
Latest verification:
|
||||
|
||||
- Internal: `.venv/bin/python -m pytest tests/cmis -q` -> `48 passed`.
|
||||
- OpenCMIS: `run-20260508T153316Z` in
|
||||
`/tmp/open-cmis-tck-kontextual-wp14-20260508T153146Z`.
|
||||
- Maturity score: `19.05`; coverage remains `2/9` groups.
|
||||
- `repository-type` is now partial/warning; `object-content` remains
|
||||
infrastructure-blocked by concrete CRUD/content edge cases.
|
||||
|
||||
Current external frontier:
|
||||
|
||||
- `getObjectByPath` path-segment failures in child checks.
|
||||
- No-content document content-stream semantics.
|
||||
- Operation-context/property filter trimming, especially folder `cmis:path`.
|
||||
- `bulkUpdate`, `deleteContent`, and change-token unsupported-action handling.
|
||||
|
||||
## Implementation Evidence - 2026-05-08T16:43:34Z
|
||||
|
||||
Evidence file:
|
||||
|
||||
- `docs/cmis-opencmis-tck-wp0014-evidence-2026-05-08T164334Z.md`
|
||||
|
||||
Implemented in this pass:
|
||||
|
||||
- Correct `relativePathSegment` behavior for document parents.
|
||||
- Browser Binding operation-context trimming for property filters, allowable
|
||||
actions, ACLs, and path segments.
|
||||
- Total-count `numItems` semantics for Browser Binding children.
|
||||
- Stable adapter-managed folder object IDs across folder rename/update.
|
||||
- Nullable no-content document stream properties and empty compatibility streams.
|
||||
- Range-aware content responses with sliced bodies, `206`, and `Content-Range`.
|
||||
- `setContent` and `deleteContent` Browser Binding action aliases.
|
||||
|
||||
Latest verification:
|
||||
|
||||
- Internal focused CMIS tests: `20 passed`.
|
||||
- Full suite: `160 passed, 14 skipped`.
|
||||
- OpenCMIS: `run-20260508T164334Z` in
|
||||
`/tmp/open-cmis-tck-kontextual-wp14-20260508T1643Z`.
|
||||
- `repository-type`: `38 pass`, `2 info`, `1 skipped`, `1 warning`.
|
||||
- `object-content`: `12 info`, `5 skipped`, `3 warning`, `3 fail`,
|
||||
`3 infrastructure_error`.
|
||||
|
||||
Current external frontier:
|
||||
|
||||
- CMIS-specific exception mapping for invalid type operations.
|
||||
- `bulkUpdateProperties` remains unsupported.
|
||||
- `deleteContentStream` needs stronger representation-removal/tombstone
|
||||
semantics.
|
||||
- Change-token conflict behavior is not implemented.
|
||||
- `createDocumentFromSource`/copy remains unsupported.
|
||||
- Offset-zero range requests are still marked partial.
|
||||
|
||||
## D14.1 - Define CMIS maturity boundary and TCK profile semantics
|
||||
|
||||
```task
|
||||
@@ -187,7 +256,7 @@ Acceptance:
|
||||
|
||||
```task
|
||||
id: KONT-WP-0014-T004
|
||||
status: in_progress
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "f9323c25-4d81-42cd-b7e6-e40d7e0487cd"
|
||||
```
|
||||
@@ -208,7 +277,7 @@ Acceptance:
|
||||
|
||||
```task
|
||||
id: KONT-WP-0014-T005
|
||||
status: todo
|
||||
status: in_progress
|
||||
priority: medium
|
||||
state_hub_task_id: "5feb6db8-24eb-4c20-8c3e-d530f396ef6a"
|
||||
```
|
||||
@@ -223,6 +292,13 @@ Acceptance:
|
||||
naturally through blob services or explicitly advertised as unsupported.
|
||||
- Blob deduplication and digest verification remain intact.
|
||||
|
||||
Progress:
|
||||
|
||||
- Done for normal reads, no-content compatibility streams, partial body slicing,
|
||||
`Content-Length`, `Content-Type`, `ETag`, and `Content-Range`.
|
||||
- Remaining: clean `deleteContentStream` semantics and offset-zero range
|
||||
classification.
|
||||
|
||||
## D14.6 - Add natural navigation and query depth
|
||||
|
||||
```task
|
||||
@@ -242,11 +318,18 @@ Acceptance:
|
||||
indexed/available metadata fields, or returns precise unsupported diagnostics.
|
||||
- Capability flags are updated only for behavior that is actually supported.
|
||||
|
||||
Progress:
|
||||
|
||||
- Done for `getObjectByPath`, `getFolderParent`, parent path segments, and
|
||||
folder rename path stability.
|
||||
- Remaining: query predicate/order depth and any deliberate descendants/tree
|
||||
expansion.
|
||||
|
||||
## D14.7 - Polish read-side relationships, ACL discovery, and change tokens
|
||||
|
||||
```task
|
||||
id: KONT-WP-0014-T007
|
||||
status: todo
|
||||
status: in_progress
|
||||
priority: medium
|
||||
state_hub_task_id: "60f7b222-6eea-4add-822d-3439d568d4f6"
|
||||
```
|
||||
@@ -261,6 +344,11 @@ Acceptance:
|
||||
- ACL mutation, policy mutation, PWC/versioning, and type mutability remain
|
||||
unsupported unless a later task explicitly changes scope.
|
||||
|
||||
Progress:
|
||||
|
||||
- Started by isolating OpenCMIS change-token failures as the main T007 maturity
|
||||
gap. Relationship and ACL discovery were not expanded in this pass.
|
||||
|
||||
## D14.8 - Expand OpenCMIS assessment and update maturity scorecard
|
||||
|
||||
```task
|
||||
|
||||
Reference in New Issue
Block a user