CDS pluginCDS plugintypeInstalls into a CAP project and extends the runtime. CAP loads it through cds.requires or the plugin hook.See every one →What this type is →independently maintainedindependently maintainedoriginPublished by one person or a small company outside SAP. Says nothing about quality; the readings say that.See every one →

cap-collaborative-draft

A cds-plugin that gives a CAP Node.js service the OData contract, tables and handlers Fiori elements needs for collaborative draft, so a second user joins an open draft instead of meeting HTTP 423.

CAP's lean draft belongs to one editor: a second user opening it is refused with HTTP 423, locked by another user. This plugin makes the draft shared — it adds the participant, field-lock and collaborative-state tables to the model, injects the CollaborativeDraftEnabled, DraftAccessType, DraftAdministrativeUser and ColDraftShare parts of the OData contract into $metadata, and prepends handlers so a second user joins the draft while a PATCH on a field another participant holds answers 409. Two annotations on the service entity switch it on, @odata.draft.enabled and @CollaborativeDraft.enabled: true, and entities without the second are unaffected. It stops at the backend contract: the participant avatar group and the discard dialog are UI5 snippets the consuming application adds to its own Object Page, and cross-participant conflicts found on activation are logged rather than merged. Live presence needs @cap-js-community/websocket, and that relay is per process, so without the adapter the plugin runs pull-only and with it one CAP instance still does not broadcast to another instance's participants.

Facts

Latestv1.0.2ReleasedLast pushLicenceApache-2.0

Where it sits

Lives in
Provides
Draft handlingCAPDraft handlingcapabilityLetting a user edit a record over several requests before it becomes real — saving unfinished changes, locking, and activating a completed draft.Concurrency controlCAPConcurrency controlcapabilityStopping two people who opened the same record from silently overwriting each other — detecting that the version you read is no longer the version you are saving over.
Flavors
Requires
@sap/cds≥ 8.0@sap/cdsartifactThe peer range, as published.
Optional
@cap-js-community/websocket@cap-js-community/websocketartifactRelaying join, leave, lock and change messages between the participants of a draft; without it presence and lock state are still read over OData, in pull-only mode.presence relay

Who is behind it

Sören LeibachSören LeibachPersonMaintains this
In the atlas: maintains 6, contributes to 5, wrote 1
@sleibach
maintainer

Before choosing this

Conditions

LicenceSAP Developer License Agreement 3.2 CAP§ 1, 2, 3A cds-plugin is used with SAP CAP materials, or an artifact is explicitly identified as a CAP plugin or application.
  • § 1 License. Productive customer applications are limited to SAP BTP or another SAP-licensed platform, and mass extraction to a non-SAP product is restricted except where solely required for SAP interoperability.
  • § 2 Intellectual Property. The agreement retains SAP's rights in the covered materials and limits making APIs, tools or software available to third parties, while preserving ownership of customer applications subject to SAP's underlying rights.
  • § 3 Artificial Intelligence Training. The agreement prohibits use of the covered materials and customer applications for training artificial-intelligence models or systems.
SAP Developer License Agreement 3.2 CAP →
PracticalFunctional boundary

The maintainer marks this under active development; public APIs, annotation names and OData contracts may still change.

What it declares

Every artifact

Type
CDS plugin
Origin
Independently maintained
Runtime
Node.js

As a CDS plugin

Declared for
>=8.0
Distributed through
npm

Connections3

