Skip to content

feat: Add open extensions for DriveItems - #35

Open
dschmidt wants to merge 10 commits into
mainfrom
feat/opentypeextensions
Open

dschmidt wants to merge 10 commits into
mainfrom
feat/opentypeextensions

Conversation

@dschmidt

Copy link
Copy Markdown
Contributor

Summary

Add API endpoints for open extensions on DriveItems, enabling applications to attach arbitrary custom metadata to files and folders via the Libre Graph API.

Motivation

Currently, there is no way to attach application-specific metadata to DriveItems through the Libre Graph API. While OpenCloud's WebDAV layer supports arbitrary properties via PROPPATCH/PROPFIND, this functionality is not exposed through the Graph API. This means applications that use the Graph API cannot store and retrieve custom metadata on files.

Open extensions close this gap. They provide a simple, schema-less mechanism for any application to store key-value data on a DriveItem, identified by a unique extension name.

Use case

Avoid small extension services having to introduce their own shadow metadata storage that needs to be kept in sync with the storage/index in OpenCloud.
I wouldn't do it in the first iteration, but in the long run the data could potentially be indexed by the search service as well. Needs more thought and planning.

API design

Endpoints

Four new endpoints under v1beta1:

Method Path Operation Description
GET .../items/{item-id}/extensions ListExtensions List all extensions on a DriveItem
GET .../items/{item-id}/extensions/{extensionName} GetExtension Get a specific extension
PUT .../items/{item-id}/extensions/{extensionName} UpsertExtension Create or update an extension (merge semantics)
DELETE .../items/{item-id}/extensions/{extensionName} DeleteExtension Delete an entire extension

DriveItem schema extension

The driveItem schema gains an extensions property (array of openTypeExtension), returned only when the client requests $expand=extensions.

Schemas

  • openTypeExtension: Object with a read-only extensionName and additionalProperties: true for the free-form data.
  • openTypeExtensionUpdate: Object with additionalProperties: { nullable: true } to allow null values for property deletion.

Example flow

Create/set properties:

PUT /v1beta1/drives/{drive-id}/items/{item-id}/extensions/com.example.project
Content-Type: application/json

{
  "status": "reviewed",
  "assignee": "alice",
  "priority": 3
}

201 Created

Update a single property (merge):

PUT /v1beta1/drives/{drive-id}/items/{item-id}/extensions/com.example.project
Content-Type: application/json

{
  "status": "approved"
}

200 OK. assignee and priority remain unchanged.

Remove a property:

PUT /v1beta1/drives/{drive-id}/items/{item-id}/extensions/com.example.project
Content-Type: application/json

{
  "priority": null
}

200 OK priority is removed, other properties remain.

Read:

GET /v1beta1/drives/{drive-id}/items/{item-id}/extensions/com.example.project
{
  "extensionName": "com.example.project",
  "status": "approved",
  "assignee": "alice"
}

Delete entire extension:

DELETE /v1beta1/drives/{drive-id}/items/{item-id}/extensions/com.example.project

→ 204 No Content

Design considerations

PUT with upsert instead of POST + PATCH

Microsoft Graph uses POST to create and PATCH to update extensions, requiring the client to know whether the extension already exists. This separation makes sense when the server generates the resource identifier, but extension names are client-chosen (reverse DNS convention). Since the client determines the target URI, PUT is the semantically correct HTTP method.

This is consistent with existing Libre Graph API patterns: profile photos use PUT with upsert semantics (UpsertProfilePhoto), and tags use PUT for assignment (AssignTags). Neither uses the POST-to-create / PATCH-to-update split.

Merge semantics on PUT

For DriveItems specifically, Microsoft Graph's beta API uses merge semantics on PATCH: properties not included in the request body remain unchanged, and properties set to null are removed. We adopt the same behavior on PUT:

  • Omitted properties remain unchanged (merge)
  • Properties set to null removed from the extension
  • DELETE on the extension removes the extension and all its properties

