Compare commits

...

123 Commits
v4.0.2 ... main

Author SHA1 Message Date
Lucas Holmquist c23895145a
chore(main): release 10.0.0 (#615)
Signed-off-by: Lucas Holmquist <lholmqui@redhat.com>
2025-06-10 09:00:08 -04:00
Lucas Holmquist 3c8819e2bc
feat!: remove Node 18 support (#616)
This is a breaking change

Signed-off-by: Lucas Holmquist <lholmqui@redhat.com>
2025-06-05 12:57:05 -04:00
Lucas Holmquist beac7356a7
feat: add node 24 support (#614)
* feat: add node 24 support

Signed-off-by: Lucas Holmquist <lholmqui@redhat.com>

* squash: fixes for running tests

Signed-off-by: Lucas Holmquist <lholmqui@redhat.com>

---------

Signed-off-by: Lucas Holmquist <lholmqui@redhat.com>
2025-06-05 08:53:12 -04:00
Lucas Holmquist fc760976bf
chore(main): release 9.0.0 (#611)
Signed-off-by: Lucas Holmquist <lholmqui@redhat.com>
2025-04-09 08:31:17 -04:00
Lucas Holmquist 3ff6fdd3bf
feat!: remove node 16 (#610)
Signed-off-by: Lucas Holmquist <lholmqui@redhat.com>
2025-04-03 12:04:46 -04:00
Lucas Holmquist c07afa9b77
chore(main): release 8.0.3 (#606)
Signed-off-by: Lucas Holmquist <lholmqui@redhat.com>
2025-04-02 12:17:04 -04:00
Alex Tennant f475cdfd7e
fix: add generics to `Binding` type (#604)
Fixes: https://github.com/cloudevents/sdk-javascript/issues/487

Signed-off-by: Alex Tennant <atennant@skedulo.com>
2025-04-02 12:14:50 -04:00
Lucas Holmquist 8357719bab
chore(main): release 8.0.2 (#594)
Signed-off-by: Lucas Holmquist <lholmqui@redhat.com>
2024-07-22 13:26:06 -04:00
Calum Murray 6977113d7b
fix: creating an event does not error when the event attribute name is too long (#593)
* fix: creating an event does not error when the event attribute name is too long

* From the spec, the requirement for the length of attribute names is only a SHOULD, not a MUST. Currently, if the length is over 20 then the sdk throws an error, which I believe is incorrect because the length requirement is not a MUST.

Signed-off-by: Calum Murray <cmurray@redhat.com>

* fix(test): test expects not to throw

Signed-off-by: Calum Murray <cmurray@redhat.com>

---------

Signed-off-by: Calum Murray <cmurray@redhat.com>
2024-07-22 13:22:43 -04:00
Lucas Holmquist 154e913717
chore(main): release 8.0.1 (#574)
Signed-off-by: Lucas Holmquist <lholmqui@redhat.com>
2024-06-12 11:23:17 -04:00
Remi Cattiau e7626077ed
fix: allow Node 22 and use it by default (#587)
Signed-off-by: Remi Cattiau <remi@cattiau.com>
2024-06-12 07:04:01 -04:00
Rathan Kumar c65afe94d2
chore: updated check mark symbol to show some green checkboxes (#582)
Signed-off-by: Rathan Kumar <RathanKumar@users.noreply.github.com>
2024-04-16 09:56:25 -04:00
Tim Nunamaker 245bae92d1
chore: Update compatible node version (#573)
* chore: Update compatible node version

Signed-off-by: Tim Nunamaker <tim@vana.com>

* Add Node.js 21 to the CI matrix

Signed-off-by: Tim Nunamaker <tim@vana.com>

---------

Signed-off-by: Tim Nunamaker <tim@vana.com>
2023-12-05 08:36:13 -05:00
Doug Davis f93f896002
add link to our security mailing list (#568) 2023-10-17 10:49:20 -04:00
Doug Davis 16d0c0f91f
Governance docs per CE PR 1226 (#565)
Signed-off-by: Doug Davis <dug@microsoft.com>
2023-09-28 10:03:26 -04:00
Lucas Holmquist db60220762
chore(main): release 8.0.0 (#558)
* chore(main): release 8.0.0

Signed-off-by: Lucas Holmquist <lholmqui@redhat.com>


---------

Signed-off-by: Lucas Holmquist <lholmqui@redhat.com>
2023-07-24 14:15:23 -04:00
Lucas Holmquist 1ed43c8486
chore: release 8.0.0 (#563)
Release-As: 8.0.0

Signed-off-by: Lucas Holmquist <lholmqui@redhat.com>
2023-07-24 13:55:39 -04:00
Lance Ball df059e9696
BREAKING CHANGE!: use string instead of enum for Version (#562)
This is an empty commit that provides a reference to https://github.com/cloudevents/sdk-javascript/pull/561
which was not considered a breaking change by release-please.

Release-As: 8.0.0

Signed-off-by: Lance Ball <lball@redhat.com>
2023-07-20 13:48:21 -04:00
Lance Ball 15f6505a58
BREAKING CHANGE: use string instead of enum for `Version` (#561)
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.

Changing the `enum` to a string is pretty straightforward, but should be
considered a breaking change since TypeScript dependents will
potentially fail the build with a change like this.

Signed-off-by: Lance Ball <lball@redhat.com>
2023-07-19 10:53:03 -04:00
Lance Ball 089520a4cc
chore: modify release-please to use Signed-Off-By on commits (#559)
Signed-off-by: Lance Ball <lball@redhat.com>
2023-07-13 21:13:37 -04:00
Lance Ball fa388f7dc6
chore: add `npm run build:schema` to the doc generation action (#557)
The github action that generates the documentation fails currently due
to the fact that it's expecting the generated schema to exist, but it
does not exist unless it's explicitly built.

This commit adds `npm run build:schema` to the workflow action.

Signed-off-by: Lance Ball <lball@redhat.com>
2023-07-11 12:57:57 -04:00
github-actions[bot] 0d923a4f92
chore(main): release 7.0.2 (#555)
Signed-off-by: Lucas Holmquist <lholmqui@redhat.com>
Co-authored-by: Lucas Holmquist <lholmqui@redhat.com>
2023-07-11 11:54:47 -04:00
Lucas Holmquist a0d8682613
chore: add the provenance flag when publishing to npm (#556)
* This also splits the GH release and npm publish workflows

Signed-off-by: Lucas Holmquist <lholmqui@redhat.com>
2023-07-05 09:37:35 -04:00
Lucas Holmquist 023171d9a0
chore: fix the release-please automation script. (#554)
The id of release was left out, so the checks to see if there needs to be a publish were being skipped

Signed-off-by: Lucas Holmquist <lholmqui@redhat.com>
2023-05-31 11:54:34 -04:00
github-actions[bot] 3d961371da
chore(main): release 7.0.1 (#548)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-31 09:49:04 -04:00
Pieter Wigboldus (HckrNews) f3659ebfc6
chore: Update compatible node version (#552)
Signed-off-by: Pieter Wigboldus <pieter.wigboldus@pon.com>
Undo lock file

Co-authored-by: Pieter Wigboldus <pieter.wigboldus@pon.com>
2023-05-30 10:26:50 -04:00
Lucas Holmquist 11442d32d3
chore: remove old Node versions from the readme (#549)
Signed-off-by: Lucas Holmquist <lholmqui@redhat.com>
2023-05-16 16:14:18 -04:00
Lucas Holmquist 3931b224cb
chore: add publish automation (#550)
Signed-off-by: Lucas Holmquist <lholmqui@redhat.com>
2023-05-16 16:13:49 -04:00
Lance Ball 43c3584b98
fix: handle big integers in incoming events (#495)
* fix: handle big integers in incoming events

An event may have data that contains a BigInt. The builtin `JSON` parser
for JavaScript does not handle the `BigInt` types. The introduced
`json-bigint` dependency (34k) does.

Fixes: https://github.com/cloudevents/sdk-javascript/issues/489

Signed-off-by: Lance Ball <lball@redhat.com>
2023-05-10 12:45:43 -04:00
github-actions[bot] 1995b5778e
chore(main): release 7.0.0 (#547)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-03 12:13:52 -04:00
Lance Ball 0120d224ab
chore: release 7.0.0 (#546)
Release-As: 7.0.0

Signed-off-by: Lance Ball <lball@redhat.com>
2023-05-03 12:10:06 -04:00
Lucas Holmquist 2cb9364a25
feat!: remove node 12 and node 14 (#545)
* feat!: remove node 12 and node 14

Node 12 has been EOL since the end of April 2022 and Node 14 just became EOL at the end of April 2023

Signed-off-by: Lucas Holmquist <lholmqui@redhat.com>
2023-05-03 11:10:47 -04:00
Lance Ball 4626529d56
fixup: resolve package-lock.json conflict (#544)
Signed-off-by: Lance Ball <lball@redhat.com>
2023-05-03 09:29:55 -04:00
Lance Ball ec83abc827
chore: fix release-please-action (#543)
Signed-off-by: Lance Ball <lball@redhat.com>
2023-05-03 09:29:41 -04:00
sskular-wonderlab 1cf8f8acae
540 fix node version (#541)
* build(deps-dev): bump webpack from 5.75.0 to 5.76.0

Bumps [webpack](https://github.com/webpack/webpack) from 5.75.0 to 5.76.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.75.0...v5.76.0)

---
updated-dependencies:
- dependency-name: webpack
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore: include node v20.0.0 in package.json

Signed-off-by: Slavko Skular <slavko.skular@wonderlab-it.co.uk>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Slavko Skular <slavko.skular@wonderlab-it.co.uk>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-02 14:44:02 -04:00
Lucas Holmquist 343382ebde
chore: release 6.0.5 (#542)
Release-As: 6.0.5

Signed-off-by: Lucas Holmquist <lholmqui@redhat.com>
2023-05-02 14:38:31 -04:00
Lucas Holmquist c06ffc1963
chore: add the build script to the pretest script. (#539)
This small change allows a developer to just run npm install and then npm test without having to run the build step separately, which compiles the schema that is needed to run the tests successfully.

Signed-off-by: Lucas Holmquist <lholmqui@redhat.com>
2023-05-01 17:30:12 -04:00
Jordan Moore 7ff64f8b82
chore: Update CI action to node 18.x (#533)
* Update CI action to node 18.x

Signed-off-by: Jordan Moore <1930631+OneCricketeer@users.noreply.github.com>

* doc: update README

Signed-off-by: Jordan Moore <1930631+OneCricketeer@users.noreply.github.com>

---------

Signed-off-by: Jordan Moore <1930631+OneCricketeer@users.noreply.github.com>
2023-03-08 10:48:51 -05:00
Lucas Holmquist 870d2118cd 6.0.4 2023-02-16 11:28:58 -05:00
github-actions[bot] 78b8e0a372
chore: release 6.0.3 (#524)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-02-16 11:20:39 -05:00
Craig Dennis 953bc2a143 chore: Typos
Receive corrections

Signed-off-by: Lucas Holmquist <lholmqui@redhat.com>
2023-02-16 11:15:11 -05:00
viraj-s15 bc3aaca2ef chore: added the engines property to the package.json
Signed-off-by: viraj-s15 <veer15102003@gmail.com>

Co-authored-by: Lance Ball <lball@redhat.com>
Signed-off-by: Lucas Holmquist <lholmqui@redhat.com>
2023-02-16 11:10:45 -05:00
Lucas Holmquist e5ee8369ba
fix: This fixes bug #525 where the browser version was breaking becuase of process not being found. (#526)
fixes #525

Signed-off-by: Lucas Holmquist <lholmqui@redhat.com>
2023-02-12 15:25:38 -05:00
Xavier Serrano b374d9ac33
chore(examples): add mqtt example (#523)
Signed-off-by: Xavier Serrano <zombispormedio007@gmail.com>
2023-02-06 12:25:35 -05:00
dependabot[bot] 64e527c120
build(deps): bump cookiejar from 2.1.3 to 2.1.4 (#521)
Bumps [cookiejar](https://github.com/bmeck/node-cookiejar) from 2.1.3 to 2.1.4.
- [Release notes](https://github.com/bmeck/node-cookiejar/releases)
- [Commits](https://github.com/bmeck/node-cookiejar/commits)

---
updated-dependencies:
- dependency-name: cookiejar
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-06 12:22:49 -05:00
dependabot[bot] 1b449c4c9a
build(deps): bump http-cache-semantics from 4.1.0 to 4.1.1 (#522)
Bumps [http-cache-semantics](https://github.com/kornelski/http-cache-semantics) from 4.1.0 to 4.1.1.
- [Release notes](https://github.com/kornelski/http-cache-semantics/releases)
- [Commits](https://github.com/kornelski/http-cache-semantics/compare/v4.1.0...v4.1.1)

---
updated-dependencies:
- dependency-name: http-cache-semantics
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-06 12:22:30 -05:00
dependabot[bot] eccc00ee67
build(deps): bump json5 from 2.2.0 to 2.2.3 (#520)
Bumps [json5](https://github.com/json5/json5) from 2.2.0 to 2.2.3.
- [Release notes](https://github.com/json5/json5/releases)
- [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md)
- [Commits](https://github.com/json5/json5/compare/v2.2.0...v2.2.3)

---
updated-dependencies:
- dependency-name: json5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-10 16:32:57 -05:00
dependabot[bot] 94f1a3d470
build(deps): bump qs and formidable (#518)
Bumps [qs](https://github.com/ljharb/qs) and [formidable](https://github.com/node-formidable/formidable). These dependencies needed to be updated together.

Updates `qs` from 6.10.2 to 6.11.0
- [Release notes](https://github.com/ljharb/qs/releases)
- [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/qs/compare/v6.10.2...v6.11.0)

Updates `formidable` from 2.0.1 to 2.1.1
- [Release notes](https://github.com/node-formidable/formidable/releases)
- [Changelog](https://github.com/node-formidable/formidable/blob/master/CHANGELOG.md)
- [Commits](https://github.com/node-formidable/formidable/commits)

---
updated-dependencies:
- dependency-name: qs
  dependency-type: indirect
- dependency-name: formidable
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-14 12:50:46 -05:00
github-actions[bot] 3619ef2bbd
chore: release 6.0.3 (#503)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-11-08 14:33:50 -05:00
Philip Sanetra 2d5fab1b71
fix: Make CloudEvent data field immutable and enumerable using Object.keys() (#515) (#516)
Signed-off-by: Philip Sanetra <code@psanetra.de>
2022-11-01 12:46:26 -04:00
Lance Ball c09a9cc20a
chore: bump cucumber to full release version (#514) 2022-10-26 20:28:13 -04:00
Lance Ball 4831e6a1a5
chore: bump mocha to 10.1.0 (#512)
Duplicates https://github.com/cloudevents/sdk-javascript/pull/510

Severity                   | Priority Score (*)                   | Issue                   | Breaking Change                   | Exploit Maturity
:-------------------------:|-------------------------|:-------------------------|:-------------------------|:-------------------------
![high severity](https://res.cloudinary.com/snyk/image/upload/w_20,h_20/v1561977819/icon/h.png "high severity")  |  **589/1000**  <br/> **Why?** Has a fix available, CVSS 7.5  | Regular Expression Denial of Service (ReDoS) <br/>[SNYK-JS-MOCHA-2863123](https://snyk.io/vuln/SNYK-JS-MOCHA-2863123) |  Yes  | No Known Exploit

Signed-off-by: Lance Ball <lball@redhat.com>

Signed-off-by: Lance Ball <lball@redhat.com>
2022-10-24 16:11:56 -04:00
Lance Ball 760a024067
chore: bump webpack to 5.74.0 (#509)
Signed-off-by: Lance Ball <lball@redhat.com>
2022-10-19 13:32:46 -04:00
dependabot[bot] c282922ef9
build(deps): bump terser from 5.10.0 to 5.14.2 (#505)
Bumps [terser](https://github.com/terser/terser) from 5.10.0 to 5.14.2.
- [Release notes](https://github.com/terser/terser/releases)
- [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/terser/terser/commits)

---
updated-dependencies:
- dependency-name: terser
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-02 17:03:13 -04:00
Lance Ball ea94a4d779
fix: improve validation on extension attribute (#502)
* fix: improve validation on extension attribute

Fixes: https://github.com/cloudevents/sdk-javascript/issues/500

Adds a regular expression check to the attribute name validation code to
ensure that attribute names only use a-z0-9 (except for `data_base64`,
which apparently is an exception to the rule.

Signed-off-by: Lance Ball <lball@redhat.com>
2022-06-22 15:27:41 -04:00
github-actions[bot] 847f6bfcc7
chore: release 6.0.2 (#497)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-06-21 17:12:15 -04:00
snyk-bot ed63f14339
fix: package.json & package-lock.json to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-GOT-2932019
2022-06-19 23:49:48 +00:00
Lance Ball 921e273ede
fix: allow `TypedArray` for binary data (#494)
* fix: allow `Uint16|8Array` for binary data

Previously we only considered `Uint32Array` binary data. This was an
oversight. This fixes that issue.

Fixes: https://github.com/cloudevents/sdk-javascript/issues/491

Signed-off-by: Lance Ball <lball@redhat.com>
2022-06-15 00:51:36 -04:00
Grant Timmerman a62eb44669
chore: update owners (#499)
* chore: update owners

Signed-off-by: Grant Timmerman <744973+grant@users.noreply.github.com>
2022-06-15 00:50:18 -04:00
Lance Ball d6f52ca65f
fix: HTTP headers for extensions with false values (#493)
* fix: HTTP headers for extensions with false values

CloudEvent objects may include extensions that have a defined key and a
`false` value. This change ensures that HTTP messages for CloudEvents
containing these extension values include the appropriate headers.

Signed-off-by: Lance Ball <lball@redhat.com>
2022-06-14 17:44:06 -04:00
Lance Ball ce02e0a1f3
chore: bump ajv and remove old dep dependency (#496)
* chore: bump ajv and remove old dep dependency

This should allow the existing, meaningful PRs to pass CI validation if they get a rebase.

Signed-off-by: Lance Ball <lball@redhat.com>
2022-06-14 17:30:29 -04:00
dependabot[bot] d9ee0e05d1
build(deps): bump minimist from 1.2.5 to 1.2.6 (#486)
Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6.
- [Release notes](https://github.com/substack/minimist/releases)
- [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6)

---
updated-dependencies:
- dependency-name: minimist
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-10 12:00:21 -04:00
Jason Lock addbd9acf1
Fix typo in README (#488)
Signed-off-by: Jason Lock <jasonlock@Jasons-MacBook-Pro.local>
2022-05-10 11:59:56 -04:00
github-actions[bot] a512aad5d5
chore: release 6.0.1 (#485)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-03-21 15:40:44 -04:00
Lance Ball c0b1f7705a
chore: update dependencies to inlude ajv-formats (#484)
In this PR https://github.com/cloudevents/sdk-javascript/pull/471/files#diff-7ae45ad102eab3b6d7e7896acd08c427a9b25b346470d7bc6507b6481575d519R128
it was not noticed that the addition of `ajv-formats` to devDependencies
is insufficient. Unfortunately, users of v6.0.0 will receive an error when
using the module, unless they explicitly install `ajv-formats` in their
project. This commit fixes that, and should result in an immediate release
of version 6.0.1.

Signed-off-by: Lance Ball <lball@redhat.com>
2022-03-21 15:38:31 -04:00
github-actions[bot] 0164f72eaa
chore: release 6.0.0 (#482)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-03-21 14:19:43 -04:00
Lance Ball 4ab6356bd7
docs: update readme to include http builtin transport (#483)
Signed-off-by: Lance Ball <lball@redhat.com>
2022-03-21 08:40:16 -04:00
Lance Ball 0362a4f11c
feat!: add http transport and remove axios (#481)
* feat: add builtin HTTP emitter

Adds a builtin HTTP event emitter that can be used with `emitterFor()`
to send events over HTTP without pulling in any additional dependencies.

In the past we chose to keep this in our code base by considering axios a
peer dependency - users were required to include it in their projects
explicitly. In working on the HTTP emitter, it became more and more
apparent that the axios emitter was probably no longer needed, and in fact
I doubt it was really used at all. To use it, users would have been required
to do this, since it isn't exported at the top level.

const { axiosEmitter } = require("cloudevents/transport/http");

Based on this, I think the usage in the wild is probably very minimal,
and I like the idea of eliminating this dependency.

Signed-off-by: Lance Ball <lball@redhat.com>
2022-03-18 13:36:12 -04:00
Lance Ball b4d7aa9adb
chore: add an npm test:once script (#480)
Adds a convenience npm script that allows you to run only a single test
file at a time. Example usage:

```
sdk-javascript on  lance/update-deps-package-json [!?] is 📦 v5.3.2 via  v16.14.0
❯ npm run test:one

> cloudevents@5.3.2 test:one
> mocha --require ts-node/register

Error: No test files found

sdk-javascript on  lance/update-deps-package-json [!?] is 📦 v5.3.2 via  v16.14.0
❯ npm run test:one -- ./test/integration/batch_test.ts

> cloudevents@5.3.2 test:one
> mocha --require ts-node/register "./test/integration/batch_test.ts"

  A batched CloudEvent message over HTTP
    ✔ Can be created with a typed Message

  A batched CloudEvent message over Kafka
    ✔ Can be created with a typed Message

  2 passing (5ms)

sdk-javascript on  lance/update-deps-package-json [!?] is 📦 v5.3.2 via  v16.14.0
```

Signed-off-by: Lance Ball <lball@redhat.com>
2022-03-17 23:10:30 -04:00
Lance Ball 6204805bfc
chore: update package.json format and deps (#479)
Signed-off-by: Lance Ball <lball@redhat.com>
2022-03-17 23:09:29 -04:00
Lance Ball ae8cb96f8a
doc: update maintainers in README.md (#478)
This commit modifies the structure of the README.md to add a markdown
heading. This change is motiviated by the CLO tool provided by CNCF.
This repository is failing a check for maintainers as you can see on
the dashboard:
https://clomonitor.io/projects/cloudevents/cloudevents#sdk-javascript

Documentation for this change can be found here:
https://github.com/cncf/clomonitor/blob/main/docs/checks.md#maintainers

Signed-off-by: Lance Ball <lball@redhat.com>
2022-03-17 13:21:48 -04:00
Lance Ball c420da4793
chore: update the release documentation (#476)
It has been out of date since we switched to using release-please
for our release automation.

Fixes: https://github.com/cloudevents/sdk-javascript/issues/474

Signed-off-by: Lance Ball <lball@redhat.com>
2022-02-16 12:09:45 -05:00
Lance Ball b13bde9b49
feat: precompile cloudevent schema (#471)
* feat: precompile cloudevent schema

This commit modifies the build pipleline so that the cloudevent schema is
precompiled for runtime validation. This eliminates the need to compile the
schema at runtime, improving both performance and security.

Fixes: https://github.com/cloudevents/sdk-javascript/issues/423

Signed-off-by: Lance Ball <lball@redhat.com>
2022-02-15 14:06:42 -05:00
dependabot[bot] 4d8f03f7c6
build(deps): bump follow-redirects from 1.14.7 to 1.14.8 (#473)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.7 to 1.14.8.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.7...v1.14.8)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-14 15:26:46 -05:00
github-actions[bot] 9046b369cf
chore: release 5.3.2 (#470)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-02-11 15:42:37 -08:00
Lance Ball c3d9f39a53
chore: bump typedoc to remove vuln (#472) 2022-02-10 22:25:48 -06:00
Lance Ball b5c0b56f52
fix: use `isolatedModules: true` in tsconfig.json (#469)
* fix: use `isolatedModules: true` in tsconfig.json

This setting ensures the module can be used in projects where the workflow
includes type checking and transpilation as two separate steps.

See: https://ncjamieson.com/dont-export-const-enums/

Fixes: https://github.com/cloudevents/sdk-javascript/issues/456

Signed-off-by: Lance Ball <lball@redhat.com>
2022-02-10 08:39:09 -05:00
github-actions[bot] f36a1f0428
chore: release 5.3.1 (#466)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-02-03 13:45:21 -05:00
Lance Ball cd4dea954b
fix: improve binary data detection in HTTP transport (#468) 2022-02-02 07:18:08 -05:00
Lucas Holmquist 8abbc114af
chore: update readme with current Node LTS versions and add Node 16 to the testing matrix(#465)
* chore: update readme with current Node LTS versions

* squash: add node 16 to action matrix

Signed-off-by: Lucas Holmquist <lholmqui@redhat.com>
2022-01-27 09:43:42 -05:00
Grant Timmerman 349b84c3da
docs: fix ts example (#467)
* docs: fix ts example

Signed-off-by: Grant Timmerman <timmerman+devrel@google.com>

* docs: fix ts example

Signed-off-by: Grant Timmerman <timmerman+devrel@google.com>

* docs: update quote mark

Signed-off-by: Grant Timmerman <timmerman+devrel@google.com>
2022-01-26 22:50:36 -06:00
David Pereira c603831e93
docs: add TS examples for CloudEvent usage (#461)
* docs: add TS examples for CloudEvent usage

Co-authored-by: Lance Ball <lball@redhat.com>
2022-01-26 16:54:49 -05:00
Snyk bot ae8fa799af
fix: package.json & package-lock.json to reduce vulnerabilities (#462)
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-NANOID-2332193

Signed-off-by: Lucas Holmquist <lholmqui@redhat.com>
2022-01-24 18:49:01 -05:00
dependabot[bot] 225836f68f
build(deps): bump follow-redirects from 1.14.6 to 1.14.7 (#460)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.6 to 1.14.7.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.6...v1.14.7)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-21 17:05:05 -05:00
github-actions[bot] 98009d910d
chore: release 5.3.0 (#458)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-01-18 14:07:12 -05:00
Lance Ball 591d133f31
feat: add MQTT transport messaging (#459)
Add MQTT as a `Message` format.

This commit adds `MQTT` to the supported transport protocols by adding a `Binding` and the `MQTTMessage<T>` type, extending the base `Message` type, adding the MQTT fields for `payload`, `PUBLISH` and `User Properties`. The `payload` field directly maps to `Message#body`, while `User Properties` roughly maps to `Message#headers`, even though the properties here are not formatted with a `ce-` prefix like other transport protocols. This is per the spec. See: https://github.com/cloudevents/spec/blob/v1.0.1/mqtt-protocol-binding.md. 

Signed-off-by: Lance Ball <lball@redhat.com>
2022-01-14 11:41:15 -05:00
Lance Ball 5d1f744f50
feat: add support for kafka transport (#455)
This commit extends the `message` package to include Kafka transport.

Additionally, some of the type information has changed across the project
to more accurately reflect the type of `Message` (by including `T`).

Related: https://github.com/cloudevents/sdk-javascript/issues/390
Signed-off-by: Lance Ball <lball@redhat.com>
2022-01-07 16:14:09 -05:00
Lance Ball 2ac731eb88
chore(refactor): prefer interfaces over concrete classes (#457)
* chore(refactor): protocol bindings use interfaces

This change modifies the protocol binding interfaces such as `Binding`,
`Serializer` and the like to use the `CloudEventV1` interface instead of the
implementation class `CloudEvent`. This should make extending the interfaces
simpler as this work has grown out of efforts around the implementation of
a second transport interface, Kafka.

See: https://github.com/cloudevents/sdk-javascript/pull/455/

This commit also includes the addition of a generic type to the `Message`
interface, defaulting to `string`.

There is also some minor clean up involving what is exported from the
`message/http` modules. Now, instead of exporting the entire implementation,
only the `HTTP` binding implementation is exported, and it is then reexported
by `message`.

Also, a static `CloudEvent.cloneWith()` method has been added which the
instance methods now use.

Signed-off-by: Lance Ball <lball@redhat.com>

* fixup: make the `cloneWith()` method is dependent on interfaces

Signed-off-by: Lance Ball <lball@redhat.com>

* fixup: remove unnecessary cast

Signed-off-by: Lance Ball <lball@redhat.com>
2022-01-07 14:24:56 -05:00
Lance Ball 320354f750
chore: update cucumber dependency and remove prettier (#453)
The combination of prettier and eslint was causing some conflicting error
messages in formatting between VSCode and using npm in the CLI. For the most
part, there were only a couple of required formatting changes that prettier
was covering, so the change is minor.

The cucumber dependency had a major version bump and was carrying some unsafe
dependencies in the older version. This commit bumps to the new version and
makes appropriate configuration changes.

Signed-off-by: Lance Ball <lball@redhat.com>
2021-12-22 10:45:35 -05:00
github-actions[bot] d4cb42f94b
chore: release 5.2.0 (#451)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2021-12-07 18:05:22 -05:00
Lance Ball 9a46e335f5
feat: add batch mode (#448)
Adds a batched content mode for incoming events.

```js
// It's possible for this to return 1:N events
const ceArray = HTTP.toEvent(req.headers, req.body);
```

Signed-off-by: Lance Ball <lball@redhat.com>
2021-12-07 15:36:10 -05:00
github-actions[bot] d7e1c4178a
chore: release 5.1.0 (#449)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2021-12-02 08:36:34 -05:00
Snyk bot 0f5a4c0de2
fix: package.json & package-lock.json to reduce vulnerabilities (#439)
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-ANSIREGEX-1583908

Signed-off-by: Lucas Holmquist <lholmqui@redhat.com>
2021-12-01 09:55:58 -05:00
Lance Ball d941e2d4d9
feat: use generic type for CloudEvent data (#446)
Instead of using a big union of types, use a generic type for event data.

Fixes: https://github.com/cloudevents/sdk-javascript/issues/445

Signed-off-by: Lance Ball <lball@redhat.com>
2021-11-29 17:03:12 -05:00
Lance Ball 52ea7de80d
fix: do not assume an empty content-type header is JSON (#444)
The parser for HTTP binary made the assumption that if there was no `content-type`
header in the incoming message, it should inject `application/json`. Discussion
about the rationale for this is in https://github.com/cloudevents/sdk-javascript/issues/441.

This commit, removes that injection and adds a test to ensure the bytes are
simply not parsed, but just passed along untouched.

Fixes: https://github.com/cloudevents/sdk-javascript/issues/441

Signed-off-by: Lance Ball <lball@redhat.com>
2021-11-24 11:02:50 -05:00
Lance Ball b4266b1f37
chore: add test for text/plain data (#442)
Signed-off-by: Lance Ball <lball@redhat.com>
2021-11-23 18:05:15 -05:00
github-actions[bot] f7b2840f82
chore: release 5.0.0 (#430)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2021-10-05 13:21:01 -04:00
Grant Timmerman b38a48fa59
feat: add native logging with headers and body to CloudEvent (#437)
* feat: add native logging with headers and body to CloudEvent

Signed-off-by: Grant Timmerman <timmerman+devrel@google.com>

* ci: simplify

Signed-off-by: Grant Timmerman <timmerman+devrel@google.com>

* ci: simplify

Signed-off-by: Grant Timmerman <timmerman+devrel@google.com>

* ci: returns to return

Signed-off-by: Grant Timmerman <timmerman+devrel@google.com>

* ci: Update test/integration/cloud_event_test.ts

Co-authored-by: Lance Ball <lball@redhat.com>

* ci: revert symbol change

Signed-off-by: Grant Timmerman <timmerman+devrel@google.com>

* refactor: improve ts for toString

Signed-off-by: Grant Timmerman <timmerman+devrel@google.com>

Co-authored-by: Lance Ball <lball@redhat.com>
2021-10-04 11:11:38 -07:00
Lance Ball 2ff7852c36
fix: ensure source property has min length of 1 (#438)
Fixes: https://github.com/cloudevents/sdk-javascript/issues/383

Signed-off-by: Lance Ball <lball@redhat.com>
2021-09-29 09:55:37 -04:00
Snyk bot 2dc846c659
fix: package.json & package-lock.json to reduce vulnerabilities (#436)
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-ANSIREGEX-1583908

Signed-off-by: Lucas Holmquist <lholmqui@redhat.com>
2021-09-17 12:09:18 -04:00
Snyk bot 8814919923
fix: package.json & package-lock.json to reduce vulnerabilities (#434)
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-ANSIREGEX-1583908

Signed-off-by: Lucas Holmquist <lholmqui@redhat.com>
2021-09-17 12:08:59 -04:00
Lucas Holmquist a7db466c6e
chore: remove node 10 from ci (#435)
Signed-off-by: Lucas Holmquist <lholmqui@redhat.com>
2021-09-17 11:02:08 -04:00
Snyk bot cf47248d25
fix: package.json & package-lock.json to reduce vulnerabilities (#433)
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-AXIOS-1579269

Signed-off-by: Lucas Holmquist <lholmqui@redhat.com>
2021-09-13 11:33:16 -04:00
Lucas Holmquist 272bcea2d8
fix: update express example with framework features. (#429)
fixes #379

Signed-off-by: Lucas Holmquist <lholmqui@redhat.com>
2021-08-30 16:46:12 -04:00
dependabot[bot] 1ceed024f8
build(deps): bump path-parse from 1.0.6 to 1.0.7 (#431)
Bumps [path-parse](https://github.com/jbgutierrez/path-parse) from 1.0.6 to 1.0.7.
- [Release notes](https://github.com/jbgutierrez/path-parse/releases)
- [Commits](https://github.com/jbgutierrez/path-parse/commits/v1.0.7)

---
updated-dependencies:
- dependency-name: path-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-08-27 10:37:19 -04:00
Lance Ball 2bd9a5a1e4
src!: remove support for 0.3 events (#425)
It has been nearly two years since 1.0 became final. This change removes
support for 0.3 events in the interest of simplifying the project a little.

Signed-off-by: Lance Ball <lball@redhat.com>
2021-08-27 10:34:32 -04:00
dependabot[bot] 36f5e2b5f8
build(deps): bump path-parse from 1.0.6 to 1.0.7 (#428)
Bumps [path-parse](https://github.com/jbgutierrez/path-parse) from 1.0.6 to 1.0.7.
- [Release notes](https://github.com/jbgutierrez/path-parse/releases)
- [Commits](https://github.com/jbgutierrez/path-parse/commits/v1.0.7)

---
updated-dependencies:
- dependency-name: path-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-08-26 07:54:43 -04:00
Lance Ball 2118488a14
chore: use git submodules for conformance tests (#427)
I don't think downloading to `/tmp` for each `npm test` is such a great
idea. This does mean that contributors to this repo will need to run the
following command once on their clone after this commit lands.

```
git submodule init
git submodule update
```

Signed-off-by: Lance Ball <lball@redhat.com>
2021-08-05 09:47:43 -04:00
Lance Ball 061c122b86
chore: update eslint and prettier dependencies (#424)
There were some minor changes that resulted in a few code style changes, but not much.

Signed-off-by: Lance Ball <lball@redhat.com>
2021-08-04 15:51:37 -04:00
github-actions[bot] b5100566c6
chore: release 4.0.3 (#412)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2021-07-06 14:34:19 -04:00
dependabot[bot] 5cbe1783fe
build(deps): bump set-getter from 0.1.0 to 0.1.1 (#422)
Bumps [set-getter](https://github.com/doowb/set-getter) from 0.1.0 to 0.1.1.
- [Release notes](https://github.com/doowb/set-getter/releases)
- [Commits](https://github.com/doowb/set-getter/commits/0.1.1)

---
updated-dependencies:
- dependency-name: set-getter
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-07-06 11:10:10 -04:00
dependabot[bot] b3d9cd4585
build(deps): bump browserslist from 4.14.7 to 4.16.6 (#421)
Bumps [browserslist](https://github.com/browserslist/browserslist) from 4.14.7 to 4.16.6.
- [Release notes](https://github.com/browserslist/browserslist/releases)
- [Changelog](https://github.com/browserslist/browserslist/blob/main/CHANGELOG.md)
- [Commits](https://github.com/browserslist/browserslist/compare/4.14.7...4.16.6)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-07-06 11:09:55 -04:00
Lance Ball 22e42ddb80
fix: do not modify incoming event's specversion (#419)
Even if the specversion is totally invalid, we should not change the value
received in an incoming `Message`. Previously we defaulted to 1.0 if we did
not recognize the version number. This commit changes that, leaving the value
unmodified. We default to parsing this mystery event with the 1.0 spec. When
the event is validated with `event.validate()` we return `false`.

One additional small change to eliminate a prettier warning about `parer`
being previously declared.

Fixes: https://github.com/cloudevents/sdk-javascript/issues/332
Fixes: https://github.com/cloudevents/sdk-javascript/issues/333

Signed-off-by: Lance Ball <lball@redhat.com>
2021-05-25 11:10:46 -04:00
Lance Ball 7c05adee7b
fix: do not modify incoming event's specversion (#419)
Even if the specversion is totally invalid, we should not change the value
received in an incoming `Message`. Previously we defaulted to 1.0 if we did
not recognize the version number. This commit changes that, leaving the value
unmodified. We default to parsing this mystery event with the 1.0 spec. When
the event is validated with `event.validate()` we return `false`.

One additional small change to eliminate a prettier warning about `parer`
being previously declared.

Fixes: https://github.com/cloudevents/sdk-javascript/issues/332
Fixes: https://github.com/cloudevents/sdk-javascript/issues/333

Signed-off-by: Lance Ball <lball@redhat.com>
2021-05-25 11:10:20 -04:00
Lance Ball 7f6b658858
fix: throw on validation if extensions are improperly named (#420)
Also fixes the case where UPPERCASED extension names were silently changed
to lowercase and then set as undefined. Even though uppercased extension
names are invalid, we should still accept them in incoming messsages and
only throw when validating the event.

Fixes: https://github.com/cloudevents/sdk-javascript/issues/380

Signed-off-by: Lance Ball <lball@redhat.com>
2021-05-24 14:53:07 -04:00
Remi Cattiau 80d987c1f6
chore: add copyrights header and lint rules (#418)
Signed-off-by: Remi Cattiau <remi@cattiau.com>
2021-05-14 09:28:49 -04:00
Lance Ball db4be6b1da
src: be more forgiving parsing JSON as a string (#417)
* src: be more forgiving parsing JSON as a string

A simple string is considered valid JSON. However, our parsers do
not accept that unless the string has quotation marks. This commit
modifies the parser to look for strings declared as application/json
which do not begin with '[' '{' or '"' and surrounds them with
quotes.

Signed-off-by: Lance Ball <lball@redhat.com>
2021-05-13 12:46:36 -04:00
dependabot[bot] e06147b9de
build(deps): bump handlebars from 4.7.6 to 4.7.7 (#414)
Bumps [handlebars](https://github.com/wycats/handlebars.js) from 4.7.6 to 4.7.7.
- [Release notes](https://github.com/wycats/handlebars.js/releases)
- [Changelog](https://github.com/handlebars-lang/handlebars.js/blob/master/release-notes.md)
- [Commits](https://github.com/wycats/handlebars.js/compare/v4.7.6...v4.7.7)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-13 10:08:31 -04:00
dependabot[bot] 6d7fb24636
build(deps): bump underscore from 1.11.0 to 1.13.1 (#413)
Bumps [underscore](https://github.com/jashkenas/underscore) from 1.11.0 to 1.13.1.
- [Release notes](https://github.com/jashkenas/underscore/releases)
- [Commits](https://github.com/jashkenas/underscore/compare/1.11.0...1.13.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-13 10:08:13 -04:00
dependabot[bot] d0ff345ef1
build(deps): bump lodash from 4.17.20 to 4.17.21 (#415)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.20 to 4.17.21.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.20...4.17.21)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-13 10:07:53 -04:00
dependabot[bot] 2c8cc45f64
build(deps): bump hosted-git-info from 2.8.8 to 2.8.9 (#416)
Bumps [hosted-git-info](https://github.com/npm/hosted-git-info) from 2.8.8 to 2.8.9.
- [Release notes](https://github.com/npm/hosted-git-info/releases)
- [Changelog](https://github.com/npm/hosted-git-info/blob/v2.8.9/CHANGELOG.md)
- [Commits](https://github.com/npm/hosted-git-info/compare/v2.8.8...v2.8.9)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-13 10:07:24 -04:00
Lance Ball 0c17ff3af4
doc: improve generated documentation (#410)
Adds some additional JSDoc properties so the docs are clearer.

Signed-off-by: Lance Ball <lball@redhat.com>
2021-04-27 12:31:45 -04:00
Lance Ball d68b85a227
chore: add Lance Ball to maintainers in package.json (#411)
Signed-off-by: Lance Ball <lball@redhat.com>
2021-04-27 12:12:07 -04:00
57 changed files with 11839 additions and 9811 deletions

View File

@ -5,15 +5,17 @@
"sourceType": "module"
},
"extends": [
"plugin:@typescript-eslint/recommended",
"prettier/@typescript-eslint",
"plugin:prettier/recommended"
"plugin:@typescript-eslint/recommended"
],
"env": {
"es6": true,
"node": true,
"mocha": true
},
"plugins": [
"header"
],
"ignorePatterns": ["**/schema/*"],
"rules": {
"no-var": "error",
"standard/no-callback-literal": "off",
@ -22,15 +24,15 @@
"arrow-body-style": ["error", "as-needed"],
"prefer-template": "error",
"max-len": ["warn", { "code": 120 }],
"no-unused-vars": ["warn", {
"argsIgnorePattern": "^_$|^e$|^reject$|^resolve$"
}],
"no-console": ["error", {
"allow": ["warn", "error"]
}],
"valid-jsdoc": "warn",
"semi": ["error", "always"],
"quotes": ["error", "double", { "allowTemplateLiterals": true }],
"@typescript-eslint/no-explicit-any": "off"
"@typescript-eslint/no-explicit-any": "off",
"header/header": [2, "block", ["", " Copyright 2021 The CloudEvents Authors"," SPDX-License-Identifier: Apache-2.0", ""], 2],
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error"]
}
}

View File

@ -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()

View File

@ -15,12 +15,12 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [10.x, 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,16 +31,18 @@ jobs:
name: Code coverage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Generate coverage report
uses: actions/setup-node@v1
- uses: actions/checkout@v4
with:
node-version: 14.x
submodules: true
- name: Generate coverage report
uses: actions/setup-node@v4
with:
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
@ -50,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:

21
.github/workflows/publish-to-npm.yml vendored Normal file
View File

@ -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 }}

View File

@ -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}]'

4
.gitignore vendored
View File

@ -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

3
.gitmodules vendored
View File

@ -0,0 +1,3 @@
[submodule "conformance"]
path = conformance
url = git@github.com:cloudevents/conformance.git

View File

@ -1,7 +0,0 @@
module.exports = {
semi: true,
trailingComma: "all",
doubleQuote: true,
printWidth: 120,
tabWidth: 2
}

View File

@ -2,6 +2,310 @@
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.
## [10.0.0](https://github.com/cloudevents/sdk-javascript/compare/v9.0.0...v10.0.0) (2025-06-05)
### ⚠ BREAKING CHANGES
* remove Node 18 support ([#616](https://github.com/cloudevents/sdk-javascript/issues/616))
### Features
* add node 24 support ([#614](https://github.com/cloudevents/sdk-javascript/issues/614)) ([beac735](https://github.com/cloudevents/sdk-javascript/commit/beac7356a789944cf4a8d76d2508fa6d7c5bcf7e))
* remove Node 18 support ([#616](https://github.com/cloudevents/sdk-javascript/issues/616)) ([3c8819e](https://github.com/cloudevents/sdk-javascript/commit/3c8819e2bcf97fe7b352b2a004d511965a4cc414))
## [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)
### Features
* add batch mode ([#448](https://www.github.com/cloudevents/sdk-javascript/issues/448)) ([9a46e33](https://www.github.com/cloudevents/sdk-javascript/commit/9a46e335f5fc4b1d01520fc02b5229ce35956709))
## [5.1.0](https://www.github.com/cloudevents/sdk-javascript/compare/v5.0.0...v5.1.0) (2021-12-01)
### Features
* use generic type for CloudEvent data ([#446](https://www.github.com/cloudevents/sdk-javascript/issues/446)) ([d941e2d](https://www.github.com/cloudevents/sdk-javascript/commit/d941e2d4d9e491912860e30acd7fa34e9dda7669))
### Bug Fixes
* do not assume an empty content-type header is JSON ([#444](https://www.github.com/cloudevents/sdk-javascript/issues/444)) ([52ea7de](https://www.github.com/cloudevents/sdk-javascript/commit/52ea7de80dd98c6766531936fa95963990119612))
* package.json & package-lock.json to reduce vulnerabilities ([#439](https://www.github.com/cloudevents/sdk-javascript/issues/439)) ([0f5a4c0](https://www.github.com/cloudevents/sdk-javascript/commit/0f5a4c0de26e0181f4a33be9b9e91687879f38b1))
### Miscellaneous
* add test for text/plain data ([#442](https://www.github.com/cloudevents/sdk-javascript/issues/442)) ([b4266b1](https://www.github.com/cloudevents/sdk-javascript/commit/b4266b1f378d16c796aa4b7c5bbdbda8d9f8c1e8))
## [5.0.0](https://www.github.com/cloudevents/sdk-javascript/compare/v4.0.3...v5.0.0) (2021-10-04)
### ⚠ BREAKING CHANGES
* remove support for 0.3 events (#425)
### Features
* add native logging with headers and body to CloudEvent ([#437](https://www.github.com/cloudevents/sdk-javascript/issues/437)) ([b38a48f](https://www.github.com/cloudevents/sdk-javascript/commit/b38a48fa5986fd3fc6f9a1ec728526742e945f69))
### Bug Fixes
* update express example with framework features. ([#429](https://www.github.com/cloudevents/sdk-javascript/issues/429)) ([272bcea](https://www.github.com/cloudevents/sdk-javascript/commit/272bcea2d81145e6e62cab46e66d8c5b6d66c264)), closes [#379](https://www.github.com/cloudevents/sdk-javascript/issues/379)
* ensure source property has min length of 1 ([#438](https://www.github.com/cloudevents/sdk-javascript/issues/438)) ([2ff7852](https://www.github.com/cloudevents/sdk-javascript/commit/2ff7852c3689c486261ec4cd45a18315750b0f2e))
* package.json & package-lock.json to reduce vulnerabilities ([#433](https://www.github.com/cloudevents/sdk-javascript/issues/433)) ([cf47248](https://www.github.com/cloudevents/sdk-javascript/commit/cf47248d25c1039a8bf0afe44b86c08837ca4977))
* package.json & package-lock.json to reduce vulnerabilities ([#434](https://www.github.com/cloudevents/sdk-javascript/issues/434)) ([8814919](https://www.github.com/cloudevents/sdk-javascript/commit/8814919923acbffaaac5538d869ccb35ee93a058))
* package.json & package-lock.json to reduce vulnerabilities ([#436](https://www.github.com/cloudevents/sdk-javascript/issues/436)) ([2dc846c](https://www.github.com/cloudevents/sdk-javascript/commit/2dc846c6594d2c19b0564a5e8394235aaa5c2713))
### Miscellaneous
* remove node 10 from ci ([#435](https://www.github.com/cloudevents/sdk-javascript/issues/435)) ([a7db466](https://www.github.com/cloudevents/sdk-javascript/commit/a7db466c6ee85a9d6d86944ee376dc3a2f40b428))
* remove support for 0.3 events ([#425](https://www.github.com/cloudevents/sdk-javascript/issues/425)) ([2bd9a5a](https://www.github.com/cloudevents/sdk-javascript/commit/2bd9a5a1e429bb641f629cfc3f18a894df8c4650))
* update eslint and prettier dependencies ([#424](https://www.github.com/cloudevents/sdk-javascript/issues/424)) ([061c122](https://www.github.com/cloudevents/sdk-javascript/commit/061c122b867cbc116d0389d88c3198b25d7be0c1))
* use git submodules for conformance tests ([#427](https://www.github.com/cloudevents/sdk-javascript/issues/427)) ([2118488](https://www.github.com/cloudevents/sdk-javascript/commit/2118488a148651d94c99df2ecf9e3eda5ae97f33))
### [4.0.3](https://www.github.com/cloudevents/sdk-javascript/compare/v4.0.2...v4.0.3) (2021-07-06)
### Bug Fixes
* do not modify incoming event's specversion ([#419](https://www.github.com/cloudevents/sdk-javascript/issues/419)) ([22e42dd](https://www.github.com/cloudevents/sdk-javascript/commit/22e42ddb80d21058a74219a1c24b409c245f030f))
* do not modify incoming event's specversion ([#419](https://www.github.com/cloudevents/sdk-javascript/issues/419)) ([7c05ade](https://www.github.com/cloudevents/sdk-javascript/commit/7c05adee7b3d5d56ff5602f044a9581534ab8957))
* throw on validation if extensions are improperly named ([#420](https://www.github.com/cloudevents/sdk-javascript/issues/420)) ([7f6b658](https://www.github.com/cloudevents/sdk-javascript/commit/7f6b658858533bfbc33edbec30d79099aeb0d021))
### Miscellaneous
* add copyrights header and lint rules ([#418](https://www.github.com/cloudevents/sdk-javascript/issues/418)) ([80d987c](https://www.github.com/cloudevents/sdk-javascript/commit/80d987c1f6046efb5e0c89b0472d653ccd35ee2c))
* add Lance Ball to maintainers in package.json ([#411](https://www.github.com/cloudevents/sdk-javascript/issues/411)) ([d68b85a](https://www.github.com/cloudevents/sdk-javascript/commit/d68b85a2278e46e0f5dac44b561cfcb1dd8b5404))
* be more forgiving parsing JSON as a string ([#417](https://www.github.com/cloudevents/sdk-javascript/issues/417)) ([db4be6b](https://www.github.com/cloudevents/sdk-javascript/commit/db4be6b1da479f27903efc6694d06f7cc8b054e2))
### [4.0.2](https://www.github.com/cloudevents/sdk-javascript/compare/v4.0.1...v4.0.2) (2021-04-21)

9
MAINTAINERS.md Normal file
View File

@ -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)

141
README.md
View File

@ -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,33 +104,23 @@ 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 listener to send the event to several endpoint
// You can also have several listeners to send the event to several endpoints
```
## CloudEvent Objects
@ -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,25 +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/tree/v1.0) |
| 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: |
---
| Transport Protocols | [v0.3](https://github.com/cloudevents/spec/tree/v0.3) | [v1.0](https://github.com/cloudevents/spec/tree/v1.0) |
| 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 | :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
@ -176,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
@ -198,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)

View File

@ -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.

22
RELEASING.md Normal file
View File

@ -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
```

1
conformance Submodule

@ -0,0 +1 @@
Subproject commit eddc279339609ed92d128bcd2b0d5c558a7ce396

View File

@ -1,9 +1,12 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
// cucumber.js
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 = {

View File

@ -28,12 +28,17 @@ app.post("/", (req, res) => {
const responseEventMessage = new CloudEvent({
source: '/',
type: 'event:response',
...event
...event,
data: {
hello: 'world'
}
});
responseEventMessage.data = {
hello: 'world'
};
res.status(201).json(responseEventMessage);
// const message = HTTP.binary(responseEventMessage)
const message = HTTP.structured(responseEventMessage)
res.set(message.headers)
res.send(message.body)
} catch (err) {
console.error(err);
res.status(415).header("Content-Type", "application/json").send(JSON.stringify(err));

View File

@ -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
```

View File

@ -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"
}
}

View File

@ -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();
});

View File

@ -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
}

17661
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,18 +1,22 @@
{
"name": "cloudevents",
"version": "4.0.2",
"version": "10.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": "npx downtotemp https://raw.githubusercontent.com/cloudevents/conformance/master/features/http-protocol-binding.feature && cucumber-js /tmp/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",
@ -39,6 +43,65 @@
"name": "Fábio José de Moraes",
"email": "fabiojose@gmail.com",
"url": "https://github.com/fabiojose"
},
{
"name": "Lance Ball",
"email": "lball@redhat.com",
"url": "https://github.com/lance"
},
{
"name": "Lucas Holmquist",
"email": "lholmqui@redhat.com",
"url": "https://github.com/lholmquist"
},
{
"name": "Grant Timmerman",
"url": "https://github.com/grant"
},
{
"name": "Daniel Bevenius",
"email": "daniel.bevenius@gmail.com",
"url": "https://github.com/danbev"
},
{
"name": "Helio Frota",
"url": "https://github.com/helio-frota"
},
{
"name": "Doug Davis",
"email": "dug@us.ibm.com",
"url": "https://github.com/duglin"
},
{
"name": "Remi Cattiau",
"email": "rcattiau@gmail.com",
"url": "https://github.com/loopingz"
},
{
"name": "Michele Angioni",
"url": "https://github.com/micheleangioni"
},
{
"name": "Ali Ok",
"email": "aliok@redhat.com",
"url": "https://github.com/aliok"
},
{
"name": "Philip Hayes",
"url": "https://github.com/deewhyweb"
},
{
"name": "Jingwen Peng",
"url": "https://github.com/pengsrc"
},
{
"name": "Sidharth Vinod",
"email": "sidharthv96@gmail.com",
"url": "https://github.com/sidharthv96"
},
{
"name": "Matej Vasek",
"url": "https://github.com/matejvasek"
}
],
"license": "Apache-2.0",
@ -47,51 +110,56 @@
},
"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",
"@typescript-eslint/eslint-plugin": "^3.4.0",
"@typescript-eslint/parser": "^3.4.0",
"axios": "^0.21.1",
"@types/uuid": "^8.3.4",
"@typescript-eslint/eslint-plugin": "^4.29.0",
"@typescript-eslint/parser": "^4.29.0",
"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",
"downtotemp": "^0.1.2",
"eslint": "^7.3.0",
"eslint-config-prettier": "^6.11.0",
"eslint-config-standard": "^14.1.1",
"eslint-plugin-import": "^2.20.2",
"eslint": "^7.32.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.1.4",
"got": "^11.7.0",
"eslint-plugin-promise": "^5.1.0",
"got": "^11.8.5",
"http-parser-js": "^0.5.2",
"mocha": "~8.2.0",
"mocha": "^10.1.0",
"nock": "~12.0.3",
"nyc": "~15.0.0",
"prettier": "^2.0.5",
"remark-cli": "^9.0.0",
"remark-cli": "^10.0.0",
"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.20.24",
"typescript": "^3.8.3",
"webpack": "^5.1.1",
"webpack-cli": "^4.0.0"
"superagent": "^7.1.1",
"ts-node": "^10.8.1",
"typedoc": "^0.22.11",
"typescript": "^4.3.5",
"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"
}
}

View File

@ -1,3 +1,8 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
const CONSTANTS = Object.freeze({
CHARSET_DEFAULT: "utf-8",
EXTENSIONS_PREFIX: "ce-",
@ -8,6 +13,7 @@ const CONSTANTS = Object.freeze({
MIME_OCTET_STREAM: "application/octet-stream",
MIME_CE: "application/cloudevents",
MIME_CE_JSON: "application/cloudevents+json",
MIME_CE_BATCH: "application/cloudevents-batch+json",
HEADER_CONTENT_TYPE: "content-type",
DEFAULT_CONTENT_TYPE: "application/json; charset=utf-8",
DEFAULT_CE_CONTENT_TYPE: "application/cloudevents+json; charset=utf-8",
@ -48,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;

View File

@ -1,40 +1,37 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
import { ErrorObject } from "ajv";
import { v4 as uuidv4 } from "uuid";
import { Emitter } from "..";
import {
CloudEventV03,
CloudEventV03Attributes,
CloudEventV03OptionalAttributes,
CloudEventV1,
CloudEventV1Attributes,
CloudEventV1OptionalAttributes,
} from "./interfaces";
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
* interoperability across services, platforms and systems.
* @see https://github.com/cloudevents/spec/blob/v1.0/spec.md
*/
export class CloudEvent implements CloudEventV1, CloudEventV03 {
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?: Record<string, unknown | string | number | boolean> | string | number | boolean | null | unknown;
data?: T;
data_base64?: string;
// Extensions should not exist as it's own object, but instead
@ -53,7 +50,7 @@ export class CloudEvent implements CloudEventV1, CloudEventV03 {
* @param {object} event the event properties
* @param {boolean?} strict whether to perform event validation when creating the object - default: true
*/
constructor(event: CloudEventV1 | CloudEventV1Attributes | CloudEventV03 | CloudEventV03Attributes, strict = true) {
constructor(event: Partial<CloudEventV1<T>>, strict = true) {
// copy the incoming event so that we can delete properties as we go
// everything left after we have deleted know properties becomes an extension
const properties = { ...event };
@ -64,13 +61,13 @@ export class CloudEvent implements CloudEventV1, CloudEventV03 {
this.time = properties.time || new Date().toISOString();
delete properties.time;
this.type = properties.type;
delete properties.type;
this.type = properties.type as string;
delete (properties as any).type;
this.source = properties.source;
delete properties.source;
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;
@ -86,26 +83,35 @@ export class CloudEvent implements CloudEventV1, CloudEventV03 {
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
@ -128,17 +134,6 @@ See: https://github.com/cloudevents/spec/blob/v1.0/spec.md#type-system`);
Object.freeze(this);
}
get data(): unknown {
return this.#_data;
}
set data(value: unknown) {
if (isBinary(value)) {
this.data_base64 = asBase64(value as Uint32Array);
}
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.
@ -148,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;
}
@ -168,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[]);
}
}
}
@ -185,22 +184,52 @@ 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
* 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/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<CloudEventV1<D>>, strict?: boolean): CloudEvent<D>;
/**
* 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(
options:
| CloudEventV1
| CloudEventV1Attributes
| CloudEventV1OptionalAttributes
| CloudEventV03
| CloudEventV03Attributes
| CloudEventV03OptionalAttributes,
strict = true,
): CloudEvent {
return new CloudEvent(Object.assign({}, this.toJSON(), options) as CloudEvent, strict);
public cloneWith<D>(options: Partial<CloudEventV1<D>>, strict = true): CloudEvent<D | T> {
return CloudEvent.cloneWith(this, options, strict);
}
/**
* The native `console.log` value of the CloudEvent.
* @return {string} The string representation of the CloudEvent.
*/
[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);
}
}

View File

@ -1,8 +1,13 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
/**
* The object interface for CloudEvents 1.0.
* @see https://github.com/cloudevents/spec/blob/v1.0/spec.md
*/
export interface CloudEventV1 extends CloudEventV1Attributes {
export interface CloudEventV1<T> extends CloudEventV1Attributes<T> {
// REQUIRED Attributes
/**
* [REQUIRED] Identifies the event. Producers MUST ensure that `source` + `id`
@ -25,7 +30,7 @@ export interface CloudEventV1 extends CloudEventV1Attributes {
specversion: string;
}
export interface CloudEventV1Attributes extends CloudEventV1OptionalAttributes {
export interface CloudEventV1Attributes<T> extends CloudEventV1OptionalAttributes<T> {
/**
* [REQUIRED] Identifies the context in which an event happened. Often this
* will include information such as the type of the event source, the
@ -60,7 +65,7 @@ export interface CloudEventV1Attributes extends CloudEventV1OptionalAttributes {
type: string;
}
export interface CloudEventV1OptionalAttributes {
export interface CloudEventV1OptionalAttributes<T> {
/**
* The following fields are optional.
*/
@ -121,7 +126,7 @@ export interface CloudEventV1OptionalAttributes {
* specified by the datacontenttype attribute (e.g. application/json), and adheres
* to the dataschema format when those respective attributes are present.
*/
data?: Record<string, unknown | string | number | boolean> | string | number | boolean | null | unknown;
data?: T;
/**
* [OPTIONAL] The event payload encoded as base64 data. This is used when the
@ -135,139 +140,3 @@ export interface CloudEventV1OptionalAttributes {
*/
[key: string]: unknown;
}
/**
* The object interface for CloudEvents 0.3.
* @see https://github.com/cloudevents/spec/blob/v0.3/spec.md
*/
export interface CloudEventV03 extends CloudEventV03Attributes {
// REQUIRED Attributes
/**
* [REQUIRED] Identifies the event. Producers MUST ensure that `source` + `id`
* is unique for each distinct event. If a duplicate event is re-sent (e.g. due
* to a network error) it MAY have the same `id`. Consumers MAY assume that
* Events with identical `source` and `id` are duplicates.
* @required Non-empty string. Unique within producer.
* @example An event counter maintained by the producer
* @example A UUID
*/
id: string;
/**
* [REQUIRED] The version of the CloudEvents specification which the event
* uses. This enables the interpretation of the context. Compliant event
* producers MUST use a value of `1.0` when referring to this version of the
* specification.
* @required MUST be a non-empty string.
*/
specversion: string;
}
export interface CloudEventV03Attributes extends CloudEventV03OptionalAttributes {
/**
* [REQUIRED] Identifies the context in which an event happened. Often this
* will include information such as the type of the event source, the
* organization publishing the event or the process that produced the event. The
* exact syntax and semantics behind the data encoded in the URI is defined by
* the event producer.
* Producers MUST ensure that `source` + `id` is unique for each distinct event.
* An application MAY assign a unique `source` to each distinct producer, which
* makes it easy to produce unique IDs since no other producer will have the same
* source. The application MAY use UUIDs, URNs, DNS authorities or an
* application-specific scheme to create unique `source` identifiers.
* A source MAY include more than one producer. In that case the producers MUST
* collaborate to ensure that `source` + `id` is unique for each distinct event.
* @required Non-empty URI-reference
*/
source: string;
/**
* [REQUIRED] This attribute contains a value describing the type of event
* related to the originating occurrence. Often this attribute is used for
* routing, observability, policy enforcement, etc. The format of this is
* producer defined and might include information such as the version of the
* `type` - see
* [Versioning of Attributes in the Primer](primer.md#versioning-of-attributes)
* for more information.
* @required MUST be a non-empty string
* @should SHOULD be prefixed with a reverse-DNS name. The prefixed domain dictates the
* organization which defines the semantics of this event type.
* @example com.github.pull.create
* @example com.example.object.delete.v2
*/
type: string;
}
export interface CloudEventV03OptionalAttributes {
/**
* The following fields are optional.
*/
/**
* [OPTIONAL] Describes the content encoding for the data attribute for when the
* data field MUST be encoded as a string, like with structured transport binding
* modes using the JSON event format, but the datacontenttype indicates a non-string
* media type. When the data field's effective data type is not String, this attribute
* MUST NOT be set and MUST be ignored when set.
*/
datacontentencoding?: string;
/**
* [OPTIONAL] Content type of `data` value. This attribute enables `data` to
* carry any type of content, whereby format and encoding might differ from that
* of the chosen event format. For example, an event rendered using the
* [JSON envelope](./json-format.md#3-envelope) format might carry an XML payload
* in `data`, and the consumer is informed by this attribute being set to
* "application/xml". The rules for how `data` content is rendered for different
* `datacontenttype` values are defined in the event format specifications; for
* example, the JSON event format defines the relationship in
* [section 3.1](./json-format.md#31-handling-of-data).
*/
datacontenttype?: string;
/**
* [OPTIONAL] A link to the schema that the data attribute adheres to.
* Incompatible changes to the schema SHOULD be reflected by a different URL.
* If present, MUST be a non-empty URI.
*/
schemaurl?: string;
/**
* [OPTIONAL] This describes the subject of the event in the context of the
* event producer (identified by `source`). In publish-subscribe scenarios, a
* subscriber will typically subscribe to events emitted by a `source`, but the
* `source` identifier alone might not be sufficient as a qualifier for any
* specific event if the `source` context has internal sub-structure.
*
* Identifying the subject of the event in context metadata (opposed to only in
* the `data` payload) is particularly helpful in generic subscription filtering
* scenarios where middleware is unable to interpret the `data` content. In the
* above example, the subscriber might only be interested in blobs with names
* ending with '.jpg' or '.jpeg' and the `subject` attribute allows for
* constructing a simple and efficient string-suffix filter for that subset of
* events.
*
* If present, MUST be a non-empty string.
* @example "https://example.com/storage/tenant/container"
* @example "mynewfile.jpg"
*/
subject?: string;
/**
* [OPTIONAL] Timestamp of when the occurrence happened. If the time of the
* occurrence cannot be determined then this attribute MAY be set to some other
* time (such as the current time) by the CloudEvents producer, however all
* producers for the same `source` MUST be consistent in this respect. In other
* words, either they all use the actual time of the occurrence or they all use
* the same algorithm to determine the value used.
* @example "2020-08-08T14:48:09.769Z"
*/
time?: string;
/**
* [OPTIONAL] The event payload. This specification does not place any restriction
* on the type of this information. It is encoded into a media format which is
* specified by the datacontenttype attribute (e.g. application/json), and adheres
* to the dataschema format when those respective attributes are present.
*/
data?: Record<string, unknown | string | number | boolean> | string | number | boolean | null | unknown;
/**
* [OPTIONAL] CloudEvents extension attributes.
*/
[key: string]: unknown;
}

View File

@ -1,153 +0,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",
},
},
type: "object",
};
export const schemaV03 = {
$ref: "#/definitions/event",
definitions: {
specversion: {
const: "0.3",
},
datacontenttype: {
type: "string",
},
data: {
type: ["object", "string", "array", "number", "boolean", "null"],
},
event: {
properties: {
specversion: {
$ref: "#/definitions/specversion",
},
datacontenttype: {
$ref: "#/definitions/datacontenttype",
},
data: {
$ref: "#/definitions/data",
},
id: {
$ref: "#/definitions/id",
},
time: {
$ref: "#/definitions/time",
},
schemaurl: {
$ref: "#/definitions/schemaurl",
},
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",
},
schemaurl: {
type: "string",
format: "uri-reference",
},
subject: {
type: "string",
minLength: 1,
},
type: {
type: "string",
minLength: 1,
},
source: {
format: "uri-reference",
type: "string",
},
},
type: "object",
};

View File

@ -1,48 +1,28 @@
import Ajv from "ajv";
import { ValidationError, isBase64 } from "./validation";
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
import { CloudEventV1, CloudEventV03 } from "./interfaces";
import { schemaV03, schemaV1 } from "./schemas";
import { Version } from "./cloudevent";
import CONSTANTS from "../constants";
import { ValidationError } from "./validation";
const ajv = new Ajv({ extendRefs: true });
import { CloudEventV1 } from "./interfaces";
import { V1 } from "./cloudevent";
import validate from "../schema/v1";
// 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);
const isValidAgainstSchemaV03 = ajv.compile(schemaV03);
export function validateCloudEvent(event: CloudEventV03 | CloudEventV1): boolean {
if (event.specversion === Version.V1) {
if (!isValidAgainstSchemaV1(event)) {
throw new ValidationError("invalid payload", isValidAgainstSchemaV1.errors);
export function validateCloudEvent<T>(event: CloudEventV1<T>): boolean {
if (event.specversion === V1) {
if (!validate(event)) {
throw new ValidationError("invalid payload", (validate as any).errors);
}
return true;
} else if (event.specversion === Version.V03) {
if (!isValidAgainstSchemaV03(event)) {
throw new ValidationError("invalid payload", isValidAgainstSchemaV03.errors);
}
return checkDataContentEncoding(event);
} else {
return false;
}
return false;
}
function checkDataContentEncoding(event: CloudEventV03): boolean {
if (event.datacontentencoding) {
// we only support base64
const encoding = event.datacontentencoding.toLocaleLowerCase();
if (encoding !== CONSTANTS.ENCODING_BASE64) {
throw new ValidationError("invalid payload", [`Unsupported content encoding: ${encoding}`]);
} else {
if (!isBase64(event.data)) {
throw new ValidationError("invalid payload", [`Invalid content encoding of data: ${event.data}`]);
}
// attribute names must all be [a-z|0-9]
const validation = /^[a-z0-9]+$/;
for (const key in event) {
if (validation.test(key) === false && key !== "data_base64") {
throw new ValidationError(`invalid attribute name: "${key}"`);
}
}
return true;

View File

@ -1,5 +1,27 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
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.
@ -14,7 +36,7 @@ export class ValidationError extends TypeError {
// @ts-ignore
errors?.reduce(
(accum: string, err: Record<string, string>) =>
(accum as string).concat(`
accum.concat(`
${err instanceof Object ? JSON.stringify(err) : err}`),
message,
)
@ -26,12 +48,12 @@ export class ValidationError extends TypeError {
export const isString = (v: unknown): boolean => typeof v === "string";
export const isObject = (v: unknown): boolean => typeof v === "object";
export const isDefined = (v: unknown): boolean => v && typeof v !== "undefined";
export const isDefined = (v: unknown): boolean => v !== null && typeof v !== "undefined";
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): boolean => v instanceof Date;
export const isBinary = (v: unknown): boolean => v instanceof Uint32Array;
export const isDate = (v: unknown): v is Date => v instanceof Date;
export const isBinary = (v: unknown): boolean => ArrayBuffer.isView(v);
export const isStringOrThrow = (v: unknown, t: Error): boolean =>
isString(v)
@ -68,16 +90,25 @@ 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 string)
? Buffer.from((value as unknown) as string)
: isBuffer(value)
? (value as 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));
@ -92,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);

View File

@ -1,35 +1,52 @@
import { CloudEvent, Version } from "./event/cloudevent";
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
import { CloudEvent, V1, V03 } from "./event/cloudevent";
import { ValidationError } from "./event/validation";
import { CloudEventV03, CloudEventV03Attributes, CloudEventV1, CloudEventV1Attributes } from "./event/interfaces";
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,
CloudEventV03,
CloudEventV03Attributes,
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
};

View File

@ -1,7 +1,12 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
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];
@ -19,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(event: CloudEvent): 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;
@ -31,7 +36,7 @@ export function headersFor(event: CloudEvent): 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;
@ -60,11 +65,6 @@ export function sanitize(headers: Headers): Headers {
.filter((header) => Object.hasOwnProperty.call(headers, header))
.forEach((header) => (sanitized[header.toLowerCase()] = headers[header]));
// If no content-type header is sent, assume application/json
if (!sanitized[CONSTANTS.HEADER_CONTENT_TYPE]) {
sanitized[CONSTANTS.HEADER_CONTENT_TYPE] = CONSTANTS.MIME_JSON;
}
return sanitized;
}

View File

@ -1,5 +1,12 @@
import { CloudEvent, CloudEventV03, CloudEventV1, CONSTANTS, Mode, Version } from "../..";
import { Message, Headers } from "..";
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
import { types } from "util";
import { CloudEvent, CloudEventV1, CONSTANTS, Mode, V1, V03 } from "../..";
import { Message, Headers, Binding } from "..";
import {
headersFor,
@ -12,14 +19,21 @@ import {
import { isStringOrObjectOrThrow, ValidationError } from "../../event/validation";
import { JSONParser, MappedParser, Parser, parserByContentType } from "../../parsers";
// implements Serializer
export function binary(event: CloudEvent): Message {
/**
* Serialize a CloudEvent for HTTP transport in binary mode
* @implements {Serializer}
* @see https://github.com/cloudevents/spec/blob/v1.0.1/http-protocol-binding.md#31-binary-content-mode
*
* @param {CloudEvent} event The event to serialize
* @returns {Message} a Message object with headers and body
*/
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);
body = (JSON.stringify(event.data) as unknown) as T;
}
return {
headers,
@ -27,11 +41,18 @@ export function binary(event: CloudEvent): Message {
};
}
// implements Serializer
export function structured(event: CloudEvent): Message {
/**
* Serialize a CloudEvent for HTTP transport in structured mode
* @implements {Serializer}
* @see https://github.com/cloudevents/spec/blob/v1.0.1/http-protocol-binding.md#32-structured-content-mode
*
* @param {CloudEvent} event the CloudEvent to be serialized
* @returns {Message} a Message object with headers and body
*/
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: {
@ -41,9 +62,15 @@ export function structured(event: CloudEvent): Message {
};
}
// implements Detector
// TODO: this could probably be optimized
export function isEvent(message: Message): boolean {
/**
* Determine if a Message is a CloudEvent
* @implements {Detector}
*
* @param {Message} message an incoming Message object
* @returns {boolean} true if this Message is a CloudEvent
*/
function isEvent(message: Message): boolean {
// TODO: this could probably be optimized
try {
deserialize(message);
return true;
@ -54,23 +81,22 @@ export function isEvent(message: Message): boolean {
/**
* Converts a Message to a CloudEvent
* @implements {Deserializer}
*
* @param {Message} message the incoming message
* @return {CloudEvent} A new {CloudEvent} instance
*/
export function deserialize(message: Message): CloudEvent {
function deserialize<T>(message: Message): CloudEvent<T> | CloudEvent<T>[] {
const cleanHeaders: Headers = sanitize(message.headers);
const mode: Mode = getMode(cleanHeaders);
let version = getVersion(mode, cleanHeaders, message.body);
if (version !== Version.V03 && version !== Version.V1) {
console.error(`Unknown spec version ${version}. Default to ${Version.V1}`);
version = Version.V1;
}
const version = getVersion(mode, cleanHeaders, message.body);
switch (mode) {
case Mode.BINARY:
return parseBinary(message, version);
case Mode.STRUCTURED:
return parseStructured(message, version);
case Mode.BATCH:
return parseBatched(message);
default:
throw new ValidationError("Unknown Message mode");
}
@ -84,9 +110,14 @@ export function deserialize(message: Message): CloudEvent {
*/
function getMode(headers: Headers): Mode {
const contentType = headers[CONSTANTS.HEADER_CONTENT_TYPE];
if (contentType && contentType.startsWith(CONSTANTS.MIME_CE)) {
return Mode.STRUCTURED;
if (contentType) {
if (contentType.startsWith(CONSTANTS.MIME_CE_BATCH)) {
return Mode.BATCH;
} else if (contentType.startsWith(CONSTANTS.MIME_CE)) {
return Mode.STRUCTURED;
}
}
if (headers[CONSTANTS.CE_HEADERS.ID]) {
return Mode.BINARY;
}
@ -110,9 +141,13 @@ function getVersion(mode: Mode, headers: Headers, body: string | Record<string,
}
} else {
// structured mode - the version is in the body
return typeof body === "string" ? JSON.parse(body).specversion : (body as CloudEvent).specversion;
if (typeof body === "string") {
return JSON.parse(body).specversion;
} else {
return (body as Record<string, string>).specversion;
}
}
return Version.V1;
return V1;
}
/**
@ -120,12 +155,12 @@ 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(message: Message, version: Version): CloudEvent {
const headers = message.headers;
function parseBinary<T>(message: Message, version: string): CloudEvent<T> {
const headers = { ...message.headers };
let body = message.body;
if (!headers) throw new ValidationError("headers is null or undefined");
@ -134,18 +169,19 @@ function parseBinary(message: Message, version: Version): CloudEvent {
const sanitizedHeaders = sanitize(headers);
const eventObj: { [key: string]: unknown | string | Record<string, unknown> } = {};
const parserMap: Record<string, MappedParser> = version === Version.V1 ? v1binaryParsers : v03binaryParsers;
const parserMap: Record<string, MappedParser> = version === V03 ? v03binaryParsers : v1binaryParsers;
for (const header in parserMap) {
if (sanitizedHeaders[header]) {
const mappedParser: MappedParser = parserMap[header];
eventObj[mappedParser.name] = mappedParser.parser.parse(sanitizedHeaders[header]);
delete sanitizedHeaders[header];
delete headers[header];
}
}
// Every unprocessed header can be an extension
for (const header in sanitizedHeaders) {
for (const header in headers) {
if (header.startsWith(CONSTANTS.EXTENSIONS_PREFIX)) {
eventObj[header.substring(CONSTANTS.EXTENSIONS_PREFIX.length)] = headers[header];
}
@ -163,18 +199,18 @@ function parseBinary(message: Message, version: Version): CloudEvent {
delete eventObj.datacontentencoding;
}
return new CloudEvent({ ...eventObj, data: body } as CloudEventV1 | CloudEventV03, false);
return new CloudEvent<T>({ ...eventObj, data: body } as CloudEventV1<T>, false);
}
/**
* 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(message: Message, version: Version): CloudEvent {
function parseStructured<T>(message: Message, version: string): CloudEvent<T> {
const payload = message.body;
const headers = message.headers;
@ -191,13 +227,13 @@ function parseStructured(message: Message, version: Version): CloudEvent {
const incoming = { ...(parser.parse(payload as string) as Record<string, unknown>) };
const eventObj: { [key: string]: unknown } = {};
const parserMap: Record<string, MappedParser> = version === Version.V1 ? v1structuredParsers : v03structuredParsers;
const parserMap: Record<string, MappedParser> = version === V03 ? v03structuredParsers : v1structuredParsers;
for (const key in parserMap) {
const property = incoming[key];
if (property) {
const parser: MappedParser = parserMap[key];
eventObj[parser.name] = parser.parser.parse(property as string);
const mappedParser: MappedParser = parserMap[key];
eventObj[mappedParser.name] = mappedParser.parser.parse(property as string);
}
delete incoming[key];
}
@ -216,5 +252,25 @@ function parseStructured(message: Message, version: Version): CloudEvent {
delete eventObj.data_base64;
delete eventObj.datacontentencoding;
}
return new CloudEvent(eventObj as CloudEventV1 | CloudEventV03, false);
return new CloudEvent<T>(eventObj as CloudEventV1<T>, false);
}
function parseBatched<T>(message: Message): CloudEvent<T> | CloudEvent<T>[] {
const ret: CloudEvent<T>[] = [];
const events = JSON.parse(message.body as string);
events.forEach((element: CloudEvent) => {
ret.push(new CloudEvent<T>(element));
});
return ret;
}
/**
* Bindings for HTTP transport support
* @implements {@linkcode Binding}
*/
export const HTTP: Binding = {
binary,
structured,
toEvent: deserialize,
isEvent: isEvent,
};

View File

@ -1,15 +1,30 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
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,
* which provides functions for sending CloudEvent Messages over
* the wire.
* @interface
*
* @property {@link Serializer} `binary` - converts a CloudEvent into a Message in binary mode
* @property {@link Serializer} `structured` - converts a CloudEvent into a Message in structured mode
* @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;
}
@ -17,6 +32,7 @@ export interface Binding {
/**
* Headers is an interface representing transport-agnostic headers as
* key/value string pairs
* @interface
*/
export interface Headers extends IncomingHttpHeaders {
[key: string]: string | string[] | undefined;
@ -25,48 +41,48 @@ export interface Headers extends IncomingHttpHeaders {
/**
* Message is an interface representing a CloudEvent as a
* transport-agnostic message
* @interface
* @property {@linkcode Headers} `headers` - the headers for 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;
}
/**
* An enum representing the two transport modes, binary and structured
* @interface
*/
export enum Mode {
BINARY = "binary",
STRUCTURED = "structured",
BATCH = "batch",
}
/**
* Serializer is an interface for functions that can convert a
* CloudEvent into a Message.
* @interface
*/
export interface Serializer {
(event: CloudEvent): Message;
export interface Serializer<M extends Message> {
<T>(event: CloudEventV1<T>): M;
}
/**
* Deserializer is a function interface that converts a
* Message to a CloudEvent
* @interface
*/
export interface Deserializer {
(message: Message): CloudEvent;
<T>(message: Message): CloudEventV1<T> | CloudEventV1<T>[];
}
/**
* Detector is a function interface that detects whether
* a message contains a valid CloudEvent
* @interface
*/
export interface Detector {
(message: Message): boolean;
}
// HTTP Message capabilities
export const HTTP: Binding = {
binary: binary as Serializer,
structured: structured as Serializer,
toEvent: deserialize as Deserializer,
isEvent: isEvent as Detector,
};

View File

@ -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;
}

273
src/message/kafka/index.ts Normal file
View File

@ -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;
}

150
src/message/mqtt/index.ts Normal file
View File

@ -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;
}

View File

@ -1,6 +1,13 @@
/*
Copyright 2021 The CloudEvents Authors
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;
}
@ -17,12 +24,27 @@ export class JSONParser implements Parser {
* @return {object} the parsed JSON payload.
*/
parse(payload: Record<string, unknown> | string): string {
if (typeof payload === "string") {
// This is kind of a hack, but the payload data could be JSON in the form of a single
// string, such as "some data". But without the quotes in the string, JSON.parse blows
// up. We can check for this scenario and add quotes. Not sure if this is ideal.
if (!/^[[|{|"]/.test(payload)) {
payload = `"${payload}"`;
}
}
if (this.decorator) {
payload = this.decorator.parse(payload);
}
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);
}

128
src/schema/cloudevent.json Normal file
View File

@ -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"
}
}
}

10
src/schema/formats.js Normal file
View File

@ -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;

View File

@ -1,3 +1,8 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
import { CloudEvent } from "../event/cloudevent";
import { HTTP, Message, Mode } from "../message";
import { EventEmitter } from "events";
@ -5,36 +10,40 @@ import { EventEmitter } from "events";
/**
* Options is an additional, optional dictionary of options that may
* be passed to an EmitterFunction and TransportFunction
* @interface
*/
export interface Options {
[key: string]: string | Record<string, unknown> | unknown;
}
/**
* EmitterFunction is an invokable interface returned by the emitterFactory
* function. Invoke an EmitterFunction with a CloudEvent and optional transport
* EmitterFunction is an invokable interface returned by {@linkcode emitterFor}.
* Invoke an EmitterFunction with a CloudEvent and optional transport
* options to send the event as a Message across supported transports.
* @interface
*/
export interface EmitterFunction {
(event: CloudEvent, options?: Options): Promise<unknown>;
<T>(event: CloudEvent<T>, options?: Options): Promise<unknown>;
}
/**
* TransportFunction is an invokable interface provided to the emitterFactory.
* A TransportFunction's responsiblity is to send a JSON encoded event Message
* across the wire.
* @interface
*/
export interface TransportFunction {
(message: Message, options?: Options): Promise<unknown>;
}
const emitterDefaults = { binding: HTTP, mode: Mode.BINARY };
const emitterDefaults: Options = { binding: HTTP, mode: Mode.BINARY };
/**
* emitterFactory creates and returns an EmitterFunction using the supplied
* TransportFunction. The returned EmitterFunction will invoke the Binding's
* `binary` or `structured` function to convert a CloudEvent into a JSON
* Message based on the Mode provided, and invoke the TransportFunction with
* the Message and any supplied options.
* Creates and returns an {@linkcode EmitterFunction} using the supplied
* {@linkcode TransportFunction}. The returned {@linkcode EmitterFunction}
* will invoke the {@linkcode Binding}'s `binary` or `structured` function
* to convert a {@linkcode CloudEvent} into a JSON
* {@linkcode Message} based on the {@linkcode Mode} provided, and invoke the
* TransportFunction with the Message and any supplied options.
*
* @param {TransportFunction} fn a TransportFunction that can accept an event Message
* @param { {Binding, Mode} } options network binding and message serialization options
@ -46,8 +55,8 @@ export function emitterFor(fn: TransportFunction, options = emitterDefaults): Em
if (!fn) {
throw new TypeError("A TransportFunction is required");
}
const { binding, mode } = { ...emitterDefaults, ...options };
return function emit(event: CloudEvent, opts?: Options): Promise<unknown> {
const { binding, mode }: any = { ...emitterDefaults, ...options };
return function emit<T>(event: CloudEvent<T>, opts?: Options): Promise<unknown> {
opts = opts || {};
switch (mode) {
@ -62,7 +71,7 @@ export function emitterFor(fn: TransportFunction, options = emitterDefaults): Em
}
/**
* A static class to emit CloudEvents within an application
* A helper class to emit CloudEvents within an application
*/
export class Emitter {
/**
@ -97,10 +106,10 @@ export class Emitter {
* Emit an event inside this application
*
* @param {CloudEvent} event to emit
* @param {boolean} ensureDelivery fail the promise if one listener fail
* @param {boolean} ensureDelivery fail the promise if one listener fails
* @return {void}
*/
static async emitEvent(event: CloudEvent, ensureDelivery = true): Promise<void> {
static async emitEvent<T>(event: CloudEvent<T>, ensureDelivery = true): Promise<void> {
if (!ensureDelivery) {
// Ensure delivery is disabled so we don't wait for Promise
Emitter.getInstance().emit("cloudevent", event);

View File

@ -1,17 +1,63 @@
import { Message, Options } from "../..";
import axios from "axios";
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
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 { Socket } from "net";
import http, { OutgoingHttpHeaders } from "http";
import https, { RequestOptions } from "https";
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);
}
});
};
}

View File

@ -1,6 +1,12 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
/**
* An enum representing the transport protocols for an event
*/
export const enum Protocol {
HTTPBinary,
HTTPStructured,

View File

@ -1,13 +1,45 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
/* 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;
});

View File

@ -0,0 +1,61 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
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";
// Create a bunch of cloudevents that we can bunch together in a batch
const fixture: any[] = [];
for (let id = 0; id < 10; id++) {
fixture.push(
new CloudEvent({
id: `${id}`,
source,
type,
}),
);
}
/**
* A basic test to validate that we can handle simple batch mode
*/
describe("A batched CloudEvent message over HTTP", () => {
it("Can be created with a typed Message", () => {
const message: Message = {
headers: {
"content-type": "application/cloudevents-batch+json",
},
body: JSON.stringify(fixture),
};
const batch = HTTP.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");
});
});
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");
});
});

View File

@ -1,26 +1,45 @@
/*
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, ValidationError, Version } from "../../src";
import { CloudEventV03, CloudEventV1 } from "../../src/event/interfaces";
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: CloudEventV1 = {
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);
@ -29,17 +48,17 @@ describe("A CloudEvent", () => {
});
it("Can be constructed with loose validation", () => {
const ce = new CloudEvent({} as CloudEventV1, false);
const ce = new CloudEvent({}, false);
expect(ce).to.be.instanceOf(CloudEvent);
});
it("Loosely validated events can be cloned", () => {
const ce = new CloudEvent({} as CloudEventV1, false);
const ce = new CloudEvent({}, false);
expect(ce.cloneWith({}, false)).to.be.instanceOf(CloudEvent);
});
it("Loosely validated events throw when validated", () => {
const ce = new CloudEvent({} as CloudEventV1, false);
const ce = new CloudEvent({}, false);
expect(ce.validate).to.throw(ValidationError, "invalid payload");
});
@ -50,16 +69,23 @@ describe("A CloudEvent", () => {
expect(new CloudEvent(JSON.parse(JSON.stringify(ce)))).to.deep.equal(ce);
});
it("serializes as JSON with raw log", () => {
const ce = new CloudEvent({ ...fixture, data: { lunch: "tacos" } });
const inspectSymbol = (Symbol.for("nodejs.util.inspect.custom") as unknown) as string;
const ceToString = (ce[inspectSymbol] as CallableFunction).bind(ce);
expect(ce.toString()).to.deep.equal(ceToString());
});
it("Throw a validation error for invalid extension names", () => {
expect(() => {
new CloudEvent({ "ext-1": "extension1", ...fixture });
}).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", () => {
@ -67,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", () => {
@ -87,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");
});
@ -192,100 +270,33 @@ 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");
}
});
it("correctly formats a CloudEvent as JSON", () => {
const ce = new CloudEvent({ ...fixture });
const json = ce.toString();
const obj = JSON.parse((json as unknown) as string);
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);
});
});
describe("A 0.3 CloudEvent", () => {
const v03fixture: CloudEventV03 = { ...fixture };
v03fixture.specversion = Version.V03;
it("has retreivable source and type attributes", () => {
const ce = new CloudEvent(v03fixture);
expect(ce.source).to.equal("http://unit.test");
expect(ce.type).to.equal("org.cncf.cloudevents.example");
expect(obj.specversion).to.equal(V1);
});
it("generates an ID if one is not provided in the constructor", () => {
const ce = new CloudEvent({ source, type, specversion: Version.V03 });
expect(ce.id).to.not.be.empty;
expect(ce.specversion).to.equal(Version.V03);
});
it("generates a timestamp by default", () => {
const ce = new CloudEvent(v03fixture);
expect(ce.time).to.not.be.empty;
});
it("can be constructed with a timestamp", () => {
const time = new Date().toISOString();
const ce = new CloudEvent({ time, ...v03fixture });
expect(ce.time).to.equal(time);
});
it("can be constructed with a datacontenttype", () => {
const ce = new CloudEvent({ datacontenttype: "application/json", ...v03fixture });
expect(ce.datacontenttype).to.equal("application/json");
});
it("can be constructed with a datacontentencoding", () => {
const ce = new CloudEvent({ datacontentencoding: "Base64", ...v03fixture, data: "SSB3YXMgZnVubnkg8J+Ygg==" });
expect(ce.datacontentencoding).to.equal("Base64");
});
it("can be constructed with a schemaurl", () => {
const ce = new CloudEvent({ schemaurl: "http://my.schema", ...v03fixture });
expect(ce.schemaurl).to.equal("http://my.schema");
});
it("can be constructed with a subject", () => {
const ce = new CloudEvent({ subject: "science", ...v03fixture });
expect(ce.subject).to.equal("science");
});
// Handle 1.0 attribute - should this really throw?
it("throws a TypeError when constructed with a dataschema", () => {
expect(() => {
new CloudEvent({ dataschema: "http://throw.com", ...v03fixture });
}).to.throw(TypeError, "cannot set dataschema on version 0.3 event");
});
it("can be constructed with data", () => {
const ce = new CloudEvent({
...v03fixture,
data: { lunch: "tacos" },
});
expect(ce.data).to.deep.equal({ lunch: "tacos" });
});
it("throws TypeError if the CloudEvent does not conform to the schema", () => {
it("throws if the provded source is empty string", () => {
try {
new CloudEvent({
...v03fixture,
source: (null as unknown) as string,
id: "0815",
specversion: "1.0",
type: "my.event.type",
source: "",
});
} catch (err) {
} catch (err: any) {
expect(err).to.be.instanceOf(ValidationError);
const error = err.errors[0] as any;
expect(err.message).to.include("invalid payload");
expect(error.instancePath).to.equal("/source");
expect(error.keyword).to.equal("minLength");
}
});
it("correctly formats a CloudEvent as JSON", () => {
const ce = new CloudEvent({ ...v03fixture });
const json = ce.toString();
const obj = JSON.parse((json as unknown) as string);
expect(obj.type).to.equal(type);
expect(obj.source).to.equal(source);
expect(obj.specversion).to.equal(Version.V03);
});
});

View File

@ -1,3 +1,8 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
import { expect } from "chai";
import { CONSTANTS } from "../../src";

View File

@ -1,12 +1,18 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
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/";
@ -33,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> {
@ -55,7 +61,7 @@ function superagentEmitter(message: Message, options?: Options): Promise<unknown
function gotEmitter(message: Message, options?: Options): Promise<unknown> {
return Promise.resolve(
got.post(sink, { headers: message.headers, body: message.body as string, ...((options as unknown) as Options) }),
got.post(sink, { headers: message.headers, body: message.body as string, ...(options as Options) }),
);
}
@ -78,16 +84,12 @@ 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);
expect(body.id).to.equal("1234");
return Promise.resolve();
}
// Ignore the next line to ensure that HTTP transport is still the default.
// Otherwise, tslint would complain that the param did not have `binding: <val>`
/* @ts-ignore */
const emitter = emitterFor(transport, { mode: Mode.STRUCTURED });
emitter(
new CloudEvent({
@ -99,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>>;
@ -135,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>>>;

View File

@ -1,3 +1,8 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
import "mocha";
import { emitterFor, HTTP, Mode, Message, Emitter } from "../../src";
@ -17,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 () => {
@ -32,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 () => {

View File

@ -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);
});
});

View File

@ -1,9 +1,14 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
import path from "path";
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";
@ -36,9 +41,78 @@ 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<undefined> = {
body,
headers: {
"ce-source": "/test/type",
"ce-type": "test.type",
"ce-id": "1234",
},
};
const event: CloudEvent = HTTP.toEvent(message) as CloudEvent;
expect(event.data).to.equal(body);
expect(event.datacontentype).to.equal(undefined);
});
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",
@ -59,6 +133,39 @@ describe("HTTP transport", () => {
expect(HTTP.isEvent(message)).to.be.true;
});
it("Handles CloudEvents with datacontenttype of text/plain", () => {
const message: Message = HTTP.binary(
new CloudEvent({
source: "/test",
type: "example",
datacontenttype: "text/plain",
data: "Hello, friends!",
}),
);
const event = HTTP.toEvent(message) as CloudEvent<string>;
expect(event.validate()).to.be.true;
});
it("Respects extension attribute casing (even if against spec)", () => {
// Now create a message that is an event
const message = {
body: `{ "greeting": "hello" }`,
headers: {
[CONSTANTS.CE_HEADERS.ID]: "1234",
[CONSTANTS.CE_HEADERS.SOURCE]: "test",
[CONSTANTS.CE_HEADERS.TYPE]: "test.event",
[CONSTANTS.CE_HEADERS.SPEC_VERSION]: V1,
"ce-LUNCH": "tacos",
},
};
expect(HTTP.isEvent(message)).to.be.true;
const event = HTTP.toEvent(message) as CloudEvent;
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 = {
@ -71,7 +178,9 @@ describe("HTTP transport", () => {
},
};
expect(HTTP.isEvent(message)).to.be.true;
expect(HTTP.toEvent(message)).not.to.throw;
const event = HTTP.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", () => {
@ -88,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",
@ -98,7 +207,7 @@ describe("HTTP transport", () => {
// no required ce-source header, thus an invalid event
},
};
const event = HTTP.toEvent(message);
const event = HTTP.toEvent(message) as CloudEvent;
expect(event).to.be.instanceOf(CloudEvent);
// ensure that we actually now have an invalid event
expect(event.validate).to.throw;
@ -128,20 +237,20 @@ describe("HTTP transport", () => {
id,
type,
source,
specversion: Version.V1,
specversion: V1,
data: { lunch: "tacos" },
});
const message: Message = {
const message: Message<undefined> = {
headers,
body,
};
const event = HTTP.toEvent(message);
const event = HTTP.toEvent(message) as CloudEvent;
expect(event.data).to.deep.equal({ lunch: "tacos" });
});
describe("Specification version V1", () => {
const fixture: CloudEvent = new CloudEvent({
specversion: Version.V1,
const fixture = new CloudEvent({
specversion: V1,
id,
type,
source,
@ -159,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);
@ -171,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);
@ -208,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);
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);
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);
const eventDeserialized = HTTP.toEvent(message) as CloudEvent<Uint32Array>;
expect(eventDeserialized.data).to.deep.equal(imageData);
expect(eventDeserialized.data_base64).to.equal(image_base64);
});
@ -236,15 +345,15 @@ 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);
const eventDeserialized = HTTP.toEvent(message) as CloudEvent<Uint32Array>;
expect(eventDeserialized.data).to.deep.equal(dataBinary);
expect(eventDeserialized.data_base64).to.equal(data_base64);
});
});
describe("Specification version V03", () => {
const fixture: CloudEvent = new CloudEvent({
specversion: Version.V03,
const fixture = new CloudEvent({
specversion: V03,
id,
type,
source,
@ -262,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);
@ -278,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);
@ -313,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);
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);
const eventDeserialized = HTTP.toEvent(message) as CloudEvent<Uint32Array>;
expect(eventDeserialized.data).to.deep.equal(imageData);
expect(eventDeserialized.data_base64).to.equal(image_base64);
});

View File

@ -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);
});
});

View File

@ -1,3 +1,8 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
import "mocha";
import { expect } from "chai";
@ -47,16 +52,23 @@ describe("JSON Event Format Parser", () => {
it("Throw error when payload is an invalid JSON", () => {
// setup
const payload = "gg";
const payload = "{gg";
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 0");
expect(parser.parse.bind(parser, payload)).to.throw(SyntaxError);
});
it("Accepts a string as valid JSON", () => {
// setup
const payload = "I am a string!";
const parser = new Parser();
expect(parser.parse(payload)).to.equal("I am a string!");
});
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();

View File

@ -1,10 +1,17 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
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", () => {
@ -14,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");
});
});
});

View File

@ -1,192 +0,0 @@
import "mocha";
import { expect } from "chai";
import { CloudEvent, Version, ValidationError, Mode } from "../../src";
import Constants from "../../src/constants";
const id = "97699ec2-a8d9-47c1-bfa0-ff7aa526f838";
const type = "com.github.pull.create";
const source = "urn:event:from:myapi/resourse/123";
const time = new Date().toISOString();
const schemaurl = "http://example.com/registry/myschema.json";
const data = {
much: "wow",
};
const subject = "subject-x0";
let cloudevent = new CloudEvent({
specversion: Version.V03,
id,
source,
type,
subject,
time,
data,
schemaurl,
datacontenttype: Constants.MIME_JSON,
});
describe("CloudEvents Spec v0.3", () => {
describe("REQUIRED Attributes", () => {
it("Should have 'id'", () => {
expect(cloudevent.id).to.equal(id);
});
it("Should have 'source'", () => {
expect(cloudevent.source).to.equal(source);
});
it("Should have 'specversion'", () => {
expect(cloudevent.specversion).to.equal(Version.V03);
});
it("Should have 'type'", () => {
expect(cloudevent.type).to.equal(type);
});
});
describe("OPTIONAL Attributes", () => {
it("Should have 'datacontentencoding'", () => {
cloudevent = cloudevent.cloneWith({
datacontentencoding: Constants.ENCODING_BASE64,
data: "SSB3YXMgZnVubnkg8J+Ygg==",
});
expect(cloudevent.datacontentencoding).to.equal(Constants.ENCODING_BASE64);
cloudevent = cloudevent.cloneWith({ datacontentencoding: undefined, data: data });
});
it("Should have 'datacontenttype'", () => {
expect(cloudevent.datacontenttype).to.equal(Constants.MIME_JSON);
});
it("Should have 'schemaurl'", () => {
expect(cloudevent.schemaurl).to.equal(schemaurl);
});
it("Should have 'subject'", () => {
expect(cloudevent.subject).to.equal(subject);
});
it("Should have 'time'", () => {
expect(cloudevent.time).to.equal(time);
});
it("Should have 'data'", () => {
expect(cloudevent.data).to.deep.equal(data);
});
it("Should have the 'extension1'", () => {
cloudevent = cloudevent.cloneWith({ extension1: "value1" });
expect(cloudevent.extension1).to.equal("value1");
});
});
describe("The Constraints check", () => {
describe("'id'", () => {
it("should throw an error when trying to remove", () => {
expect(() => {
delete cloudevent.id;
}).to.throw(TypeError);
});
it("defaut ID create when an empty string", () => {
cloudevent = cloudevent.cloneWith({ id: "" });
expect(cloudevent.id.length).to.be.greaterThan(0);
});
});
describe("'source'", () => {
it("should throw an error when trying to remove", () => {
expect(() => {
delete cloudevent.source;
}).to.throw(TypeError);
});
});
describe("'specversion'", () => {
it("should throw an error when trying to remove", () => {
expect(() => {
delete cloudevent.specversion;
}).to.throw(TypeError);
});
});
describe("'type'", () => {
it("should throw an error when trying to remove", () => {
expect(() => {
delete cloudevent.type;
}).to.throw(TypeError);
});
it("should throw an error when is an empty string", () => {
expect(() => {
cloudevent.cloneWith({ type: "" });
}).to.throw(ValidationError, "invalid payload");
});
it("must be a non-empty string", () => {
cloudevent.cloneWith({ type: type });
expect(cloudevent.type).to.equal(type);
});
});
describe("'datacontentencoding'", () => {
it("should throw an error when is a unsupported encoding", () => {
expect(() => {
cloudevent.cloneWith({ data: "Y2xvdWRldmVudHMK", datacontentencoding: Mode.BINARY });
}).to.throw(ValidationError, "invalid payload");
cloudevent.cloneWith({ data: data, datacontentencoding: undefined });
});
it("should throw an error when 'data' does not carry base64", () => {
expect(() => {
cloudevent.cloneWith({
data: "no base 64 value",
datacontentencoding: Constants.ENCODING_BASE64,
datacontenttype: "text/plain",
});
}).to.throw(ValidationError, "invalid payload");
cloudevent.cloneWith({
data: data,
datacontentencoding: undefined,
});
});
it("should accept when 'data' is a string", () => {
cloudevent.cloneWith({ data: "Y2xvdWRldmVudHMK", datacontentencoding: Constants.ENCODING_BASE64 });
expect(cloudevent.validate()).to.be.true;
cloudevent.cloneWith({ data: data, datacontentencoding: undefined });
});
});
describe("'data'", () => {
it("should maintain the type of data when no data content type", () => {
cloudevent = cloudevent.cloneWith({ datacontenttype: undefined });
cloudevent.data = JSON.stringify(data);
expect(typeof cloudevent.data).to.equal("string");
});
});
describe("'subject'", () => {
it("should throw an error when is an empty string", () => {
expect(() => {
cloudevent.cloneWith({ subject: "" });
}).to.throw(ValidationError);
});
});
describe("'time'", () => {
it("must adhere to the format specified in RFC 3339", () => {
const d = new Date();
cloudevent = cloudevent.cloneWith({ time: d.toString() });
// ensure that we always get back the same thing we passed in
expect(cloudevent.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());
});
});
});
});

View File

@ -1,6 +1,11 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
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";
@ -14,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,
@ -94,26 +99,36 @@ 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", () => {
describe("'id'", () => {
it("should throw an error when trying to remove", () => {
expect(() => {
delete cloudevent.id;
delete (cloudevent as any).id;
}).to.throw(TypeError);
});
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);
});
});
describe("'source'", () => {
it("should throw an error when trying to remove", () => {
expect(() => {
delete cloudevent.source;
delete (cloudevent as any).source;
}).to.throw(TypeError);
});
});
@ -121,7 +136,7 @@ describe("CloudEvents Spec v1.0", () => {
describe("'specversion'", () => {
it("should throw an error when trying to remove", () => {
expect(() => {
delete cloudevent.specversion;
delete (cloudevent as any).specversion;
}).to.throw(TypeError);
});
});
@ -129,7 +144,7 @@ describe("CloudEvents Spec v1.0", () => {
describe("'type'", () => {
it("should throw an error when trying to remove", () => {
expect(() => {
delete cloudevent.type;
delete (cloudevent as any).type;
}).to.throw(TypeError);
});
});
@ -145,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());
});
});
});
@ -159,24 +174,69 @@ describe("CloudEvents Spec v1.0", () => {
expect(cloudevent.data).to.deep.equal(data);
});
it("should maintain the type of data when no data content type", () => {
const dct = cloudevent.datacontenttype;
cloudevent = cloudevent.cloneWith({ datacontenttype: undefined });
cloudevent.data = JSON.stringify(data);
expect(typeof cloudevent.data).to.equal("string");
cloudevent = cloudevent.cloneWith({ datacontenttype: dct });
it("should maintain the type of data when no datacontenttype is provided", () => {
const ce = new CloudEvent({
source: "/cloudevents/test",
type: "cloudevents.test",
data: JSON.stringify(data),
});
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);
cloudevent = cloudevent.cloneWith({ datacontenttype: "text/plain", data: dataBinary });
expect(cloudevent.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);
});
});
});
});

View File

@ -1,3 +1,8 @@
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
import "mocha";
import { expect } from "chai";
import { isStringOrThrow, equalsOrThrow, isBase64, asData } from "../../src/event/validation";

View File

@ -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": [

View File

@ -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",