cap-collaborative-draft connectionscap-collaborative-draftPROVIDES→ provides2provides stated Locks a shared draft field by field. A PATCH acquires a lock per OData property name in DRAFT.DraftFieldLocks and answers HTTP 409 when another participant already holds a non-expired lock on one of the patched fields. Locks older than the configured TTL, 120 seconds by default, stop blocking, and the check reads the table rather than memory, so it holds across CAP instances. Nested child entities and associations are not covered. sleibach/cap-collaborative-draft — README: https://raw.githubusercontent.com/sleibach/cap-collaborative-draft/main/README.md sleibach/cap-collaborative-draft — README: https://raw.githubusercontent.com/sleibach/cap-collaborative-draft/main/README.md sleibach/cap-collaborative-draft — README: https://raw.githubusercontent.com/sleibach/cap-collaborative-draft/main/README.md sleibach/cap-collaborative-draft — README: https://raw.githubusercontent.com/sleibach/cap-collaborative-draft/main/README.md sleibach/cap-collaborative-draft — README: https://raw.githubusercontent.com/sleibach/cap-collaborative-draft/main/README.mdConcurrency controlprovides stated Turns a CAP Node.js draft from one editor's into several editors'. A user opening a draft that already exists joins it rather than being refused, the plugin deploys DRAFT.DraftParticipants, DRAFT.DraftFieldLocks and DRAFT.CollaborativeDraftState alongside the standard draft tables, and any participant can activate rather than only the originator. Fiori elements is told through injected $metadata: CollaborativeDraftEnabled, DraftAccessType, a DraftAdministrativeUser navigation property for the avatar group, and a ColDraftShare bound action behind the invite dialog. Entities that do not carry @CollaborativeDraft.enabled are untouched. sleibach/cap-collaborative-draft — README: https://raw.githubusercontent.com/sleibach/cap-collaborative-draft/main/README.md sleibach/cap-collaborative-draft — README: https://raw.githubusercontent.com/sleibach/cap-collaborative-draft/main/README.md sleibach/cap-collaborative-draft — README: https://raw.githubusercontent.com/sleibach/cap-collaborative-draft/main/README.md sleibach/cap-collaborative-draft — README: https://raw.githubusercontent.com/sleibach/cap-collaborative-draft/main/README.md sleibach/cap-collaborative-draft — README: https://raw.githubusercontent.com/sleibach/cap-collaborative-draft/main/README.mdDraft handlingNEEDS→ optionally needs1optionally needs stated If: Real-time presence, participant avatars and field-lock indicators in the Fiori elements UI. Relaying join, leave, lock and change messages between the participants of a draft; without it presence and lock state are still read over OData, in pull-only mode. sleibach/cap-collaborative-draft — README: https://raw.githubusercontent.com/sleibach/cap-collaborative-draft/main/README.md sleibach/cap-collaborative-draft — README: https://raw.githubusercontent.com/sleibach/cap-collaborative-draft/main/README.md sleibach/cap-collaborative-draft — README: https://raw.githubusercontent.com/sleibach/cap-collaborative-draft/main/README.md@cap-js-community/websocketif

Install

npm add cap-collaborative-draft

Derived from the package name. No recipe recorded; the README has the configuration.

README

quoted from sleibach/cap-collaborative-draft under Apache-2.0, read
sleibach/cap-collaborative-draft/README.mdOn GitHub

cap-collaborative-draft

Active Development Notice This plugin is under active development. Public APIs, annotation names, OData contracts, and configuration keys may change without prior notice until a stable 1.0 release is published. Pin to an exact version in production and review the changelog before upgrading.

A cds-plugin that enables SAP Fiori Elements Collaborative Draft on CAP Node.js backends — a feature normally exclusive to ABAP RAP.

Collaborative Draft lets multiple authenticated users co-edit the same draft instance simultaneously, with live presence awareness, field-level locking, and automatic participant management. This plugin brings that capability to any CAP Node.js service by implementing the required OData contract, database schema, and handler logic — without requiring any ABAP infrastructure.


Table of Contents


Overview

SAP Fiori Elements Collaborative Draft is the standard mechanism for multi-user concurrent editing of draft objects in SAP enterprise applications. In ABAP RAP, it is implemented through a combination of:

  • RAP collaborative draft toggle in the BOPF framework
  • OData annotations (@Common.DraftRoot.ShareAction, CollaborativeDraftEnabled, DraftAccessType)
  • ABAP Push Channel (WebSockets) for real-time presence relay
  • OData actions for participant join, field lock, and share operations

This plugin replicates the full OData contract on CAP Node.js, allowing Fiori Elements clients to activate their collaborative draft UI against a CAP backend. The plugin is additive — existing entities without @CollaborativeDraft.enabled are completely unaffected.

Preview

Preview


Installation

npm add cap-collaborative-draft

Or pin to an exact version (recommended while the API is still under active development — see the notice at the top of this README):

npm add cap-collaborative-draft@1.0.0

The plugin is auto-discovered by CAP via the cds-plugin.js convention. No additional wiring, service registration, or cds.serve() call is required.

Source: github.com/sleibach/cap-collaborative-draft npm: npmjs.com/package/cap-collaborative-draft

Peer dependency: @sap/cds >= 8.0

Required peer dependency: @cap-js-community/websocket — the plugin registers a WebSocket relay service for real-time presence updates, participant avatars, and field-lock indicators. Without it, the Fiori Elements collaborative draft UI will not function: participants won't see each other, field locks won't appear, and no live updates will occur. The plugin will still start, but collaborative draft is effectively broken in pull-only mode.


Quick Start

1. Annotate your service entity

Add both @odata.draft.enabled and @CollaborativeDraft.enabled: true to the service-level entity projection. Both annotations must be on the service entity (not the underlying DB entity):

// srv/service.cds
using { my.orders } from '../db/schema';

