Skip to content

Client language split #901

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 32 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
26bdf96
start client split
Deflaimun Jun 9, 2025
2b6c660
docs: initial client-split cleanup for NodeJS
Jun 16, 2025
a785ee2
docs: initial client-split cleanup for Python
Jun 16, 2025
00c4d54
docs: initial client split cleanup for Rust
Jun 16, 2025
b279058
docs: client split work - fixing broken image link
Jun 16, 2025
4df00f4
docs: initial client split cleanup for Java
Jun 16, 2025
b473bce
docs: initial client split cleanup for Go
Jun 16, 2025
afb4160
docs: initial client split cleanup for C#
Jun 16, 2025
6413532
docs: client split cleanup - tighten up release notes topic
Jun 16, 2025
7237167
docs: client split cleanup - fix subscription topic
Jun 17, 2025
1ba2d0f
docs: client split cleanup - tighten up required packages sections
Jun 17, 2025
f9506ba
docs: removed old non-split client files
Jun 17, 2025
c849f01
docs: fix code sample
Jun 17, 2025
abc4080
Update samples script and adjust navbar
Deflaimun Jun 17, 2025
c739bd4
remove "client" from headers
Deflaimun Jun 17, 2025
9af6713
Merge branch 'master' into client-split
stktung Jul 9, 2025
2529767
Renamed c# dir to dotnet, javascript dir to nodejs
stktung Jul 9, 2025
3ff3a80
Update nav and sidebar for language split
stktung Jul 9, 2025
dac7101
Remove remaining snippets with multiple languages
stktung Jul 10, 2025
b5fb901
Removed links to observability samples of unrelated languages
stktung Jul 10, 2025
265d03a
MD cleanup
stktung Jul 10, 2025
fd54ab2
Made note that delete projection is not available for dotnet client
stktung Jul 10, 2025
3150a91
Removed unnecessary package installation instructions
stktung Jul 10, 2025
7f6f698
Removed observability page for python since it's not available
stktung Jul 10, 2025
d7071b3
Removed user cert samples for unrelated languages
stktung Jul 10, 2025
d8acd1e
Re-added the old pages to help redirect to right lang
stktung Jul 10, 2025
f28ef57
Fixed breadcrumbs
stktung Jul 10, 2025
e94e6bc
Updated custom browser title for SEO
stktung Jul 10, 2025
f8e94d9
Ask SEO not to index the old client pages anymore
stktung Jul 10, 2025
e67cce1
Added missing rust user cert sample
stktung Jul 10, 2025
370d65e
Added redirects for language pages straight to getting started
stktung Jul 10, 2025
15be5cd
Fixed custom title of .net client appending events
stktung Jul 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion docs/.vuepress/configs/navbar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,12 @@ export const navbarEn: NavbarOptions = [
{
text: "Clients",
children: [
{ text: "KurrentDB clients", link: "/clients/grpc/getting-started" },
{ text: ".NET", link: "/clients/grpc/dotnet/getting-started.html" },
{ text: "Python", link: "/clients/grpc/python/getting-started.html" },
{ text: "Node.js", link: "/clients/grpc/nodejs/getting-started.html" },
{ text: "Java", link: "/clients/grpc/java/getting-started.html" },
{ text: "Go", link: "/clients/grpc/go/getting-started.html" },
{ text: "Rust", link: "/clients/grpc/rust/getting-started.html" },
],
},
{ text: "HTTP API", children: ver.linksFor("http-api", false) },
Expand Down
7 changes: 6 additions & 1 deletion docs/.vuepress/configs/sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,12 @@ export const sidebarEn: EsSidebarOptions = {
},

],
"/clients/grpc/": "structure",
"/clients/grpc/dotnet/": "structure",
"/clients/grpc/python/": "structure",
"/clients/grpc/nodejs/": "structure",
"/clients/grpc/java/": "structure",
"/clients/grpc/go/": "structure",
"/clients/grpc/rust/": "structure",
"/cloud/": "structure",
...ver.getSidebars(),
"/clients/tcp/dotnet/21.2/": "structure",
Expand Down
47 changes: 38 additions & 9 deletions docs/.vuepress/lib/samples.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,38 @@
import {logger, path} from 'vuepress/utils';
import {type ResolvedImport} from "../markdown/xode/types";
import version from "./version";
import * as fs from 'fs';

