@serialized/serialized-client

Client library for Serialized APIs.

Usage no npm install needed!

<script type="module">
  import serializedSerializedClient from 'https://cdn.skypack.dev/@serialized/serialized-client';
</script>

README

Serialized Javascript & Typescript client

The official Javascript/Typescript client for Serialized.

✨ Features

  • Client for Event Sourcing & CQRS APIs provided by Serialized
  • Works both for Typescript and Javascript on Node version >= 10.
  • Promise-based API that supports async/await
  • Built with Typescript
  • Provides an easy way to implement DDD Aggregates using Event Sourcing.

💡 Getting Started

Register for a free account at https://serialized.io to get your access keys to the API (if you haven't already).

Install the Serialized TS/JS client via the npm package manager:

npm install @serialized/serialized-client

Import the library and initialize the client instance:

import {Serialized} from "@serialized/serialized-client"

const serialized = Serialized.create({
    accessKey: "<YOUR_ACCESS_KEY>", 
    secretAccessKey: "<YOUR_SECRET_ACCESS_KEY>"
});

Create our domain

State

The state type holds the assembled state from the events during the load of the aggregate.

// The different statuses our game can be in
enum GameStatus {
  UNDEFINED = 'UNDEFINED',
  CREATED = 'CREATED',
  STARTED = 'STARTED',
  FINISHED = 'FINISHED'
}

type GameState = {
  readonly gameId?: string;
  readonly status?: GameStatus;
}

Events

Define your domain events as immutable Typescript classes.

class GameCreated {
  constructor(readonly gameId: string,
              readonly creationTime: number) {
  };
}

class GameStarted {
  constructor(readonly gameId: string,
              readonly startTime: number) {
  };
}

Next, we create the state builder, which can handle loading events one-by-one to create the current state.

The state builder has methods decorated with @EventHandler to mark its event handling methods:

class GameStateBuilder {

  get initialState(): GameState {
    return {
      status: GameStatus.UNDEFINED
    }
  }

  @EventHandler(GameCreated)
  handleGameCreated(state: GameState, event: DomainEvent<GameCreated>): GameState {
    return {gameId: state.gameId, status: GameStatus.CREATED};
  }

  @EventHandler(GameStarted)
  handleGameStarted(state: GameState, event: DomainEvent<GameStarted>): GameState {
    return {...state, status: GameStatus.STARTED};
  }

}

Aggregate

The aggregate contains the domain logic and each method should return 0..n events that should be stored for a successful operation.

Any unsuccessful operation should throw an error.

@Aggregate('game', GameStateBuilder)
class Game {

  constructor(private readonly state: GameState) {
  }

  create(gameId: string, creationTime: number) {
    return [DomainEvent.create(new GameCreated(gameId, creationTime))];
  }

  start(gameId: string, startTime: number) {
    if (this.state.status !== GameStatus.CREATED) {
      throw new Error('Must create Game before you can start it');
    }
    return [DomainEvent.create(new GameStarted(gameId, startTime))];
  }

-

}

Test the client by creating a Game:

const gameClient = serialized.aggregateClient(Game);
const gameId = uuidv4();
await gameClient.create(gameId, (game) => (game.create(gameId, Date.now())));

To perform an update operation, which means loading all events, performing business logic and then appending more events

await gameClient.update(gameId, (game: Game) => (game.start(gameId, startTime)));

📄 Client reference

📄 More resources

❓ Troubleshooting

Encountering an issue? Don't feel afraid to add an issue here on Github or to reach out via Serialized.