service OrderService {

  @odata.draft.enabled
  @CollaborativeDraft.enabled: true
  entity Orders as projection on my.orders.Orders {
    *, Items
  }
}

Annotation placement: Both annotations belong on the service-layer entity, not the database entity. Placing @odata.draft.enabled on a database entity is non-standard in CDS and may produce unexpected draft table generation. @CollaborativeDraft.enabled annotations on database entities are silently ignored by this plugin.

2. Start your CAP application
cds watch

The plugin logs two info-level messages at startup:

[collab-draft] - cap-collaborative-draft plugin loaded
[collab-draft] - Collaborative draft enabled for OrderService [ Orders ]
3. Verify the OData contract
curl http://localhost:4004/odata/v4/order/$metadata | grep -E 'ColDraftShare|CollaborativeDraftEnabled|DraftAdministrativeUser'

You should see all three terms in the output, confirming the plugin has correctly augmented the metadata.


How It Works

The plugin hooks into four distinct stages of the CAP bootstrap lifecycle.

Phase 0 — Raw CSN Augmentation

Hook: cds.on('loaded')

At raw CSN stage (before cds.compile.for.nodejs), the plugin:

  1. Scans csn.definitions for all service entities that carry both @CollaborativeDraft.enabled: true and @odata.draft.enabled. Only entities with a query or projection property are considered (i.e., service projections — not bare database entities).
  2. Creates DRAFT.DraftParticipants, DRAFT.DraftFieldLocks and DRAFT.CollaborativeDraftState entity definitions in the raw CSN if they do not already exist, causing CDS to deploy the corresponding database tables alongside the standard draft tables. DRAFT.CollaborativeDraftState (keyed by DraftUUID) holds the per-draft collaborative state — CollaborativeDraftEnabled and DraftAccessType. Modeling this state as a normal entity (rather than adding columns to the compiler-owned DRAFT_DraftAdministrativeData table) means it is created by the regular deployment on any database, including SAP HANA Cloud / HDI where the runtime user has no DDL privileges.
  3. Adds @Common.DraftRoot.ShareAction annotation to each qualifying entity, naming the action <EntityShortName>_ColDraftShare.
Phase 1 — Compiled Model Augmentation

Hook: cds.on('served') (compiled model available)

After CDS compiles the model for Node.js, the plugin:

  1. Adds CollaborativeDraftEnabled: Boolean and DraftAccessType: String(1) to the DRAFT.DraftAdministrativeData element map (and all service-projected copies of it) as virtual OData properties. They are exposed in $metadata and the OData payload but are not physical columns of DRAFT_DraftAdministrativeData; the after READ DraftAdministrativeData handler populates them at read time from the DRAFT.CollaborativeDraftState table.
  2. Creates DRAFT.DraftAdministrativeUser as a virtual entity (no persistence) and adds it as a contained navigation property on DraftAdministrativeData. Fiori Elements reads this navigation property to display the participant avatar group.
  3. Creates <ServiceNs>.ColDraftUsers as a virtual entity for each service. This entity backs the user search value help in the invite dialog.
  4. Registers the <EntityShortName>_ColDraftShare bound action definition on each collaborative entity, including its Users (Collection(ColDraftShareUser)) parameter and ShareAll/IsDeltaUpdate flags.
  5. Creates the <ServiceNs>.ColDraftShareUser complex type used as the action parameter.
Phase 2 — $metadata Middleware

Hook: Express middleware registered via cds.on('bootstrap')

CAP generates $metadata XML from the compiled model. For certain aspects of the collaborative draft contract, runtime XML injection is more reliable than compiled model manipulation. The middleware intercepts every $metadata response and idempotently patches:

InjectionCondition
CollaborativeDraftEnabled + DraftAccessType propertiesAdded to DraftAdministrativeData EntityType if absent
DraftAdministrativeUser NavigationPropertyAdded to DraftAdministrativeData EntityType if absent
DraftAdministrativeUser EntityTypeInjected as fallback if not produced by model compiler
NavigationPropertyBinding for DraftAdministrativeData/DraftAdministrativeUserAdded to all relevant EntitySets
ColDraftShareUser ComplexTypeInjected if absent
ColDraftUsers EntityType and EntitySetInjected if absent
@Common.ValueList on ColDraftShareUser/UserIDPoints to ColDraftUsers for the invite dialog user search
@Common.WebSocketBaseURL / @Common.WebSocketChannelInjected on the EntityContainer when WebSocket is available
@Common.SideEffects for presence and draft change eventsInjected on the collaborative entity's Annotations block
Phase 3 — Handler Registration

Hook: cds.on('served'), registered via srv.prepend()

Handlers are prepended before CAP's built-in lean-draft handlers so they execute first. Only services that contain at least one collaborative draft entity receive these handlers.

