Skip to content

Commit

Permalink
Docs
Browse files Browse the repository at this point in the history
  • Loading branch information
oklemenz2 committed Sep 30, 2024
1 parent 210c8d6 commit da363a5
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 15 deletions.
105 changes: 97 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -765,6 +765,25 @@ specification. All event annotation values (static or dynamic) and header values
emit according to their kind. Values of all headers and annotations of same semantic type are unified for
single and array values.
#### Format Headers
In addition to the above event emit headers, format specific event headers can be specified in the `websocket` or `ws` section
during event emit.
```js
await srv.emit("customEvent", { ... }, {
ws: {
a: 1
},
websocket: {
b: "c"
}
});
```
These headers are made available to the format `compose(event, data, headers)` function, to be included in the
composed WebSocket message, if applicable (e.g. format: `pcp`, `cloudevent`).
### Ignore Elements
To ignore elements during event emit, the annotation `@websocket.ignore` or `@ws.ignore` is available on event element level.
Expand Down Expand Up @@ -850,8 +869,15 @@ A Cloud Event message has the following structure:
}
```
To create a cloud-event compatible CDS event, either the event is modeled as CDS service event according to the specification
or a CDS event is mapped via annotations to a cloud-event compatible event.
##### Modeling Cloud Event
Cloud event can be explicitly modelled as CDS event, matching the specification of cloud event attributes.
**Example:**
```cds
event cloudEvent {
specversion : String;
Expand All @@ -871,30 +897,78 @@ event cloudEvent {
}
```
The CDS event `cloudEvent` is explicitly modeled according to the cloud-event specification.
The event data is passed inbound and outbound in the exact same representation as JSON object, as specified.
No additional annotation is necessary to be defined.
##### Mapping Cloud Event
CDS events can also be mapped to cloud-event compatible events via headers and CDS annotations. The implementation is based on the
`generic` formatter (see section below), that allows to map CDS events to cloud-event compatible events based on
cloud event specific headers and wildcard annotations, starting with `@websocket.cloudevent.<annotation>` or `@ws.cloudevent.<annotation>`
to match the cloud-event specific attributes.
The provided header values in the `websocket` or `ws` section are mapped to the cloud event attributes generically, if available.
**Example:**
```js
await srv.emit("cloudEvent",
{
appinfoA,
appinfoB,
appinfoC,
},
{
ws: {
specversion: "1.0",
type: "com.example.someevent.cloudEvent4",
source: "/mycontext",
subject: req.data._subject || "example",
id: "C234-1234-1234",
time: "2018-04-05T17:31:00Z",
comexampleextension1: "value",
comexampleothervalue: 5,
datacontenttype: "application/json",
},
},
);
```
Subsequently, the following annotations are respected:
- **Event level**:
- `@websocket.cloudevent.<attribute>: <value>`
- Type: `any` (according to Cloud Event JSON format)
- Provide static cloud event attribute value, according to cloud event specification
- **Event element level**:
- `@websocket.cloudevent.<attribute>`
- Type: `Boolean`
- Value from event data for the annotated element is used as dynamic cloud event attribute value, according to cloud event attribute specification
**Examples:**
**Event Level:**
```cds
@ws.cloudevent.specversion : '1.1'
@ws.cloudevent.specversion : '1.0'
@ws.cloudevent.type : 'com.example.someevent'
@ws.cloudevent.source : '/mycontext'
@ws.cloudevent.subject : 'example'
@ws.cloudevent.id : 'C234-1234-1234'
@ws.cloudevent.time : '2018-04-05T17:31:00Z'
@ws.cloudevent.comexampleextension1: 'value'
@ws.cloudevent.comexampleothervalue: 5
@ws.cloudevent.datacontenttype : 'application/cloudevents+json'
@ws.cloudevent.datacontenttype : 'application/json'
event cloudEvent2 {
appinfoA : String;
appinfoB : Integer;
appinfoC : Boolean;
}
```
Event is published only via cloud event sub-protocol, with the specified static cloud event attributes.
Event is published via cloud event sub-protocol, with the specified static cloud event attributes. The event data is
consumed as cloud event data section.
**Event Element Level:**
Expand Down Expand Up @@ -924,8 +998,11 @@ event cloudEvent3 {
}
```
Event is published only via cloud event sub-protocol, with the specified dynamic cloud event attributes derived from
CDS event elements.
Event is published via cloud event sub-protocol, with the specified dynamic cloud event attributes derived from
CDS event elements. Annotated elements are consumed as cloud event attributes, non-annotated elements are consumed as
cloud event data section.
Static and dynamic annotations can be combined. Dynamnic values are overwritten by static values, if defined.
#### Custom Format
Expand All @@ -944,9 +1021,21 @@ In addition, it can implement the following functions (optional):
#### Generic Format
Additionally, a custom formatter can be based on the generic implementation `format/generic.js` providing a name.
CDS annotations and header values are then derived from format name based on wildcard annotations
`@websocket.<name>.<annotation>` or `@ws.<name>.<annotation>`.
Additionally, a custom formatter can be based on the generic implementation `format/generic.js` providing a name and identifier.
Values are derived via CDS annotations based on wildcard annotations
`@websocket.<format>.<annotation>` or `@ws.<format>.<annotation>` using the formatter name.
In addition, provided header values in the `websocket` or `ws` section are also used to derived values from.
The following generic implementation specifics are included:
- **parse:** Data is parsed generically
- Parsing is based on formatter specific wildcard annotations on operation level (static) or operation parameter level (dynamic), if available.
- CDS operation (action or function) is derived from generic annotation `@websocket.<format>.name` or `@ws.<format>.name`.
- Operation identification is based on the formatter identifier (default `name`) on event data, that can be specified per formatter.
- Data is passed further as-is, in case no CDS annotations are present for format
- **compose:** Data is composed generically
- First data is composed based on headers, if available
- Subsequently, formatter specific wildcard annotations on event level (static) or event element level (dynamic) are processed
### Connect & Disconnect
Expand Down
8 changes: 4 additions & 4 deletions src/format/generic.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,14 +171,14 @@ class GenericFormat extends BaseFormat {
* @param {String} name Definition name (event, operation)
* @param {Object} data Data
* @param {Object} mappedData Data to be mapped into
* @param {[String]} [localAnnotationNames] Local annotation names to be mapped
* @param {[String]} [localAnnotations] Local annotation names to be mapped
* @returns {*} Derived value
*/
mapValues(name, data, mappedData, localAnnotationNames) {
mapValues(name, data, mappedData, localAnnotations) {
data ??= {};
const definition = this.service.events()[name] || this.service.operations()[this.localName(name)];
if (definition) {
for (const localAnnotation of localAnnotationNames || []) {
for (const localAnnotation of localAnnotations || []) {
for (const annotation of [
`@websocket.${this.name}.${localAnnotation}`,
`@ws.${this.name}.${localAnnotation}`,
Expand All @@ -194,7 +194,7 @@ class GenericFormat extends BaseFormat {
continue;
}
let mapped = false;
for (const localAnnotation of localAnnotationNames || []) {
for (const localAnnotation of localAnnotations || []) {
if (mapped) {
break;
}
Expand Down
3 changes: 2 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,8 @@ function bindServiceEvents(socketServer, service, path) {
const user = deriveUser(event, req.data, req.headers, req);
const context = deriveContext(event, req.data, req.headers);
const identifier = deriveIdentifier(event, req.data, req.headers);
const headers = req.headers?.websocket || req.headers?.ws;
const headers =
req.headers?.websocket || req.headers?.ws ? { ...req.headers?.websocket, ...req.headers?.ws } : undefined;
path = normalizeEventPath(event["@websocket.path"] || event["@ws.path"] || path);
await socketServer.broadcast({
service,
Expand Down
6 changes: 5 additions & 1 deletion test/_env/srv/handlers/chat.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
module.exports = (srv) => {
srv.on("message", async (req) => {
req.data.user = req.user.id;
await srv.emit("received", req.data);
await srv.emit("received", req.data, {
ws: {
header: "value",
},
});
return req.data.text;
});
};
2 changes: 1 addition & 1 deletion test/ws/redis_ws.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ describe("Redis", () => {
expect(redis.client.subscribe).toHaveBeenNthCalledWith(4, "websocket/main", expect.any(Function));
expect(redis.client.publish).toHaveBeenCalledWith(
"websocket/chat",
`{"event":"received","data":{"text":"test","user":"alice"},"tenant":"t1"}`,
'{"event":"received","data":{"text":"test","user":"alice"},"tenant":"t1","headers":{"header":"value"}}',
);

// Duplicated because Redis mock publishes to same client (not done for real Redis)
Expand Down

0 comments on commit da363a5

Please sign in to comment.