const base = "../../samples";

export function resolveMultiSamplesPath(src: string): ResolvedImport[] {
const split = src.split(':');
const cat = split.length < 2 ? undefined : split[0];
const paths = split.length === 1 ? src : split[1];
return paths.split(';').map(x => {
const r = resolveSamplesPath(x, cat);
return {label: r.label, importPath: r.path};
})
return paths.split(';')
.filter(x => x.trim() !== '') // Filter out empty strings
.map(x => {
const r = resolveSamplesPath(x, cat);
return {label: r.label, importPath: r.path};
})
}

export function resolveSamplesPath(src: string, srcCat: string | undefined) {
const def = (s: string) => {
return {label: "", path: s}
};

const ext = src.split('.').pop()!;
// Handle empty src
if (!src || src.trim() === '') {
console.warn(`Empty source path provided, srcCat: "${srcCat}"`);
return def(src);
}

const srcParts = src.split('.');
const ext = srcParts.length > 1 ? srcParts.pop()! : '';
const pseudo = src.split('/');
const includesCat = pseudo[0].startsWith('@');

if (!includesCat && srcCat === undefined) return def(src);

const cats: Record<string, Record<string, {path: string, version?: string, label?: string}>> = {
Expand Down Expand Up @@ -78,18 +89,36 @@ export function resolveSamplesPath(src: string, srcCat: string | undefined) {
}

let lang = cat[ext] ?? cat["default"];
if (lang === undefined && cat.path === undefined) {
logger.warn(`Unknown extension ${ext} in ${cat}`);
if (lang === undefined) {
// If no extension match and no default, try to find by partial match or return default
logger.warn(`Unknown extension "${ext}" in category "${catName}". Available extensions: ${Object.keys(cat).join(', ')}`);
return def(src);
}

// If we don't have an extension but we have a default, use it
if (ext === '' && cat["default"]) {
lang = cat["default"];
}

const samplesVersion = isVersion ? pseudo[1] : lang.version;
const langPath = samplesVersion !== undefined ? `${lang.path}/${samplesVersion}` : lang.path;
const toReplace = isVersion ? `${pseudo[0]}/${pseudo[1]}` : `${pseudo[0]}`;

const p = includesCat ? src.replace(toReplace, `${base}/${langPath}`) : `${base}/${langPath}/${src}`;
const resolvedPath = path.resolve(__dirname, p);

// Check if the resolved path is a directory, and if so, warn and return the original src
try {
const stat = fs.statSync(resolvedPath);
if (stat.isDirectory()) {
logger.warn(`Resolved path is a directory, not a file: ${resolvedPath}`);
return def(src);
}
} catch (error) {
// File doesn't exist, which is handled elsewhere
}

return {label: lang.label, path: path.resolve(__dirname, p)};
return {label: lang.label, path: resolvedPath};
}

export const projectionSamplesPath = "https://github.com/kurrent-io/KurrentDB/53f84e55ea56ccfb981aff0e432581d72c23fbf6/samples/http-api/data/";
export const projectionSamplesPath = "https://github.com/kurrent-io/KurrentDB/53f84e55ea56ccfb981aff0e432581d72c23fbf6/samples/http-api/data/";
9 changes: 8 additions & 1 deletion docs/.vuepress/public/_redirects
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
# ######################

# redirect for internet search on "esdb .net client
/clients/dotnet/5.0/connecting.html /clients/grpc/getting-started.html#connecting-to-eventstoredb 301
/clients/dotnet/5.0/connecting.html /clients/grpc/dotnet/getting-started.html#connecting-to-eventstoredb 301

# TCP Clients
/clients/dotnet/21.2/migration-to-gRPC.html#appending-events /clients/tcp/dotnet/21.2/migration-to-gRPC.html#appending-events 301
Expand Down Expand Up @@ -53,6 +53,13 @@
/clients/http-api/generated/v5/docs/introduction/reading-streams.html /http-api/v5/#reading-streams-and-events 301
/clients/http-api/generated/v5/docs/introduction/optimistic-concurrency-and-idempotence.html /http-api/v5/#optimistic-concurrency-and-idempotence 301

/clients/grpc/python /clients/grpc/python/getting-started.html 301
/clients/grpc/rust /clients/grpc/rust/getting-started.html 301
/clients/grpc/nodejs /clients/grpc/nodejs/getting-started.html 301
/clients/grpc/java /clients/grpc/java/getting-started.html 301
/clients/grpc/dotnet /clients/grpc/dotnet/getting-started.html 301
/clients/grpc/go /clients/grpc/go/getting-started.html 301

/server/generated/v5/http-api/persistent-subscriptions.html /http-api/v5/persistent.html 301
/server/generated/v5/http-api/reading-subscribing-events.html /http-api/v5/#reading-an-event-from-a-stream 301
/server/v5/samples/http-api/event.json /http-api/v5/api.html 301
Expand Down
22 changes: 18 additions & 4 deletions docs/clients/grpc/README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
---
index: false
sitemap:
priority: 0
changefreq: monthly
breadcrumbExclude: true
---

# Clients
# KurrentDB Clients

Learn how to use the KurrentDB client libraries to interact with the database.
KurrentDB offers official client libraries for multiple programming languages, making it easy to build applications that work with event-native workloads.

<Catalog/>
Select your client library to view the documentation:

<img src="https://skillicons.dev/icons?i=dotnet" alt=".NET" style="height: 1.5em; vertical-align: middle;" /> [.NET](/clients/grpc/dotnet/getting-started.md)

<img src="https://skillicons.dev/icons?i=python" alt="Python" style="height: 1.5em; vertical-align: middle;" /> [Python](/clients/grpc/python/getting-started.md)

<img src="https://skillicons.dev/icons?i=nodejs" alt="Node.js" style="height: 1.5em; vertical-align: middle;" /> [Node.js](/clients/grpc/nodejs/getting-started.md)

<img src="https://skillicons.dev/icons?i=java" alt="Java" style="height: 1.5em; vertical-align: middle;" /> [Java](/clients/grpc/java/getting-started.md)

<img src="https://skillicons.dev/icons?i=go" alt="Go" style="height: 1.5em; vertical-align: middle;" /> [Go](/clients/grpc/go/getting-started.md)

<img src="https://skillicons.dev/icons?i=rust" alt="Rust" style="height: 1.5em; vertical-align: middle;" /> [Rust](/clients/grpc/rust/getting-started.md)
82 changes: 10 additions & 72 deletions docs/clients/grpc/appending-events.md
Original file line number Diff line number Diff line change
@@ -1,83 +1,21 @@
---
order: 2
sitemap:
priority: 0
changefreq: monthly
---

# Appending events

When you start working with KurrentDB, it is empty. The first meaningful operation is to add one or more events to the database using one of the available client SDKs.
To redirect you to the right page, please select a client:

::: tip
Check the [Getting Started](getting-started.md) guide to learn how to configure and use the client SDK.
:::
<img src="https://skillicons.dev/icons?i=dotnet" alt=".NET" style="height: 1.5em; vertical-align: middle;" /> [.NET](/clients/grpc/dotnet/appending-events.md)

## Append your first event
<img src="https://skillicons.dev/icons?i=python" alt="Python" style="height: 1.5em; vertical-align: middle;" /> [Python](/clients/grpc/python/appending-events.md)

The simplest way to append an event to KurrentDB is to create an `EventData` object and call `AppendToStream` method.
<img src="https://skillicons.dev/icons?i=nodejs" alt="Node.js" style="height: 1.5em; vertical-align: middle;" /> [Node.js](/clients/grpc/nodejs/appending-events.md)

@[code{append-to-stream}](@grpc:appending_events.py;appending-events.js;appending-events.ts;appending_events/AppendingEvents.java;appending-events/Program.cs;appendingEvents.go;appending_events.rs)
<img src="https://skillicons.dev/icons?i=java" alt="Java" style="height: 1.5em; vertical-align: middle;" /> [Java](/clients/grpc/java/appending-events.md)

`AppendToStream` takes a collection of `EventData`, which allows you to save more than one event in a single batch.

Outside the example above, other options exist for dealing with different scenarios.

::: tip
If you are new to Event Sourcing, please study the [Handling concurrency](#handling-concurrency) section below.
:::

## Working with EventData

Events appended to KurrentDB must be wrapped in an `EventData` object. This allows you to specify the event's content, the type of event, and whether it's in JSON format. In its simplest form, you need three arguments: **eventId**, **type**, and **data**.

### eventId

This takes the format of a `Uuid` and is used to uniquely identify the event you are trying to append. If two events with the same `Uuid` are appended to the same stream in quick succession, KurrentDB will only append one of the events to the stream.

For example, the following code will only append a single event:

@[code{append-duplicate-event}](@grpc:appending_events.py;appending-events.js;appending-events.ts;appending_events/AppendingEvents.java;appending-events/Program.cs;appendingEvents.go;appending_events.rs)

![Duplicate Event](./images/duplicate-event.png)

### type

Each event should be supplied with an event type. This unique string is used to identify the type of event you are saving.

It is common to see the explicit event code type name used as the type as it makes serialising and de-serialising of the event easy. However, we recommend against this as it couples the storage to the type and will make it more difficult if you need to version the event at a later date.

### data

Representation of your event data. It is recommended that you store your events as JSON objects. This allows you to take advantage of all of KurrentDB's functionality, such as projections. That said, you can save events using whatever format suits your workflow. Eventually, the data will be stored as encoded bytes.

### metadata

Storing additional information alongside your event that is part of the event itself is standard practice. This can be correlation IDs, timestamps, access information, etc. KurrentDB allows you to store a separate byte array containing this information to keep it separate.

### isJson

Simple boolean field to tell KurrentDB if the event is stored as json, true by default.

## Handling concurrency

When appending events to a stream, you can supply a *stream state* or *stream revision*. Your client uses this to inform KurrentDB of the state or version you expect the stream to be in when appending an event. If the stream isn't in that state, an exception will be thrown.

For example, if you try to append the same record twice, expecting both times that the stream doesn't exist, you will get an exception on the second:

@[code{append-with-no-stream}](@grpc:appending_events.py;appending-events.js;appending-events.ts;appending_events/AppendingEvents.java;appending-events/Program.cs;appendingEvents.go;appending_events.rs)

There are three available stream states:
- `Any`
- `NoStream`
- `StreamExists`

This check can be used to implement optimistic concurrency. When retrieving a stream from KurrentDB, note the current version number. When you save it back, you can determine if somebody else has modified the record in the meantime.

@[code{append-with-concurrency-check}](@grpc:appending_events.py;appending-events.js;appending-events.ts;appending_events/AppendingEvents.java;appending-events/Program.cs;appendingEvents.go;appending_events.rs)

<!-- ## Options TODO -->

## User credentials

You can provide user credentials to append the data as follows. This will override the default credentials set on the connection.

@[code{overriding-user-credentials}](@grpc:appending_events.py;appending-events.js;appending-events.ts;appending_events/AppendingEvents.java;appending-events/Program.cs;appendingEvents.go;appending_events.rs)
<img src="https://skillicons.dev/icons?i=go" alt="Go" style="height: 1.5em; vertical-align: middle;" /> [Go](/clients/grpc/go/appending-events.md)

<img src="https://skillicons.dev/icons?i=rust" alt="Rust" style="height: 1.5em; vertical-align: middle;" /> [Rust](/clients/grpc/rust/appending-events.md)
68 changes: 11 additions & 57 deletions docs/clients/grpc/authentication.md
Original file line number Diff line number Diff line change
@@ -1,67 +1,21 @@
---
title: Authentication
order: 7
sitemap:
priority: 0
changefreq: monthly
---

## Client x.509 certificate <Badge type="warning" text="Commercial" vertical="middle"/>
# Authentication

X.509 certificates are digital certificates that use the X.509 public key infrastructure (PKI) standard to verify the identity of clients and servers. They play a crucial role in establishing a secure connection by providing a way to authenticate identities and establish trust.
To redirect you to the right page, please select a client:

### Prerequisites
<img src="https://skillicons.dev/icons?i=dotnet" alt=".NET" style="height: 1.5em; vertical-align: middle;" /> [.NET](/clients/grpc/dotnet/authentication.md)

1. KurrentDB 25.0 or greater, or EventStoreDB 24.10.
2. A commercial license with the User Certificates entitlement.
3. A valid x.509 certificate, which can be created using version `1.3` or higher of the [gencert tool](https://github.com/kurrent-io/es-gencert-cli).
4. The server must run in secure mode. See [Security Options](@server/security/protocol-security.md) for more information.
5. [Enable User Certificates plugin on the server](@server/security/user-authentication.md#user-x509-certificates)
<img src="https://skillicons.dev/icons?i=python" alt="Python" style="height: 1.5em; vertical-align: middle;" /> [Python](/clients/grpc/python/authentication.md)

#### Generate user certificates
<img src="https://skillicons.dev/icons?i=nodejs" alt="Node.js" style="height: 1.5em; vertical-align: middle;" /> [Node.js](/clients/grpc/nodejs/authentication.md)

The following command uses the [gencert tool](https://github.com/kurrent-io/es-gencert-cli) to generate a user certificate for the user `admin` that will expire in 10 days:

::: tabs#os
@tab bash
```bash
./es-gencert-cli create-user -username admin -days 10 -ca-certificate ./es-ca/ca.crt -ca-key ./es-ca/ca.key
```
@tab PowerShell
```powershell
.\es-gencert-cli.exe create-user -username admin -days 10 -ca-certificate ./es-ca/ca.crt -ca-key ./es-ca/ca.key
```
:::

### Connect to KurrentDB using an x.509 certificate

To connect to KurrentDB using an x.509 certificate, you need to provide the
certificate and the private key to the client. If both username/password and
certificate authentication data are supplied, the client prioritizes user
credentials for authentication. The client will throw an error if the
certificate and the key are not both provided.

::: tip
Please note that currently, password-protected private key files are not supported.
:::

The client supports the following parameters:

| Parameter | Description |
|----------------|--------------------------------------------------------------------------------|
| `userCertFile` | The file containing the X.509 user certificate in PEM format. |
| `userKeyFile` | The file containing the user certificate’s matching private key in PEM format. |

To authenticate, include these two parameters in your connection string or constructor when initializing the client.

Check the samples for the following clients:

::: code-tabs
@tab TypeScript
@[code{client-with-user-certificates}](@grpc:user-certificates.ts)
@tab Java
@[code{client-with-user-certificates}](@grpc:authentication/UserCertificate.java)
@tab C##
@[code{client-with-user-certificates}](@grpc:user-certificates/Program.cs)
@tab Go
@[code{client-with-user-certificates}](@grpc:/userCertificates.go)
:::
<img src="https://skillicons.dev/icons?i=java" alt="Java" style="height: 1.5em; vertical-align: middle;" /> [Java](/clients/grpc/java/authentication.md)

<img src="https://skillicons.dev/icons?i=go" alt="Go" style="height: 1.5em; vertical-align: middle;" /> [Go](/clients/grpc/go/authentication.md)

<img src="https://skillicons.dev/icons?i=rust" alt="Rust" style="height: 1.5em; vertical-align: middle;" /> [Rust](/clients/grpc/rust/authentication.md)
Loading