EventBehavior
EDITIf a draft already exists for the entity, the current user joins it (added to the presence store) rather than receiving HTTP 423 "locked by another user". Sets InProcessByUser = '' to indicate shared ownership.
after EDITRegisters the user as originator if they created the draft, or as participant if they joined. Persists to DRAFT.DraftParticipants and runs the DB migration for CollaborativeDraftEnabled/DraftAccessType columns if not already present.
before PATCHAcquires field-level locks in DRAFT.DraftFieldLocks. Responds HTTP 409 if another participant holds a non-expired lock on any of the patched fields. Sets InProcessByUser to the current user to satisfy CAP's internal ownership check.
before draftPrepareValidates cross-participant field consistency. Logs conflicts; does not block activation.
before draftActivateSets InProcessByUser to the current user so any participant can activate (not just the originator).
after draftActivateCleans up all presence records and field locks for the draft UUID.
before CANCELIf the requesting user is the originator, the cancel proceeds for all participants. If a non-originator, removes only that participant from the presence store and returns HTTP 409 to keep the draft alive for others.
after READ DraftAdministrativeDataPopulates the virtual CollaborativeDraftEnabled (boolean) and DraftAccessType fields from the DRAFT.CollaborativeDraftState table and the presence store. Ensures the response always carries proper JSON booleans (not SQLite integers).
READ DraftAdministrativeUserResolves participants from the in-memory presence store (with DB fallback) and maps them to the {DraftUUID, UserID, UserDescription, UserEditingState} shape expected by Fiori Elements.
READ ColDraftUsersServes the user directory for the invite dialog value help. See User Directory.
<EntityName>_ColDraftShareHandles the bound share action. On self-join (ShareAll: true, empty Users): registers the calling user as participant, sets DraftAccessType = 'S' and CollaborativeDraftEnabled = 1, broadcasts CollaborativePresenceChanged via WebSocket. On explicit invite (Users non-empty): additionally emits the collab-draft:shareInvite event for app code to send notifications, and queues DraftMessages feedback so FE shows a confirmation toast.
Presence Tracking

Implemented in lib/presence.ts.

In-memory store: Map<DraftUUID, Map<UserID, { displayName, lastSeen, isOriginator }>> — O(1) lookups per draft.

DB persistence: DRAFT.DraftParticipants — written on every join/heartbeat/leave so that presence survives server restarts. Loaded back into memory on startup via loadFromDB().

Eviction: A background setInterval (default: every 30 seconds) scans all entries and removes any participant whose lastSeen exceeds the configured TTL (default: 5 minutes). The timer is .unref()-ed so it does not block process exit. Evicted entries are also deleted from the DB.

FunctionDescription
join(draftUUID, userID, opts)Adds or refreshes a participant. Preserves isOriginator on rejoin.
heartbeat(draftUUID, userID)Updates lastSeen without changing other fields.
leave(draftUUID, userID)Explicit removal (non-originator cancel, LEAVE WebSocket message).
removeAll(draftUUID)Removes all participants (draftActivate, originator cancel).
getParticipants(draftUUID)Returns the current participant list as ParticipantRecord[].
isOriginator(draftUUID, userID)Returns whether the user created the draft.
Field-Level Locking

Implemented in lib/field-locks.ts.

Locks are stored in DRAFT.DraftFieldLocks with a composite key of (DraftUUID, EntityKey, FieldName). Each lock record carries a LockedAt timestamp; locks older than the configured TTL (default: 120 seconds) are treated as expired and do not block new acquisitions.

The locking granularity corresponds to individual OData property names (field names as they appear in the PATCH request body). An entity key is serialized as a stable JSON string to handle composite keys.

FunctionDescription
acquireLocks(draftUUID, userID, entityKey, fields)Attempts to acquire all specified locks atomically. Returns a result indicating which locks were acquired and which were blocked by another participant.
releaseLocks(draftUUID, userID)Releases all locks held by a user for a given draft. Called on participant leave and cancel.
releaseAllLocks(draftUUID)Releases all locks for a draft. Called on activation and originator cancel.
getActiveLocks(draftUUID)Returns non-expired locks. Used by Fiori Elements lock echo response.
User Directory

The ColDraftUsers OData entity set backs the user search value help in the Fiori Elements invite dialog. It supports two operating modes:

Point the plugin at any CDS entity or projection — a real database table is used, making it suitable for thousands of users. The READ ColDraftUsers handler delegates to cds.run(SELECT …) and pushes $filter/$search down to the database.

// package.json → "cds" block, or .cdsrc.json
{
  "collab": {
    "users": {
      "entity": "OrderService.Users",
      "userIdField": "email",
      "userDescriptionField": "displayName"
    }
  }
}
PropertyRequiredDefaultDescription
entityyes—Fully qualified CDS entity name (service projection or direct DB entity)
userIdFieldno"UserID"Source field mapped to UserID in the OData response
userDescriptionFieldno"UserDescription"Source field mapped to UserDescription in the OData response

The entity only needs to be readable by the CAP runtime — it does not have to be exposed as its own OData entity set. A projection without @odata.draft.enabled on a dedicated service (or the same application service) works well:

// srv/service.cds
extend service OrderService {
  // Read-only user directory, not directly exposed via FE but reachable by the plugin
  @readonly entity Users as projection on my.Users {
    key email       as UserID,
        displayName as UserDescription
  };
}

When userIdField/userDescriptionField match the entity's actual column names, the AS aliases are redundant but harmless.

Mode 2 — Static map (development / small environments)

cds.env.collab.users as a plain object map, or the standard CAP mock auth user list:

Data sources (in priority order):

  1. cds.env.collab.users — a plain object map of { "<userID>": { "displayName": "…" } }.
  2. cds.env.requires.auth.users — the standard CAP mock auth user map (populated automatically in development from .cdsrc.json).
  3. Empty list — if neither source is configured.
{
  "collab": {
    "users": {
      "john.doe@example.com": { "displayName": "John Doe" },
      "jane.smith@example.com": { "displayName": "Jane Smith" }
    }
  }
}

In static mode, $filter and $search are applied in memory after loading the full map.

WebSocket Integration

When @cap-js-community/websocket is installed and active, the plugin registers a CollabDraftWebSocketService on the /ws/collab-draft path. The WebSocket service relays the following message types (PCP-style JSON envelope):

Message TypeDirectionDescription
JOINClient → ServerUser opens a draft. Server responds with JOINECHO carrying current participant list.
JOINECHOServer → ClientBroadcasts participant list to all connected clients for the draft.
LEAVEClient → ServerUser closes a draft or navigates away. Server broadcasts updated presence.
LOCKClient → ServerUser focuses a field. Server acquires DB lock and broadcasts LOCKECHO.
LOCKECHOServer → ClientBroadcasts lock state to all clients. FE renders field lock indicators.
UNLOCKClient → ServerUser blurs a field. Server releases the lock.
CHANGEServer → ClientBroadcasts a field value change to all other participants.
ACTIVATEServer → ClientNotifies all clients that the draft has been activated.
DISCARDServer → ClientNotifies all clients that the draft has been discarded.

Without WebSocket, the plugin operates in pull-only mode: presence and lock state are correct and available via standard OData reads, but updates from other participants only appear after a client-triggered re-read (governed by @Common.SideEffects).


OData Contract

DraftAdministrativeData Extensions

Two properties are added to DraftAdministrativeData (and all service-projected copies):

PropertyOData TypeDescription
CollaborativeDraftEnabledEdm.Booleantrue when the draft has active participants. Always returns a proper JSON boolean — never a SQLite integer.
DraftAccessTypeEdm.String (MaxLength=1)"S" for shared (collaborative) drafts. null for standard exclusive drafts.
DraftAdministrativeUser Navigation

A contained navigation property DraftAdministrativeUser is added to DraftAdministrativeData, pointing to a collection of:

PropertyTypeDescription
DraftUUIDEdm.Guid (key)UUID of the draft
UserIDEdm.String (key)Authenticated user ID
UserDescriptionEdm.StringDisplay name
UserEditingStateEdm.String"N" (no changes) or "P" (pending changes)

Fiori Elements reads this navigation property (with UserID ne '<currentUser>' filter) to display the participant avatar group and to populate the discard confirmation dialog.

ColDraftShare Bound Action

Added as a bound action on each collaborative entity:

<Action Name="Orders_ColDraftShare" IsBound="true">
  <Parameter Name="bindingParameter" Type="Collection(OrderService.Orders)"/>
  <Parameter Name="Users" Type="Collection(OrderService.ColDraftShareUser)" Nullable="true"/>
  <Parameter Name="ShareAll" Type="Edm.Boolean" Nullable="true"/>
  <Parameter Name="IsDeltaUpdate" Type="Edm.Boolean" Nullable="true"/>
</Action>

Referenced via @Common.DraftRoot.ShareAction = 'OrderService.Orders_ColDraftShare'. Fiori Elements calls this action from the invite dialog when the user clicks "Invite".

The action accepts:

  • ShareAll: true with an empty Users array — registers the calling user as a participant (self-join, called by the Fiori Elements client on draft open).
  • A non-empty Users array — records the invitation intent, emits a collab-draft:shareInvite event for application code to act on (see Events), and returns feedback messages via DraftMessages so Fiori Elements shows a confirmation toast instead of "No additional users were invited".