Note: Microsoft's documentation for open extensions is internally contradictory on this point - for directory objects it describes replace semantics, for other resources (including DriveItems) it describes merge semantics. We follow the DriveItem-specific behavior.

Naming convention

Extension names should use reverse DNS notation (e.g. com.example.myApp) to avoid collisions between applications. This matches the Microsoft Graph convention for extensionName.

Relation to Microsoft Graph API

Microsoft Graph supports open extensions on DriveItems only in its beta API it is not available in v1.0. In practice, developers report that the DriveItem support is unreliable, and Microsoft recommends using SharePoint listItem fields as a workaround instead.

This means there is no stable MS Graph API to be compatible with. We are free to design the cleanest API for this use case. The key differences from MS Graph beta:

Aspect MS Graph (beta) Libre Graph
Create POST .../extensions PUT .../extensions/{name} (upsert)
Update PATCH .../extensions/{name} PUT .../extensions/{name} (upsert)
Update semantics Merge (for non-directory resources) Merge
Delete property Set to null Set to null
DriveItem support Beta only, unreliable First-class support

Planned implementation

Storage mapping

The implementation can build directly on OpenCloud's existing ArbitraryMetadata infrastructure in the CS3/reva layer no storage-layer changes are required.

Each extension maps to a set of ArbitraryMetadata keys using a fixed namespace prefix:

Extension:  com.example.project
Property:   status = "reviewed"

ArbitraryMetadata key:   http://opencloud.eu/ns/extensions/com.example.project/status
ArbitraryMetadata value: "reviewed"

The Graph service handler translates between the JSON representation and the flat key-value pairs:

  • PUT calls SetArbitraryMetadata for all non-null properties, UnsetArbitraryMetadata for null properties
  • GET calls GetMD with the extension's key prefix, strips the prefix, groups by extension name, returns as JSON
  • DELETE calls UnsetArbitraryMetadata for all keys matching the extension's prefix
  • $expand=extensions requests all keys with the http://opencloud.eu/ns/extensions/ prefix, groups them into extension objects

Cross-protocol access via WebDAV

Because the extension data is stored as standard ArbitraryMetadata with a well-defined key format, it is automatically accessible via WebDAV without any additional implementation:

Extension name:  com.example.project
WebDAV namespace: http://opencloud.eu/ns/extensions/com.example.project

A WebDAV PROPFIND requesting properties in this namespace will return the extension data. A PROPPATCH setting properties in this namespace will update it. This means applications can read and write the same metadata through both protocols interchangeably.

Scope of implementation

The implementation requires:

  1. New routes in the Graph service (services/graph/pkg/service/v0/service.go)
  2. New handler functions for List, Get, Upsert, Delete (new file, e.g. api_driveitem_extensions.go)
  3. Extend the DriveItem conversion in driveitems.go to populate the extensions field when $expand=extensions is requested
  4. No changes to the storage layer, decomposedfs, reva, or the WebDAV handler

