diff --git a/docs/modules/eventstoredb.md b/docs/modules/eventstoredb.md new file mode 100644 index 00000000..05dcdb1d --- /dev/null +++ b/docs/modules/eventstoredb.md @@ -0,0 +1,19 @@ +# EventStoreDB Module + +[EventStoreDB](https://eventstore.com) is an event sourcing database that stores data in streams of immutable events. + +## Install + +```bash +npm install @testcontainers/eventstoredb --save-dev +``` + +## Examples + + +[Start container:](../../packages/modules/eventstoredb/src/eventstoredb-container.test.ts) inside_block:startContainer + + + +[Subscribe to standard projection:](../../packages/modules/eventstoredb/src/eventstoredb-container.test.ts) inside_block:usingStandardProjections + diff --git a/mkdocs.yml b/mkdocs.yml index 4bf2e32d..92091d74 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -48,6 +48,7 @@ nav: - ChromaDB: modules/chromadb.md - Couchbase: modules/couchbase.md - Elasticsearch: modules/elasticsearch.md + - EventStoreDB: modules/eventstoredb.md - GCloud: modules/gcloud.md - HiveMQ: modules/hivemq.md - K3s: modules/k3s.md diff --git a/package-lock.json b/package-lock.json index e2796cc3..0aea29ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2560,6 +2560,32 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@eventstore/db-client": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@eventstore/db-client/-/db-client-6.2.1.tgz", + "integrity": "sha512-utUkIO3Ns9TzTHOTDg+eVTm2Z/Jj5X3VT/otZEWwgmOm7CgSnrWdOxEsAN1jfMht7oVBRB5iGHYFGA3IiQm/Ng==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.9.12", + "@types/debug": "^4.1.12", + "@types/google-protobuf": "^3.15.12", + "@types/node": "^16.18.67", + "debug": "^4.3.2", + "google-protobuf": "^3.21.2", + "uuid": "^8.3.2" + }, + "engines": { + "node": "^12.18.3 || >=14" + } + }, + "node_modules/@eventstore/db-client/node_modules/@types/node": { + "version": "16.18.123", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.123.tgz", + "integrity": "sha512-/n7I6V/4agSpJtFDKKFEa763Hc1z3hmvchobHS1TisCOTKD5nxq8NJ2iK7SRIMYL276Q9mgWOx2AWp5n2XI6eA==", + "dev": true, + "license": "MIT" + }, "node_modules/@fastify/busboy": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", @@ -5736,6 +5762,10 @@ "resolved": "packages/modules/elasticsearch", "link": true }, + "node_modules/@testcontainers/eventstoredb": { + "resolved": "packages/modules/eventstoredb", + "link": true + }, "node_modules/@testcontainers/gcloud": { "resolved": "packages/modules/gcloud", "link": true @@ -6093,6 +6123,13 @@ "integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==", "dev": true }, + "node_modules/@types/google-protobuf": { + "version": "3.15.12", + "resolved": "https://registry.npmjs.org/@types/google-protobuf/-/google-protobuf-3.15.12.tgz", + "integrity": "sha512-40um9QqwHjRS92qnOaDpL7RmDK15NuZYo9HihiJRbYkMQZlWnuH8AdvbMy8/o6lgLmKbDUKa+OALCltHdbOTpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -11842,6 +11879,13 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/google-protobuf": { + "version": "3.21.4", + "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.4.tgz", + "integrity": "sha512-MnG7N936zcKTco4Jd2PX2U96Kf9PxygAPKBug+74LHzmHXmceN16MmRcdgZv+DGef/S9YvQAfRsNCn4cjf9yyQ==", + "dev": true, + "license": "(BSD-3-Clause AND Apache-2.0)" + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -21168,6 +21212,17 @@ "@elastic/elasticsearch": "^7.17.14" } }, + "packages/modules/eventstoredb": { + "name": "@testcontainers/eventstoredb", + "version": "10.16.0", + "license": "MIT", + "dependencies": { + "testcontainers": "^10.16.0" + }, + "devDependencies": { + "@eventstore/db-client": "^6.2.1" + } + }, "packages/modules/gcloud": { "name": "@testcontainers/gcloud", "version": "10.16.0", diff --git a/packages/modules/eventstoredb/jest.config.ts b/packages/modules/eventstoredb/jest.config.ts new file mode 100644 index 00000000..1f677baa --- /dev/null +++ b/packages/modules/eventstoredb/jest.config.ts @@ -0,0 +1,11 @@ +import type { Config } from "jest"; +import * as path from "path"; + +const config: Config = { + preset: "ts-jest", + moduleNameMapper: { + "^testcontainers$": path.resolve(__dirname, "../../testcontainers/src"), + }, +}; + +export default config; diff --git a/packages/modules/eventstoredb/package.json b/packages/modules/eventstoredb/package.json new file mode 100644 index 00000000..30a93274 --- /dev/null +++ b/packages/modules/eventstoredb/package.json @@ -0,0 +1,32 @@ +{ + "name": "@testcontainers/eventstoredb", + "version": "10.16.0", + "description": "EventStoreDB module for Testcontainers", + "main": "build/index.js", + "scripts": { + "prepack": "shx cp ../../../README.md . && shx cp ../../../LICENSE .", + "build": "tsc --project tsconfig.build.json" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/testcontainers/testcontainers-node.git" + }, + "keywords": [ + "eventstoredb", + "testing", + "docker", + "testcontainers" + ], + "author": "", + "license": "MIT", + "bugs": { + "url": "https://github.com/testcontainers/testcontainers-node/issues" + }, + "homepage": "https://github.com/testcontainers/testcontainers-node#readme", + "dependencies": { + "testcontainers": "^10.16.0" + }, + "devDependencies": { + "@eventstore/db-client": "^6.2.1" + } +} diff --git a/packages/modules/eventstoredb/src/eventstoredb-container.test.ts b/packages/modules/eventstoredb/src/eventstoredb-container.test.ts new file mode 100644 index 00000000..b5a7c624 --- /dev/null +++ b/packages/modules/eventstoredb/src/eventstoredb-container.test.ts @@ -0,0 +1,106 @@ +import { EventStoreDBClient, StreamingRead, StreamSubscription } from "@eventstore/db-client"; +import { EventStoreDBContainer } from "./eventstoredb-container"; + +describe("EventStoreDBContainer", () => { + jest.setTimeout(240_000); + + // startContainer { + it("should execute write and read", async () => { + const container = await new EventStoreDBContainer().start(); + + const client = EventStoreDBClient.connectionString(container.getConnectionString()); + + await client.appendToStream("User-1", [ + { + contentType: "application/json", + data: { email: "john@foo.local" }, + type: "UserCreated", + id: "28ab6bca-d9ae-418b-a1af-eb65dd653c38", + metadata: { + someMetadata: "bar", + }, + }, + ]); + + expect(await consumeSteamingRead(client.readStream("User-1"))).toEqual([ + expect.objectContaining({ + event: expect.objectContaining({ + data: { + email: "john@foo.local", + }, + id: "28ab6bca-d9ae-418b-a1af-eb65dd653c38", + isJson: true, + metadata: { + someMetadata: "bar", + }, + revision: 0n, + streamId: "User-1", + type: "UserCreated", + }), + }), + ]); + + await container.stop(); + }); + // } + + // usingStandardProjections { + it("should use built-in projections", async () => { + const container = await new EventStoreDBContainer().start(); + const client = EventStoreDBClient.connectionString(container.getConnectionString()); + + await client.appendToStream("Todo-1", [ + { + contentType: "application/json", + data: { title: "Do something" }, + metadata: {}, + id: "7eccc3a7-0664-4348-a621-029125741e22", + type: "TodoCreated", + }, + ]); + const stream = client.subscribeToStream("$ce-Todo", { resolveLinkTos: true }); + + expect(await getStreamFirstEvent(stream)).toEqual( + expect.objectContaining({ + event: expect.objectContaining({ + data: { title: "Do something" }, + id: "7eccc3a7-0664-4348-a621-029125741e22", + isJson: true, + metadata: {}, + revision: 0n, + streamId: "Todo-1", + type: "TodoCreated", + }), + link: expect.objectContaining({ + isJson: false, + metadata: expect.objectContaining({ + $causedBy: "7eccc3a7-0664-4348-a621-029125741e22", + $o: "Todo-1", + }), + revision: 0n, + streamId: "$ce-Todo", + type: "$>", + }), + }) + ); + await stream.unsubscribe(); + await container.stop(); + }); + // } +}); + +async function consumeSteamingRead(read: StreamingRead): Promise { + const events = []; + + for await (const event of read) { + events.push(event); + } + + return events; +} + +async function getStreamFirstEvent(stream: StreamSubscription): Promise { + for await (const event of stream) { + return event; + } +} diff --git a/packages/modules/eventstoredb/src/eventstoredb-container.ts b/packages/modules/eventstoredb/src/eventstoredb-container.ts new file mode 100644 index 00000000..7a22c1d5 --- /dev/null +++ b/packages/modules/eventstoredb/src/eventstoredb-container.ts @@ -0,0 +1,29 @@ +import { AbstractStartedContainer, GenericContainer, Wait } from "testcontainers"; + +export class StartedEventStoreDBContainer extends AbstractStartedContainer { + getConnectionString(): string { + return `esdb://${this.getHost()}:${this.getFirstMappedPort()}?tls=false`; + } +} + +const EVENT_STORE_DB_PORT = 2113; + +export class EventStoreDBContainer extends GenericContainer { + constructor(image = "eventstore/eventstore:24.10") { + super(image); + + this.withExposedPorts(EVENT_STORE_DB_PORT) + .withEnvironment({ + EVENTSTORE_CLUSTER_SIZE: "1", + EVENTSTORE_RUN_PROJECTIONS: "All", + EVENTSTORE_START_STANDARD_PROJECTIONS: "true", + EVENTSTORE_INSECURE: "true", + }) + .withStartupTimeout(120_000) + .withWaitStrategy(Wait.forHealthCheck()); + } + + public override async start(): Promise { + return new StartedEventStoreDBContainer(await super.start()); + } +} diff --git a/packages/modules/eventstoredb/src/index.ts b/packages/modules/eventstoredb/src/index.ts new file mode 100644 index 00000000..a47095f1 --- /dev/null +++ b/packages/modules/eventstoredb/src/index.ts @@ -0,0 +1 @@ +export { StartedEventStoreDBContainer, EventStoreDBContainer } from "./eventstoredb-container"; diff --git a/packages/modules/eventstoredb/tsconfig.build.json b/packages/modules/eventstoredb/tsconfig.build.json new file mode 100644 index 00000000..0222f6ff --- /dev/null +++ b/packages/modules/eventstoredb/tsconfig.build.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "exclude": [ + "build", + "jest.config.ts", + "src/**/*.test.ts" + ], + "references": [ + { + "path": "../../testcontainers" + } + ] +} \ No newline at end of file diff --git a/packages/modules/eventstoredb/tsconfig.json b/packages/modules/eventstoredb/tsconfig.json new file mode 100644 index 00000000..39b16581 --- /dev/null +++ b/packages/modules/eventstoredb/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "build", + "paths": { + "testcontainers": [ + "../../testcontainers/src" + ] + } + }, + "exclude": [ + "build", + "jest.config.ts" + ], + "references": [ + { + "path": "../../testcontainers" + } + ] +} \ No newline at end of file