The plugin does not send any notification itself — the application is responsible for actually reaching out to the invited users (see Events).

ColDraftUsers Entity

A virtual (non-persisted) entity set used exclusively for the value help in the invite dialog:

PropertyType
UserIDEdm.String (key, MaxLength=256)
UserDescriptionEdm.String (MaxLength=256)

The corresponding @Common.ValueList annotation is injected on ColDraftShareUser/UserID during $metadata patching, pointing the Fiori Elements ValueHelpDelegate to this entity set.


Configuration

All settings are namespaced under cds.collab and can be set in package.json ("cds" key) or .cdsrc.json:

{
  "cds": {
    "collab": {
      "presenceTtlMs": 300000,
      "lockTtlMs": 120000,
      "cleanupIntervalMs": 30000,
      "users": {
        "john.doe@example.com": { "displayName": "John Doe" }
      }
    }
  }
}
KeyTypeDefaultDescription
presenceTtlMsnumber300000 (5 min)Milliseconds of inactivity before a participant is automatically evicted from the presence store.
lockTtlMsnumber120000 (2 min)Milliseconds after a PATCH before a field lock is considered stale and non-blocking.
cleanupIntervalMsnumber30000 (30 s)How frequently the background cleanup timer scans for stale presence entries.
usersobjectundefinedStatic user directory for the invite value help. Each key is a UserID; values may carry displayName. Falls back to cds.env.requires.auth.users if not set.

Events

The plugin emits CDS events that application code can subscribe to. Register listeners in your CAP service file or in a custom cds-plugin.js.

collab-draft:shareInvite

Emitted when the Fiori Elements share dialog submits a list of users to invite (i.e. the Users parameter of ColDraftShare is non-empty). The plugin does not send any notification — this event is the hook for application code to do so.

const cds = require('@sap/cds')
const { SHARE_INVITE_EVENT } = require('cap-collaborative-draft/lib/draft-handlers')

cds.on(SHARE_INVITE_EVENT, ({ draftUUID, invitedBy, users }) => {
  // users: Array<{ UserID: string, UserAccessRole?: string }>
  for (const user of users) {
    sendInviteEmail({ to: user.UserID, draftUUID, invitedBy })
  }
})

Or use the event name string directly:

cds.on('collab-draft:shareInvite', ({ draftUUID, invitedBy, users }) => { ... })
PropertyTypeDescription
draftUUIDstringUUID of the draft being shared
invitedBystringreq.user.id of the user who triggered the invite
usersArray<{ UserID, UserAccessRole? }>Users selected in the invite dialog

Fiori Elements Integration

Required Annotations

The plugin automatically adds all required annotations to $metadata. No manual annotation in service.cds is needed beyond the entity-level @CollaborativeDraft.enabled: true. Specifically, the plugin injects:

  • @Common.DraftRoot.ShareAction — triggers the invite button in the Object Page header
  • @Common.WebSocketBaseURL and @Common.WebSocketChannel — enable WebSocket connectivity in FE (when WebSocket is available)
  • @Common.SideEffects for CollaborativePresenceChanged and CollaborativeDraftChanged events — instructs FE when to re-read the entity binding

The Fiori Elements invite dialog (CollaborationDraft building block) renders a "Search User" input field backed by a value help. The value help reads from the ColDraftUsers entity set served by this plugin.

In development, users from cds.env.requires.auth.users are automatically available. In production, configure an entity-backed user directory via cds.env.collab.users.entity (see User Directory) so that search queries are pushed to the database rather than loaded in memory.

Participant Avatars

Participant avatars in the Object Page header are rendered by the CollaborationDraft building block (a custom Fiori Elements building block, available as a code snippet in ui5-snippets/CollaborationDraft.tsx). The building block reads from DraftAdministrativeData/DraftAdministrativeUser. This building block is not automatically injected by the plugin — it must be added to the Object Page by the consuming application, typically via a custom Object Page header section in manifest.json or a controller extension.

WebSocket Real-Time Updates

When @cap-js-community/websocket is installed, the Fiori Elements client connects to /ws/collab-draft immediately on draft open. The WebSocket URL parameters include the draft UUID and, optionally, user identity (userID, userName) when the FLP sandbox is started with ?useFLPUser=true.

Without WebSocket, Fiori Elements falls back gracefully to OData polling triggered by @Common.SideEffects. Presence and lock correctness are unaffected — only the real-time latency of updates changes.


Multi-Instance Deployment