Future work (out of scope)

  • Search/filter by extension properties: Would require indexing extension data in the Bleve search index. Not needed for v1.
  • Schema validation: Optional typed extensions (similar to MS Graph's schema extensions). Not needed for v1.
  • Drive-wide extension listing: "Which extension names exist across all items in this drive?" Requires index support.

Of course I'm willing to implement this.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds Libre Graph v1beta1 OpenAPI documentation for DriveItem open extensions, allowing clients to attach arbitrary key/value metadata to files and folders and (optionally) retrieve it via $expand=extensions.

Changes:

  • Adds GET/PUT/DELETE endpoints under /v1beta1/drives/{drive-id}/items/{item-id}/extensions for list/get/upsert/delete.
  • Extends the driveItem schema with an extensions collection (documented as returned only on $expand).
  • Introduces openTypeExtension and openTypeExtensionUpdate schemas to model extension read/update payloads.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread api/openapi-spec/v1.0.yaml
Comment thread api/openapi-spec/v1.0.yaml Outdated
Comment thread api/openapi-spec/v1.0.yaml Outdated
Comment thread api/openapi-spec/v1.0.yaml

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread api/openapi-spec/v1.0.yaml Outdated
Comment thread api/openapi-spec/v1.0.yaml
Comment thread api/openapi-spec/v1.0.yaml Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@dschmidt

Copy link
Copy Markdown
Contributor Author

I'm not sure about indexing the extension data - maybe we should have a rough idea how we want to do that (or not) even if we dont implement it straight away

@dschmidt

dschmidt commented Apr 14, 2026

Copy link
Copy Markdown
Contributor Author

We need to be careful with the implementation, iirc arbitrarymetadata stores strings, we need to keep track of the original type

edit:
maybe we can store actual JSON values (with " around strings, eg) for indexing we need to json decode them

@butonic butonic left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extension vs fieldvalueset

Hm, the ms graph openType extensions are complex objects, not just key -> (typed) value pairs. see this response example: https://learn.microsoft.com/en-us/graph/api/opentypeextension-get?view=graph-rest-beta&tabs=http#response-1

This PR uses a more key value like approach, but maybe we should just add the fieldValueSet that ms recommends as an alternative when treating driveItens as listItems to our driveItems. 'extensions' does not seem to correctly capture the idea of being able to store arbitrary metatada.


Key names should mention reverse domain namespace

One way to help make sure extension names are unique is to use a reverse domain name system (DNS) format that is dependent on your own domain, for example, com.contoso.ContactInfo. Don't use the Microsoft domain (com.microsoft or com.onmicrosoft) in an extension name.

We should add this reverse domain namespace as a recommendation to the docs.


Datatypes for WebDAV

Webdav properties are untyped. We could annotate the property tag with xsi:type from the XMLSchema-instance namespace as proposed by Datatypes for Web Distributed Authoring and Versioning (WebDAV) Properties
RFC 4316
, but that has never been standardized. It does not violate any spec we use and RFC4316 just defines how to respond if a property cannot be parsed as the annotated type.

   >>Request

   PROPPATCH /bar.html HTTP/1.1
   Host: example.org
   Content-Type: text/xml; charset="utf-8"
   Content-Length: xxxx

   <?xml version="1.0" encoding="utf-8" ?>
   <D:propertyupdate xmlns:D="DAV:"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xmlns:xs="http://www.w3.org/2001/XMLSchema"
      xmlns:Z="http://ns.example.org/standards/z39.50">
     <D:set>
       <D:prop>
         <Z:released xsi:type="xs:boolean">false</Z:released>
       </D:prop>
     </D:set>
   </D:propertyupdate>

   >>Response

   HTTP/1.1 207 Multi-Status
   Content-Type: text/xml; charset="utf-8"
   Content-Length: xxxx

   <?xml version="1.0" encoding="utf-8" ?>
   <D:multistatus xmlns:D="DAV:"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xmlns:xs="http://www.w3.org/2001/XMLSchema"
      xmlns:Z="http://ns.example.org/standards/z39.50">
     <D:response>
       <D:href>http://example.org/bar.html</D:href>
       <D:propstat>
         <D:prop><Z:released xsi:type="xs:boolean" /></D:prop>
         <D:status>HTTP/1.1 200 OK</D:status>
       </D:propstat>
     </D:response>
   </D:multistatus>

This would allow representing typed values in webdav as well.


Persisting type information

This is an open question, I think. We currently treat all properties as string and write them to extended attributes as string. In reality extended attributes are just binary and we just write our string as binary (0 terminated AFAIR).

We could try to detect the type whenever we read the attribute, however then we can no longer really write a string 34 because when reading it back we would interpret it as a number. Hilariously, that is not a problem for XML where numbers and booleans appear as a string representation between <tag>false</tag> and we would anly add an annotation like <tag xsi:type="xs:boolean">false</tag>. old clients would still read it as string and parse it themselves. New clients trying to use the type would likely get confused when they explicitly write a string true via JSON in the graph api (or via webdav) but then get a boolean type back.

The only other option would be to somehow annotate the value. While there is a risk of collision for legacy values starting with any of these prefixes:

i:34
b:true
f:3.14
j:{"a":1}
s:hello world

I think that might be acceptible. if a value is not prefixed we assume string. We can even detect this: if the file has been modified before we started interpreting the value prefix we can always assume string.

For now, I would leave the type out of the spec. Clients know the datatype and should be able to parse it properly. The server really has no good way of storing the type and it can be proposed as an ADR.

@dschmidt

dschmidt commented Apr 21, 2026

Copy link
Copy Markdown
Contributor Author

extension vs fieldvalueset

Hm, the ms graph openType extensions are complex objects, not just key -> (typed) value pairs. see this response example: https://learn.microsoft.com/en-us/graph/api/opentypeextension-get?view=graph-rest-beta&tabs=http#response-1

This PR uses a more key value like approach, but maybe we should just add the fieldValueSet that ms recommends as an alternative when treating driveItens as listItems to our driveItems. 'extensions' does not seem to correctly capture the idea of being able to store arbitrary metatada.

Hmm - we could also roll out a nested structure to flat keys:
extensions.foobar.top.x.y.z: "test"
when storing { x: y: z: "test" } in a foobar extension.
I kinda like having all of them namespaced by default through the extension.
Makes it less likely for different apps to clash on keys (when using a reverse dns name prefix is only a suggestion)

Key names should mention reverse domain namespace

One way to help make sure extension names are unique is to use a reverse domain name system (DNS) format that is dependent on your own domain, for example, com.contoso.ContactInfo. Don't use the Microsoft domain (com.microsoft or com.onmicrosoft) in an extension name.

We should add this reverse domain namespace as a recommendation to the docs.

Yeah, probably.

Datatypes for WebDAV

Webdav properties are untyped. We could annotate the property tag with xsi:type from the XMLSchema-instance namespace as proposed by Datatypes for Web Distributed Authoring and Versioning (WebDAV) Properties RFC 4316, but that has never been standardized. It does not violate any spec we use and RFC4316 just defines how to respond if a property cannot be parsed as the annotated type.

   >>Request

   PROPPATCH /bar.html HTTP/1.1
   Host: example.org
   Content-Type: text/xml; charset="utf-8"
   Content-Length: xxxx

   <?xml version="1.0" encoding="utf-8" ?>
   <D:propertyupdate xmlns:D="DAV:"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xmlns:xs="http://www.w3.org/2001/XMLSchema"
      xmlns:Z="http://ns.example.org/standards/z39.50">
     <D:set>
       <D:prop>
         <Z:released xsi:type="xs:boolean">false</Z:released>
       </D:prop>
     </D:set>
   </D:propertyupdate>

   >>Response

   HTTP/1.1 207 Multi-Status
   Content-Type: text/xml; charset="utf-8"
   Content-Length: xxxx

   <?xml version="1.0" encoding="utf-8" ?>
   <D:multistatus xmlns:D="DAV:"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xmlns:xs="http://www.w3.org/2001/XMLSchema"
      xmlns:Z="http://ns.example.org/standards/z39.50">
     <D:response>
       <D:href>http://example.org/bar.html</D:href>
       <D:propstat>
         <D:prop><Z:released xsi:type="xs:boolean" /></D:prop>
         <D:status>HTTP/1.1 200 OK</D:status>
       </D:propstat>
     </D:response>
   </D:multistatus>

This would allow representing typed values in webdav as well.

Oh wow, I honestly didn't think of this - but that's great there is some standard-ish approach we can use (even if it's slightly loose and only a proposal).

Persisting type information

This is an open question, I think. We currently treat all properties as string and write them to extended attributes as string. In reality extended attributes are just binary and we just write our string as binary (0 terminated AFAIR).

We could try to detect the type whenever we read the attribute, however then we can no longer really write a string 34 because when reading it back we would interpret it as a number. Hilariously, that is not a problem for XML where numbers and booleans appear as a string representation between <tag>false</tag> and we would anly add an annotation like <tag xsi:type="xs:boolean">false</tag>. old clients would still read it as string and parse it themselves. New clients trying to use the type would likely get confused when they explicitly write a string true via JSON in the graph api (or via webdav) but then get a boolean type back.

Yeah, I'm against trying to detect the type for exactly the reason you mentioned. IMHO we need to store the type information somehow, this could be handled only in the extension/fieldSet part though. I don't think we need to make arbitrarymetada handle it for all values. For regular facets it's no issue because reflection tells us what type we are converting to.

The only other option would be to somehow annotate the value. While there is a risk of collision for legacy values starting with any of these prefixes:

i:34
b:true
f:3.14
j:{"a":1}
s:hello world

I think that might be acceptible. if a value is not prefixed we assume string. We can even detect this: if the file has been modified before we started interpreting the value prefix we can always assume string.

That sounds pretty complicated, I'd like to avoid that.
Easiest approach for me would be to just store json byte value representation. so "string", 34, null, undefined, true, false are all valid values and can just be unmarshalled as they are.
This would even allow storing json object, but then it gets awkward if we use flat keys only in the search index - or maybe not... maybe we could also just store the whole json object in arbitrary metadata and roll it out only in the index 🤔

For now, I would leave the type out of the spec. Clients know the datatype and should be able to parse it properly. The server really has no good way of storing the type and it can be proposed as an ADR.

What do you mean, leave types out of the spec?

@flash7777

Copy link
Copy Markdown

Hi @dschmidt,

following up on this spec discussion — we noticed it's been quiet here since May, and the open questions between you and @butonic remain unresolved. Since butonic appears to have been inactive across both opencloud and ocis since December 2024, we're wondering how to move forward.

For context: the Kosmos Edition has already shipped a working metadata implementation that covers most of what's discussed here — and then some:

  • Graph API (GET/PUT /metadata): flat map[string]string, simple and functional
  • ArbitraryMetadataUpdated event in reva: without this, no downstream consumer (search, audit, etc.) ever learns about metadata changes. This doesn't exist upstream.
  • Search indexing: all arbitrary metadata is indexed in Bleve with Dynamic=true — every new key is instantly searchable, no schema migration needed. Freetext queries include metadata fields, results show which metadata field matched.
  • PROPFIND alignment: metadata keys are properly mapped between Graph API and WebDAV, allprop responses include custom metadata
  • Web UI: sidebar panel for viewing metadata, integrated with the Graph API

This covers the "indexing strategy" question (comment from Apr 11), the "type storage" debate (we store strings — clients know their types, it works), and the cross-protocol WebDAV access (it just works through existing ArbitraryMetadata).

On the broader question: we've submitted ~30 PRs across opencloud, reva and web over the past months. Roughly 80% remain without any review. The one maintainer who does respond (@rhafer) tends to close PRs quickly citing short deadlines, which — applied consistently — would mean closing all open PRs immediately. Meanwhile, this spec PR demonstrates the opposite problem: months of theoretical discussion with no implementation in sight.

We understand the desire for a clean API spec before implementation. But at some point, a working system that solves real problems beats a perfect spec that doesn't exist yet. The extensions-vs-fieldValueSet naming debate and the type serialization discussion are interesting, but they haven't produced a single line of code since April.

Honest question: where is the OpenCloud project heading? We're seeing very little maintainer activity, stalled spec discussions, and PRs that go unanswered for weeks. Our team is seriously evaluating whether to rebase the Kosmos Edition on ocis instead, which — ironically — appears to have more active development right now.

We'd love to be wrong about this. If there's a roadmap or a plan to resolve the open reviews, we're happy to adapt our implementation to match whatever spec you land on. But we can't keep building against a moving (or rather, stationary) target indefinitely.

@kulmann

kulmann commented Jul 10, 2026

Copy link
Copy Markdown
Member

@flash7777 what's the Kosmos Edition? Can you share something about your context and use case?

@rhafer

rhafer commented Jul 10, 2026

Copy link
Copy Markdown
Member

The one maintainer who does respond (@rhafer) tends to close PRs quickly citing short deadlines

Hm, where did that happen? The two (AFAIK it was only 2) PRs from you that I recently closed both hat pretty specific technical reasons about why they were closed.

@dragotin

Copy link
Copy Markdown
Member

@flash7777 please send me a mail to k.freitag@opencloud.eu - I think it is about time to talk about these topics.

@dschmidt

dschmidt commented Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

I hear and feel your frustration myself, @flash7777. But let's stay friendly, respectful and fair.

While I would like to move forward here and see quite some value in doing so, there's a working API that you can use today.

See Cross-protocol access via WebDAV in the PR description.

Let's get back to topic.

@dschmidt

Copy link
Copy Markdown
Contributor Author

I have given this some more thought and also went through the Microsoft Graph docs again.

On the "complex objects" point: the reference for both creating and updating an openTypeExtension says the payload "can be primitive types, or arrays of primitive types", and all examples are flat (the only non-scalar one is the topPicks string array). So I've now made the spec explicit about this: values are scalars or arrays of scalars and values are stored and returned exactly as the client wrote them. That also answers your "34" example: a JSON string stays a string, a JSON number stays a number, nothing is reinterpreted on the way out. The one exception to "no objects" is a geoCoordinates object, allowed only when annotated. Types stay out of the schema; where they matter for search (date-time, geo), clients say so with a <property>@odata.type sibling, which is plain OData. Reverse-DNS naming is documented, and xsi:type is the plan for the WebDAV side. See the latest commits on the branch.

I also thought through what this means for search. Indexing untyped extension values in a typed index is solvable without a schema per file or per drive, and without changing the index mapping at runtime; the details belong in the implementation PR, but they don't push back on the API shape.

Where I'd like to understand your proposal better is fieldValueSet. As far as I can tell it is only meaningful together with columnDefinitions: a listItem's fields must match the columns of the list, and the list is the drive's document library. That would make custom metadata a per-drive schema (/drives/{id}/list/columns with all the column types, plus listItem on the driveItem), which is a much larger API and ties the definition of a field to one drive. Without columns, a fieldValueSet is essentially the extension body without the extension namespace.

So: what would fieldValueSet give us that is missing, or simpler than with open extensions?

Added endpoints for managing open extensions on DriveItems, including listing, retrieving, creating, updating, and deleting extensions.
@dschmidt
dschmidt force-pushed the feat/opentypeextensions branch from a56ec1a to d7902f9 Compare September 15, 2026 17:50
@dschmidt

Copy link
Copy Markdown
Contributor Author

Here is what my proposal looks like end to end, with the spec as it is on the branch now.

Graph: create or update an extension

PUT /v1beta1/drives/{drive-id}/items/{item-id}/extensions/com.example.project
Content-Type: application/json

{
  "status": "reviewed",
  "priority": 3,
  "due": "2026-10-01T00:00:00Z",
  "due@odata.type": "#DateTimeOffset",
  "site": { "latitude": 52.5, "longitude": 13.4 },
  "site@odata.type": "#microsoft.graph.geoCoordinates",
  "tags": ["urgent", "customer"]
}
HTTP/1.1 201 Created

{
  "extensionName": "com.example.project",
  "status": "reviewed",
  "priority": 3,
  "due": "2026-10-01T00:00:00Z",
  "due@odata.type": "#DateTimeOffset",
  "site": { "latitude": 52.5, "longitude": 13.4 },
  "site@odata.type": "#microsoft.graph.geoCoordinates",
  "tags": ["urgent", "customer"]
}

The client only annotates what JSON cannot express, the date and the geo object; everything else keeps its JSON type.

Graph: what gets rejected

{ "due": "next week", "due@odata.type": "#DateTimeOffset" }   -> 400, value does not match the annotation
{ "assignee": { "name": "alice" } }                            -> 400, objects are only allowed as annotated geoCoordinates

WebDAV

Each extension becomes an XML namespace, each member a property, and the type travels as xsi:type per RFC 4316 wherever the value is not a string:

<d:propstat xmlns:d="DAV:"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xmlns:xs="http://www.w3.org/2001/XMLSchema"
            xmlns:oc="http://owncloud.org/ns"
            xmlns:p="urn:opencloud:extension:com.example.project">
  <d:prop>
    <p:status>reviewed</p:status>
    <p:priority xsi:type="xs:integer">3</p:priority>
    <p:due      xsi:type="xs:dateTime">2026-10-01T00:00:00Z</p:due>
    <p:site     xsi:type="oc:geoCoordinates"><oc:latitude>52.5</oc:latitude><oc:longitude>13.4</oc:longitude></p:site>
    <p:tags     xsi:type="oc:list"><oc:item>urgent</oc:item><oc:item>customer</oc:item></p:tags>
  </d:prop>
  <d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>

PROPPATCH goes the other way; a property without xsi:type is a string, which is what every existing WebDAV client sends today:

<d:propertyupdate xmlns:d="DAV:"
                  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                  xmlns:xs="http://www.w3.org/2001/XMLSchema"
                  xmlns:p="urn:opencloud:extension:com.example.project">
  <d:set>
    <d:prop>
      <p:status>approved</p:status>
      <p:priority xsi:type="xs:integer">4</p:priority>
    </d:prop>
  </d:set>
  <d:remove>
    <d:prop><p:due/></d:prop>
  </d:remove>
</d:propertyupdate>

Both protocols read and write the same stored JSON, so a value written over WebDAV shows up in Graph with the matching @odata.type, and vice versa. Existing PROPPATCH properties in other namespaces are untouched; only the urn:opencloud:extension: prefix maps to extensions.

@dschmidt

Copy link
Copy Markdown
Contributor Author

I have changed my mind on the type persistence and would like to pick up your prefix proposal after all, with one restriction: the prefixes apply only to properties in the open extension namespace, never to other custom properties. Everything outside that namespace keeps working exactly as today, untyped and untouched (so we don't need a migration there).

The reason is the storage layout. I first tried one metadata value per extension holding the whole JSON, which forces a read-modify-write on every PROPPATCH and PUT and makes concurrent writers of different properties overwrite each other. Your layout of one arbitrary metadata key per property, http://opencloud.eu/ns/extensions/<extensionName>/<property>, avoids both: a PROPPATCH or PUT writes and removes exactly the properties it names, and two clients touching different properties of the same extension never conflict. The key doubles as the WebDAV namespace plus local name, so it fits the existing custom property storage one to one. The urn: namespace I had in the previous comment is dropped in favour of this URL.

With one key per property, the type has to travel with the value, and a short prefix is the cheapest way to do that: s: string, n: number, b: boolean, d: RFC 3339 date-time, g: geo as lat,lon[,alt], and the upper case letter for a homogeneous array of that kind holding the JSON array. I use a single n: instead of i: and f: because the literal is kept as written, so integer or decimal is still visible, and the typed array codes replace j: so a list of dates keeps its type. Type and value are written together, so re-typing a property is one write and an annotation can never go stale.

Decoding is strict. A value in that namespace without a known prefix is unreadable and readers skip it. Since nothing existed under the namespace before, there is no legacy to be lenient about, and leniency would make a genuine string like x:1 indistinguishable from a future code.

On WebDAV this gives exactly what you sketched with RFC 4316: PROPPATCH maps xsi:type to the stored prefix and PROPFIND renders the prefix back as xsi:type, with xs:integer, xs:decimal, xs:boolean, xs:dateTime, an oc:geoCoordinates element with latitude, longitude and optional altitude, and an oc:list of oc:item elements. A property without xsi:type is a string, as every client writes today. Extension properties are returned only when requested by name and stay out of allprop.

On the Graph side the API stays as proposed, a flat object of scalars and arrays of scalars. The JSON type carries the kind, only date-times and geo coordinates need a <property>@odata.type annotation, which is validated against the value on write and regenerated on read. Nothing about the storage encoding is visible in the API.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants