Compare commits
88 Commits
Author | SHA1 | Date |
---|---|---|
|
3c8819e2bc | |
|
beac7356a7 | |
|
fc760976bf | |
|
3ff6fdd3bf | |
|
c07afa9b77 | |
|
f475cdfd7e | |
|
8357719bab | |
|
6977113d7b | |
|
154e913717 | |
|
e7626077ed | |
|
c65afe94d2 | |
|
245bae92d1 | |
|
f93f896002 | |
|
16d0c0f91f | |
|
db60220762 | |
|
1ed43c8486 | |
|
df059e9696 | |
|
15f6505a58 | |
|
089520a4cc | |
|
fa388f7dc6 | |
|
0d923a4f92 | |
|
a0d8682613 | |
|
023171d9a0 | |
|
3d961371da | |
|
f3659ebfc6 | |
|
11442d32d3 | |
|
3931b224cb | |
|
43c3584b98 | |
|
1995b5778e | |
|
0120d224ab | |
|
2cb9364a25 | |
|
4626529d56 | |
|
ec83abc827 | |
|
1cf8f8acae | |
|
343382ebde | |
|
c06ffc1963 | |
|
7ff64f8b82 | |
|
870d2118cd | |
|
78b8e0a372 | |
|
953bc2a143 | |
|
bc3aaca2ef | |
|
e5ee8369ba | |
|
b374d9ac33 | |
|
64e527c120 | |
|
1b449c4c9a | |
|
eccc00ee67 | |
|
94f1a3d470 | |
|
3619ef2bbd | |
|
2d5fab1b71 | |
|
c09a9cc20a | |
|
4831e6a1a5 | |
|
760a024067 | |
|
c282922ef9 | |
|
ea94a4d779 | |
|
847f6bfcc7 | |
|
ed63f14339 | |
|
921e273ede | |
|
a62eb44669 | |
|
d6f52ca65f | |
|
ce02e0a1f3 | |
|
d9ee0e05d1 | |
|
addbd9acf1 | |
|
a512aad5d5 | |
|
c0b1f7705a | |
|
0164f72eaa | |
|
4ab6356bd7 | |
|
0362a4f11c | |
|
b4d7aa9adb | |
|
6204805bfc | |
|
ae8cb96f8a | |
|
c420da4793 | |
|
b13bde9b49 | |
|
4d8f03f7c6 | |
|
9046b369cf | |
|
c3d9f39a53 | |
|
b5c0b56f52 | |
|
f36a1f0428 | |
|
cd4dea954b | |
|
8abbc114af | |
|
349b84c3da | |
|
c603831e93 | |
|
ae8fa799af | |
|
225836f68f | |
|
98009d910d | |
|
591d133f31 | |
|
5d1f744f50 | |
|
2ac731eb88 | |
|
320354f750 |
|
@ -5,7 +5,6 @@
|
|||
"sourceType": "module"
|
||||
},
|
||||
"extends": [
|
||||
"plugin:prettier/recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"env": {
|
||||
|
@ -16,6 +15,7 @@
|
|||
"plugins": [
|
||||
"header"
|
||||
],
|
||||
"ignorePatterns": ["**/schema/*"],
|
||||
"rules": {
|
||||
"no-var": "error",
|
||||
"standard/no-callback-literal": "off",
|
||||
|
|
|
@ -8,10 +8,10 @@ jobs:
|
|||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Generate API documentation
|
||||
run: npm install && npm run generate-docs
|
||||
run: npm install && npm run build:schema && npm run generate-docs
|
||||
-
|
||||
name: Deploy to GitHub Pages
|
||||
if: success()
|
||||
|
|
|
@ -15,12 +15,12 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [12.x, 14.x]
|
||||
node-version: [20.x, 22.x, 24.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Test on Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: npm ci
|
||||
|
@ -31,18 +31,18 @@ jobs:
|
|||
name: Code coverage
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- name: Generate coverage report
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 14.x
|
||||
node-version: 22.x
|
||||
- run: npm ci
|
||||
- run: npm run build --if-present
|
||||
- run: npm run coverage
|
||||
- name: Upload coverage report to storage
|
||||
uses: actions/upload-artifact@v1
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage
|
||||
path: coverage/lcov.info
|
||||
|
@ -52,15 +52,15 @@ jobs:
|
|||
needs: coverage
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Download coverage report from storage
|
||||
uses: actions/download-artifact@v1
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: coverage
|
||||
- name: Upload coverage report to codacy
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 14.x
|
||||
node-version: 22.x
|
||||
- run: |
|
||||
( [[ "${CODACY_PROJECT_TOKEN}" != "" ]] && npm run coverage-publish ) || echo "Coverage report not published"
|
||||
env:
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
name: Publish to npmjs
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22.x'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- run: npm install -g npm
|
||||
- run: npm ci
|
||||
- run: npm publish --provenance --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.CLOUDEVENTS_PUBLISH }}
|
|
@ -7,9 +7,11 @@ jobs:
|
|||
release-please:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: GoogleCloudPlatform/release-please-action@v2.5.5
|
||||
- uses: GoogleCloudPlatform/release-please-action@v3
|
||||
id: release
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
token: ${{ secrets.CLOUDEVENTS_RELEASES_TOKEN }}
|
||||
release-type: node
|
||||
package-name: cloudevents
|
||||
signoff: "Lucas Holmquist <lholmqui@redhat.com>"
|
||||
changelog-types: '[{"type":"feat","section":"Features","hidden":false},{"type":"fix","section":"Bug Fixes","hidden":false},{"type":"docs","section":"Documentation","hidden":false},{"type":"chore","section":"Miscellaneous","hidden":false},{"type":"src","section":"Miscellaneous","hidden":false},{"type":"style","section":"Miscellaneous","hidden":false},{"type":"refactor","section":"Miscellaneous","hidden":false},{"type":"perf","section":"Performance","hidden":false},{"type":"test","section":"Tests","hidden":false}]'
|
||||
|
|
|
@ -13,6 +13,7 @@ index.js
|
|||
/bundles
|
||||
/dist
|
||||
/docs
|
||||
src/schema/v1.js
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
|
@ -90,3 +91,6 @@ typings/
|
|||
|
||||
# Package lock
|
||||
package-lock.json
|
||||
|
||||
# Jetbrains IDE directories
|
||||
.idea
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
module.exports = {
|
||||
semi: true,
|
||||
trailingComma: "all",
|
||||
doubleQuote: true,
|
||||
printWidth: 120,
|
||||
tabWidth: 2
|
||||
}
|
223
CHANGELOG.md
223
CHANGELOG.md
|
@ -2,6 +2,229 @@
|
|||
|
||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||
|
||||
## [9.0.0](https://github.com/cloudevents/sdk-javascript/compare/v8.0.3...v9.0.0) (2025-04-03)
|
||||
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* remove node 16 ([#610](https://github.com/cloudevents/sdk-javascript/issues/610))
|
||||
|
||||
### Features
|
||||
|
||||
* remove node 16 ([#610](https://github.com/cloudevents/sdk-javascript/issues/610)) ([3ff6fdd](https://github.com/cloudevents/sdk-javascript/commit/3ff6fdd3bf1a9d77be9cd1d1ed589de47a86f7c1))
|
||||
|
||||
## [8.0.3](https://github.com/cloudevents/sdk-javascript/compare/v8.0.2...v8.0.3) (2025-04-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add generics to `Binding` type ([#604](https://github.com/cloudevents/sdk-javascript/issues/604)) ([f475cdf](https://github.com/cloudevents/sdk-javascript/commit/f475cdfd7ee7e80a375b997ba2f41b1655a44a03))
|
||||
|
||||
## [8.0.2](https://github.com/cloudevents/sdk-javascript/compare/v8.0.1...v8.0.2) (2024-07-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* creating an event does not error when the event attribute name is too long ([#593](https://github.com/cloudevents/sdk-javascript/issues/593)) ([6977113](https://github.com/cloudevents/sdk-javascript/commit/6977113d7b49bd2b702632cc09e29cc0c003e2a1))
|
||||
|
||||
## [8.0.1](https://github.com/cloudevents/sdk-javascript/compare/v8.0.0...v8.0.1) (2024-06-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* allow Node 22 and use it by default ([#587](https://github.com/cloudevents/sdk-javascript/issues/587)) ([e762607](https://github.com/cloudevents/sdk-javascript/commit/e7626077ed22b2bcbfa71b0403a58ac187c57cba))
|
||||
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
* Update compatible node version ([#573](https://github.com/cloudevents/sdk-javascript/issues/573)) ([245bae9](https://github.com/cloudevents/sdk-javascript/commit/245bae92d1c84b4a44fe7aae2f82c5a90818f1c5))
|
||||
* updated check mark symbol to show some green checkboxes ([#582](https://github.com/cloudevents/sdk-javascript/issues/582)) ([c65afe9](https://github.com/cloudevents/sdk-javascript/commit/c65afe94d2320eae9b8b74de9b1e1bd8793baa6a))
|
||||
|
||||
## [8.0.0](https://github.com/cloudevents/sdk-javascript/compare/v7.0.2...v8.0.0) (2023-07-24)
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* use string instead of enum for Version ([#561](https://github.com/cloudevents/sdk-javascript/issues/561)) ([15f6505](https://github.com/cloudevents/sdk-javascript/commit/15f6505a580b2bbf8d6b2e89feea10cbd40ab827))
|
||||
TypeScript does not consider enum values equivalent, even if the string
|
||||
representation is the same. So, when a module imports `cloudevents` and
|
||||
also has a dependency on `cloudevents` this can cause conflicts where
|
||||
the `CloudEvent.version` attribute is not considered equal when, in
|
||||
fact, it is.
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
* add `npm run build:schema` to the doc generation action ([#557](https://github.com/cloudevents/sdk-javascript/issues/557)) ([fa388f7](https://github.com/cloudevents/sdk-javascript/commit/fa388f7dc65c1739864d7a885d6d28111ce07775))
|
||||
* modify release-please to use Signed-Off-By on commits ([#559](https://github.com/cloudevents/sdk-javascript/issues/559)) ([089520a](https://github.com/cloudevents/sdk-javascript/commit/089520a4cc8304e39ac9bfccf0ed59c76ea8c11a))
|
||||
* release 8.0.0 ([#563](https://github.com/cloudevents/sdk-javascript/issues/563)) ([1ed43c8](https://github.com/cloudevents/sdk-javascript/commit/1ed43c84868ccfd18531deaf6cc9d4e4fcb21a08))
|
||||
|
||||
## [7.0.2](https://github.com/cloudevents/sdk-javascript/compare/v7.0.1...v7.0.2) (2023-07-05)
|
||||
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
* add the provenance flag when publishing to npm ([#556](https://github.com/cloudevents/sdk-javascript/issues/556)) ([a0d8682](https://github.com/cloudevents/sdk-javascript/commit/a0d86826138be31072c9a30edf26f4b91da576ed))
|
||||
* fix the release-please automation script. ([#554](https://github.com/cloudevents/sdk-javascript/issues/554)) ([023171d](https://github.com/cloudevents/sdk-javascript/commit/023171d9a08484c32e24f8228602ef4d5173c749))
|
||||
|
||||
## [7.0.1](https://github.com/cloudevents/sdk-javascript/compare/v7.0.0...v7.0.1) (2023-05-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* handle big integers in incoming events ([#495](https://github.com/cloudevents/sdk-javascript/issues/495)) ([43c3584](https://github.com/cloudevents/sdk-javascript/commit/43c3584b984aa170b1c1c4dff7218d027cd28d02))
|
||||
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
* add publish automation ([#550](https://github.com/cloudevents/sdk-javascript/issues/550)) ([3931b22](https://github.com/cloudevents/sdk-javascript/commit/3931b224cb3140ad3ba759edcf564621a2e34542))
|
||||
* remove old Node versions from the readme ([#549](https://github.com/cloudevents/sdk-javascript/issues/549)) ([11442d3](https://github.com/cloudevents/sdk-javascript/commit/11442d32d307a0e8416ed573ce34fc825d3b63c4))
|
||||
* Update compatible node version ([#552](https://github.com/cloudevents/sdk-javascript/issues/552)) ([f3659eb](https://github.com/cloudevents/sdk-javascript/commit/f3659ebfc6251b4c57997f70709688916a061a2d))
|
||||
|
||||
## [7.0.0](https://github.com/cloudevents/sdk-javascript/compare/v6.0.4...v7.0.0) (2023-05-03)
|
||||
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* remove node 12 and node 14 ([#545](https://github.com/cloudevents/sdk-javascript/issues/545))
|
||||
|
||||
### Features
|
||||
|
||||
* remove node 12 and node 14 ([#545](https://github.com/cloudevents/sdk-javascript/issues/545)) ([2cb9364](https://github.com/cloudevents/sdk-javascript/commit/2cb9364a25a5e82f2a68504dbe19839a7fbfd9d4))
|
||||
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
* add the build script to the pretest script. ([#539](https://github.com/cloudevents/sdk-javascript/issues/539)) ([c06ffc1](https://github.com/cloudevents/sdk-javascript/commit/c06ffc196389fedd7d5141d69fac3f4d95156193))
|
||||
* fix release-please-action ([#543](https://github.com/cloudevents/sdk-javascript/issues/543)) ([ec83abc](https://github.com/cloudevents/sdk-javascript/commit/ec83abc82799159aa1f64c791c92e035ef6f42b8))
|
||||
* release 6.0.5 ([#542](https://github.com/cloudevents/sdk-javascript/issues/542)) ([343382e](https://github.com/cloudevents/sdk-javascript/commit/343382ebdedc9a2efbc5b3ba5cd36e4e069fd38f))
|
||||
* release 7.0.0 ([#546](https://github.com/cloudevents/sdk-javascript/issues/546)) ([0120d22](https://github.com/cloudevents/sdk-javascript/commit/0120d224ab67e804e201625e0a9d59947a5a212d))
|
||||
* Update CI action to node 18.x ([#533](https://github.com/cloudevents/sdk-javascript/issues/533)) ([7ff64f8](https://github.com/cloudevents/sdk-javascript/commit/7ff64f8b82e1c5a824bbe985df4948d79e919e8c))
|
||||
|
||||
### [6.0.3](https://www.github.com/cloudevents/sdk-javascript/compare/v6.0.2...v6.0.3) (2023-02-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* improve validation on extension attribute ([#502](https://www.github.com/cloudevents/sdk-javascript/issues/502)) ([ea94a4d](https://www.github.com/cloudevents/sdk-javascript/commit/ea94a4d779d0744ef40abc81d08ab8b7e93e9133))
|
||||
* Make CloudEvent data field immutable and enumerable using Object.keys() ([#515](https://www.github.com/cloudevents/sdk-javascript/issues/515)) ([#516](https://www.github.com/cloudevents/sdk-javascript/issues/516)) ([2d5fab1](https://www.github.com/cloudevents/sdk-javascript/commit/2d5fab1b7133241493bb9327aa26e7de4117616d))
|
||||
* This fixes bug [#525](https://www.github.com/cloudevents/sdk-javascript/issues/525) where the browser version was breaking becuase of process not being found. ([#526](https://www.github.com/cloudevents/sdk-javascript/issues/526)) ([e5ee836](https://www.github.com/cloudevents/sdk-javascript/commit/e5ee8369ba5838aa24c2d99efeb81788757b71d1))
|
||||
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
* added the engines property to the package.json ([bc3aaca](https://www.github.com/cloudevents/sdk-javascript/commit/bc3aaca2ef250e4acd72b909488b326233237c83))
|
||||
* bump cucumber to full release version ([#514](https://www.github.com/cloudevents/sdk-javascript/issues/514)) ([c09a9cc](https://www.github.com/cloudevents/sdk-javascript/commit/c09a9cc20a601ddc36c5c1b56fb52dc9c2161e1b))
|
||||
* bump mocha to 10.1.0 ([#512](https://www.github.com/cloudevents/sdk-javascript/issues/512)) ([4831e6a](https://www.github.com/cloudevents/sdk-javascript/commit/4831e6a1a5003c4c1c7bcbd5a3a2fc5c48e0ba4c))
|
||||
* bump webpack to 5.74.0 ([#509](https://www.github.com/cloudevents/sdk-javascript/issues/509)) ([760a024](https://www.github.com/cloudevents/sdk-javascript/commit/760a0240674c79ca6be142ae9f9b242080c4d59d))
|
||||
* release 6.0.3 ([#503](https://www.github.com/cloudevents/sdk-javascript/issues/503)) ([3619ef2](https://www.github.com/cloudevents/sdk-javascript/commit/3619ef2bbd6e2b3e9e6e5bb5ad904689d40f4b79))
|
||||
* Typos ([953bc2a](https://www.github.com/cloudevents/sdk-javascript/commit/953bc2a143a66d04d850c727305a5a465e843bff))
|
||||
* **examples:** add mqtt example ([#523](https://www.github.com/cloudevents/sdk-javascript/issues/523)) ([b374d9a](https://www.github.com/cloudevents/sdk-javascript/commit/b374d9ac3313023e4f8a59cb22785751bbb0f686))
|
||||
|
||||
### [6.0.3](https://www.github.com/cloudevents/sdk-javascript/compare/v6.0.2...v6.0.3) (2022-11-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* improve validation on extension attribute ([#502](https://www.github.com/cloudevents/sdk-javascript/issues/502)) ([ea94a4d](https://www.github.com/cloudevents/sdk-javascript/commit/ea94a4d779d0744ef40abc81d08ab8b7e93e9133))
|
||||
* Make CloudEvent data field immutable and enumerable using Object.keys() ([#515](https://www.github.com/cloudevents/sdk-javascript/issues/515)) ([#516](https://www.github.com/cloudevents/sdk-javascript/issues/516)) ([2d5fab1](https://www.github.com/cloudevents/sdk-javascript/commit/2d5fab1b7133241493bb9327aa26e7de4117616d))
|
||||
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
* bump cucumber to full release version ([#514](https://www.github.com/cloudevents/sdk-javascript/issues/514)) ([c09a9cc](https://www.github.com/cloudevents/sdk-javascript/commit/c09a9cc20a601ddc36c5c1b56fb52dc9c2161e1b))
|
||||
* bump mocha to 10.1.0 ([#512](https://www.github.com/cloudevents/sdk-javascript/issues/512)) ([4831e6a](https://www.github.com/cloudevents/sdk-javascript/commit/4831e6a1a5003c4c1c7bcbd5a3a2fc5c48e0ba4c))
|
||||
* bump webpack to 5.74.0 ([#509](https://www.github.com/cloudevents/sdk-javascript/issues/509)) ([760a024](https://www.github.com/cloudevents/sdk-javascript/commit/760a0240674c79ca6be142ae9f9b242080c4d59d))
|
||||
|
||||
### [6.0.2](https://www.github.com/cloudevents/sdk-javascript/compare/v6.0.1...v6.0.2) (2022-06-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* allow `TypedArray` for binary data ([#494](https://www.github.com/cloudevents/sdk-javascript/issues/494)) ([921e273](https://www.github.com/cloudevents/sdk-javascript/commit/921e273ede100ab9a262fdfa1f3d6561d3fab0f9))
|
||||
* HTTP headers for extensions with false values ([#493](https://www.github.com/cloudevents/sdk-javascript/issues/493)) ([d6f52ca](https://www.github.com/cloudevents/sdk-javascript/commit/d6f52ca65f893fdb581bf06b2ff97b3d6eeeb744))
|
||||
* package.json & package-lock.json to reduce vulnerabilities ([ed63f14](https://www.github.com/cloudevents/sdk-javascript/commit/ed63f14339fb7774bff865726370fe72a49abca3))
|
||||
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
* bump ajv and remove old dep dependency ([#496](https://www.github.com/cloudevents/sdk-javascript/issues/496)) ([ce02e0a](https://www.github.com/cloudevents/sdk-javascript/commit/ce02e0a1f3b24624bd8ba443c744b4a6c0cfcb44))
|
||||
* update owners ([#499](https://www.github.com/cloudevents/sdk-javascript/issues/499)) ([a62eb44](https://www.github.com/cloudevents/sdk-javascript/commit/a62eb4466985972cd3112e6f8e3e0b62cb01c1c1))
|
||||
|
||||
### [6.0.1](https://www.github.com/cloudevents/sdk-javascript/compare/v6.0.0...v6.0.1) (2022-03-21)
|
||||
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
* update dependencies to inlude ajv-formats ([#484](https://www.github.com/cloudevents/sdk-javascript/issues/484)) ([c0b1f77](https://www.github.com/cloudevents/sdk-javascript/commit/c0b1f7705a448dda3e6292d872a5bf435d26fab4)), closes [/github.com/cloudevents/sdk-javascript/pull/471/files#diff-7ae45ad102eab3b6d7e7896acd08c427a9b25b346470d7bc6507b6481575d519R128](https://www.github.com/cloudevents//github.com/cloudevents/sdk-javascript/pull/471/files/issues/diff-7ae45ad102eab3b6d7e7896acd08c427a9b25b346470d7bc6507b6481575d519R128)
|
||||
|
||||
## [6.0.0](https://www.github.com/cloudevents/sdk-javascript/compare/v5.3.2...v6.0.0) (2022-03-21)
|
||||
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* add http transport and remove axios (#481)
|
||||
|
||||
### Features
|
||||
|
||||
* add http transport and remove axios ([#481](https://www.github.com/cloudevents/sdk-javascript/issues/481)) ([0362a4f](https://www.github.com/cloudevents/sdk-javascript/commit/0362a4f11c7bdc74a3a9a05b5bb4a94516b15a44))
|
||||
* precompile cloudevent schema ([#471](https://www.github.com/cloudevents/sdk-javascript/issues/471)) ([b13bde9](https://www.github.com/cloudevents/sdk-javascript/commit/b13bde9b4967f5c8b02b788a40a89dd4cec5b78a))
|
||||
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
* add an npm test:once script ([#480](https://www.github.com/cloudevents/sdk-javascript/issues/480)) ([b4d7aa9](https://www.github.com/cloudevents/sdk-javascript/commit/b4d7aa9adbb92bb5d037c464dd3d4bcd1ba88fe6))
|
||||
* update package.json format and deps ([#479](https://www.github.com/cloudevents/sdk-javascript/issues/479)) ([6204805](https://www.github.com/cloudevents/sdk-javascript/commit/6204805bfcebf68fd1b94777ecb3df6d7473e10e))
|
||||
* update the release documentation ([#476](https://www.github.com/cloudevents/sdk-javascript/issues/476)) ([c420da4](https://www.github.com/cloudevents/sdk-javascript/commit/c420da479343bc71a5ba4d5ed41841280f4c989a))
|
||||
|
||||
|
||||
### Documentation
|
||||
|
||||
* update readme to include http builtin transport ([#483](https://www.github.com/cloudevents/sdk-javascript/issues/483)) ([4ab6356](https://www.github.com/cloudevents/sdk-javascript/commit/4ab6356bd70434e55938ff89e940952f8b0105a3))
|
||||
|
||||
### [5.3.2](https://www.github.com/cloudevents/sdk-javascript/compare/v5.3.1...v5.3.2) (2022-02-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* use `isolatedModules: true` in tsconfig.json ([#469](https://www.github.com/cloudevents/sdk-javascript/issues/469)) ([b5c0b56](https://www.github.com/cloudevents/sdk-javascript/commit/b5c0b56f52dd6119949df1a583b76a48c6e3cec7))
|
||||
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
* bump typedoc to remove vuln ([#472](https://www.github.com/cloudevents/sdk-javascript/issues/472)) ([c3d9f39](https://www.github.com/cloudevents/sdk-javascript/commit/c3d9f39a53afaf411fa91aeb2323fef2eddb4d32))
|
||||
|
||||
### [5.3.1](https://www.github.com/cloudevents/sdk-javascript/compare/v5.3.0...v5.3.1) (2022-02-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* improve binary data detection in HTTP transport ([#468](https://www.github.com/cloudevents/sdk-javascript/issues/468)) ([cd4dea9](https://www.github.com/cloudevents/sdk-javascript/commit/cd4dea954b1797eb0e0fe2acd1b32ef75a3b7b65))
|
||||
* package.json & package-lock.json to reduce vulnerabilities ([#462](https://www.github.com/cloudevents/sdk-javascript/issues/462)) ([ae8fa79](https://www.github.com/cloudevents/sdk-javascript/commit/ae8fa799afea279adfbd1f35103fb168621c8a24))
|
||||
|
||||
|
||||
### Documentation
|
||||
|
||||
* add TS examples for CloudEvent usage ([#461](https://www.github.com/cloudevents/sdk-javascript/issues/461)) ([c603831](https://www.github.com/cloudevents/sdk-javascript/commit/c603831e934c68c1f430708b5bff4dad938093dd))
|
||||
* fix ts example ([#467](https://www.github.com/cloudevents/sdk-javascript/issues/467)) ([349b84c](https://www.github.com/cloudevents/sdk-javascript/commit/349b84c3dad5d282d24780a884a0f94643871247))
|
||||
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
* update readme with current Node LTS versions and add Node 16 to the testing matrix([#465](https://www.github.com/cloudevents/sdk-javascript/issues/465)) ([8abbc11](https://www.github.com/cloudevents/sdk-javascript/commit/8abbc114af4b784c5061737f432f0af9ccb6c6f2))
|
||||
|
||||
## [5.3.0](https://www.github.com/cloudevents/sdk-javascript/compare/v5.2.0...v5.3.0) (2022-01-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add MQTT transport messaging ([#459](https://www.github.com/cloudevents/sdk-javascript/issues/459)) ([591d133](https://www.github.com/cloudevents/sdk-javascript/commit/591d133f31d5802e526952d6177dcb0a3383c221))
|
||||
* add support for kafka transport ([#455](https://www.github.com/cloudevents/sdk-javascript/issues/455)) ([5d1f744](https://www.github.com/cloudevents/sdk-javascript/commit/5d1f744f503dbb56f4cfb3365d66cac635cc03b3))
|
||||
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
* **refactor:** prefer interfaces over concrete classes ([#457](https://www.github.com/cloudevents/sdk-javascript/issues/457)) ([2ac731e](https://www.github.com/cloudevents/sdk-javascript/commit/2ac731eb884965e91a19bb3529100a6aee7069dd))
|
||||
* update cucumber dependency and remove prettier ([#453](https://www.github.com/cloudevents/sdk-javascript/issues/453)) ([320354f](https://www.github.com/cloudevents/sdk-javascript/commit/320354f750420f74ac1258f1e0530962a9c58788))
|
||||
|
||||
## [5.2.0](https://www.github.com/cloudevents/sdk-javascript/compare/v5.1.0...v5.2.0) (2021-12-07)
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
# Maintainers
|
||||
|
||||
Current active maintainers of this SDK:
|
||||
|
||||
- [Lance Ball](https://github.com/lance)
|
||||
- [Daniel Bevenius](https://github.com/danbev)
|
||||
- [Lucas Holmquist](https://github.com/lholmquist)
|
||||
- [Fabio Jose](https://github.com/fabiojose)
|
||||
- [Helio Frota](https://github.com/helio-frota)
|
134
README.md
134
README.md
|
@ -12,14 +12,14 @@ The CloudEvents SDK for JavaScript.
|
|||
|
||||
- Represent CloudEvents in memory
|
||||
- Serialize and deserialize CloudEvents in different [event formats](https://github.com/cloudevents/spec/blob/v1.0/spec.md#event-format).
|
||||
- Send and recieve CloudEvents with via different [protocol bindings](https://github.com/cloudevents/spec/blob/v1.0/spec.md#protocol-binding).
|
||||
- Send and receive CloudEvents with via different [protocol bindings](https://github.com/cloudevents/spec/blob/v1.0/spec.md#protocol-binding).
|
||||
|
||||
_Note:_ Supports CloudEvent versions 0.3, 1.0
|
||||
_Note:_ Supports CloudEvent version 1.0
|
||||
|
||||
## Installation
|
||||
|
||||
The CloudEvents SDK requires a current LTS version of Node.js. At the moment
|
||||
those are Node.js 10.x and Node.js 12.x. To install in your Node.js project:
|
||||
those are Node.js 16.x, and Node.js 18.x. To install in your Node.js project:
|
||||
|
||||
```console
|
||||
npm install cloudevents
|
||||
|
@ -46,9 +46,26 @@ app.post("/", (req, res) => {
|
|||
|
||||
#### Emitting Events
|
||||
|
||||
You can send events over HTTP in either binary or structured format
|
||||
using the `HTTP` binding to create a `Message` which has properties
|
||||
for `headers` and `body`.
|
||||
The easiest way to send events is to use the built-in HTTP emitter.
|
||||
|
||||
```js
|
||||
const { httpTransport, emitterFor, CloudEvent } = require("cloudevents");
|
||||
|
||||
// Create an emitter to send events to a receiver
|
||||
const emit = emitterFor(httpTransport("https://my.receiver.com/endpoint"));
|
||||
|
||||
// Create a new CloudEvent
|
||||
const ce = new CloudEvent({ type, source, data });
|
||||
|
||||
// Send it to the endpoint - encoded as HTTP binary by default
|
||||
emit(ce);
|
||||
```
|
||||
|
||||
If you prefer to use another transport mechanism for sending events
|
||||
over HTTP, you can use the `HTTP` binding to create a `Message` which
|
||||
has properties for `headers` and `body`, allowing greater flexibility
|
||||
and customization. For example, the `axios` module is used here to send
|
||||
a CloudEvent.
|
||||
|
||||
```js
|
||||
const axios = require("axios").default;
|
||||
|
@ -87,30 +104,20 @@ const emit = emitterFor(sendWithAxios, { mode: Mode.BINARY });
|
|||
emit(new CloudEvent({ type, source, data }));
|
||||
```
|
||||
|
||||
You may also use the `Emitter` singleton
|
||||
You may also use the `Emitter` singleton to send your `CloudEvents`.
|
||||
|
||||
```js
|
||||
const axios = require("axios").default;
|
||||
const { emitterFor, Mode, CloudEvent, Emitter } = require("cloudevents");
|
||||
const { emitterFor, httpTransport, Mode, CloudEvent, Emitter } = require("cloudevents");
|
||||
|
||||
function sendWithAxios(message) {
|
||||
// Do what you need with the message headers
|
||||
// and body in this function, then send the
|
||||
// event
|
||||
axios({
|
||||
method: "post",
|
||||
url: "...",
|
||||
data: message.body,
|
||||
headers: message.headers,
|
||||
});
|
||||
}
|
||||
// Create a CloudEvent emitter function to send events to our receiver
|
||||
const emit = emitterFor(httpTransport("https://example.com/receiver"));
|
||||
|
||||
const emit = emitterFor(sendWithAxios, { mode: Mode.BINARY });
|
||||
// Set the emit
|
||||
// Use the emit() function to send a CloudEvent to its endpoint when a "cloudevent" event is emitted
|
||||
// (see: https://nodejs.org/api/events.html#class-eventemitter)
|
||||
Emitter.on("cloudevent", emit);
|
||||
|
||||
...
|
||||
// In any part of the code will send the event
|
||||
// In any part of the code, calling `emit()` on a `CloudEvent` instance will send the event
|
||||
new CloudEvent({ type, source, data }).emit();
|
||||
|
||||
// You can also have several listeners to send the event to several endpoints
|
||||
|
@ -132,6 +139,43 @@ const ce = new CloudEvent({...});
|
|||
const ce2 = ce.cloneWith({extension: "Value"});
|
||||
```
|
||||
|
||||
You can create a `CloudEvent` object in many ways, for example, in TypeScript:
|
||||
|
||||
```ts
|
||||
import { CloudEvent, CloudEventV1, CloudEventV1Attributes } from "cloudevents";
|
||||
const ce: CloudEventV1<string> = {
|
||||
specversion: "1.0",
|
||||
source: "/some/source",
|
||||
type: "example",
|
||||
id: "1234"
|
||||
};
|
||||
const event = new CloudEvent(ce);
|
||||
const ce2: CloudEventV1Attributes<string> = {
|
||||
specversion: "1.0",
|
||||
source: "/some/source",
|
||||
type: "example",
|
||||
};
|
||||
const event2 = new CloudEvent(ce2);
|
||||
const event3 = new CloudEvent({
|
||||
source: "/some/source",
|
||||
type: "example",
|
||||
});
|
||||
```
|
||||
|
||||
### A Note About Big Integers
|
||||
|
||||
When parsing JSON data, if a JSON field value is a number, and that number
|
||||
is really big, JavaScript loses precision. For example, the Twitter API exposes
|
||||
the Tweet ID. This is a large number that exceeds the integer space of `Number`.
|
||||
|
||||
In order to address this situation, you can set the environment variable
|
||||
`CE_USE_BIG_INT` to the string value `"true"` to enable the use of the
|
||||
[`json-bigint`](https://www.npmjs.com/package/json-bigint) package. This
|
||||
package is not used by default due to the resulting slowdown in parse speed
|
||||
by a factor of 7x.
|
||||
|
||||
See for more information: https://github.com/cloudevents/sdk-javascript/issues/489
|
||||
|
||||
### Example Applications
|
||||
|
||||
There are a few trivial example applications in
|
||||
|
@ -147,32 +191,38 @@ There you will find Express.js, TypeScript and Websocket examples.
|
|||
|
||||
| Core Specification | [v0.3](https://github.com/cloudevents/spec/blob/v0.3/spec.md) | [v1.0](https://github.com/cloudevents/spec/blob/v1.0/spec.md) |
|
||||
| ------------------ | ------------------------------------------------------------- | ------------------------------------------------------------- |
|
||||
| CloudEvents Core | :heavy_check_mark: | :heavy_check_mark: |
|
||||
| CloudEvents Core | :white_check_mark: | :white_check_mark: |
|
||||
|
||||
---
|
||||
|
||||
| Event Formats | [v0.3](https://github.com/cloudevents/spec/tree/v0.3) | [v1.0](https://github.com/cloudevents/spec/blob/v1.0/spec.md#event-format) |
|
||||
| ----------------- | ----------------------------------------------------- | ----------------------------------------------------- |
|
||||
| AVRO Event Format | :x: | :x: |
|
||||
| JSON Event Format | :heavy_check_mark: | :heavy_check_mark: |
|
||||
| JSON Event Format | :white_check_mark: | :white_check_mark: |
|
||||
|
||||
---
|
||||
|
||||
| Protocol Bindings | [v0.3](https://github.com/cloudevents/spec/tree/v0.3) | [v1.0](https://github.com/cloudevents/spec/blob/v1.0/spec.md#protocol-binding) |
|
||||
| ---------------------- | ----------------------------------------------------- | ----------------------------------------------------- |
|
||||
| AMQP Protocol Binding | :x: | :x: |
|
||||
| HTTP Protocol Binding | :heavy_check_mark: | :heavy_check_mark: |
|
||||
| Kafka Protocol Binding | :x: | :x: |
|
||||
| MQTT Protocol Binding | :x: | :x: |
|
||||
| HTTP Protocol Binding | :white_check_mark: | :white_check_mark: |
|
||||
| Kafka Protocol Binding | :x: | :white_check_mark: |
|
||||
| MQTT Protocol Binding | :white_check_mark: | :x: |
|
||||
| NATS Protocol Binding | :x: | :x: |
|
||||
|
||||
---
|
||||
|
||||
| Content Modes | [v0.3](https://github.com/cloudevents/spec/tree/v0.3) | [v1.0](https://github.com/cloudevents/spec/blob/v1.0/http-protocol-binding.md#13-content-modes) |
|
||||
| ---------------------- | ----------------------------------------------------- | ----------------------------------------------------- |
|
||||
| HTTP Binary | :heavy_check_mark: | :heavy_check_mark: |
|
||||
| HTTP Structured | :heavy_check_mark: | :heavy_check_mark: |
|
||||
| HTTP Batch | :heavy_check_mark: | :heavy_check_mark: |
|
||||
| HTTP Binary | :white_check_mark: | :white_check_mark: |
|
||||
| HTTP Structured | :white_check_mark: | :white_check_mark: |
|
||||
| HTTP Batch | :white_check_mark: | :white_check_mark: |
|
||||
| Kafka Binary | :white_check_mark: | :white_check_mark: |
|
||||
| Kafka Structured | :white_check_mark: | :white_check_mark: |
|
||||
| Kafka Batch | :white_check_mark: | :white_check_mark:
|
||||
| MQTT Binary | :white_check_mark: | :white_check_mark: |
|
||||
| MQTT Structured | :white_check_mark: | :white_check_mark: |
|
||||
|
||||
## Community
|
||||
|
||||
- There are bi-weekly calls immediately following the [Serverless/CloudEvents
|
||||
|
@ -183,12 +233,15 @@ There you will find Express.js, TypeScript and Websocket examples.
|
|||
to determine which week will have the call.
|
||||
- Slack: #cloudeventssdk channel under
|
||||
[CNCF's Slack workspace](https://slack.cncf.io/).
|
||||
- Maintainers typically available on Slack
|
||||
- Lance Ball
|
||||
- Lucas Holmquist
|
||||
- Grant Timmerman
|
||||
- Email: https://lists.cncf.io/g/cncf-cloudevents-sdk
|
||||
|
||||
## Maintainers
|
||||
|
||||
Currently active maintainers who may be found in the CNCF Slack.
|
||||
|
||||
- Lance Ball (@lance)
|
||||
- Lucas Holmquist (@lholmquist)
|
||||
|
||||
## Contributing
|
||||
|
||||
We love contributions from the community! Please check the
|
||||
|
@ -205,3 +258,14 @@ how SDK projects are
|
|||
for how PR reviews and approval, and our
|
||||
[Code of Conduct](https://github.com/cloudevents/spec/blob/master/community/GOVERNANCE.md#additional-information)
|
||||
information.
|
||||
|
||||
If there is a security concern with one of the CloudEvents specifications, or
|
||||
with one of the project's SDKs, please send an email to
|
||||
[cncf-cloudevents-security@lists.cncf.io](mailto:cncf-cloudevents-security@lists.cncf.io).
|
||||
|
||||
## Additional SDK Resources
|
||||
|
||||
- [List of current active maintainers](MAINTAINERS.md)
|
||||
- [How to contribute to the project](CONTRIBUTING.md)
|
||||
- [SDK's License](LICENSE)
|
||||
- [SDK's Release process](RELEASING.md)
|
||||
|
|
|
@ -1,62 +0,0 @@
|
|||
# Module Release Guidelines
|
||||
|
||||
## Create a Proposal Issue
|
||||
|
||||
To prepare for a new release, create a [new issue](https://github.com/cloudevents/sdk-javascript/issues/new?assignees=&labels=&template=feature-request.md&title=) where the title of the issue cleary reflects the version to be released.
|
||||
|
||||
For example: "Proposal for 3.2.0 release", or something similar. If you are not sure which version is the next version to be released, you can run `npm run release -- --dry-run` to find out what the next version will be.
|
||||
|
||||
The body of the issue should be the commits that will be part of the release. This can be easily accomplished by running a git log command with a defined **range**. This range should start at the most recent version tag and end at the latest commit in the main branch.
|
||||
|
||||
For example:
|
||||
|
||||
```
|
||||
git log v3.0.1..upstream/main --oneline
|
||||
```
|
||||
|
||||
This will output all the commits from the 3.0.1 tag to the latest commits in the remote upstream/main branch.
|
||||
|
||||
This output should be pasted into the issue as normal text. This will allow Github to magically turn all commit hashes and PR/Issues numbers to links.
|
||||
|
||||
### Get Consensus
|
||||
|
||||
Before a release can be finalized, other maintainers should give a +1 or a thumbs up or some other identifying mark that they are good with the changes.
|
||||
|
||||
## Create and Publish the release
|
||||
|
||||
Once consensus has been reached on the proposal it is time to create the release and publish it to npm.
|
||||
|
||||
### Create the Release
|
||||
|
||||
Creating the release is as simple as running the release script:
|
||||
|
||||
```
|
||||
npm run release
|
||||
```
|
||||
|
||||
This will update the CHANGELOG.md and create a new tag based on the version. This can then be pushed upstream by doing:
|
||||
|
||||
```
|
||||
git push upstream main --follow-tags
|
||||
```
|
||||
|
||||
### Create the release on GitHub
|
||||
|
||||
Once the release tag has been created and pushed up to Github, we should draft a new release using the Github UI, which is [located here](https://github.com/cloudevents/sdk-javascript/releases/new)
|
||||
|
||||
* Tag Version should be the tag that was just created
|
||||
* The release title should be something like "VERSION Release"
|
||||
* And the Changelog entries for the current release should be copied/pasted into the comments
|
||||
|
||||
|
||||
### Publish to npm
|
||||
|
||||
Once the new version has been created, we need to push it to npm. Assuming you have all the rights to do so, just run:
|
||||
|
||||
```
|
||||
npm publish
|
||||
```
|
||||
|
||||
## Close the Issue
|
||||
|
||||
Once the release has been completed, the issue can be closed.
|
|
@ -0,0 +1,22 @@
|
|||
# Module Release Guidelines
|
||||
|
||||
## `release-please`
|
||||
|
||||
This project uses [`release-please-action`](https://github.com/google-github-actions/release-please-action)
|
||||
to manage CHANGELOG.md and automate our releases. It does so by parsing the git history, looking for
|
||||
[Conventional Commit](https://www.conventionalcommits.org/en/v1.0.0/) messages, and creating release PRs.
|
||||
|
||||
For example: https://github.com/cloudevents/sdk-javascript/pull/475
|
||||
|
||||
Each time a commit lands on `main`, the workflow updates the pull request to include the commit message
|
||||
in CHANGELOG.md, and bump the version in package.json. When you are ready to create a new release, simply
|
||||
land the pull request. This will result in a release commit, updating CHANGELOG.md and package.json, a version
|
||||
tag is created on that commit SHA, and a release is drafted in github.com.
|
||||
|
||||
### Publish to npm
|
||||
|
||||
Once the new version has been created, we need to push it to npm. Assuming you have all the rights to do so, just run:
|
||||
|
||||
```
|
||||
npm publish
|
||||
```
|
|
@ -7,8 +7,6 @@
|
|||
let common = [
|
||||
"--require-module ts-node/register", // Load TypeScript module
|
||||
"--require test/conformance/steps.ts", // Load step definitions
|
||||
"--format progress-bar", // Load custom formatter
|
||||
"--format node_modules/cucumber-pretty", // Load custom formatter
|
||||
].join(" ");
|
||||
|
||||
module.exports = {
|
||||
|
|
|
@ -28,11 +28,11 @@ app.post("/", (req, res) => {
|
|||
const responseEventMessage = new CloudEvent({
|
||||
source: '/',
|
||||
type: 'event:response',
|
||||
...event
|
||||
...event,
|
||||
data: {
|
||||
hello: 'world'
|
||||
}
|
||||
});
|
||||
responseEventMessage.data = {
|
||||
hello: 'world'
|
||||
};
|
||||
|
||||
// const message = HTTP.binary(responseEventMessage)
|
||||
const message = HTTP.structured(responseEventMessage)
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
# MQTT Example
|
||||
|
||||
The MQTT message protocol are available since v5.3.0
|
||||
|
||||
## How To Start
|
||||
|
||||
Install and compile:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run compile
|
||||
```
|
||||
|
||||
Start a MQTT broker using Docker:
|
||||
|
||||
```bash
|
||||
docker run -it -d -p 1883:1883 eclipse-mosquitto:2.0 mosquitto -c /mosquitto-no-auth.conf
|
||||
```
|
||||
|
||||
Then, start
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"name": "mqtt-ex",
|
||||
"version": "1.0.0",
|
||||
"description": "Simple mqtt example using CloudEvents types",
|
||||
"repository": "https://github.com/cloudevents/sdk-javascript.git",
|
||||
"main": "build/src/index.js",
|
||||
"types": "build/src/index.d.ts",
|
||||
"files": [
|
||||
"build/src"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"keywords": [],
|
||||
"scripts": {
|
||||
"start": "node build/index.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"check": "gts check",
|
||||
"clean": "gts clean",
|
||||
"compile": "tsc -p .",
|
||||
"watch": "tsc -p . --watch",
|
||||
"fix": "gts fix",
|
||||
"prepare": "npm run compile",
|
||||
"pretest": "npm run compile",
|
||||
"posttest": "npm run check"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^14.14.10",
|
||||
"@types/ws": "^8.5.4",
|
||||
"gts": "^3.0.3",
|
||||
"typescript": "~4.1.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"cloudevents": "^6.0.3",
|
||||
"mqtt": "^4.3.7"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
/* eslint-disable */
|
||||
import { CloudEvent, MQTT } from "cloudevents";
|
||||
import * as mqtt from "mqtt";
|
||||
|
||||
const client = mqtt.connect("mqtt://localhost:1883");
|
||||
|
||||
client.on("connect", function () {
|
||||
client.subscribe("presence", function (err) {
|
||||
if (err) return;
|
||||
const event = new CloudEvent({
|
||||
source: "presence",
|
||||
type: "presence.event",
|
||||
datacontenttype: "application/json",
|
||||
data: {
|
||||
hello: "world",
|
||||
},
|
||||
});
|
||||
const { body, headers } = MQTT.binary(event);
|
||||
|
||||
client.publish("presence", JSON.stringify(body), {
|
||||
properties: {
|
||||
userProperties: headers as mqtt.UserProperties,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
client.on("message", function (topic, message, packet) {
|
||||
const event = MQTT.toEvent({
|
||||
body: JSON.parse(message.toString()),
|
||||
headers: packet.properties?.userProperties || {},
|
||||
});
|
||||
console.log(event);
|
||||
client.end();
|
||||
});
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"extends": "./node_modules/gts/tsconfig-google.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./build/",
|
||||
"lib": [
|
||||
"es6",
|
||||
"dom"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"test/**/*.ts"
|
||||
],
|
||||
"allowJs": true
|
||||
}
|
File diff suppressed because it is too large
Load Diff
52
package.json
52
package.json
|
@ -1,18 +1,22 @@
|
|||
{
|
||||
"name": "cloudevents",
|
||||
"version": "5.2.0",
|
||||
"version": "9.0.0",
|
||||
"description": "CloudEvents SDK for JavaScript",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"watch": "tsc --project tsconfig.json --watch",
|
||||
"build": "tsc --project tsconfig.json && tsc --project tsconfig.browser.json && webpack",
|
||||
"build:src": "tsc --project tsconfig.json",
|
||||
"build:browser": "tsc --project tsconfig.browser.json && webpack",
|
||||
"build:schema": "ajv compile -c ./src/schema/formats.js -s src/schema/cloudevent.json --strict-types false -o src/schema/v1.js",
|
||||
"build": "npm run build:schema && npm run build:src && npm run build:browser",
|
||||
"lint": "npm run lint:md && npm run lint:js",
|
||||
"lint:js": "eslint 'src/**/*.{js,ts}' 'test/**/*.{js,ts}' cucumber.js",
|
||||
"lint:md": "remark .",
|
||||
"lint:fix": "eslint 'src/**/*.{js,ts}' 'test/**/*.{js,ts}' --fix",
|
||||
"pretest": "npm run lint && npm run conformance",
|
||||
"pretest": "npm run lint && npm run build && npm run conformance",
|
||||
"test": "mocha --require ts-node/register ./test/integration/**/*.ts",
|
||||
"conformance": "cucumber-js ./conformance/features/http-protocol-binding.feature -p default",
|
||||
"test:one": "mocha --require ts-node/register",
|
||||
"conformance": "cucumber-js ./conformance/features/*-protocol-binding.feature -p default",
|
||||
"coverage": "nyc --reporter=lcov --reporter=text npm run test",
|
||||
"coverage-publish": "wget -qO - https://coverage.codacy.com/get.sh | bash -s report -l JavaScript -r coverage/lcov.info",
|
||||
"generate-docs": "typedoc --excludeNotDocumented --out docs src",
|
||||
|
@ -106,36 +110,37 @@
|
|||
},
|
||||
"homepage": "https://github.com/cloudevents/sdk-javascript#readme",
|
||||
"dependencies": {
|
||||
"ajv": "~6.12.3",
|
||||
"uuid": "~8.3.0"
|
||||
"ajv": "^8.11.0",
|
||||
"ajv-formats": "^2.1.1",
|
||||
"process": "^0.11.10",
|
||||
"json-bigint": "^1.0.0",
|
||||
"util": "^0.12.4",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/ajv": "^1.0.0",
|
||||
"@cucumber/cucumber": "^8.0.0",
|
||||
"@types/chai": "^4.2.11",
|
||||
"@types/cucumber": "^6.0.1",
|
||||
"@types/got": "^9.6.11",
|
||||
"@types/json-bigint": "^1.0.1",
|
||||
"@types/mocha": "^7.0.2",
|
||||
"@types/node": "^14.14.10",
|
||||
"@types/superagent": "^4.1.10",
|
||||
"@types/uuid": "^8.0.0",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@typescript-eslint/eslint-plugin": "^4.29.0",
|
||||
"@typescript-eslint/parser": "^4.29.0",
|
||||
"axios": "^0.21.3",
|
||||
"ajv-cli": "^5.0.0",
|
||||
"axios": "^0.26.1",
|
||||
"chai": "~4.2.0",
|
||||
"cucumber": "^6.0.5",
|
||||
"cucumber-pretty": "^6.0.0",
|
||||
"cucumber-tsflow": "^3.2.0",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-config-standard": "^16.0.3",
|
||||
"eslint-plugin-header": "^3.1.1",
|
||||
"eslint-plugin-import": "^2.23.4",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^3.4.0",
|
||||
"eslint-plugin-promise": "^5.1.0",
|
||||
"got": "^11.7.0",
|
||||
"got": "^11.8.5",
|
||||
"http-parser-js": "^0.5.2",
|
||||
"mocha": "~9.1.2",
|
||||
"mocha": "^10.1.0",
|
||||
"nock": "~12.0.3",
|
||||
"nyc": "~15.0.0",
|
||||
"prettier": "^2.0.5",
|
||||
|
@ -143,15 +148,18 @@
|
|||
"remark-lint": "^8.0.0",
|
||||
"remark-lint-list-item-indent": "^2.0.1",
|
||||
"remark-preset-lint-recommended": "^5.0.0",
|
||||
"superagent": "^6.1.0",
|
||||
"ts-node": "^8.10.2",
|
||||
"typedoc": "^0.21.5",
|
||||
"superagent": "^7.1.1",
|
||||
"ts-node": "^10.8.1",
|
||||
"typedoc": "^0.22.11",
|
||||
"typescript": "^4.3.5",
|
||||
"webpack": "^5.1.1",
|
||||
"webpack-cli": "^4.0.0"
|
||||
"webpack": "^5.76.0",
|
||||
"webpack-cli": "^4.10.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"types": "./dist/index.d.ts"
|
||||
"types": "./dist/index.d.ts",
|
||||
"engines": {
|
||||
"node": ">=20 <=24"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,6 +54,7 @@ const CONSTANTS = Object.freeze({
|
|||
DATA_SCHEMA: "dataschema",
|
||||
DATA_BASE64: "data_base64",
|
||||
},
|
||||
USE_BIG_INT_ENV: "CE_USE_BIG_INT"
|
||||
} as const);
|
||||
|
||||
export default CONSTANTS;
|
||||
|
|
|
@ -3,20 +3,19 @@
|
|||
SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { ErrorObject } from "ajv";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { Emitter } from "..";
|
||||
|
||||
import { CloudEventV1 } from "./interfaces";
|
||||
import { validateCloudEvent } from "./spec";
|
||||
import { ValidationError, isBinary, asBase64, isValidType } from "./validation";
|
||||
import { ValidationError, isBinary, asBase64, isValidType, base64AsBinary } from "./validation";
|
||||
|
||||
/**
|
||||
* An enum representing the CloudEvent specification version
|
||||
* Constants representing the CloudEvent specification version
|
||||
*/
|
||||
export const enum Version {
|
||||
V1 = "1.0",
|
||||
V03 = "0.3",
|
||||
}
|
||||
export const V1 = "1.0";
|
||||
export const V03 = "0.3";
|
||||
|
||||
/**
|
||||
* A CloudEvent describes event data in common formats to provide
|
||||
|
@ -27,12 +26,12 @@ export class CloudEvent<T = undefined> implements CloudEventV1<T> {
|
|||
id: string;
|
||||
type: string;
|
||||
source: string;
|
||||
specversion: Version;
|
||||
specversion: string;
|
||||
datacontenttype?: string;
|
||||
dataschema?: string;
|
||||
subject?: string;
|
||||
time?: string;
|
||||
#_data?: T;
|
||||
data?: T;
|
||||
data_base64?: string;
|
||||
|
||||
// Extensions should not exist as it's own object, but instead
|
||||
|
@ -68,7 +67,7 @@ export class CloudEvent<T = undefined> implements CloudEventV1<T> {
|
|||
this.source = properties.source as string;
|
||||
delete (properties as any).source;
|
||||
|
||||
this.specversion = (properties.specversion as Version) || Version.V1;
|
||||
this.specversion = (properties.specversion) || V1;
|
||||
delete properties.specversion;
|
||||
|
||||
this.datacontenttype = properties.datacontenttype;
|
||||
|
@ -84,26 +83,35 @@ export class CloudEvent<T = undefined> implements CloudEventV1<T> {
|
|||
delete properties.dataschema;
|
||||
|
||||
this.data_base64 = properties.data_base64 as string;
|
||||
|
||||
if (this.data_base64) {
|
||||
this.data = base64AsBinary(this.data_base64) as unknown as T;
|
||||
}
|
||||
|
||||
delete properties.data_base64;
|
||||
|
||||
this.schemaurl = properties.schemaurl as string;
|
||||
delete properties.schemaurl;
|
||||
|
||||
this.data = properties.data;
|
||||
if (isBinary(properties.data)) {
|
||||
this.data_base64 = asBase64(properties.data as unknown as Buffer);
|
||||
}
|
||||
|
||||
this.data = typeof properties.data !== "undefined" ? properties.data : this.data;
|
||||
delete properties.data;
|
||||
|
||||
// sanity checking
|
||||
if (this.specversion === Version.V1 && this.schemaurl) {
|
||||
if (this.specversion === V1 && this.schemaurl) {
|
||||
throw new TypeError("cannot set schemaurl on version 1.0 event");
|
||||
} else if (this.specversion === Version.V03 && this.dataschema) {
|
||||
} else if (this.specversion === V03 && this.dataschema) {
|
||||
throw new TypeError("cannot set dataschema on version 0.3 event");
|
||||
}
|
||||
|
||||
// finally process any remaining properties - these are extensions
|
||||
for (const [key, value] of Object.entries(properties)) {
|
||||
// Extension names should only allow lowercase a-z and 0-9 in the name
|
||||
// Extension names must only allow lowercase a-z and 0-9 in the name
|
||||
// names should not exceed 20 characters in length
|
||||
if (!key.match(/^[a-z0-9]{1,20}$/) && strict) {
|
||||
if (!key.match(/^[a-z0-9]+$/) && strict) {
|
||||
throw new ValidationError(`invalid extension name: ${key}
|
||||
CloudEvents attribute names MUST consist of lower-case letters ('a' to 'z')
|
||||
or digits ('0' to '9') from the ASCII character set. Attribute names SHOULD
|
||||
|
@ -126,17 +134,6 @@ See: https://github.com/cloudevents/spec/blob/v1.0/spec.md#type-system`);
|
|||
Object.freeze(this);
|
||||
}
|
||||
|
||||
get data(): T | undefined {
|
||||
return this.#_data;
|
||||
}
|
||||
|
||||
set data(value: T | undefined) {
|
||||
if (isBinary(value)) {
|
||||
this.data_base64 = asBase64(value);
|
||||
}
|
||||
this.#_data = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by JSON.stringify(). The name is confusing, but this method is called by
|
||||
* JSON.stringify() when converting this object to JSON.
|
||||
|
@ -146,7 +143,11 @@ See: https://github.com/cloudevents/spec/blob/v1.0/spec.md#type-system`);
|
|||
toJSON(): Record<string, unknown> {
|
||||
const event = { ...this };
|
||||
event.time = new Date(this.time as string).toISOString();
|
||||
event.data = !isBinary(this.data) ? this.data : undefined;
|
||||
|
||||
if (event.data_base64 && event.data) {
|
||||
delete event.data;
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
|
@ -166,7 +167,7 @@ See: https://github.com/cloudevents/spec/blob/v1.0/spec.md#type-system`);
|
|||
if (e instanceof ValidationError) {
|
||||
throw e;
|
||||
} else {
|
||||
throw new ValidationError("invalid payload", e);
|
||||
throw new ValidationError("invalid payload", [e] as ErrorObject[]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -183,30 +184,30 @@ See: https://github.com/cloudevents/spec/blob/v1.0/spec.md#type-system`);
|
|||
}
|
||||
|
||||
/**
|
||||
* Clone a CloudEvent with new/update attributes
|
||||
* @param {object} options attributes to augment the CloudEvent with an `data` property
|
||||
* Clone a CloudEvent with new/updated attributes
|
||||
* @param {object} options attributes to augment the CloudEvent without a `data` property
|
||||
* @param {boolean} strict whether or not to use strict validation when cloning (default: true)
|
||||
* @throws if the CloudEvent does not conform to the schema
|
||||
* @return {CloudEvent} returns a new CloudEvent<T>
|
||||
*/
|
||||
public cloneWith(options: Partial<Exclude<CloudEventV1<never>, "data">>, strict?: boolean): CloudEvent<T>;
|
||||
/**
|
||||
* Clone a CloudEvent with new/update attributes
|
||||
* @param {object} options attributes to augment the CloudEvent with a `data` property
|
||||
* Clone a CloudEvent with new/updated attributes and new data
|
||||
* @param {object} options attributes to augment the CloudEvent with a `data` property and type
|
||||
* @param {boolean} strict whether or not to use strict validation when cloning (default: true)
|
||||
* @throws if the CloudEvent does not conform to the schema
|
||||
* @return {CloudEvent} returns a new CloudEvent<D>
|
||||
*/
|
||||
public cloneWith<D>(options: Partial<CloudEvent<D>>, strict?: boolean): CloudEvent<D>;
|
||||
public cloneWith<D>(options: Partial<CloudEventV1<D>>, strict?: boolean): CloudEvent<D>;
|
||||
/**
|
||||
* Clone a CloudEvent with new/update attributes
|
||||
* Clone a CloudEvent with new/updated attributes and possibly different data types
|
||||
* @param {object} options attributes to augment the CloudEvent
|
||||
* @param {boolean} strict whether or not to use strict validation when cloning (default: true)
|
||||
* @throws if the CloudEvent does not conform to the schema
|
||||
* @return {CloudEvent} returns a new CloudEvent
|
||||
*/
|
||||
public cloneWith<D>(options: Partial<CloudEventV1<D>>, strict = true): CloudEvent<D | T> {
|
||||
return new CloudEvent(Object.assign({}, this.toJSON(), options), strict);
|
||||
return CloudEvent.cloneWith(this, options, strict);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -216,4 +217,19 @@ See: https://github.com/cloudevents/spec/blob/v1.0/spec.md#type-system`);
|
|||
[Symbol.for("nodejs.util.inspect.custom")](): string {
|
||||
return this.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone a CloudEvent with new or updated attributes.
|
||||
* @param {CloudEventV1<any>} event an object that implements the {@linkcode CloudEventV1} interface
|
||||
* @param {Partial<CloudEventV1<any>>} options an object with new or updated attributes
|
||||
* @param {boolean} strict `true` if the resulting event should be valid per the CloudEvent specification
|
||||
* @throws {ValidationError} if `strict` is `true` and the resulting event is invalid
|
||||
* @returns {CloudEvent<any>} a CloudEvent cloned from `event` with `options` applied.
|
||||
*/
|
||||
public static cloneWith(
|
||||
event: CloudEventV1<any>,
|
||||
options: Partial<CloudEventV1<any>>,
|
||||
strict = true): CloudEvent<any> {
|
||||
return new CloudEvent(Object.assign({}, event, options), strict);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,86 +0,0 @@
|
|||
/*
|
||||
Copyright 2021 The CloudEvents Authors
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
export const schemaV1 = {
|
||||
$ref: "#/definitions/event",
|
||||
definitions: {
|
||||
specversion: {
|
||||
type: "string",
|
||||
minLength: 1,
|
||||
const: "1.0",
|
||||
},
|
||||
datacontenttype: {
|
||||
type: "string",
|
||||
},
|
||||
data: {
|
||||
type: ["object", "string", "array", "number", "boolean", "null"],
|
||||
},
|
||||
data_base64: {
|
||||
type: "string",
|
||||
},
|
||||
event: {
|
||||
properties: {
|
||||
specversion: {
|
||||
$ref: "#/definitions/specversion",
|
||||
},
|
||||
datacontenttype: {
|
||||
$ref: "#/definitions/datacontenttype",
|
||||
},
|
||||
data: {
|
||||
$ref: "#/definitions/data",
|
||||
},
|
||||
data_base64: {
|
||||
$ref: "#/definitions/data_base64",
|
||||
},
|
||||
id: {
|
||||
$ref: "#/definitions/id",
|
||||
},
|
||||
time: {
|
||||
$ref: "#/definitions/time",
|
||||
},
|
||||
dataschema: {
|
||||
$ref: "#/definitions/dataschema",
|
||||
},
|
||||
subject: {
|
||||
$ref: "#/definitions/subject",
|
||||
},
|
||||
type: {
|
||||
$ref: "#/definitions/type",
|
||||
},
|
||||
source: {
|
||||
$ref: "#/definitions/source",
|
||||
},
|
||||
},
|
||||
required: ["specversion", "id", "type", "source"],
|
||||
type: "object",
|
||||
},
|
||||
id: {
|
||||
type: "string",
|
||||
minLength: 1,
|
||||
},
|
||||
time: {
|
||||
format: "js-date-time",
|
||||
type: "string",
|
||||
},
|
||||
dataschema: {
|
||||
type: "string",
|
||||
format: "uri",
|
||||
},
|
||||
subject: {
|
||||
type: "string",
|
||||
minLength: 1,
|
||||
},
|
||||
type: {
|
||||
type: "string",
|
||||
minLength: 1,
|
||||
},
|
||||
source: {
|
||||
format: "uri-reference",
|
||||
type: "string",
|
||||
minLength: 1,
|
||||
},
|
||||
},
|
||||
type: "object",
|
||||
};
|
|
@ -3,36 +3,26 @@
|
|||
SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import Ajv, { Options } from "ajv";
|
||||
import { ValidationError } from "./validation";
|
||||
|
||||
import { CloudEventV1 } from "./interfaces";
|
||||
import { schemaV1 } from "./schemas";
|
||||
import { Version } from "./cloudevent";
|
||||
import { V1 } from "./cloudevent";
|
||||
import validate from "../schema/v1";
|
||||
|
||||
const ajv = new Ajv({ extendRefs: true } as Options);
|
||||
|
||||
// handle date-time format specially because a user could pass
|
||||
// Date().toString(), which is not spec compliant date-time format
|
||||
ajv.addFormat("js-date-time", function (dateTimeString) {
|
||||
const date = new Date(Date.parse(dateTimeString));
|
||||
return date.toString() !== "Invalid Date";
|
||||
});
|
||||
|
||||
const isValidAgainstSchemaV1 = ajv.compile(schemaV1);
|
||||
|
||||
export function validateCloudEvent<T>(event: CloudEventV1<T>): boolean {
|
||||
if (event.specversion === Version.V1) {
|
||||
if (!isValidAgainstSchemaV1(event)) {
|
||||
throw new ValidationError("invalid payload", isValidAgainstSchemaV1.errors);
|
||||
if (event.specversion === V1) {
|
||||
if (!validate(event)) {
|
||||
throw new ValidationError("invalid payload", (validate as any).errors);
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
// attribute names must all be lowercase
|
||||
// attribute names must all be [a-z|0-9]
|
||||
const validation = /^[a-z0-9]+$/;
|
||||
for (const key in event) {
|
||||
if (key !== key.toLowerCase()) {
|
||||
throw new ValidationError(`invalid attribute name: ${key}`);
|
||||
if (validation.test(key) === false && key !== "data_base64") {
|
||||
throw new ValidationError(`invalid attribute name: "${key}"`);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
|
|
@ -5,6 +5,23 @@
|
|||
|
||||
import { ErrorObject } from "ajv";
|
||||
|
||||
export type TypeArray = Int8Array | Uint8Array | Int16Array | Uint16Array |
|
||||
Int32Array | Uint32Array | Uint8ClampedArray | Float32Array | Float64Array;
|
||||
|
||||
const globalThisPolyfill = (function() {
|
||||
try {
|
||||
return globalThis;
|
||||
}
|
||||
catch (e) {
|
||||
try {
|
||||
return self;
|
||||
}
|
||||
catch (e) {
|
||||
return global;
|
||||
}
|
||||
}
|
||||
}());
|
||||
|
||||
/**
|
||||
* An Error class that will be thrown when a CloudEvent
|
||||
* cannot be properly validated against a specification.
|
||||
|
@ -36,7 +53,7 @@ export const isDefined = (v: unknown): boolean => v !== null && typeof v !== "un
|
|||
export const isBoolean = (v: unknown): boolean => typeof v === "boolean";
|
||||
export const isInteger = (v: unknown): boolean => Number.isInteger(v as number);
|
||||
export const isDate = (v: unknown): v is Date => v instanceof Date;
|
||||
export const isBinary = (v: unknown): v is Uint32Array => v instanceof Uint32Array;
|
||||
export const isBinary = (v: unknown): boolean => ArrayBuffer.isView(v);
|
||||
|
||||
export const isStringOrThrow = (v: unknown, t: Error): boolean =>
|
||||
isString(v)
|
||||
|
@ -73,7 +90,7 @@ export const isBase64 = (value: unknown): boolean =>
|
|||
|
||||
export const isBuffer = (value: unknown): boolean => value instanceof Buffer;
|
||||
|
||||
export const asBuffer = (value: string | Buffer | Uint32Array): Buffer =>
|
||||
export const asBuffer = (value: string | Buffer | TypeArray): Buffer =>
|
||||
isBinary(value)
|
||||
? Buffer.from((value as unknown) as string)
|
||||
: isBuffer(value)
|
||||
|
@ -82,7 +99,16 @@ export const asBuffer = (value: string | Buffer | Uint32Array): Buffer =>
|
|||
throw new TypeError("is not buffer or a valid binary");
|
||||
})();
|
||||
|
||||
export const asBase64 = (value: string | Buffer | Uint32Array): string => asBuffer(value).toString("base64");
|
||||
export const base64AsBinary = (base64String: string): Uint8Array => {
|
||||
const toBinaryString = (base64Str: string): string => globalThisPolyfill.atob
|
||||
? globalThisPolyfill.atob(base64Str)
|
||||
: Buffer.from(base64Str, "base64").toString("binary");
|
||||
|
||||
return Uint8Array.from(toBinaryString(base64String), (c) => c.charCodeAt(0));
|
||||
};
|
||||
|
||||
export const asBase64 =
|
||||
(value: string | Buffer | TypeArray): string => asBuffer(value).toString("base64");
|
||||
|
||||
export const clone = (o: Record<string, unknown>): Record<string, unknown> => JSON.parse(JSON.stringify(o));
|
||||
|
||||
|
@ -97,5 +123,5 @@ export const asData = (data: unknown, contentType: string): string => {
|
|||
return isBinary(maybeJson) ? asBase64(maybeJson) : maybeJson;
|
||||
};
|
||||
|
||||
export const isValidType = (v: boolean | number | string | Date | Uint32Array | unknown): boolean =>
|
||||
export const isValidType = (v: boolean | number | string | Date | TypeArray | unknown): boolean =>
|
||||
isBoolean(v) || isInteger(v) || isString(v) || isDate(v) || isBinary(v) || isObject(v);
|
||||
|
|
36
src/index.ts
36
src/index.ts
|
@ -3,36 +3,50 @@
|
|||
SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { CloudEvent, Version } from "./event/cloudevent";
|
||||
import { CloudEvent, V1, V03 } from "./event/cloudevent";
|
||||
import { ValidationError } from "./event/validation";
|
||||
import { CloudEventV1, CloudEventV1Attributes } from "./event/interfaces";
|
||||
|
||||
import { Options, TransportFunction, EmitterFunction, emitterFor, Emitter } from "./transport/emitter";
|
||||
import { Headers, Mode, Binding, HTTP, Message, Serializer, Deserializer } from "./message";
|
||||
import { httpTransport } from "./transport/http";
|
||||
import {
|
||||
Headers, Mode, Binding, HTTP, Kafka, KafkaEvent, KafkaMessage, Message, MQTT, MQTTMessage, MQTTMessageFactory,
|
||||
Serializer, Deserializer } from "./message";
|
||||
|
||||
import CONSTANTS from "./constants";
|
||||
|
||||
export {
|
||||
// From event
|
||||
CloudEvent,
|
||||
V1,
|
||||
V03,
|
||||
ValidationError,
|
||||
Mode,
|
||||
HTTP,
|
||||
Kafka,
|
||||
MQTT,
|
||||
MQTTMessageFactory,
|
||||
emitterFor,
|
||||
httpTransport,
|
||||
Emitter,
|
||||
// From Constants
|
||||
CONSTANTS
|
||||
};
|
||||
|
||||
export type {
|
||||
CloudEventV1,
|
||||
CloudEventV1Attributes,
|
||||
Version,
|
||||
ValidationError,
|
||||
// From message
|
||||
Headers,
|
||||
Mode,
|
||||
Binding,
|
||||
Message,
|
||||
Deserializer,
|
||||
Serializer,
|
||||
HTTP,
|
||||
KafkaEvent,
|
||||
KafkaMessage,
|
||||
MQTTMessage,
|
||||
// From transport
|
||||
TransportFunction,
|
||||
EmitterFunction,
|
||||
emitterFor,
|
||||
Emitter,
|
||||
Options,
|
||||
// From Constants
|
||||
CONSTANTS,
|
||||
Options
|
||||
};
|
||||
|
|
|
@ -4,9 +4,9 @@
|
|||
*/
|
||||
|
||||
import { PassThroughParser, DateParser, MappedParser } from "../../parsers";
|
||||
import { CloudEvent } from "../..";
|
||||
import { CloudEventV1 } from "../..";
|
||||
import { Headers } from "../";
|
||||
import { Version } from "../../event/cloudevent";
|
||||
import { V1 } from "../../event/cloudevent";
|
||||
import CONSTANTS from "../../constants";
|
||||
|
||||
export const allowedContentTypes = [CONSTANTS.DEFAULT_CONTENT_TYPE, CONSTANTS.MIME_JSON, CONSTANTS.MIME_OCTET_STREAM];
|
||||
|
@ -24,10 +24,10 @@ export const requiredHeaders = [
|
|||
* @param {CloudEvent} event a CloudEvent
|
||||
* @returns {Object} the headers that will be sent for the event
|
||||
*/
|
||||
export function headersFor<T>(event: CloudEvent<T>): Headers {
|
||||
export function headersFor<T>(event: CloudEventV1<T>): Headers {
|
||||
const headers: Headers = {};
|
||||
let headerMap: Readonly<{ [key: string]: MappedParser }>;
|
||||
if (event.specversion === Version.V1) {
|
||||
if (event.specversion === V1) {
|
||||
headerMap = v1headerMap;
|
||||
} else {
|
||||
headerMap = v03headerMap;
|
||||
|
@ -36,7 +36,7 @@ export function headersFor<T>(event: CloudEvent<T>): Headers {
|
|||
// iterate over the event properties - generate a header for each
|
||||
Object.getOwnPropertyNames(event).forEach((property) => {
|
||||
const value = event[property];
|
||||
if (value) {
|
||||
if (value !== undefined) {
|
||||
const map: MappedParser | undefined = headerMap[property] as MappedParser;
|
||||
if (map) {
|
||||
headers[map.name] = map.parser.parse(value as string) as string;
|
||||
|
|
|
@ -3,8 +3,10 @@
|
|||
SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { CloudEvent, CloudEventV1, CONSTANTS, Mode, Version } from "../..";
|
||||
import { Message, Headers } from "..";
|
||||
import { types } from "util";
|
||||
|
||||
import { CloudEvent, CloudEventV1, CONSTANTS, Mode, V1, V03 } from "../..";
|
||||
import { Message, Headers, Binding } from "..";
|
||||
|
||||
import {
|
||||
headersFor,
|
||||
|
@ -25,11 +27,11 @@ import { JSONParser, MappedParser, Parser, parserByContentType } from "../../par
|
|||
* @param {CloudEvent} event The event to serialize
|
||||
* @returns {Message} a Message object with headers and body
|
||||
*/
|
||||
export function binary<T>(event: CloudEvent<T>): Message {
|
||||
function binary<T>(event: CloudEventV1<T>): Message {
|
||||
const contentType: Headers = { [CONSTANTS.HEADER_CONTENT_TYPE]: CONSTANTS.DEFAULT_CONTENT_TYPE };
|
||||
const headers: Headers = { ...contentType, ...headersFor(event) };
|
||||
let body = event.data;
|
||||
if (typeof event.data === "object" && !(event.data instanceof Uint32Array)) {
|
||||
if (typeof event.data === "object" && !types.isTypedArray(event.data)) {
|
||||
// we'll stringify objects, but not binary data
|
||||
body = (JSON.stringify(event.data) as unknown) as T;
|
||||
}
|
||||
|
@ -47,10 +49,10 @@ export function binary<T>(event: CloudEvent<T>): Message {
|
|||
* @param {CloudEvent} event the CloudEvent to be serialized
|
||||
* @returns {Message} a Message object with headers and body
|
||||
*/
|
||||
export function structured<T>(event: CloudEvent<T>): Message {
|
||||
function structured<T>(event: CloudEventV1<T>): Message {
|
||||
if (event.data_base64) {
|
||||
// The event's data is binary - delete it
|
||||
event = event.cloneWith({ data: undefined });
|
||||
event = (event as CloudEvent).cloneWith({ data: undefined });
|
||||
}
|
||||
return {
|
||||
headers: {
|
||||
|
@ -67,7 +69,7 @@ export function structured<T>(event: CloudEvent<T>): Message {
|
|||
* @param {Message} message an incoming Message object
|
||||
* @returns {boolean} true if this Message is a CloudEvent
|
||||
*/
|
||||
export function isEvent(message: Message): boolean {
|
||||
function isEvent(message: Message): boolean {
|
||||
// TODO: this could probably be optimized
|
||||
try {
|
||||
deserialize(message);
|
||||
|
@ -84,7 +86,7 @@ export function isEvent(message: Message): boolean {
|
|||
* @param {Message} message the incoming message
|
||||
* @return {CloudEvent} A new {CloudEvent} instance
|
||||
*/
|
||||
export function deserialize<T>(message: Message): CloudEvent<T> | CloudEvent<T>[] {
|
||||
function deserialize<T>(message: Message): CloudEvent<T> | CloudEvent<T>[] {
|
||||
const cleanHeaders: Headers = sanitize(message.headers);
|
||||
const mode: Mode = getMode(cleanHeaders);
|
||||
const version = getVersion(mode, cleanHeaders, message.body);
|
||||
|
@ -145,7 +147,7 @@ function getVersion(mode: Mode, headers: Headers, body: string | Record<string,
|
|||
return (body as Record<string, string>).specversion;
|
||||
}
|
||||
}
|
||||
return Version.V1;
|
||||
return V1;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -153,11 +155,11 @@ function getVersion(mode: Mode, headers: Headers, body: string | Record<string,
|
|||
* instance if it conforms to the Cloud Event specification for this receiver.
|
||||
*
|
||||
* @param {Message} message the incoming HTTP Message
|
||||
* @param {Version} version the spec version of the incoming event
|
||||
* @param {string} version the spec version of the incoming event
|
||||
* @returns {CloudEvent} an instance of CloudEvent representing the incoming request
|
||||
* @throws {ValidationError} of the event does not conform to the spec
|
||||
*/
|
||||
function parseBinary<T>(message: Message, version: Version): CloudEvent<T> {
|
||||
function parseBinary<T>(message: Message, version: string): CloudEvent<T> {
|
||||
const headers = { ...message.headers };
|
||||
let body = message.body;
|
||||
|
||||
|
@ -167,7 +169,7 @@ function parseBinary<T>(message: Message, version: Version): CloudEvent<T> {
|
|||
const sanitizedHeaders = sanitize(headers);
|
||||
|
||||
const eventObj: { [key: string]: unknown | string | Record<string, unknown> } = {};
|
||||
const parserMap: Record<string, MappedParser> = version === Version.V03 ? v03binaryParsers : v1binaryParsers;
|
||||
const parserMap: Record<string, MappedParser> = version === V03 ? v03binaryParsers : v1binaryParsers;
|
||||
|
||||
for (const header in parserMap) {
|
||||
if (sanitizedHeaders[header]) {
|
||||
|
@ -204,11 +206,11 @@ function parseBinary<T>(message: Message, version: Version): CloudEvent<T> {
|
|||
* Creates a new CloudEvent instance based on the provided payload and headers.
|
||||
*
|
||||
* @param {Message} message the incoming Message
|
||||
* @param {Version} version the spec version of this message (v1 or v03)
|
||||
* @param {string} version the spec version of this message (v1 or v03)
|
||||
* @returns {CloudEvent} a new CloudEvent instance for the provided headers and payload
|
||||
* @throws {ValidationError} if the payload and header combination do not conform to the spec
|
||||
*/
|
||||
function parseStructured<T>(message: Message, version: Version): CloudEvent<T> {
|
||||
function parseStructured<T>(message: Message, version: string): CloudEvent<T> {
|
||||
const payload = message.body;
|
||||
const headers = message.headers;
|
||||
|
||||
|
@ -225,7 +227,7 @@ function parseStructured<T>(message: Message, version: Version): CloudEvent<T> {
|
|||
const incoming = { ...(parser.parse(payload as string) as Record<string, unknown>) };
|
||||
|
||||
const eventObj: { [key: string]: unknown } = {};
|
||||
const parserMap: Record<string, MappedParser> = version === Version.V03 ? v03structuredParsers : v1structuredParsers;
|
||||
const parserMap: Record<string, MappedParser> = version === V03 ? v03structuredParsers : v1structuredParsers;
|
||||
|
||||
for (const key in parserMap) {
|
||||
const property = incoming[key];
|
||||
|
@ -261,3 +263,14 @@ function parseBatched<T>(message: Message): CloudEvent<T> | CloudEvent<T>[] {
|
|||
});
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bindings for HTTP transport support
|
||||
* @implements {@linkcode Binding}
|
||||
*/
|
||||
export const HTTP: Binding = {
|
||||
binary,
|
||||
structured,
|
||||
toEvent: deserialize,
|
||||
isEvent: isEvent,
|
||||
};
|
||||
|
|
|
@ -4,8 +4,12 @@
|
|||
*/
|
||||
|
||||
import { IncomingHttpHeaders } from "http";
|
||||
import { CloudEvent } from "..";
|
||||
import { binary, deserialize, structured, isEvent } from "./http";
|
||||
import { CloudEventV1 } from "..";
|
||||
|
||||
// reexport the protocol bindings
|
||||
export * from "./http";
|
||||
export * from "./kafka";
|
||||
export * from "./mqtt";
|
||||
|
||||
/**
|
||||
* Binding is an interface for transport protocols to implement,
|
||||
|
@ -18,9 +22,9 @@ import { binary, deserialize, structured, isEvent } from "./http";
|
|||
* @property {@link Deserializer} `toEvent` - converts a Message into a CloudEvent
|
||||
* @property {@link Detector} `isEvent` - determines if a Message can be converted to a CloudEvent
|
||||
*/
|
||||
export interface Binding {
|
||||
binary: Serializer;
|
||||
structured: Serializer;
|
||||
export interface Binding<B extends Message = Message, S extends Message = Message> {
|
||||
binary: Serializer<B>;
|
||||
structured: Serializer<S>;
|
||||
toEvent: Deserializer;
|
||||
isEvent: Detector;
|
||||
}
|
||||
|
@ -39,11 +43,11 @@ export interface Headers extends IncomingHttpHeaders {
|
|||
* transport-agnostic message
|
||||
* @interface
|
||||
* @property {@linkcode Headers} `headers` - the headers for the event Message
|
||||
* @property string `body` - the body of the event Message
|
||||
* @property {T | string | Buffer | unknown} `body` - the body of the event Message
|
||||
*/
|
||||
export interface Message {
|
||||
export interface Message<T = string> {
|
||||
headers: Headers;
|
||||
body: string | unknown;
|
||||
body: T | string | Buffer | unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -61,8 +65,8 @@ export enum Mode {
|
|||
* CloudEvent into a Message.
|
||||
* @interface
|
||||
*/
|
||||
export interface Serializer {
|
||||
<T>(event: CloudEvent<T>): Message;
|
||||
export interface Serializer<M extends Message> {
|
||||
<T>(event: CloudEventV1<T>): M;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -71,7 +75,7 @@ export interface Serializer {
|
|||
* @interface
|
||||
*/
|
||||
export interface Deserializer {
|
||||
<T>(message: Message): CloudEvent<T> | CloudEvent<T>[];
|
||||
<T>(message: Message): CloudEventV1<T> | CloudEventV1<T>[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -82,14 +86,3 @@ export interface Deserializer {
|
|||
export interface Detector {
|
||||
(message: Message): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bindings for HTTP transport support
|
||||
* @implements {@linkcode Binding}
|
||||
*/
|
||||
export const HTTP: Binding = {
|
||||
binary: binary as Serializer,
|
||||
structured: structured as Serializer,
|
||||
toEvent: deserialize as Deserializer,
|
||||
isEvent: isEvent as Detector,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
Copyright 2021 The CloudEvents Authors
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { CloudEventV1, CONSTANTS, Headers } from "../..";
|
||||
|
||||
type KafkaHeaders = Readonly<{
|
||||
ID: string;
|
||||
TYPE: string;
|
||||
SOURCE: string;
|
||||
SPEC_VERSION: string;
|
||||
TIME: string;
|
||||
SUBJECT: string;
|
||||
DATACONTENTTYPE: string;
|
||||
DATASCHEMA: string;
|
||||
[key: string]: string;
|
||||
}>
|
||||
|
||||
/**
|
||||
* The set of CloudEvent headers that may exist on a Kafka message
|
||||
*/
|
||||
export const KAFKA_CE_HEADERS: KafkaHeaders = Object.freeze({
|
||||
/** corresponds to the CloudEvent#id */
|
||||
ID: "ce_id",
|
||||
/** corresponds to the CloudEvent#type */
|
||||
TYPE: "ce_type",
|
||||
/** corresponds to the CloudEvent#source */
|
||||
SOURCE: "ce_source",
|
||||
/** corresponds to the CloudEvent#specversion */
|
||||
SPEC_VERSION: "ce_specversion",
|
||||
/** corresponds to the CloudEvent#time */
|
||||
TIME: "ce_time",
|
||||
/** corresponds to the CloudEvent#subject */
|
||||
SUBJECT: "ce_subject",
|
||||
/** corresponds to the CloudEvent#datacontenttype */
|
||||
DATACONTENTTYPE: "ce_datacontenttype",
|
||||
/** corresponds to the CloudEvent#dataschema */
|
||||
DATASCHEMA: "ce_dataschema",
|
||||
} as const);
|
||||
|
||||
export const HEADER_MAP: { [key: string]: string } = {
|
||||
[KAFKA_CE_HEADERS.ID]: "id",
|
||||
[KAFKA_CE_HEADERS.TYPE]: "type",
|
||||
[KAFKA_CE_HEADERS.SOURCE]: "source",
|
||||
[KAFKA_CE_HEADERS.SPEC_VERSION]: "specversion",
|
||||
[KAFKA_CE_HEADERS.TIME]: "time",
|
||||
[KAFKA_CE_HEADERS.SUBJECT]: "subject",
|
||||
[KAFKA_CE_HEADERS.DATACONTENTTYPE]: "datacontenttype",
|
||||
[KAFKA_CE_HEADERS.DATASCHEMA]: "dataschema"
|
||||
};
|
||||
|
||||
/**
|
||||
* A conveninece function to convert a CloudEvent into headers
|
||||
* @param {CloudEvent} event a CloudEvent object
|
||||
* @returns {Headers} the CloudEvent attribute as Kafka headers
|
||||
*/
|
||||
export function headersFor<T>(event: CloudEventV1<T>): Headers {
|
||||
const headers: Headers = {};
|
||||
|
||||
Object.getOwnPropertyNames(event).forEach((property) => {
|
||||
// Ignore the 'data' property
|
||||
// it becomes the Kafka message's 'value' field
|
||||
if (property != CONSTANTS.CE_ATTRIBUTES.DATA && property != CONSTANTS.STRUCTURED_ATTRS_1.DATA_BASE64) {
|
||||
// all CloudEvent property names get prefixed with 'ce_'
|
||||
// https://github.com/cloudevents/spec/blob/v1.0.1/kafka-protocol-binding.md#3231-property-names
|
||||
headers[`ce_${property}`] = event[property] as string;
|
||||
}
|
||||
});
|
||||
|
||||
return headers;
|
||||
}
|
|
@ -0,0 +1,273 @@
|
|||
/*
|
||||
Copyright 2021 The CloudEvents Authors
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { CloudEvent, CloudEventV1, CONSTANTS, Mode, ValidationError } from "../..";
|
||||
import { Message, Headers, Binding } from "..";
|
||||
import { headersFor, HEADER_MAP, KAFKA_CE_HEADERS } from "./headers";
|
||||
import { sanitize } from "../http/headers";
|
||||
|
||||
// Export the binding implementation and message interface
|
||||
export {
|
||||
Kafka
|
||||
};
|
||||
|
||||
export type {
|
||||
KafkaMessage,
|
||||
KafkaEvent
|
||||
};
|
||||
|
||||
/**
|
||||
* Bindings for Kafka transport
|
||||
* @implements {@linkcode Binding}
|
||||
*/
|
||||
const Kafka: Binding<KafkaMessage<unknown>, KafkaMessage<string>> = {
|
||||
binary: toBinaryKafkaMessage,
|
||||
structured: toStructuredKafkaMessage,
|
||||
toEvent: deserializeKafkaMessage,
|
||||
isEvent: isKafkaEvent,
|
||||
};
|
||||
|
||||
type Key = string | Buffer;
|
||||
|
||||
/**
|
||||
* Extends the base Message type to include
|
||||
* Kafka-specific fields
|
||||
*/
|
||||
interface KafkaMessage<T = string | Buffer | unknown> extends Message {
|
||||
key: Key
|
||||
value: T
|
||||
timestamp?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Extends the base CloudEventV1 interface to include a `partitionkey` field
|
||||
* which is explicitly mapped to KafkaMessage#key
|
||||
*/
|
||||
interface KafkaEvent<T> extends CloudEventV1<T> {
|
||||
/**
|
||||
* Maps to KafkaMessage#key per
|
||||
* https://github.com/cloudevents/spec/blob/v1.0.1/kafka-protocol-binding.md#31-key-mapping
|
||||
*/
|
||||
partitionkey: Key
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize a CloudEvent for Kafka in binary mode
|
||||
* @implements {Serializer}
|
||||
* @see https://github.com/cloudevents/spec/blob/v1.0.1/kafka-protocol-binding.md#32-binary-content-mode
|
||||
*
|
||||
* @param {KafkaEvent<T>} event The event to serialize
|
||||
* @returns {KafkaMessage<T>} a KafkaMessage instance
|
||||
*/
|
||||
function toBinaryKafkaMessage<T>(event: CloudEventV1<T>): KafkaMessage<T | undefined> {
|
||||
// 3.2.1. Content Type
|
||||
// For the binary mode, the header content-type property MUST be mapped directly
|
||||
// to the CloudEvents datacontenttype attribute.
|
||||
const headers: Headers = {
|
||||
...{ [CONSTANTS.HEADER_CONTENT_TYPE]: event.datacontenttype },
|
||||
...headersFor(event)
|
||||
};
|
||||
return {
|
||||
headers,
|
||||
key: event.partitionkey as Key,
|
||||
value: event.data,
|
||||
body: event.data,
|
||||
timestamp: timestamp(event.time)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize a CloudEvent for Kafka in structured mode
|
||||
* @implements {Serializer}
|
||||
* @see https://github.com/cloudevents/spec/blob/v1.0.1/kafka-protocol-binding.md#33-structured-content-mode
|
||||
*
|
||||
* @param {CloudEvent<T>} event the CloudEvent to be serialized
|
||||
* @returns {KafkaMessage<T>} a KafkaMessage instance
|
||||
*/
|
||||
function toStructuredKafkaMessage<T>(event: CloudEventV1<T>): KafkaMessage<string> {
|
||||
if ((event instanceof CloudEvent) && event.data_base64) {
|
||||
// The event's data is binary - delete it
|
||||
event = event.cloneWith({ data: undefined });
|
||||
}
|
||||
const value = event.toString();
|
||||
return {
|
||||
// All events may not have a partitionkey set, but if they do,
|
||||
// use it for the KafkaMessage#key per
|
||||
// https://github.com/cloudevents/spec/blob/v1.0.1/kafka-protocol-binding.md#31-key-mapping
|
||||
key: event.partitionkey as Key,
|
||||
value,
|
||||
headers: {
|
||||
[CONSTANTS.HEADER_CONTENT_TYPE]: CONSTANTS.DEFAULT_CE_CONTENT_TYPE,
|
||||
},
|
||||
body: value,
|
||||
timestamp: timestamp(event.time)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a Message to a CloudEvent
|
||||
* @implements {Deserializer}
|
||||
*
|
||||
* @param {Message} message the incoming message
|
||||
* @return {KafkaEvent} A new {KafkaEvent} instance
|
||||
*/
|
||||
function deserializeKafkaMessage<T>(message: Message): CloudEvent<T> | CloudEvent<T>[] {
|
||||
if (!isKafkaEvent(message)) {
|
||||
throw new ValidationError("No CloudEvent detected");
|
||||
}
|
||||
const m = message as KafkaMessage<T>;
|
||||
if (!m.value) {
|
||||
throw new ValidationError("Value is null or undefined");
|
||||
}
|
||||
if (!m.headers) {
|
||||
throw new ValidationError("Headers are null or undefined");
|
||||
}
|
||||
const cleanHeaders: Headers = sanitize(m.headers);
|
||||
const mode: Mode = getMode(cleanHeaders);
|
||||
switch (mode) {
|
||||
case Mode.BINARY:
|
||||
return parseBinary(m);
|
||||
case Mode.STRUCTURED:
|
||||
return parseStructured(m as unknown as KafkaMessage<string>);
|
||||
case Mode.BATCH:
|
||||
return parseBatched(m as unknown as KafkaMessage<string>);
|
||||
default:
|
||||
throw new ValidationError("Unknown Message mode");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a Message is a CloudEvent via Kafka headers
|
||||
* @implements {Detector}
|
||||
*
|
||||
* @param {Message} message an incoming Message object
|
||||
* @returns {boolean} true if this Message is a CloudEvent
|
||||
*/
|
||||
function isKafkaEvent(message: Message): boolean {
|
||||
const headers = sanitize(message.headers);
|
||||
return !!headers[KAFKA_CE_HEADERS.ID] || // A binary mode event
|
||||
headers[CONSTANTS.HEADER_CONTENT_TYPE]?.startsWith(CONSTANTS.MIME_CE) as boolean || // A structured mode event
|
||||
headers[CONSTANTS.HEADER_CONTENT_TYPE]?.startsWith(CONSTANTS.MIME_CE_BATCH) as boolean; // A batch of events
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines what content mode a Kafka message is in given the provided headers
|
||||
* @param {Headers} headers the headers
|
||||
* @returns {Mode} the content mode of the KafkaMessage
|
||||
*/
|
||||
function getMode(headers: Headers): Mode {
|
||||
const contentType = headers[CONSTANTS.HEADER_CONTENT_TYPE];
|
||||
if (contentType) {
|
||||
if (contentType.startsWith(CONSTANTS.MIME_CE_BATCH)) {
|
||||
return Mode.BATCH;
|
||||
} else if (contentType.startsWith(CONSTANTS.MIME_CE)) {
|
||||
return Mode.STRUCTURED;
|
||||
}
|
||||
}
|
||||
return Mode.BINARY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a binary kafka CE message and returns a CloudEvent
|
||||
* @param {KafkaMessage} message the message
|
||||
* @returns {CloudEvent<T>} a CloudEvent<T>
|
||||
*/
|
||||
function parseBinary<T>(message: KafkaMessage<T>): CloudEvent<T> {
|
||||
const eventObj: { [key: string ]: unknown } = {};
|
||||
const headers = { ...message.headers };
|
||||
|
||||
eventObj.datacontenttype = headers[CONSTANTS.HEADER_CONTENT_TYPE];
|
||||
|
||||
for (const key in KAFKA_CE_HEADERS) {
|
||||
const h = KAFKA_CE_HEADERS[key];
|
||||
if (!!headers[h]) {
|
||||
eventObj[HEADER_MAP[h]] = headers[h];
|
||||
if (h === KAFKA_CE_HEADERS.TIME) {
|
||||
eventObj.time = new Date(eventObj.time as string).toISOString();
|
||||
}
|
||||
delete headers[h];
|
||||
}
|
||||
}
|
||||
|
||||
// Any remaining headers are extension attributes
|
||||
// TODO: The spec is unlear on whether these should
|
||||
// be prefixed with 'ce_' as headers. We assume it is
|
||||
for (const key in headers) {
|
||||
if (key.startsWith("ce_")) {
|
||||
eventObj[key.replace("ce_", "")] = headers[key];
|
||||
}
|
||||
}
|
||||
|
||||
return new CloudEvent<T>({
|
||||
...eventObj,
|
||||
data: extractBinaryData(message),
|
||||
partitionkey: message.key,
|
||||
}, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a structured kafka CE message and returns a CloudEvent
|
||||
* @param {KafkaMessage<T>} message the message
|
||||
* @returns {CloudEvent<T>} a KafkaEvent<T>
|
||||
*/
|
||||
function parseStructured<T>(message: KafkaMessage<string>): CloudEvent<T> {
|
||||
// Although the format of a structured encoded event could be something
|
||||
// other than JSON, e.g. XML, we currently only support JSON
|
||||
// encoded structured events.
|
||||
if (!message.headers[CONSTANTS.HEADER_CONTENT_TYPE]?.startsWith(CONSTANTS.MIME_CE_JSON)) {
|
||||
throw new ValidationError(`Unsupported event encoding ${message.headers[CONSTANTS.HEADER_CONTENT_TYPE]}`);
|
||||
}
|
||||
const eventObj = JSON.parse(message.value);
|
||||
eventObj.time = new Date(eventObj.time).toISOString();
|
||||
return new CloudEvent({
|
||||
...eventObj,
|
||||
partitionkey: message.key,
|
||||
}, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a batch kafka CE message and returns a CloudEvent[]
|
||||
* @param {KafkaMessage<T>} message the message
|
||||
* @returns {CloudEvent<T>[]} an array of KafkaEvent<T>
|
||||
*/
|
||||
function parseBatched<T>(message: KafkaMessage<string>): CloudEvent<T>[] {
|
||||
// Although the format of batch encoded events could be something
|
||||
// other than JSON, e.g. XML, we currently only support JSON
|
||||
// encoded structured events.
|
||||
if (!message.headers[CONSTANTS.HEADER_CONTENT_TYPE]?.startsWith(CONSTANTS.MIME_CE_BATCH)) {
|
||||
throw new ValidationError(`Unsupported event encoding ${message.headers[CONSTANTS.HEADER_CONTENT_TYPE]}`);
|
||||
}
|
||||
const events = JSON.parse(message.value) as Record<string, unknown>[];
|
||||
return events.map((e) => new CloudEvent({ ...e, partitionkey: message.key }, false));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the data from a binary kafka ce message as T
|
||||
* @param {KafkaMessage} message a KafkaMessage
|
||||
* @returns {string | undefined} the data in the message
|
||||
*/
|
||||
function extractBinaryData<T>(message: KafkaMessage<T>): T {
|
||||
let data = message.value as T;
|
||||
// If the event data is JSON, go ahead and parse it
|
||||
const datacontenttype = message.headers[CONSTANTS.HEADER_CONTENT_TYPE] as string;
|
||||
if (!!datacontenttype && datacontenttype.startsWith(CONSTANTS.MIME_JSON)) {
|
||||
if (typeof message.value === "string") {
|
||||
data = JSON.parse(message.value);
|
||||
} else if (typeof message.value === "object" && Buffer.isBuffer(message.value)) {
|
||||
data = JSON.parse(message.value.toString());
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a possible date string into a correctly formatted
|
||||
* (for CloudEvents) ISO date string.
|
||||
* @param {string | undefined} t a possible date string
|
||||
* @returns {string | undefined} a properly formatted ISO date string or undefined
|
||||
*/
|
||||
function timestamp(t: string|undefined): string | undefined {
|
||||
return !!t ? `${Date.parse(t)}` : undefined;
|
||||
}
|
|
@ -0,0 +1,150 @@
|
|||
/*
|
||||
Copyright 2021 The CloudEvents Authors
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { Binding, Deserializer, CloudEvent, CloudEventV1, CONSTANTS, Message, ValidationError, Headers } from "../..";
|
||||
import { base64AsBinary } from "../../event/validation";
|
||||
|
||||
export {
|
||||
MQTT, MQTTMessageFactory
|
||||
};
|
||||
export type { MQTTMessage };
|
||||
|
||||
/**
|
||||
* Extends the base {@linkcode Message} interface to include MQTT attributes, some of which
|
||||
* are aliases of the {Message} attributes.
|
||||
*/
|
||||
interface MQTTMessage<T = unknown> extends Message<T> {
|
||||
/**
|
||||
* Identifies this message as a PUBLISH packet. MQTTMessages created with
|
||||
* the `binary` and `structured` Serializers will contain a "Content Type"
|
||||
* property in the PUBLISH record.
|
||||
* @see https://github.com/cloudevents/spec/blob/v1.0.1/mqtt-protocol-binding.md#3-mqtt-publish-message-mapping
|
||||
*/
|
||||
PUBLISH: Record<string, string | undefined> | undefined
|
||||
/**
|
||||
* Alias of {Message#body}
|
||||
*/
|
||||
payload: T | undefined,
|
||||
/**
|
||||
* Alias of {Message#headers}
|
||||
*/
|
||||
"User Properties": Headers | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Binding for MQTT transport support
|
||||
* @implements @linkcode Binding
|
||||
*/
|
||||
const MQTT: Binding<MQTTMessage, MQTTMessage> = {
|
||||
binary,
|
||||
structured,
|
||||
toEvent: toEvent as Deserializer,
|
||||
isEvent
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a CloudEvent into an MQTTMessage<T> with the event's data as the message payload
|
||||
* @param {CloudEventV1} event a CloudEvent
|
||||
* @returns {MQTTMessage<T>} the event serialized as an MQTTMessage<T> with binary encoding
|
||||
* @implements {Serializer}
|
||||
*/
|
||||
function binary<T>(event: CloudEventV1<T>): MQTTMessage<T> {
|
||||
const properties = { ...event };
|
||||
|
||||
let body = properties.data as T;
|
||||
|
||||
if (!body && properties.data_base64) {
|
||||
body = base64AsBinary(properties.data_base64) as unknown as T;
|
||||
}
|
||||
|
||||
delete properties.data;
|
||||
delete properties.data_base64;
|
||||
|
||||
return MQTTMessageFactory(event.datacontenttype as string, properties, body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a CloudEvent into an MQTTMessage<T> with the event as the message payload
|
||||
* @param {CloudEventV1} event a CloudEvent
|
||||
* @returns {MQTTMessage<T>} the event serialized as an MQTTMessage<T> with structured encoding
|
||||
* @implements {Serializer}
|
||||
*/
|
||||
function structured<T>(event: CloudEventV1<T>): MQTTMessage<T> {
|
||||
let body;
|
||||
if (event instanceof CloudEvent) {
|
||||
body = event.toJSON();
|
||||
} else {
|
||||
body = event;
|
||||
}
|
||||
return MQTTMessageFactory(CONSTANTS.DEFAULT_CE_CONTENT_TYPE, {}, body) as MQTTMessage<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper function to create an MQTTMessage<T> object, with "User Properties" as an alias
|
||||
* for "headers" and "payload" an alias for body, and a "PUBLISH" record with a "Content Type"
|
||||
* property.
|
||||
* @param {string} contentType the "Content Type" attribute on PUBLISH
|
||||
* @param {Record<string, unknown>} headers the headers and "User Properties"
|
||||
* @param {T} body the message body/payload
|
||||
* @returns {MQTTMessage<T>} a message initialized with the provided attributes
|
||||
*/
|
||||
function MQTTMessageFactory<T>(contentType: string, headers: Record<string, unknown>, body: T): MQTTMessage<T> {
|
||||
return {
|
||||
PUBLISH: {
|
||||
"Content Type": contentType
|
||||
},
|
||||
body,
|
||||
get payload() {
|
||||
return this.body as T;
|
||||
},
|
||||
headers: headers as Headers,
|
||||
get "User Properties"() {
|
||||
return this.headers as any;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an MQTTMessage<T> into a CloudEvent
|
||||
* @param {Message<T>} message the message to deserialize
|
||||
* @param {boolean} strict determines if a ValidationError will be thrown on bad input - defaults to false
|
||||
* @returns {CloudEventV1<T>} an event
|
||||
* @implements {Deserializer}
|
||||
*/
|
||||
function toEvent<T>(message: Message<T>, strict = false): CloudEventV1<T> | CloudEventV1<T>[] {
|
||||
if (strict && !isEvent(message)) {
|
||||
throw new ValidationError("No CloudEvent detected");
|
||||
}
|
||||
if (isStructuredMessage(message as MQTTMessage<T>)) {
|
||||
const evt = (typeof message.body === "string") ? JSON.parse(message.body): message.body;
|
||||
return new CloudEvent({
|
||||
...evt as CloudEventV1<T>
|
||||
}, false);
|
||||
} else {
|
||||
return new CloudEvent<T>({
|
||||
...message.headers,
|
||||
data: message.body as T,
|
||||
}, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the message is a CloudEvent
|
||||
* @param {Message<T>} message an MQTTMessage
|
||||
* @returns {boolean} true if the message contains an event
|
||||
*/
|
||||
function isEvent<T>(message: Message<T>): boolean {
|
||||
return isBinaryMessage(message) || isStructuredMessage(message as MQTTMessage<T>);
|
||||
}
|
||||
|
||||
function isBinaryMessage<T>(message: Message<T>): boolean {
|
||||
return (!!message.headers.id && !!message.headers.source
|
||||
&& !! message.headers.type && !!message.headers.specversion);
|
||||
}
|
||||
|
||||
function isStructuredMessage<T>(message: MQTTMessage<T>): boolean {
|
||||
if (!message) { return false; }
|
||||
return (message.PUBLISH && message?.PUBLISH["Content Type"]?.startsWith(CONSTANTS.MIME_CE_JSON)) || false;
|
||||
}
|
|
@ -3,9 +3,11 @@
|
|||
SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import JSONbig from "json-bigint";
|
||||
import CONSTANTS from "./constants";
|
||||
import { isString, isDefinedOrThrow, isStringOrObjectOrThrow, ValidationError } from "./event/validation";
|
||||
|
||||
const __JSON = JSON;
|
||||
export abstract class Parser {
|
||||
abstract parse(payload: Record<string, unknown> | string | string[] | undefined): unknown;
|
||||
}
|
||||
|
@ -36,6 +38,13 @@ export class JSONParser implements Parser {
|
|||
|
||||
isDefinedOrThrow(payload, new ValidationError("null or undefined payload"));
|
||||
isStringOrObjectOrThrow(payload, new ValidationError("invalid payload type, allowed are: string or object"));
|
||||
|
||||
if (process.env[CONSTANTS.USE_BIG_INT_ENV] === "true") {
|
||||
JSON = JSONbig(({ useNativeBigInt: true })) as JSON;
|
||||
} else {
|
||||
JSON = __JSON;
|
||||
}
|
||||
|
||||
const parseJSON = (v: Record<string, unknown> | string): string => (isString(v) ? JSON.parse(v as string) : v);
|
||||
return parseJSON(payload);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "CloudEvents Specification JSON Schema",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "Identifies the event.",
|
||||
"$ref": "#/definitions/iddef",
|
||||
"examples": [
|
||||
"A234-1234-1234"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"description": "Identifies the context in which an event happened.",
|
||||
"$ref": "#/definitions/sourcedef",
|
||||
"examples" : [
|
||||
"https://github.com/cloudevents",
|
||||
"mailto:cncf-wg-serverless@lists.cncf.io",
|
||||
"urn:uuid:6e8bc430-9c3a-11d9-9669-0800200c9a66",
|
||||
"cloudevents/spec/pull/123",
|
||||
"/sensors/tn-1234567/alerts",
|
||||
"1-555-123-4567"
|
||||
]
|
||||
},
|
||||
"specversion": {
|
||||
"description": "The version of the CloudEvents specification which the event uses.",
|
||||
"$ref": "#/definitions/specversiondef",
|
||||
"examples": [
|
||||
"1.0"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"description": "Describes the type of event related to the originating occurrence.",
|
||||
"$ref": "#/definitions/typedef",
|
||||
"examples" : [
|
||||
"com.github.pull_request.opened",
|
||||
"com.example.object.deleted.v2"
|
||||
]
|
||||
},
|
||||
"datacontenttype": {
|
||||
"description": "Content type of the data value. Must adhere to RFC 2046 format.",
|
||||
"$ref": "#/definitions/datacontenttypedef",
|
||||
"examples": [
|
||||
"text/xml",
|
||||
"application/json",
|
||||
"image/png",
|
||||
"multipart/form-data"
|
||||
]
|
||||
},
|
||||
"dataschema": {
|
||||
"description": "Identifies the schema that data adheres to.",
|
||||
"$ref": "#/definitions/dataschemadef"
|
||||
},
|
||||
"subject": {
|
||||
"description": "Describes the subject of the event in the context of the event producer (identified by source).",
|
||||
"$ref": "#/definitions/subjectdef",
|
||||
"examples": [
|
||||
"mynewfile.jpg"
|
||||
]
|
||||
},
|
||||
"time": {
|
||||
"description": "Timestamp of when the occurrence happened. Must adhere to RFC 3339.",
|
||||
"$ref": "#/definitions/timedef",
|
||||
"examples": [
|
||||
"2018-04-05T17:31:00Z"
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"description": "The event payload.",
|
||||
"$ref": "#/definitions/datadef",
|
||||
"examples": [
|
||||
"<much wow=\"xml\"/>"
|
||||
]
|
||||
},
|
||||
"data_base64": {
|
||||
"description": "Base64 encoded event payload. Must adhere to RFC4648.",
|
||||
"$ref": "#/definitions/data_base64def",
|
||||
"examples": [
|
||||
"Zm9vYg=="
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": ["id", "source", "specversion", "type"],
|
||||
"definitions": {
|
||||
"iddef": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"sourcedef": {
|
||||
"type": "string",
|
||||
"format": "uri-reference",
|
||||
"minLength": 1
|
||||
},
|
||||
"specversiondef": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"typedef": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"datacontenttypedef": {
|
||||
"type": ["string", "null"],
|
||||
"minLength": 1
|
||||
},
|
||||
"dataschemadef": {
|
||||
"type": ["string", "null"],
|
||||
"format": "uri",
|
||||
"minLength": 1
|
||||
},
|
||||
"subjectdef": {
|
||||
"type": ["string", "null"],
|
||||
"minLength": 1
|
||||
},
|
||||
"timedef": {
|
||||
"type": ["string", "null"],
|
||||
"format": "date-time",
|
||||
"minLength": 1
|
||||
},
|
||||
"datadef": {
|
||||
"type": ["object", "string", "number", "array", "boolean", "null"]
|
||||
},
|
||||
"data_base64def": {
|
||||
"type": ["string", "null"],
|
||||
"contentEncoding": "base64"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
Copyright 2021 The CloudEvents Authors
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
function formats(ajv) {
|
||||
require("ajv-formats")(ajv);
|
||||
}
|
||||
|
||||
module.exports = formats;
|
|
@ -3,20 +3,61 @@
|
|||
SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { Message, Options } from "../..";
|
||||
import axios from "axios";
|
||||
import { Socket } from "net";
|
||||
import http, { OutgoingHttpHeaders } from "http";
|
||||
import https, { RequestOptions } from "https";
|
||||
|
||||
export function axiosEmitter(sink: string) {
|
||||
return function (message: Message, options?: Options): Promise<unknown> {
|
||||
options = { ...options };
|
||||
const headers = {
|
||||
...message.headers,
|
||||
...(options.headers as Record<string, string>),
|
||||
};
|
||||
delete options.headers;
|
||||
return axios.post(sink, message.body, {
|
||||
headers: headers,
|
||||
...options,
|
||||
import { Message, Options } from "../..";
|
||||
import { TransportFunction } from "../emitter";
|
||||
|
||||
/**
|
||||
* httpTransport provides a simple HTTP Transport function, which can send a CloudEvent,
|
||||
* encoded as a Message to the endpoint. The returned function can be used with emitterFor()
|
||||
* to provide an event emitter, for example:
|
||||
*
|
||||
* const emitter = emitterFor(httpTransport("http://example.com"));
|
||||
* emitter.emit(myCloudEvent)
|
||||
* .then(resp => console.log(resp));
|
||||
*
|
||||
* @param {string|URL} sink the destination endpoint for the event
|
||||
* @returns {TransportFunction} a function which can be used to send CloudEvents to _sink_
|
||||
*/
|
||||
export function httpTransport(sink: string | URL): TransportFunction {
|
||||
const url = new URL(sink);
|
||||
let base: any;
|
||||
if (url.protocol === "https:") {
|
||||
base = https;
|
||||
} else if (url.protocol === "http:") {
|
||||
base = http;
|
||||
} else {
|
||||
throw new TypeError(`unsupported protocol ${url.protocol}`);
|
||||
}
|
||||
return function(message: Message, options?: Options): Promise<unknown> {
|
||||
return new Promise((resolve, reject) => {
|
||||
options = { ...options };
|
||||
|
||||
// TODO: Callers should be able to set any Node.js RequestOptions
|
||||
const opts: RequestOptions = {
|
||||
method: "POST",
|
||||
headers: {...message.headers, ...options.headers as OutgoingHttpHeaders},
|
||||
};
|
||||
try {
|
||||
const response = {
|
||||
body: "",
|
||||
headers: {},
|
||||
};
|
||||
const req = base.request(url, opts, (res: Socket) => {
|
||||
res.setEncoding("utf-8");
|
||||
response.headers = (res as any).headers;
|
||||
res.on("data", (chunk) => response.body += chunk);
|
||||
res.on("end", () => { resolve(response); });
|
||||
});
|
||||
req.on("error", reject);
|
||||
req.write(message.body);
|
||||
req.end();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -6,14 +6,40 @@
|
|||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
|
||||
import { assert } from "chai";
|
||||
import { Given, When, Then, World } from "cucumber";
|
||||
import { Message, Headers, HTTP } from "../../src";
|
||||
import { Given, When, Then, World } from "@cucumber/cucumber";
|
||||
import { Message, Headers, HTTP, KafkaMessage, Kafka } from "../../src";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { HTTPParser } = require("http-parser-js");
|
||||
|
||||
const parser = new HTTPParser(HTTPParser.REQUEST);
|
||||
|
||||
Given("Kafka Protocol Binding is supported", function (this: World) {
|
||||
return true;
|
||||
});
|
||||
|
||||
Given("a Kafka message with payload:", function (request: string) {
|
||||
// Create a KafkaMessage from the incoming HTTP request
|
||||
const value = Buffer.from(request);
|
||||
const message: KafkaMessage = {
|
||||
key: "",
|
||||
headers: {},
|
||||
body: value,
|
||||
value,
|
||||
};
|
||||
this.message = message;
|
||||
return true;
|
||||
});
|
||||
|
||||
Then("Kafka headers:", function (attributes: { rawTable: [] }) {
|
||||
this.message.headers = tableToObject(attributes.rawTable);
|
||||
});
|
||||
|
||||
When("parsed as Kafka message", function () {
|
||||
this.cloudevent = Kafka.toEvent(this.message);
|
||||
return true;
|
||||
});
|
||||
|
||||
Given("HTTP Protocol Binding is supported", function (this: World) {
|
||||
return true;
|
||||
});
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
import { expect } from "chai";
|
||||
import { CloudEvent, HTTP, Message } from "../../src";
|
||||
import { Kafka, KafkaMessage } from "../../src/message";
|
||||
|
||||
const type = "org.cncf.cloudevents.example";
|
||||
const source = "http://unit.test";
|
||||
|
@ -39,3 +40,22 @@ describe("A batched CloudEvent message over HTTP", () => {
|
|||
expect(ce.constructor.name).to.equal("CloudEvent");
|
||||
});
|
||||
});
|
||||
|
||||
describe("A batched CloudEvent message over Kafka", () => {
|
||||
it("Can be created with a typed Message", () => {
|
||||
const value = JSON.stringify(fixture);
|
||||
const message: KafkaMessage = {
|
||||
key: "123",
|
||||
value,
|
||||
headers: {
|
||||
"content-type": "application/cloudevents-batch+json",
|
||||
},
|
||||
body: value,
|
||||
};
|
||||
const batch = Kafka.toEvent(message);
|
||||
expect(batch.length).to.equal(10);
|
||||
const ce = (batch as CloudEvent<any>[])[0];
|
||||
expect(typeof ce).to.equal("object");
|
||||
expect(ce.constructor.name).to.equal("CloudEvent");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,24 +7,39 @@ import path from "path";
|
|||
import fs from "fs";
|
||||
|
||||
import { expect } from "chai";
|
||||
import { CloudEvent, ValidationError, Version } from "../../src";
|
||||
import { CloudEvent, CloudEventV1, ValidationError, V1 } from "../../src";
|
||||
import { asBase64 } from "../../src/event/validation";
|
||||
|
||||
const type = "org.cncf.cloudevents.example";
|
||||
const source = "http://unit.test";
|
||||
const id = "b46cf653-d48a-4b90-8dfa-355c01061361";
|
||||
|
||||
const fixture = {
|
||||
const fixture = Object.freeze({
|
||||
id,
|
||||
specversion: Version.V1,
|
||||
specversion: V1,
|
||||
source,
|
||||
type,
|
||||
data: `"some data"`,
|
||||
};
|
||||
data: `"some data"`
|
||||
});
|
||||
|
||||
const imageData = new Uint32Array(fs.readFileSync(path.join(process.cwd(), "test", "integration", "ce.png")));
|
||||
const image_base64 = asBase64(imageData);
|
||||
|
||||
// Do not replace this with the assignment of a class instance
|
||||
// as we just want to test if we can enumerate all explicitly defined fields!
|
||||
const cloudEventV1InterfaceFields: (keyof CloudEventV1<unknown>)[] = Object.keys({
|
||||
id: "",
|
||||
type: "",
|
||||
data: undefined,
|
||||
data_base64: "",
|
||||
source: "",
|
||||
time: "",
|
||||
datacontenttype: "",
|
||||
dataschema: "",
|
||||
specversion: "",
|
||||
subject: ""
|
||||
} as Required<CloudEventV1<unknown>>);
|
||||
|
||||
describe("A CloudEvent", () => {
|
||||
it("Can be constructed with a typed Message", () => {
|
||||
const ce = new CloudEvent(fixture);
|
||||
|
@ -67,10 +82,10 @@ describe("A CloudEvent", () => {
|
|||
}).throw("invalid extension name");
|
||||
});
|
||||
|
||||
it("Throw a validation error for invalid extension names, more than 20 chars", () => {
|
||||
it("Not throw a validation error for invalid extension names, more than 20 chars", () => {
|
||||
expect(() => {
|
||||
new CloudEvent({ "123456789012345678901": "extension1", ...fixture });
|
||||
}).throw("invalid extension name");
|
||||
}).not.throw("invalid extension name");
|
||||
});
|
||||
|
||||
it("Throws a validation error for invalid uppercase extension names", () => {
|
||||
|
@ -78,6 +93,58 @@ describe("A CloudEvent", () => {
|
|||
new CloudEvent({ ExtensionWithCaps: "extension value", ...fixture });
|
||||
}).throw("invalid extension name");
|
||||
});
|
||||
|
||||
it("CloudEventV1 interface fields should be enumerable", () => {
|
||||
const classInstanceKeys = Object.keys(new CloudEvent({ ...fixture }));
|
||||
|
||||
for (const key of cloudEventV1InterfaceFields) {
|
||||
expect(classInstanceKeys).to.contain(key);
|
||||
}
|
||||
});
|
||||
|
||||
it("throws TypeError on trying to set any field value", () => {
|
||||
const ce = new CloudEvent({
|
||||
...fixture,
|
||||
mycustomfield: "initialValue"
|
||||
});
|
||||
|
||||
const keySet = new Set([...cloudEventV1InterfaceFields, ...Object.keys(ce)]);
|
||||
|
||||
expect(keySet).not.to.be.empty;
|
||||
|
||||
for (const cloudEventKey of keySet) {
|
||||
let threw = false;
|
||||
|
||||
try {
|
||||
ce[cloudEventKey] = "newValue";
|
||||
} catch (err) {
|
||||
threw = true;
|
||||
expect(err).to.be.instanceOf(TypeError);
|
||||
expect((err as TypeError).message).to.include("Cannot assign to read only property");
|
||||
}
|
||||
|
||||
if (!threw) {
|
||||
expect.fail(`Assigning a value to ${cloudEventKey} did not throw`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
describe("toJSON()", () => {
|
||||
it("does not return data field if data_base64 field is set to comply with JSON format spec 3.1.1", () => {
|
||||
const binaryData = new Uint8Array([1,2,3]);
|
||||
|
||||
const ce = new CloudEvent({
|
||||
...fixture,
|
||||
data: binaryData
|
||||
});
|
||||
|
||||
expect(ce.data).to.be.equal(binaryData);
|
||||
|
||||
const json = ce.toJSON();
|
||||
expect(json.data).to.not.exist;
|
||||
expect(json.data_base64).to.be.equal("AQID");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("A 1.0 CloudEvent", () => {
|
||||
|
@ -98,7 +165,7 @@ describe("A 1.0 CloudEvent", () => {
|
|||
});
|
||||
|
||||
it("can be constructed with an ID", () => {
|
||||
const ce = new CloudEvent({ id: "1234", specversion: Version.V1, source, type });
|
||||
const ce = new CloudEvent({ id: "1234", specversion: V1, source, type });
|
||||
expect(ce.id).to.equal("1234");
|
||||
});
|
||||
|
||||
|
@ -203,7 +270,7 @@ describe("A 1.0 CloudEvent", () => {
|
|||
});
|
||||
} catch (err) {
|
||||
expect(err).to.be.instanceOf(TypeError);
|
||||
expect(err.message).to.include("invalid payload");
|
||||
expect((err as TypeError).message).to.include("invalid payload");
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -213,7 +280,7 @@ describe("A 1.0 CloudEvent", () => {
|
|||
const obj = JSON.parse(json as string);
|
||||
expect(obj.type).to.equal(type);
|
||||
expect(obj.source).to.equal(source);
|
||||
expect(obj.specversion).to.equal(Version.V1);
|
||||
expect(obj.specversion).to.equal(V1);
|
||||
});
|
||||
|
||||
it("throws if the provded source is empty string", () => {
|
||||
|
@ -224,11 +291,12 @@ describe("A 1.0 CloudEvent", () => {
|
|||
type: "my.event.type",
|
||||
source: "",
|
||||
});
|
||||
} catch (err) {
|
||||
expect(err).to.be.instanceOf(TypeError);
|
||||
} catch (err: any) {
|
||||
expect(err).to.be.instanceOf(ValidationError);
|
||||
const error = err.errors[0] as any;
|
||||
expect(err.message).to.include("invalid payload");
|
||||
expect(err.errors[0].dataPath).to.equal(".source");
|
||||
expect(err.errors[0].keyword).to.equal("minLength");
|
||||
expect(error.instancePath).to.equal("/source");
|
||||
expect(error.keyword).to.equal("minLength");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,12 +6,13 @@
|
|||
import "mocha";
|
||||
import { expect } from "chai";
|
||||
import nock from "nock";
|
||||
import axios from "axios";
|
||||
import axios, { AxiosRequestHeaders } from "axios";
|
||||
import request from "superagent";
|
||||
import got from "got";
|
||||
|
||||
import CONSTANTS from "../../src/constants";
|
||||
import { CloudEvent, emitterFor, HTTP, Mode, Message, Options, TransportFunction } from "../../src";
|
||||
import { CloudEvent, HTTP, Message, Mode, Options, TransportFunction, emitterFor, httpTransport }
|
||||
from "../../src";
|
||||
|
||||
const DEFAULT_CE_CONTENT_TYPE = CONSTANTS.DEFAULT_CE_CONTENT_TYPE;
|
||||
const sink = "https://cloudevents.io/";
|
||||
|
@ -38,7 +39,7 @@ export const fixture = new CloudEvent({
|
|||
});
|
||||
|
||||
function axiosEmitter(message: Message, options?: Options): Promise<unknown> {
|
||||
return axios.post(sink, message.body, { headers: message.headers, ...options });
|
||||
return axios.post(sink, message.body, { headers: message.headers as AxiosRequestHeaders, ...options });
|
||||
}
|
||||
|
||||
function superagentEmitter(message: Message, options?: Options): Promise<unknown> {
|
||||
|
@ -83,7 +84,6 @@ describe("emitterFor() defaults", () => {
|
|||
|
||||
it("Supports HTTP binding, structured mode", () => {
|
||||
function transport(message: Message): Promise<unknown> {
|
||||
console.error(message);
|
||||
// A structured message will have the application/cloudevents+json header
|
||||
expect(message.headers["content-type"]).to.equal(CONSTANTS.DEFAULT_CE_CONTENT_TYPE);
|
||||
const body = JSON.parse(message.body as string);
|
||||
|
@ -101,33 +101,50 @@ describe("emitterFor() defaults", () => {
|
|||
});
|
||||
});
|
||||
|
||||
function setupMock(uri: string) {
|
||||
nock(uri)
|
||||
.post("/")
|
||||
.reply(function (uri: string, body: nock.Body) {
|
||||
// return the request body and the headers so they can be
|
||||
// examined in the test
|
||||
if (typeof body === "string") {
|
||||
body = JSON.parse(body);
|
||||
}
|
||||
const returnBody = { ...(body as Record<string, unknown>), ...this.req.headers };
|
||||
return [201, returnBody];
|
||||
});
|
||||
}
|
||||
|
||||
describe("HTTP Transport Binding for emitterFactory", () => {
|
||||
beforeEach(() => {
|
||||
nock(sink)
|
||||
.post("/")
|
||||
.reply(function (uri: string, body: nock.Body) {
|
||||
// return the request body and the headers so they can be
|
||||
// examined in the test
|
||||
if (typeof body === "string") {
|
||||
body = JSON.parse(body);
|
||||
}
|
||||
const returnBody = { ...(body as Record<string, unknown>), ...this.req.headers };
|
||||
return [201, returnBody];
|
||||
});
|
||||
beforeEach(() => { setupMock(sink); });
|
||||
|
||||
describe("HTTPS builtin", () => {
|
||||
testEmitterBinary(httpTransport(sink), "body");
|
||||
});
|
||||
|
||||
describe("HTTP builtin", () => {
|
||||
setupMock("http://cloudevents.io");
|
||||
testEmitterBinary(httpTransport("http://cloudevents.io"), "body");
|
||||
setupMock("http://cloudevents.io");
|
||||
testEmitterStructured(httpTransport("http://cloudevents.io"), "body");
|
||||
});
|
||||
|
||||
describe("Axios", () => {
|
||||
testEmitter(axiosEmitter, "data");
|
||||
testEmitterBinary(axiosEmitter, "data");
|
||||
testEmitterStructured(axiosEmitter, "data");
|
||||
});
|
||||
describe("SuperAgent", () => {
|
||||
testEmitter(superagentEmitter, "body");
|
||||
testEmitterBinary(superagentEmitter, "body");
|
||||
testEmitterStructured(superagentEmitter, "body");
|
||||
});
|
||||
|
||||
describe("Got", () => {
|
||||
testEmitter(gotEmitter, "body");
|
||||
testEmitterBinary(gotEmitter, "body");
|
||||
testEmitterStructured(gotEmitter, "body");
|
||||
});
|
||||
});
|
||||
|
||||
function testEmitter(fn: TransportFunction, bodyAttr: string) {
|
||||
function testEmitterBinary(fn: TransportFunction, bodyAttr: string) {
|
||||
it("Works as a binary event emitter", async () => {
|
||||
const emitter = emitterFor(fn);
|
||||
const response = (await emitter(fixture)) as Record<string, Record<string, string>>;
|
||||
|
@ -137,7 +154,9 @@ function testEmitter(fn: TransportFunction, bodyAttr: string) {
|
|||
}
|
||||
assertBinary(body);
|
||||
});
|
||||
}
|
||||
|
||||
function testEmitterStructured(fn: TransportFunction, bodyAttr: string) {
|
||||
it("Works as a structured event emitter", async () => {
|
||||
const emitter = emitterFor(fn, { binding: HTTP, mode: Mode.STRUCTURED });
|
||||
const response = (await emitter(fixture)) as Record<string, Record<string, Record<string, string>>>;
|
||||
|
|
|
@ -22,11 +22,11 @@ describe("Emitter Singleton", () => {
|
|||
|
||||
fixture.emit(false);
|
||||
});
|
||||
let body: unknown = (<Message>(<unknown>msg)).body;
|
||||
let body: unknown = (msg as Message).body;
|
||||
if (typeof body === "string") {
|
||||
body = JSON.parse(body);
|
||||
}
|
||||
assertStructured({ ...(<any>body), ...(<Message>(<unknown>msg)).headers });
|
||||
assertStructured({ ...(body as any), ...(msg as Message).headers });
|
||||
});
|
||||
|
||||
it("emit a Node.js 'cloudevent' event as an EventEmitter with ensureDelivery", async () => {
|
||||
|
@ -37,11 +37,11 @@ describe("Emitter Singleton", () => {
|
|||
const emitter = emitterFor(fn, { binding: HTTP, mode: Mode.STRUCTURED });
|
||||
Emitter.on("cloudevent", emitter);
|
||||
await fixture.emit(true);
|
||||
let body: any = (<Message>msg).body;
|
||||
let body: any = (msg as Message).body;
|
||||
if (typeof body === "string") {
|
||||
body = JSON.parse(body);
|
||||
}
|
||||
assertStructured({ ...(<any>body), ...(<Message>(<unknown>msg)).headers });
|
||||
assertStructured({ ...(body as any), ...(msg as Message).headers });
|
||||
});
|
||||
|
||||
it("emit a Node.js 'cloudevent' event as an EventEmitter with ensureDelivery Error", async () => {
|
||||
|
|
|
@ -0,0 +1,317 @@
|
|||
/*
|
||||
Copyright 2021 The CloudEvents Authors
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
|
||||
import { expect } from "chai";
|
||||
import { CloudEvent, CONSTANTS, V1 } from "../../src";
|
||||
import { asBase64 } from "../../src/event/validation";
|
||||
import { Message, Kafka, KafkaMessage, KafkaEvent } from "../../src/message";
|
||||
import { KAFKA_CE_HEADERS } from "../../src/message/kafka/headers";
|
||||
|
||||
const key = "foo/bar";
|
||||
const type = "org.cncf.cloudevents.example";
|
||||
const source = "urn:event:from:myapi/resource/123";
|
||||
const time = new Date().toISOString();
|
||||
const subject = "subject.ext";
|
||||
const dataschema = "http://cloudevents.io/schema.json";
|
||||
const datacontenttype = "application/json";
|
||||
const id = "b46cf653-d48a-4b90-8dfa-355c01061361";
|
||||
|
||||
interface Idata {
|
||||
foo: string
|
||||
}
|
||||
const data: Idata = {
|
||||
foo: "bar",
|
||||
};
|
||||
|
||||
const ext1Name = "extension1";
|
||||
const ext1Value = "foobar";
|
||||
const ext2Name = "extension2";
|
||||
const ext2Value = "acme";
|
||||
|
||||
// Binary data as base64
|
||||
const dataBinary = Uint32Array.from(JSON.stringify(data), (c) => c.codePointAt(0) as number);
|
||||
const data_base64 = asBase64(dataBinary);
|
||||
|
||||
// Since the above is a special case (string as binary), let's test
|
||||
// with a real binary file one is likely to encounter in the wild
|
||||
const imageData = new Uint32Array(fs.readFileSync(path.join(process.cwd(), "test", "integration", "ce.png")));
|
||||
const image_base64 = asBase64(imageData);
|
||||
|
||||
const fixture = new CloudEvent({
|
||||
specversion: V1,
|
||||
id,
|
||||
type,
|
||||
source,
|
||||
datacontenttype,
|
||||
subject,
|
||||
time,
|
||||
dataschema,
|
||||
data,
|
||||
[ext1Name]: ext1Value,
|
||||
[ext2Name]: ext2Value,
|
||||
partitionkey: key,
|
||||
});
|
||||
|
||||
describe("Kafka transport", () => {
|
||||
it("Handles events with no content-type and no datacontenttype", () => {
|
||||
const value = "{Something[Not:valid}JSON";
|
||||
const message: KafkaMessage<string> = {
|
||||
key,
|
||||
value,
|
||||
headers: {
|
||||
[KAFKA_CE_HEADERS.SOURCE]: "/test/kafka",
|
||||
[KAFKA_CE_HEADERS.TYPE]: "test.kafka",
|
||||
[KAFKA_CE_HEADERS.ID]: "1234",
|
||||
},
|
||||
body: undefined,
|
||||
};
|
||||
const event: CloudEvent = Kafka.toEvent(message) as CloudEvent;
|
||||
expect(event.data).to.equal(value);
|
||||
expect(event.datacontentype).to.equal(undefined);
|
||||
});
|
||||
|
||||
it("Can detect invalid CloudEvent Messages", () => {
|
||||
// Create a message that is not an actual event
|
||||
const message: KafkaMessage<string> = {
|
||||
key,
|
||||
value: "Hello world!",
|
||||
headers: {
|
||||
"Content-type": "text/plain",
|
||||
},
|
||||
body: undefined
|
||||
};
|
||||
expect(Kafka.isEvent(message)).to.be.false;
|
||||
});
|
||||
|
||||
it("Can detect valid CloudEvent Messages", () => {
|
||||
// Now create a message that is an event
|
||||
const message = Kafka.binary(
|
||||
new CloudEvent<Idata>({
|
||||
source: "/message-test",
|
||||
type: "example",
|
||||
data,
|
||||
}),
|
||||
);
|
||||
expect(Kafka.isEvent(message)).to.be.true;
|
||||
});
|
||||
|
||||
it("Handles CloudEvents with datacontenttype of text/plain", () => {
|
||||
const message: Message<string> = Kafka.binary(
|
||||
new CloudEvent({
|
||||
source: "/test",
|
||||
type: "example",
|
||||
datacontenttype: "text/plain",
|
||||
data: "Hello, friends!",
|
||||
}),
|
||||
);
|
||||
const event = Kafka.toEvent(message) as CloudEvent<string>;
|
||||
expect(event.validate()).to.be.true;
|
||||
});
|
||||
|
||||
it("Respects extension attribute casing (even if against spec)", () => {
|
||||
// Create a message that is an event
|
||||
const message: KafkaMessage<string> = {
|
||||
key,
|
||||
body: undefined,
|
||||
value: `{ "greeting": "hello" }`,
|
||||
headers: {
|
||||
[KAFKA_CE_HEADERS.ID]: "1234",
|
||||
[KAFKA_CE_HEADERS.SOURCE]: "test",
|
||||
[KAFKA_CE_HEADERS.TYPE]: "test.event",
|
||||
"ce_LUNCH": "tacos",
|
||||
},
|
||||
};
|
||||
expect(Kafka.isEvent(message)).to.be.true;
|
||||
const event = Kafka.toEvent(message) as CloudEvent<string>;
|
||||
expect(event.LUNCH).to.equal("tacos");
|
||||
expect(function () {
|
||||
event.validate();
|
||||
}).to.throw("invalid attribute name: \"LUNCH\"");
|
||||
});
|
||||
|
||||
it("Can detect CloudEvent binary Messages with weird versions", () => {
|
||||
// Now create a message that is an event
|
||||
const message: KafkaMessage<string> = {
|
||||
key,
|
||||
body: undefined,
|
||||
value: `{ "greeting": "hello" }`,
|
||||
headers: {
|
||||
[KAFKA_CE_HEADERS.ID]: "1234",
|
||||
[KAFKA_CE_HEADERS.SOURCE]: "test",
|
||||
[KAFKA_CE_HEADERS.TYPE]: "test.event",
|
||||
[KAFKA_CE_HEADERS.SPEC_VERSION]: "11.8",
|
||||
},
|
||||
};
|
||||
expect(Kafka.isEvent(message)).to.be.true;
|
||||
const event = Kafka.toEvent(message) as CloudEvent;
|
||||
expect(event.specversion).to.equal("11.8");
|
||||
expect(event.validate()).to.be.false;
|
||||
});
|
||||
|
||||
it("Can detect CloudEvent structured Messages with weird versions", () => {
|
||||
// Now create a message that is an event
|
||||
const message: KafkaMessage<string> = {
|
||||
key,
|
||||
body: undefined,
|
||||
value: `{ "source": "test", "type": "test.event", "specversion": "11.8"}`,
|
||||
headers: {
|
||||
[KAFKA_CE_HEADERS.ID]: "1234",
|
||||
},
|
||||
};
|
||||
expect(Kafka.isEvent(message)).to.be.true;
|
||||
expect(Kafka.toEvent(message)).not.to.throw;
|
||||
});
|
||||
|
||||
// Allow for external systems to send bad events - do what we can
|
||||
// to accept them
|
||||
it("Does not throw an exception when converting an invalid Message to a CloudEvent", () => {
|
||||
const message: KafkaMessage<string> = {
|
||||
key,
|
||||
body: undefined,
|
||||
value: `"hello world"`,
|
||||
headers: {
|
||||
[CONSTANTS.HEADER_CONTENT_TYPE]: "application/json",
|
||||
[KAFKA_CE_HEADERS.ID]: "1234",
|
||||
[KAFKA_CE_HEADERS.TYPE]: "example.bad.event",
|
||||
// no required ce_source header, thus an invalid event
|
||||
},
|
||||
};
|
||||
const event = Kafka.toEvent(message) as CloudEvent;
|
||||
expect(event).to.be.instanceOf(CloudEvent);
|
||||
// ensure that we actually now have an invalid event
|
||||
expect(event.validate).to.throw;
|
||||
});
|
||||
|
||||
it("Does not allow an invalid CloudEvent to be converted to a Message", () => {
|
||||
const badEvent = new CloudEvent(
|
||||
{
|
||||
source: "/example.source",
|
||||
type: "", // type is required, empty string will throw with strict validation
|
||||
},
|
||||
false, // turn off strict validation
|
||||
);
|
||||
expect(() => {
|
||||
Kafka.binary(badEvent);
|
||||
}).to.throw;
|
||||
expect(() => {
|
||||
Kafka.structured(badEvent);
|
||||
}).to.throw;
|
||||
});
|
||||
|
||||
// https://github.com/cloudevents/spec/blob/v1.0.1/kafka-protocol-binding.md#31-key-mapping
|
||||
it("Maps `KafkaMessage#key` value to CloudEvent#partitionkey property", () => {
|
||||
const message: KafkaMessage<string> = {
|
||||
key,
|
||||
body: undefined,
|
||||
value: `{ "source": "test", "type": "test.event", "specversion": "11.8"}`,
|
||||
headers: {
|
||||
[KAFKA_CE_HEADERS.ID]: "1234",
|
||||
},
|
||||
};
|
||||
const event = Kafka.toEvent(message) as KafkaEvent<string>;
|
||||
expect(event.partitionkey).to.equal(key);
|
||||
});
|
||||
|
||||
// https://github.com/cloudevents/spec/blob/v1.0.1/kafka-protocol-binding.md#31-key-mapping
|
||||
it("Maps CloudEvent#partitionkey value to a `key` in binary KafkaMessages", () => {
|
||||
const event = new CloudEvent({
|
||||
source,
|
||||
type,
|
||||
partitionkey: key,
|
||||
});
|
||||
const message = Kafka.binary(event) as KafkaMessage;
|
||||
expect(message.key).to.equal(key);
|
||||
});
|
||||
|
||||
it("Binary Messages can be created from a CloudEvent", () => {
|
||||
const message: Message<Idata> = Kafka.binary(fixture);
|
||||
expect(message.body).to.equal(data);
|
||||
// validate all headers
|
||||
expect(message.headers[CONSTANTS.HEADER_CONTENT_TYPE]).to.equal(datacontenttype);
|
||||
expect(message.headers[KAFKA_CE_HEADERS.SPEC_VERSION]).to.equal(V1);
|
||||
expect(message.headers[KAFKA_CE_HEADERS.ID]).to.equal(id);
|
||||
expect(message.headers[KAFKA_CE_HEADERS.TYPE]).to.equal(type);
|
||||
expect(message.headers[KAFKA_CE_HEADERS.SOURCE]).to.equal(source);
|
||||
expect(message.headers[KAFKA_CE_HEADERS.SUBJECT]).to.equal(subject);
|
||||
expect(message.headers[KAFKA_CE_HEADERS.TIME]).to.equal(fixture.time);
|
||||
expect(message.headers[KAFKA_CE_HEADERS.DATASCHEMA]).to.equal(dataschema);
|
||||
expect(message.headers[`ce_${ext1Name}`]).to.equal(ext1Value);
|
||||
expect(message.headers[`ce_${ext2Name}`]).to.equal(ext2Value);
|
||||
});
|
||||
|
||||
it("Structured Messages can be created from a CloudEvent", () => {
|
||||
const message: Message<Idata> = Kafka.structured(fixture);
|
||||
expect(message.headers[CONSTANTS.HEADER_CONTENT_TYPE]).to.equal(CONSTANTS.DEFAULT_CE_CONTENT_TYPE);
|
||||
// Parse the message body as JSON, then validate the attributes
|
||||
const body = JSON.parse(message.body as string);
|
||||
expect(body[CONSTANTS.CE_ATTRIBUTES.SPEC_VERSION]).to.equal(V1);
|
||||
expect(body[CONSTANTS.CE_ATTRIBUTES.ID]).to.equal(id);
|
||||
expect(body[CONSTANTS.CE_ATTRIBUTES.TYPE]).to.equal(type);
|
||||
expect(body[CONSTANTS.CE_ATTRIBUTES.SOURCE]).to.equal(source);
|
||||
expect(body[CONSTANTS.CE_ATTRIBUTES.SUBJECT]).to.equal(subject);
|
||||
expect(body[CONSTANTS.CE_ATTRIBUTES.TIME]).to.equal(fixture.time);
|
||||
expect(body[CONSTANTS.STRUCTURED_ATTRS_1.DATA_SCHEMA]).to.equal(dataschema);
|
||||
expect(body[ext1Name]).to.equal(ext1Value);
|
||||
expect(body[ext2Name]).to.equal(ext2Value);
|
||||
});
|
||||
|
||||
it("A CloudEvent can be converted from a binary Message", () => {
|
||||
const message = Kafka.binary(fixture);
|
||||
const event = Kafka.toEvent(message);
|
||||
|
||||
// The Kafka deserializer sets a partitionkey
|
||||
expect(event).to.deep.equal({...fixture, partitionkey: (event as KafkaEvent<any>).partitionkey});
|
||||
});
|
||||
it("A CloudEvent can be converted from a binary Message", () => {
|
||||
const message = Kafka.binary(fixture);
|
||||
const event = Kafka.toEvent(message);
|
||||
expect(event).to.deep.equal(fixture);
|
||||
});
|
||||
|
||||
it("A CloudEvent can be converted from a structured Message", () => {
|
||||
const message = Kafka.structured(fixture);
|
||||
const event = Kafka.toEvent(message);
|
||||
expect(event).to.deep.equal(fixture);
|
||||
});
|
||||
|
||||
it("Converts binary data to base64 when serializing structured messages", () => {
|
||||
const event = fixture.cloneWith({ data: imageData, datacontenttype: "image/png" });
|
||||
expect(event.data).to.equal(imageData);
|
||||
const message = Kafka.structured(event);
|
||||
const messageBody = JSON.parse(message.body as string);
|
||||
expect(messageBody.data_base64).to.equal(image_base64);
|
||||
});
|
||||
|
||||
it.skip("Converts base64 encoded data to binary when deserializing structured messages", () => {
|
||||
const message = Kafka.structured(fixture.cloneWith({ data: imageData, datacontenttype: "image/png" }));
|
||||
const eventDeserialized = Kafka.toEvent(message) as CloudEvent<Uint32Array>;
|
||||
expect(eventDeserialized.data).to.deep.equal(imageData);
|
||||
expect(eventDeserialized.data_base64).to.equal(image_base64);
|
||||
});
|
||||
|
||||
it("Converts base64 encoded data to binary when deserializing binary messages", () => {
|
||||
const message = Kafka.binary(fixture.cloneWith({ data: imageData, datacontenttype: "image/png" }));
|
||||
const eventDeserialized = Kafka.toEvent(message) as CloudEvent<Uint32Array>;
|
||||
expect(eventDeserialized.data).to.deep.equal(imageData);
|
||||
expect(eventDeserialized.data_base64).to.equal(image_base64);
|
||||
});
|
||||
|
||||
it("Keeps binary data binary when serializing binary messages", () => {
|
||||
const event = fixture.cloneWith({ data: dataBinary });
|
||||
expect(event.data).to.equal(dataBinary);
|
||||
const message = Kafka.binary(event);
|
||||
expect(message.body).to.equal(dataBinary);
|
||||
});
|
||||
|
||||
it("Does not parse binary data from binary messages with content type application/json", () => {
|
||||
const message = Kafka.binary(fixture.cloneWith({ data: dataBinary }));
|
||||
const eventDeserialized = Kafka.toEvent(message) as CloudEvent<Uint32Array>;
|
||||
expect(eventDeserialized.data).to.deep.equal(dataBinary);
|
||||
expect(eventDeserialized.data_base64).to.equal(data_base64);
|
||||
});
|
||||
});
|
|
@ -8,7 +8,7 @@ import fs from "fs";
|
|||
|
||||
import { expect } from "chai";
|
||||
import { IncomingHttpHeaders } from "http";
|
||||
import { CloudEvent, CONSTANTS, Version } from "../../src";
|
||||
import { CloudEvent, CONSTANTS, V1, V03 } from "../../src";
|
||||
import { asBase64 } from "../../src/event/validation";
|
||||
import { Message, HTTP } from "../../src/message";
|
||||
|
||||
|
@ -41,9 +41,63 @@ const imageData = new Uint32Array(fs.readFileSync(path.join(process.cwd(), "test
|
|||
const image_base64 = asBase64(imageData);
|
||||
|
||||
describe("HTTP transport", () => {
|
||||
it("validates extension attribute names for incoming messages", () => {
|
||||
// create a new Message
|
||||
const msg: Message = {
|
||||
headers: {
|
||||
"ce-id": "213",
|
||||
"ce-source": "test",
|
||||
"ce-type": "test",
|
||||
"ce-bad-extension": "value"
|
||||
},
|
||||
body: undefined
|
||||
};
|
||||
const evt = HTTP.toEvent(msg) as CloudEvent;
|
||||
expect(() => evt.validate()).to.throw(TypeError);
|
||||
});
|
||||
|
||||
it("Includes extensions in binary mode when type is 'boolean' with a false value", () => {
|
||||
const evt = new CloudEvent({ source: "test", type: "test", extboolean: false });
|
||||
expect(evt.hasOwnProperty("extboolean")).to.equal(true);
|
||||
expect(evt["extboolean"]).to.equal(false);
|
||||
const message = HTTP.binary(evt);
|
||||
expect(message.headers.hasOwnProperty("ce-extboolean")).to.equal(true);
|
||||
expect(message.headers["ce-extboolean"]).to.equal(false);
|
||||
});
|
||||
|
||||
it("Includes extensions in structured when type is 'boolean' with a false value", () => {
|
||||
const evt = new CloudEvent({ source: "test", type: "test", extboolean: false });
|
||||
expect(evt.hasOwnProperty("extboolean")).to.equal(true);
|
||||
expect(evt["extboolean"]).to.equal(false);
|
||||
const message = HTTP.structured(evt);
|
||||
const body = JSON.parse(message.body as string);
|
||||
expect(body.hasOwnProperty("extboolean")).to.equal(true);
|
||||
expect(body.extboolean).to.equal(false);
|
||||
});
|
||||
|
||||
it("Handles big integers in structured mode", () => {
|
||||
process.env[CONSTANTS.USE_BIG_INT_ENV] = "true";
|
||||
const ce = HTTP.toEvent({
|
||||
headers: { "content-type": "application/cloudevents+json" },
|
||||
body: `{"data": 1524831183200260097}`
|
||||
}) as CloudEvent;
|
||||
expect(ce.data).to.equal(1524831183200260097n);
|
||||
process.env[CONSTANTS.USE_BIG_INT_ENV] = undefined;
|
||||
});
|
||||
|
||||
it("Handles big integers in binary mode", () => {
|
||||
process.env[CONSTANTS.USE_BIG_INT_ENV] = "true";
|
||||
const ce = HTTP.toEvent({
|
||||
headers: { "content-type": "application/json", "ce-id": "1234" },
|
||||
body: `{"data": 1524831183200260097}`
|
||||
}) as CloudEvent<Record<string, never>>;
|
||||
expect((ce.data as Record<string, never>).data).to.equal(1524831183200260097n);
|
||||
process.env[CONSTANTS.USE_BIG_INT_ENV] = undefined;
|
||||
});
|
||||
|
||||
it("Handles events with no content-type and no datacontenttype", () => {
|
||||
const body = "{Something[Not:valid}JSON";
|
||||
const message: Message = {
|
||||
const message: Message<undefined> = {
|
||||
body,
|
||||
headers: {
|
||||
"ce-source": "/test/type",
|
||||
|
@ -58,7 +112,7 @@ describe("HTTP transport", () => {
|
|||
|
||||
it("Can detect invalid CloudEvent Messages", () => {
|
||||
// Create a message that is not an actual event
|
||||
const message: Message = {
|
||||
const message: Message<undefined> = {
|
||||
body: "Hello world!",
|
||||
headers: {
|
||||
"Content-type": "text/plain",
|
||||
|
@ -88,7 +142,7 @@ describe("HTTP transport", () => {
|
|||
data: "Hello, friends!",
|
||||
}),
|
||||
);
|
||||
const event = HTTP.toEvent(message) as CloudEvent;
|
||||
const event = HTTP.toEvent(message) as CloudEvent<string>;
|
||||
expect(event.validate()).to.be.true;
|
||||
});
|
||||
|
||||
|
@ -100,7 +154,7 @@ describe("HTTP transport", () => {
|
|||
[CONSTANTS.CE_HEADERS.ID]: "1234",
|
||||
[CONSTANTS.CE_HEADERS.SOURCE]: "test",
|
||||
[CONSTANTS.CE_HEADERS.TYPE]: "test.event",
|
||||
[CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1,
|
||||
[CONSTANTS.CE_HEADERS.SPEC_VERSION]: V1,
|
||||
"ce-LUNCH": "tacos",
|
||||
},
|
||||
};
|
||||
|
@ -109,7 +163,7 @@ describe("HTTP transport", () => {
|
|||
expect(event.LUNCH).to.equal("tacos");
|
||||
expect(function () {
|
||||
event.validate();
|
||||
}).to.throw("invalid attribute name: LUNCH");
|
||||
}).to.throw("invalid attribute name: \"LUNCH\"");
|
||||
});
|
||||
|
||||
it("Can detect CloudEvent binary Messages with weird versions", () => {
|
||||
|
@ -143,7 +197,7 @@ describe("HTTP transport", () => {
|
|||
// Allow for external systems to send bad events - do what we can
|
||||
// to accept them
|
||||
it("Does not throw an exception when converting an invalid Message to a CloudEvent", () => {
|
||||
const message: Message = {
|
||||
const message: Message<undefined> = {
|
||||
body: `"hello world"`,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
|
@ -183,10 +237,10 @@ describe("HTTP transport", () => {
|
|||
id,
|
||||
type,
|
||||
source,
|
||||
specversion: Version.V1,
|
||||
specversion: V1,
|
||||
data: { lunch: "tacos" },
|
||||
});
|
||||
const message: Message = {
|
||||
const message: Message<undefined> = {
|
||||
headers,
|
||||
body,
|
||||
};
|
||||
|
@ -196,7 +250,7 @@ describe("HTTP transport", () => {
|
|||
|
||||
describe("Specification version V1", () => {
|
||||
const fixture = new CloudEvent({
|
||||
specversion: Version.V1,
|
||||
specversion: V1,
|
||||
id,
|
||||
type,
|
||||
source,
|
||||
|
@ -214,7 +268,7 @@ describe("HTTP transport", () => {
|
|||
expect(message.body).to.equal(JSON.stringify(data));
|
||||
// validate all headers
|
||||
expect(message.headers[CONSTANTS.HEADER_CONTENT_TYPE]).to.equal(datacontenttype);
|
||||
expect(message.headers[CONSTANTS.CE_HEADERS.SPEC_VERSION]).to.equal(Version.V1);
|
||||
expect(message.headers[CONSTANTS.CE_HEADERS.SPEC_VERSION]).to.equal(V1);
|
||||
expect(message.headers[CONSTANTS.CE_HEADERS.ID]).to.equal(id);
|
||||
expect(message.headers[CONSTANTS.CE_HEADERS.TYPE]).to.equal(type);
|
||||
expect(message.headers[CONSTANTS.CE_HEADERS.SOURCE]).to.equal(source);
|
||||
|
@ -226,11 +280,11 @@ describe("HTTP transport", () => {
|
|||
});
|
||||
|
||||
it("Structured Messages can be created from a CloudEvent", () => {
|
||||
const message: Message = HTTP.structured(fixture);
|
||||
const message: Message<string> = HTTP.structured(fixture);
|
||||
expect(message.headers[CONSTANTS.HEADER_CONTENT_TYPE]).to.equal(CONSTANTS.DEFAULT_CE_CONTENT_TYPE);
|
||||
// Parse the message body as JSON, then validate the attributes
|
||||
const body = JSON.parse(message.body as string);
|
||||
expect(body[CONSTANTS.CE_ATTRIBUTES.SPEC_VERSION]).to.equal(Version.V1);
|
||||
expect(body[CONSTANTS.CE_ATTRIBUTES.SPEC_VERSION]).to.equal(V1);
|
||||
expect(body[CONSTANTS.CE_ATTRIBUTES.ID]).to.equal(id);
|
||||
expect(body[CONSTANTS.CE_ATTRIBUTES.TYPE]).to.equal(type);
|
||||
expect(body[CONSTANTS.CE_ATTRIBUTES.SOURCE]).to.equal(source);
|
||||
|
@ -263,21 +317,21 @@ describe("HTTP transport", () => {
|
|||
|
||||
it("Converts base64 encoded data to binary when deserializing structured messages", () => {
|
||||
const message = HTTP.structured(fixture.cloneWith({ data: imageData, datacontenttype: "image/png" }));
|
||||
const eventDeserialized = HTTP.toEvent(message) as CloudEvent;
|
||||
const eventDeserialized = HTTP.toEvent(message) as CloudEvent<Uint32Array>;
|
||||
expect(eventDeserialized.data).to.deep.equal(imageData);
|
||||
expect(eventDeserialized.data_base64).to.equal(image_base64);
|
||||
});
|
||||
|
||||
it("Does not parse binary data from structured messages with content type application/json", () => {
|
||||
const message = HTTP.structured(fixture.cloneWith({ data: dataBinary }));
|
||||
const eventDeserialized = HTTP.toEvent(message) as CloudEvent;
|
||||
const eventDeserialized = HTTP.toEvent(message) as CloudEvent<Uint32Array>;
|
||||
expect(eventDeserialized.data).to.deep.equal(dataBinary);
|
||||
expect(eventDeserialized.data_base64).to.equal(data_base64);
|
||||
});
|
||||
|
||||
it("Converts base64 encoded data to binary when deserializing binary messages", () => {
|
||||
const message = HTTP.binary(fixture.cloneWith({ data: imageData, datacontenttype: "image/png" }));
|
||||
const eventDeserialized = HTTP.toEvent(message) as CloudEvent;
|
||||
const eventDeserialized = HTTP.toEvent(message) as CloudEvent<Uint32Array>;
|
||||
expect(eventDeserialized.data).to.deep.equal(imageData);
|
||||
expect(eventDeserialized.data_base64).to.equal(image_base64);
|
||||
});
|
||||
|
@ -291,7 +345,7 @@ describe("HTTP transport", () => {
|
|||
|
||||
it("Does not parse binary data from binary messages with content type application/json", () => {
|
||||
const message = HTTP.binary(fixture.cloneWith({ data: dataBinary }));
|
||||
const eventDeserialized = HTTP.toEvent(message) as CloudEvent;
|
||||
const eventDeserialized = HTTP.toEvent(message) as CloudEvent<Uint32Array>;
|
||||
expect(eventDeserialized.data).to.deep.equal(dataBinary);
|
||||
expect(eventDeserialized.data_base64).to.equal(data_base64);
|
||||
});
|
||||
|
@ -299,7 +353,7 @@ describe("HTTP transport", () => {
|
|||
|
||||
describe("Specification version V03", () => {
|
||||
const fixture = new CloudEvent({
|
||||
specversion: Version.V03,
|
||||
specversion: V03,
|
||||
id,
|
||||
type,
|
||||
source,
|
||||
|
@ -317,7 +371,7 @@ describe("HTTP transport", () => {
|
|||
expect(message.body).to.equal(JSON.stringify(data));
|
||||
// validate all headers
|
||||
expect(message.headers[CONSTANTS.HEADER_CONTENT_TYPE]).to.equal(datacontenttype);
|
||||
expect(message.headers[CONSTANTS.CE_HEADERS.SPEC_VERSION]).to.equal(Version.V03);
|
||||
expect(message.headers[CONSTANTS.CE_HEADERS.SPEC_VERSION]).to.equal(V03);
|
||||
expect(message.headers[CONSTANTS.CE_HEADERS.ID]).to.equal(id);
|
||||
expect(message.headers[CONSTANTS.CE_HEADERS.TYPE]).to.equal(type);
|
||||
expect(message.headers[CONSTANTS.CE_HEADERS.SOURCE]).to.equal(source);
|
||||
|
@ -333,7 +387,7 @@ describe("HTTP transport", () => {
|
|||
expect(message.headers[CONSTANTS.HEADER_CONTENT_TYPE]).to.equal(CONSTANTS.DEFAULT_CE_CONTENT_TYPE);
|
||||
// Parse the message body as JSON, then validate the attributes
|
||||
const body = JSON.parse(message.body as string);
|
||||
expect(body[CONSTANTS.CE_ATTRIBUTES.SPEC_VERSION]).to.equal(Version.V03);
|
||||
expect(body[CONSTANTS.CE_ATTRIBUTES.SPEC_VERSION]).to.equal(V03);
|
||||
expect(body[CONSTANTS.CE_ATTRIBUTES.ID]).to.equal(id);
|
||||
expect(body[CONSTANTS.CE_ATTRIBUTES.TYPE]).to.equal(type);
|
||||
expect(body[CONSTANTS.CE_ATTRIBUTES.SOURCE]).to.equal(source);
|
||||
|
@ -368,14 +422,14 @@ describe("HTTP transport", () => {
|
|||
// Creating an event with binary data automatically produces base64 encoded data
|
||||
// which is then set as the 'data' attribute on the message body
|
||||
const message = HTTP.structured(fixture.cloneWith({ data: imageData, datacontenttype: "image/png" }));
|
||||
const eventDeserialized = HTTP.toEvent(message) as CloudEvent;
|
||||
const eventDeserialized = HTTP.toEvent(message) as CloudEvent<Uint32Array>;
|
||||
expect(eventDeserialized.data).to.deep.equal(imageData);
|
||||
expect(eventDeserialized.data_base64).to.equal(image_base64);
|
||||
});
|
||||
|
||||
it("Converts base64 encoded data to binary when deserializing binary messages", () => {
|
||||
const message = HTTP.binary(fixture.cloneWith({ data: imageData, datacontenttype: "image/png" }));
|
||||
const eventDeserialized = HTTP.toEvent(message) as CloudEvent;
|
||||
const eventDeserialized = HTTP.toEvent(message) as CloudEvent<Uint32Array>;
|
||||
expect(eventDeserialized.data).to.deep.equal(imageData);
|
||||
expect(eventDeserialized.data_base64).to.equal(image_base64);
|
||||
});
|
||||
|
|
|
@ -0,0 +1,309 @@
|
|||
/*
|
||||
Copyright 2021 The CloudEvents Authors
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
|
||||
import { expect } from "chai";
|
||||
import { CloudEvent, CONSTANTS, V1, Headers } from "../../src";
|
||||
import { asBase64 } from "../../src/event/validation";
|
||||
import { Message, MQTT, MQTTMessage } from "../../src/message";
|
||||
|
||||
const type = "org.cncf.cloudevents.example";
|
||||
const source = "urn:event:from:myapi/resource/123";
|
||||
const time = new Date().toISOString();
|
||||
const subject = "subject.ext";
|
||||
const dataschema = "http://cloudevents.io/schema.json";
|
||||
const datacontenttype = "application/json";
|
||||
const id = "b46cf653-d48a-4b90-8dfa-355c01061361";
|
||||
|
||||
interface Idata {
|
||||
foo: string
|
||||
}
|
||||
const data: Idata = {
|
||||
foo: "bar",
|
||||
};
|
||||
|
||||
const ext1Name = "extension1";
|
||||
const ext1Value = "foobar";
|
||||
const ext2Name = "extension2";
|
||||
const ext2Value = "acme";
|
||||
|
||||
// Binary data as base64
|
||||
const dataBinary = Uint8Array.from(JSON.stringify(data), (c) => c.codePointAt(0) as number);
|
||||
const data_base64 = asBase64(dataBinary);
|
||||
|
||||
// Since the above is a special case (string as binary), let's test
|
||||
// with a real binary file one is likely to encounter in the wild
|
||||
const imageData = new Uint8Array(fs.readFileSync(path.join(process.cwd(), "test", "integration", "ce.png")));
|
||||
const image_base64 = asBase64(imageData);
|
||||
|
||||
const PUBLISH = {"Content Type": "application/json; charset=utf-8"};
|
||||
|
||||
const fixture = new CloudEvent({
|
||||
specversion: V1,
|
||||
id,
|
||||
type,
|
||||
source,
|
||||
datacontenttype,
|
||||
subject,
|
||||
time,
|
||||
dataschema,
|
||||
data,
|
||||
[ext1Name]: ext1Value,
|
||||
[ext2Name]: ext2Value,
|
||||
});
|
||||
|
||||
describe("MQTT transport", () => {
|
||||
it("Handles events with no content-type and no datacontenttype", () => {
|
||||
const payload = "{Something[Not:valid}JSON";
|
||||
const userProperties = fixture.toJSON() as Headers;
|
||||
const message: MQTTMessage<string> = {
|
||||
PUBLISH: undefined, // no Content Type applied
|
||||
payload,
|
||||
"User Properties": userProperties,
|
||||
headers: userProperties,
|
||||
body: payload,
|
||||
};
|
||||
const event = MQTT.toEvent(message) as CloudEvent<undefined>;
|
||||
expect(event.data).to.equal(payload);
|
||||
expect(event.datacontentype).to.equal(undefined);
|
||||
});
|
||||
|
||||
it("Can detect invalid CloudEvent Messages", () => {
|
||||
// Create a message that is not an actual event
|
||||
const message: MQTTMessage<string> = {
|
||||
payload: "Hello world!",
|
||||
PUBLISH: {
|
||||
"Content type": "text/plain",
|
||||
},
|
||||
"User Properties": {},
|
||||
headers: {},
|
||||
body: undefined
|
||||
};
|
||||
expect(MQTT.isEvent(message)).to.be.false;
|
||||
});
|
||||
|
||||
it("Can detect valid CloudEvent Messages", () => {
|
||||
// Now create a message that is an event
|
||||
const message = MQTT.binary(
|
||||
new CloudEvent<Idata>({
|
||||
source: "/message-test",
|
||||
type: "example",
|
||||
data,
|
||||
}),
|
||||
);
|
||||
expect(MQTT.isEvent(message)).to.be.true;
|
||||
});
|
||||
|
||||
it("Handles CloudEvents with datacontenttype of text/plain", () => {
|
||||
const message: Message<string> = MQTT.binary(
|
||||
new CloudEvent({
|
||||
source: "/test",
|
||||
type: "example",
|
||||
datacontenttype: "text/plain",
|
||||
data: "Hello, friends!",
|
||||
}),
|
||||
);
|
||||
const event = MQTT.toEvent(message) as CloudEvent<string>;
|
||||
expect(event.data).to.equal(message.body);
|
||||
expect(event.validate()).to.be.true;
|
||||
});
|
||||
|
||||
it("Respects extension attribute casing (even if against spec)", () => {
|
||||
// Create a message that is an event
|
||||
const body = `{ "greeting": "hello" }`;
|
||||
const headers = {
|
||||
id: "1234",
|
||||
source: "test",
|
||||
type: "test.event",
|
||||
specversion: "1.0",
|
||||
LUNCH: "tacos",
|
||||
};
|
||||
const message: MQTTMessage<string> = {
|
||||
body,
|
||||
payload: body,
|
||||
PUBLISH,
|
||||
"User Properties": headers,
|
||||
headers
|
||||
};
|
||||
expect(MQTT.isEvent(message)).to.be.true;
|
||||
const event = MQTT.toEvent(message) as CloudEvent<string>;
|
||||
expect(event.LUNCH).to.equal("tacos");
|
||||
expect(function () {
|
||||
event.validate();
|
||||
}).to.throw("invalid attribute name: \"LUNCH\"");
|
||||
});
|
||||
|
||||
it("Can detect CloudEvent binary Messages with weird versions", () => {
|
||||
// Now create a message that is an event
|
||||
const body = `{ "greeting": "hello" }`;
|
||||
const headers = {
|
||||
id: "1234",
|
||||
source: "test",
|
||||
type: "test.event",
|
||||
specversion: "11.8",
|
||||
};
|
||||
const message: MQTTMessage<string> = {
|
||||
body,
|
||||
payload: body,
|
||||
PUBLISH,
|
||||
headers,
|
||||
"User Properties": headers,
|
||||
};
|
||||
expect(MQTT.isEvent(message)).to.be.true;
|
||||
const event = MQTT.toEvent(message) as CloudEvent;
|
||||
expect(event.specversion).to.equal("11.8");
|
||||
expect(event.validate()).to.be.false;
|
||||
});
|
||||
|
||||
it("Can detect CloudEvent structured Messages with weird versions", () => {
|
||||
// Now create a message that is an event
|
||||
const body = `{ "id": "123", "source": "test", "type": "test.event", "specversion": "11.8"}`;
|
||||
const message: MQTTMessage<string> = {
|
||||
body,
|
||||
payload: body,
|
||||
headers: {},
|
||||
PUBLISH: {"Content Type": CONSTANTS.MIME_CE_JSON},
|
||||
"User Properties": {}
|
||||
};
|
||||
expect(MQTT.isEvent(message)).to.be.true;
|
||||
expect(MQTT.toEvent(message)).not.to.throw;
|
||||
});
|
||||
|
||||
// Allow for external systems to send bad events - do what we can
|
||||
// to accept them
|
||||
it("Does not throw an exception when converting an invalid Message to a CloudEvent", () => {
|
||||
const body = `"hello world"`;
|
||||
const headers = {
|
||||
id: "1234",
|
||||
type: "example.bad.event",
|
||||
// no required source, thus an invalid event
|
||||
};
|
||||
const message: MQTTMessage<string> = {
|
||||
body,
|
||||
payload: body,
|
||||
PUBLISH,
|
||||
headers,
|
||||
"User Properties": headers,
|
||||
};
|
||||
const event = MQTT.toEvent(message) as CloudEvent;
|
||||
expect(event).to.be.instanceOf(CloudEvent);
|
||||
// ensure that we actually now have an invalid event
|
||||
expect(event.validate).to.throw;
|
||||
});
|
||||
|
||||
it("Does not allow an invalid CloudEvent to be converted to a Message", () => {
|
||||
const badEvent = new CloudEvent(
|
||||
{
|
||||
source: "/example.source",
|
||||
type: "", // type is required, empty string will throw with strict validation
|
||||
},
|
||||
false, // turn off strict validation
|
||||
);
|
||||
expect(() => {
|
||||
MQTT.binary(badEvent);
|
||||
}).to.throw;
|
||||
expect(() => {
|
||||
MQTT.structured(badEvent);
|
||||
}).to.throw;
|
||||
});
|
||||
|
||||
it("Binary Messages can be created from a CloudEvent", () => {
|
||||
const message: Message<Idata> = MQTT.binary(fixture);
|
||||
expect(message.body).to.equal(data);
|
||||
// validate all headers
|
||||
expect(message.headers.datacontenttype).to.equal(datacontenttype);
|
||||
expect(message.headers.specversion).to.equal(V1);
|
||||
expect(message.headers.id).to.equal(id);
|
||||
expect(message.headers.type).to.equal(type);
|
||||
expect(message.headers.source).to.equal(source);
|
||||
expect(message.headers.subject).to.equal(subject);
|
||||
expect(message.headers.time).to.equal(fixture.time);
|
||||
expect(message.headers.dataschema).to.equal(dataschema);
|
||||
expect(message.headers[ext1Name]).to.equal(ext1Value);
|
||||
expect(message.headers[ext2Name]).to.equal(ext2Value);
|
||||
});
|
||||
|
||||
it("Sets User Properties on binary messages", () => {
|
||||
const message: MQTTMessage<Idata> = MQTT.binary(fixture) as MQTTMessage<Idata>;
|
||||
expect(message.body).to.equal(data);
|
||||
// validate all headers
|
||||
expect(message["User Properties"]?.datacontenttype).to.equal(datacontenttype);
|
||||
expect(message["User Properties"]?.specversion).to.equal(V1);
|
||||
expect(message["User Properties"]?.id).to.equal(id);
|
||||
expect(message["User Properties"]?.type).to.equal(type);
|
||||
expect(message["User Properties"]?.source).to.equal(source);
|
||||
expect(message["User Properties"]?.subject).to.equal(subject);
|
||||
expect(message["User Properties"]?.time).to.equal(fixture.time);
|
||||
expect(message["User Properties"]?.dataschema).to.equal(dataschema);
|
||||
expect(message["User Properties"]?.[ext1Name]).to.equal(ext1Value);
|
||||
expect(message["User Properties"]?.[ext2Name]).to.equal(ext2Value);
|
||||
});
|
||||
|
||||
it("Structured Messages can be created from a CloudEvent", () => {
|
||||
const message = MQTT.structured(fixture) as MQTTMessage<string>;
|
||||
expect(message.PUBLISH?.["Content Type"]).to.equal(CONSTANTS.DEFAULT_CE_CONTENT_TYPE);
|
||||
expect(message.body).to.deep.equal(message.payload);
|
||||
expect(message.payload).to.deep.equal(fixture.toJSON());
|
||||
const body = message.body as Record<string, string>;
|
||||
expect(body[CONSTANTS.CE_ATTRIBUTES.SPEC_VERSION]).to.equal(V1);
|
||||
expect(body[CONSTANTS.CE_ATTRIBUTES.ID]).to.equal(id);
|
||||
expect(body[CONSTANTS.CE_ATTRIBUTES.TYPE]).to.equal(type);
|
||||
expect(body[CONSTANTS.CE_ATTRIBUTES.SOURCE]).to.equal(source);
|
||||
expect(body[CONSTANTS.CE_ATTRIBUTES.SUBJECT]).to.equal(subject);
|
||||
expect(body[CONSTANTS.CE_ATTRIBUTES.TIME]).to.equal(fixture.time);
|
||||
expect(body[CONSTANTS.STRUCTURED_ATTRS_1.DATA_SCHEMA]).to.equal(dataschema);
|
||||
expect(body[ext1Name]).to.equal(ext1Value);
|
||||
expect(body[ext2Name]).to.equal(ext2Value);
|
||||
});
|
||||
|
||||
it("A CloudEvent can be converted from a binary Message", () => {
|
||||
const message = MQTT.binary(fixture);
|
||||
const event = MQTT.toEvent(message);
|
||||
expect(event).to.deep.equal(fixture);
|
||||
});
|
||||
|
||||
it("A CloudEvent can be converted from a structured Message", () => {
|
||||
const message = MQTT.structured(fixture);
|
||||
const event = MQTT.toEvent(message);
|
||||
expect(event).to.deep.equal(fixture);
|
||||
});
|
||||
|
||||
it("Converts binary data to base64 when serializing structured messages", () => {
|
||||
const event = fixture.cloneWith({ data: imageData, datacontenttype: "image/png" });
|
||||
expect(event.data).to.equal(imageData);
|
||||
const message = MQTT.structured(event);
|
||||
expect((message.body as CloudEvent).data_base64).to.equal(image_base64);
|
||||
});
|
||||
|
||||
it("Converts base64 encoded data to binary when deserializing structured messages", () => {
|
||||
const message = MQTT.structured(fixture.cloneWith({ data: imageData, datacontenttype: "image/png" }));
|
||||
const eventDeserialized = MQTT.toEvent(message) as CloudEvent<Uint8Array>;
|
||||
expect(eventDeserialized.data).to.deep.equal(imageData);
|
||||
expect(eventDeserialized.data_base64).to.equal(image_base64);
|
||||
});
|
||||
|
||||
it("Converts base64 encoded data to binary when deserializing binary messages", () => {
|
||||
const message = MQTT.binary(fixture.cloneWith({ data: imageData, datacontenttype: "image/png" }));
|
||||
const eventDeserialized = MQTT.toEvent(message) as CloudEvent<Uint8Array>;
|
||||
expect(eventDeserialized.data).to.deep.equal(imageData);
|
||||
expect(eventDeserialized.data_base64).to.equal(image_base64);
|
||||
});
|
||||
|
||||
it("Keeps binary data binary when serializing binary messages", () => {
|
||||
const event = fixture.cloneWith({ data: dataBinary });
|
||||
expect(event.data).to.equal(dataBinary);
|
||||
const message = MQTT.binary(event);
|
||||
expect(message.body).to.equal(dataBinary);
|
||||
});
|
||||
|
||||
it("Does not parse binary data from binary messages with content type application/json", () => {
|
||||
const message = MQTT.binary(fixture.cloneWith({ data: dataBinary }));
|
||||
const eventDeserialized = MQTT.toEvent(message) as CloudEvent<Uint8Array>;
|
||||
expect(eventDeserialized.data).to.deep.equal(dataBinary);
|
||||
expect(eventDeserialized.data_base64).to.equal(data_base64);
|
||||
});
|
||||
});
|
|
@ -56,7 +56,7 @@ describe("JSON Event Format Parser", () => {
|
|||
const parser = new Parser();
|
||||
|
||||
// TODO: Should the parser catch the SyntaxError and re-throw a ValidationError?
|
||||
expect(parser.parse.bind(parser, payload)).to.throw(SyntaxError, "Unexpected token g in JSON at position 1");
|
||||
expect(parser.parse.bind(parser, payload)).to.throw(SyntaxError);
|
||||
});
|
||||
|
||||
it("Accepts a string as valid JSON", () => {
|
||||
|
@ -69,7 +69,6 @@ describe("JSON Event Format Parser", () => {
|
|||
|
||||
it("Must accept when the payload is a string well formed as JSON", () => {
|
||||
// setup
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
const payload = "{\"much\" : \"wow\"}";
|
||||
const parser = new Parser();
|
||||
|
||||
|
|
|
@ -5,11 +5,13 @@
|
|||
|
||||
import "mocha";
|
||||
import { expect } from "chai";
|
||||
import { CloudEvent, Version } from "../../src";
|
||||
import { CloudEvent, CloudEventV1, V1, V03 } from "../../src";
|
||||
|
||||
const fixture = {
|
||||
const fixture: CloudEventV1<undefined> = {
|
||||
id: "123",
|
||||
type: "org.cloudevents.test",
|
||||
source: "http://cloudevents.io",
|
||||
specversion: V1,
|
||||
};
|
||||
|
||||
describe("The SDK Requirements", () => {
|
||||
|
@ -19,19 +21,34 @@ describe("The SDK Requirements", () => {
|
|||
});
|
||||
|
||||
describe("v0.3", () => {
|
||||
it("should create an event using the right spec version", () => {
|
||||
it("should create an (invalid) event using the right spec version", () => {
|
||||
expect(
|
||||
new CloudEvent({
|
||||
...fixture,
|
||||
specversion: Version.V03,
|
||||
}).specversion,
|
||||
).to.equal(Version.V03);
|
||||
specversion: V03,
|
||||
}, false).specversion,
|
||||
).to.equal(V03);
|
||||
});
|
||||
});
|
||||
|
||||
describe("v1.0", () => {
|
||||
it("should create an event using the right spec version", () => {
|
||||
expect(new CloudEvent(fixture).specversion).to.equal(Version.V1);
|
||||
expect(new CloudEvent(fixture).specversion).to.equal(V1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Cloning events", () => {
|
||||
it("should clone simple objects that adhere to the CloudEventV1 interface", () => {
|
||||
const copy = CloudEvent.cloneWith(fixture, { id: "456" }, false);
|
||||
expect(copy.id).to.equal("456");
|
||||
expect(copy.type).to.equal(fixture.type);
|
||||
expect(copy.source).to.equal(fixture.source);
|
||||
expect(copy.specversion).to.equal(fixture.specversion);
|
||||
});
|
||||
|
||||
it("should clone simple objects with data that adhere to the CloudEventV1 interface", () => {
|
||||
const copy = CloudEvent.cloneWith(fixture, { data: { lunch: "tacos" } }, false);
|
||||
expect(copy.data.lunch).to.equal("tacos");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import "mocha";
|
||||
import { expect } from "chai";
|
||||
import { CloudEvent, Version, ValidationError } from "../../src";
|
||||
import { CloudEvent, V1, ValidationError } from "../../src";
|
||||
import { asBase64 } from "../../src/event/validation";
|
||||
import Constants from "../../src/constants";
|
||||
|
||||
|
@ -19,8 +19,8 @@ const data = {
|
|||
};
|
||||
const subject = "subject-x0";
|
||||
|
||||
let cloudevent = new CloudEvent({
|
||||
specversion: Version.V1,
|
||||
const cloudevent = new CloudEvent({
|
||||
specversion: V1,
|
||||
id,
|
||||
source,
|
||||
type,
|
||||
|
@ -99,6 +99,16 @@ describe("CloudEvents Spec v1.0", () => {
|
|||
it("should be ok when the type is an string converted from an object", () => {
|
||||
expect(cloudevent.cloneWith({ objectextension: JSON.stringify({ some: "object" }) }).validate()).to.equal(true);
|
||||
});
|
||||
|
||||
it("should only allow a-z|0-9 in the attribute names", () => {
|
||||
const testCases = [
|
||||
"an extension", "an_extension", "an-extension", "an.extension", "an+extension"
|
||||
];
|
||||
testCases.forEach((testCase) => {
|
||||
const evt = cloudevent.cloneWith({ [testCase]: "a value"}, false);
|
||||
expect(() => evt.validate()).to.throw(ValidationError);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("The Constraints check", () => {
|
||||
|
@ -110,8 +120,8 @@ describe("CloudEvents Spec v1.0", () => {
|
|||
});
|
||||
|
||||
it("defaut ID create when an empty string", () => {
|
||||
cloudevent = cloudevent.cloneWith({ id: "" });
|
||||
expect(cloudevent.id.length).to.be.greaterThan(0);
|
||||
const testEvent = cloudevent.cloneWith({ id: "" });
|
||||
expect(testEvent.id.length).to.be.greaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -150,11 +160,11 @@ describe("CloudEvents Spec v1.0", () => {
|
|||
describe("'time'", () => {
|
||||
it("must adhere to the format specified in RFC 3339", () => {
|
||||
const d = new Date();
|
||||
cloudevent = cloudevent.cloneWith({ time: d.toString() });
|
||||
const testEvent = cloudevent.cloneWith({ time: d.toString() }, false);
|
||||
// ensure that we always get back the same thing we passed in
|
||||
expect(cloudevent.time).to.equal(d.toString());
|
||||
expect(testEvent.time).to.equal(d.toString());
|
||||
// ensure that when stringified, the timestamp is in RFC3339 format
|
||||
expect(JSON.parse(JSON.stringify(cloudevent)).time).to.equal(new Date(d.toString()).toISOString());
|
||||
expect(JSON.parse(JSON.stringify(testEvent)).time).to.equal(new Date(d.toString()).toISOString());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -173,14 +183,60 @@ describe("CloudEvents Spec v1.0", () => {
|
|||
expect(typeof ce.data).to.equal("string");
|
||||
});
|
||||
|
||||
it("should be ok when type is 'Uint32Array' for 'Binary'", () => {
|
||||
const dataString = ")(*~^my data for ce#@#$%";
|
||||
const dataString = ")(*~^my data for ce#@#$%";
|
||||
const testCases = [
|
||||
{
|
||||
type: Int8Array,
|
||||
data: Int8Array.from(dataString, (c) => c.codePointAt(0) as number),
|
||||
expected: asBase64(Int8Array.from(dataString, (c) => c.codePointAt(0) as number))
|
||||
},
|
||||
{
|
||||
type: Uint8Array,
|
||||
data: Uint8Array.from(dataString, (c) => c.codePointAt(0) as number),
|
||||
expected: asBase64(Uint8Array.from(dataString, (c) => c.codePointAt(0) as number))
|
||||
},
|
||||
{
|
||||
type: Int16Array,
|
||||
data: Int16Array.from(dataString, (c) => c.codePointAt(0) as number),
|
||||
expected: asBase64(Int16Array.from(dataString, (c) => c.codePointAt(0) as number))
|
||||
},
|
||||
{
|
||||
type: Uint16Array,
|
||||
data: Uint16Array.from(dataString, (c) => c.codePointAt(0) as number),
|
||||
expected: asBase64(Uint16Array.from(dataString, (c) => c.codePointAt(0) as number))
|
||||
},
|
||||
{
|
||||
type: Int32Array,
|
||||
data: Int32Array.from(dataString, (c) => c.codePointAt(0) as number),
|
||||
expected: asBase64(Int32Array.from(dataString, (c) => c.codePointAt(0) as number))
|
||||
},
|
||||
{
|
||||
type: Uint32Array,
|
||||
data: Uint32Array.from(dataString, (c) => c.codePointAt(0) as number),
|
||||
expected: asBase64(Uint32Array.from(dataString, (c) => c.codePointAt(0) as number))
|
||||
},
|
||||
{
|
||||
type: Uint8ClampedArray,
|
||||
data: Uint8ClampedArray.from(dataString, (c) => c.codePointAt(0) as number),
|
||||
expected: asBase64(Uint8ClampedArray.from(dataString, (c) => c.codePointAt(0) as number))
|
||||
},
|
||||
{
|
||||
type: Float32Array,
|
||||
data: Float32Array.from(dataString, (c) => c.codePointAt(0) as number),
|
||||
expected: asBase64(Float32Array.from(dataString, (c) => c.codePointAt(0) as number))
|
||||
},
|
||||
{
|
||||
type: Float64Array,
|
||||
data: Float64Array.from(dataString, (c) => c.codePointAt(0) as number),
|
||||
expected: asBase64(Float64Array.from(dataString, (c) => c.codePointAt(0) as number))
|
||||
},
|
||||
];
|
||||
|
||||
const dataBinary = Uint32Array.from(dataString, (c) => c.codePointAt(0) as number);
|
||||
const expected = asBase64(dataBinary);
|
||||
|
||||
const ce = cloudevent.cloneWith({ datacontenttype: "text/plain", data: dataBinary });
|
||||
expect(ce.data_base64).to.equal(expected);
|
||||
testCases.forEach((test) => {
|
||||
it(`should be ok when type is '${test.type.name}' for 'Binary'`, () => {
|
||||
const ce = cloudevent.cloneWith({ datacontenttype: "text/plain", data: test.data });
|
||||
expect(ce.data_base64).to.equal(test.expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2016", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
|
||||
"target": "ES2020", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
|
||||
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
|
||||
"allowJs": true, /* Allow javascript files to be compiled. */
|
||||
"checkJs": false, /* Report errors in .js files. */
|
||||
|
@ -11,7 +11,8 @@
|
|||
"forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */
|
||||
"outDir": "./dist",
|
||||
"declaration": true,
|
||||
"experimentalDecorators": true
|
||||
"experimentalDecorators": true,
|
||||
"isolatedModules": true,
|
||||
},
|
||||
"compileOnSave": true,
|
||||
"include": [
|
||||
|
|
|
@ -1,9 +1,22 @@
|
|||
const path = require("path");
|
||||
const webpack = require("webpack");
|
||||
|
||||
module.exports = {
|
||||
entry: {
|
||||
"cloudevents": "./browser/index.js"
|
||||
},
|
||||
resolve: {
|
||||
fallback: {
|
||||
util: require.resolve("util/"),
|
||||
http: false,
|
||||
https: false
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
new webpack.ProvidePlugin({
|
||||
process: 'process/browser'
|
||||
})
|
||||
],
|
||||
output: {
|
||||
path: path.resolve(__dirname, "bundles"),
|
||||
filename: "[name].js",
|
||||
|
|
Loading…
Reference in New Issue