The in-memory presence store (lib/presence.ts) is per process. In a horizontally scaled deployment with multiple CAP instances behind a load balancer:

  • Presence reads via OData (DraftAdministrativeData/DraftAdministrativeUser) are consistent across instances because they are backed by DRAFT.DraftParticipants in the shared database. The in-memory store is a write-through cache that improves latency on the same instance.
  • Field lock checks read from DRAFT.DraftFieldLocks in the database, so they are also cross-instance consistent.
  • WebSocket relay is per-instance. A participant on instance A will not receive WebSocket messages broadcast by instance B. For multi-instance real-time relay, a shared pub/sub channel (e.g., Redis) is required — this is outside the current scope of this plugin.

Recommendation for multi-instance deployments: Rely on OData polling (via @Common.SideEffects) rather than WebSocket for presence and lock updates. The OData contract is always consistent; only real-time latency is affected.


Limitations and Known Gaps

AreaStatusNotes
Single-instance in-memory presencePartialCross-instance consistent via DB; real-time WebSocket relay is per-instance only.
WebSocket multi-instance pub/subNot implementedRequires Redis or equivalent adapter.
Conflict merge on draftActivateLogging onlyCross-participant field conflicts are detected and logged; no automatic merge or user-facing conflict resolution UI.
UserDescription in mock usersID = DescriptionCAP mock auth users do not carry display names; UserDescription equals UserID unless cds.collab.users is configured.
CollaborationDraft building blockNot auto-injectedThe participant avatar group and invite button require a custom Object Page fragment. See ui5-snippets/CollaborationDraft.tsx.
CollaborationDiscardDialogNot auto-injectedThe "other users are editing" discard confirmation dialog requires a custom controller extension. See ui5-snippets/CollaborationDiscardDialog.tsx.
PATCH field lock granularityProperty-levelLocks are per property name. Composite nested entity locking (child entities, associations) is not yet implemented.

Compatibility

DependencyVersion
@sap/cds>= 8.0
Node.js>= 18
@cap-js/sqlite>= 2.0 (development / testing)
@cap-js/hana / SAP HANA CloudSupported (production). All plugin tables are part of the deployed model, so they are created by cds deploy / HDI deploy — no runtime DDL is executed.
@cap-js-community/websocket>= 1.0 (optional, for real-time relay)
Fiori Elements (sap.fe.templates)>= 1.120 (SAPUI5 1.120+)

The plugin is tested against @sap/cds 8.x and SAPUI5 1.146. Compatibility with earlier versions of either dependency is not guaranteed.


Development and Testing

Starting the test app
cd test/app && npx cds watch

Open the Fiori Launchpad sandbox at:

http://localhost:4004/orders/webapp/test/flpSandbox.html?useFLPUser=true

The ?useFLPUser=true parameter instructs the WebSocket client to pass the FLP user identity as URL parameters on the WebSocket connection URL.

Verifying the OData contract
# Check that all collaborative draft annotations are present
curl http://localhost:4004/odata/v4/order/$metadata \
  | grep -E 'ColDraftShare|CollaborativeDraftEnabled|DraftAdministrativeUser|ColDraftUsers|ShareAction'

# Check DraftAdministrativeData for a specific draft
curl "http://localhost:4004/odata/v4/order/Orders(ID=<uuid>,IsActiveEntity=false)/DraftAdministrativeData" \
  | jq '{CollaborativeDraftEnabled, DraftAccessType}'

# List available users for the invite dialog
curl http://localhost:4004/odata/v4/order/ColDraftUsers | jq '.value[]'

# Check WebSocket connectivity
wscat -c "ws://localhost:4004/ws/collab-draft?draft=<draftUUID>"
Building from source

The plugin is authored in TypeScript and compiled to CommonJS in dist/:

npm run build        # tsc

cds-plugin.js is a thin shim (module.exports = require('./dist/cds-plugin')). CAP discovers it by filename convention.

Running tests
npm test             # Jest integration tests (SQLite, in-memory)
npx playwright test  # E2E browser tests (requires running test app)
Testing against SAP HANA (HDI)

The plugin's persistence relies on tables being created by the deployer, never by runtime DDL — because an HDI container's runtime user has no DDL privileges. The following opt-in suite verifies that contract against a real HANA Cloud HDI container.

# 1. Provide an HDI service key (writes the gitignored test/app/default-env.json)
#    Copy the service-key JSON to your clipboard, then:
cd test/app && pbpaste | node ../../scripts/wrap-hana-binding.mjs && cd ../..

# 2. Deploy the schema once (runs as the privileged deploy user via HDI)
npm run hana:deploy

# 3a. Run the smoke test (plain Node — recommended)
npm run hana:smoke

# 3b. …or the gated jest test (CI / environments with a healthy jest setup)
npm run test:hana

What the suite asserts, connected as the DML-only runtime user:

  • the modeled DRAFT.CollaborativeDraftState table exists (created at deploy time) and is queryable,
  • the collaborative read/write round-trip works (the operation the old ALTER TABLE approach broke),
  • a raw ALTER TABLE is rejected with insufficient privilege — proving runtime DDL is impossible on HDI and that the modeled-table approach is required.

The suite auto-skips unless HANA_TEST=1 and test/app/default-env.json are present, so the default npm test stays on SQLite. Never commit the service key — default-env.json is gitignored.

Note: npm run hana:smoke is a plain-Node runner provided because the jest harness can hang during database bootstrap in some local environments. npm run test:hana is the equivalent jest test for CI.


License

Apache-2.0

Apache-2.0, read

History

Latest

v1.0.2

  1. v1.0.1

  2. v1.0.0

On npm
0downloads per week, npm
Weekly downloads for cap-collaborative-draft26 complete calendar weeks from 2026-04-27 to 2026-10-25; the latest total is 0.27 Apr – 3 May: 04 May – 10 May: 011 May – 17 May: 24018 May – 24 May: 725 May – 31 May: 91 Jun – 7 Jun: 408 Jun – 14 Jun: 2615 Jun – 21 Jun: 522 Jun – 28 Jun: 529 Jun – 5 Jul: 96 Jul – 12 Jul: 1213 Jul – 19 Jul: 520 Jul – 26 Jul: 327 Jul – 2 Aug: 13 Aug – 9 Aug: 410 Aug – 16 Aug: 717 Aug – 23 Aug: 524 Aug – 30 Aug: 931 Aug – 6 Sept: 67 Sept – 13 Sept: 814 Sept – 20 Sept: 321 Sept – 27 Sept: 828 Sept – 4 Oct: 25 Oct – 11 Oct: 112 Oct – 18 Oct: 019 Oct – 25 Oct: 0

Others providing6

Draft handling
  1. com.sap.cds:cds-services-bomMaven bill of materials pinning CAP Java module versionsfrom CAP
  2. cap-handler-frameworkConvention-based handler layer for CAP with draft lifecycle hooksfrom CAP
  3. @rizing/cds-extensionCAP plugin putting a draft-capable facade in front of imported ODatafrom CAP
  4. @sap/cds-fioriFiori draft handling and UI-facing behaviour for CAP servicesfrom CAP
  5. @sap/cdsCAP Node.js runtimefrom CAP
Concurrency control
  1. lock-managerHolds SAP locks across stateless abap2UI5 roundtripsfrom ABAP
  2. com.sap.cds:cds-services-bomMaven bill of materials pinning CAP Java module versionsfrom CAP
  3. @sap/cdsCAP Node.js runtimefrom CAP

Details

Sources4
GitHub APIdiscovered here, sleibachchecked
npm registry, cap-collaborative-draftversion, releases, installschecked
github.com/git+https://github.com/sleibach/cap-collaborative-draft.gittree, pushes, contributors, CIchecked
README.mdquoted, and what the entry declareschecked

Sources are graph entities: the same page may back several entries, and an unreachable source flags every row that rests on it.

Compatibility and provenance
runtimesnode
declared for>=8.0
latestv1.0.2
first release
releases and cadence3, ~25d median
discovered viaGitHub API · sleibach
first seen in the atlas
ecosystem and domaincap, ui-and-frontend-bridge
Flavors1
Node.js
“A `cds-plugin` that enables **SAP Fiori Elements Collaborative Draft** on CAP Node.js backends — a feature normally exclusive to ABAP RAP.” — sleibach/cap-collaborative-draft — READMEchecked
History in the atlas7
  1. capability:draft-handling, capability:concurrency-control
  2. A cds-plugin that gives a CAP Node.js service the OData contract, tables and handlers Fiori elements needs for collaborative draft, so a second user joins an open draft instead of meeting HTTP 423.
  3. CAP's lean draft belongs to one editor: a second user opening it is refused with HTTP 423, locked by another user. …
  4. The README names it a required peer dependency for the real-time relay while its own compatibility table calls it optional; recorded as conditional on real-time presence, with both sentences quoted.
  5. The README and the package's peerDependencies both state @sap/cds >= 8.0; the entry carried the runtime and not the range.
  6. Stated rather than left to the cds-plugin fallback: the README's first line says CAP Node.js backends.
  7. First seen.
For your agent
Ref
artifact:sleibach-cap-collaborative-draft
JSON
/artifacts/sleibach-cap-collaborative-draft.json
MCP
Connecting an agent