1.0.0-alpha.31 • Published 4 months ago

ddd-js v1.0.0-alpha.31

Weekly downloads
5
License
MIT
Repository
github
Last release
4 months ago

ddd-js

License: MIT Build Status Coverage Status GitHub (pre-)release

Basic / boilerplate JS classes & functions.

We're still on alpha here, APIs may change any time.

Quick Start

The example application will be a small chat that allows sending messages under an alias. Implementation consists of:

  • value objects for validation of the author's name and the chat text
  • a root entity that accepts commands, validates input and returns the proper events
  • a read model keeping an API representation of all messages sent
  • an API that takes commands and gives access to the read model
const { NonEmptyStringValue } = require('ddd-js')

class Author extends NonEmptyStringValue {}
class ChatText extends NonEmptyStringValue {}

module.exports = { Author, ChatText }
const { RootEntity, DateTime } = require('ddd-js')
const { Author, ChatText } = require('./ValueObjects') // see Value Objects

class Message extends RootEntity {
  setup () {
    this.registerCommand(
      'Message.sendMessage',
      command => this.sendMessage(command.payload.author, command.payload.chatText, command.time)
    )
  }

  sendMessage (author, chatText, time) {
    // validate the input through value objects - this will throw an error if a value is invalid, rejecting the command
    new Author(author)
    new ChatText(chatText)
    new DateTime(time)

    // if all good, return the event
    return [this.createEvent('Message.messageSent', { author, chatText, commandTime: time })]
  }
}

module.exports = Message
const { ReadModel } = require('ddd-js')

class Messages extends ReadModel {
  setup () {
    this.messages = []
    this.registerEvent(
      'Message.messageSent',
      event => this.messageSent(event.payload.author, event.payload.chatText, event.payload.commandTime)
    )
  }

  messageSent (author, chatText, commandTime) { this.messages.push({ author, chatText, time: commandTime }) }

  get messages () { return this.messages }
}

module.exports = Messages
const bunyan = require('bunyan')
const { Runner } = require('ddd-js')
const Message = require('./Entities/Message') // see Root Entity
const Messages = require('./ReadModels/Messages') // see Read Model
const logger = bunyan.createLogger({ name: 'chat' })

Runner.createWithExpress(logger, '../eventstore.json')
  .attachRootEntity(Message)
  .attachReadModel('/messages', Messages, 'messages')
  .replayHistory()
  .then(runner => runner.startServer(8000))

Run node src/app.js - There's your API! You can now POST commands to http://localhost:8000/command and GET messages from http://localhost:8000/messages.

POST /command
Host: localhost:8000
Content-Type: application/json

{"name":"Message.sendMessage","time":"2019-12-08 16:06:37","payload":{"author":"Bob","chatText":"Hey, has anyone seen Jack recently!?"}}

Entity Versioning & Optimistic Lock

To utilize optmistic lock functionality, base your entity class on BaseEntity and register a function that returns all entities that will be affected by a command.

The CommandDispatcher will now monitor entity versions and block sending events if versions become inconsistent.

const { RootEntity, BaseEntity } = require('ddd-js')

class Car extends BaseEntity {
  constructor (fuelLevel) {
    super()
    this.fuelLevel = fuelLevel
  }
}

class CarPool extends RootEntity {
  constructor () { this.cars = {} }

  setup () {
    this.registerCommand(
    'removeFuel',                                             // command name
    command => this.removeFuel(carId, liters)),               // command handler function
    command => { return [this.cars[command.payload.carId]] }, // function returning affected entities per command
    5                                                         // number of retries for optimistic lock until giving up
  }

  removeFuel (carId, liters) {
    if (this.cars[carId].fuelLevel - liters < 0) throw new Error('This is more than is left in the tank.')
    return [this.createEvent('fuelRemoved', { carId, liters })]
  }
}

Sagas

To support transactions over multiple aggregates while respecting bounded contexts a saga can be used. To do so, extend the Saga class, register a command that triggers it and add tasks and their according rollback tasks and then let the Saga run.

const { Saga } = require('ddd-js')

class RentCar extends Saga {
  setup () {
    this.registerCommand('rentCar', async command => {
      // prepare a new run of the Saga and get an identifier for that
      const id = this.provision()

      this.addTask(
        id, 'Car',                                                         // Saga ID and entity name
        { ...command, name: 'reserveCar', time: new Date().toJSON() },     // command to be sent
        () => ({ ...command, name: 'freeCar', time: new Date().toJSON() }) // roll back handler if any other task fails
      )

      this.addTask(
        id, 'Payment',
        { ...command, name: 'debitAmount', time: new Date().toJSON() },
        () => ({ ...command, name: 'payAmount', time: new Date().toJSON() })
      )

      await this.run(id)

      return [] // a saga could return its own events after it has finished
    })
  }
}

Success Of A Saga

A saga will succeed only if every task succeeded. It will then emit the events that were returned by the root entities.

Failure Of A Saga

A saga will fail if

  • one or more commands failed to be executed
  • one or more commands timed out

It will then send "rollback" commands to every root entity that succeeded or timed out.

1.0.0-alpha.31

4 months ago

1.0.0-alpha.30

3 years ago

1.0.0-alpha.29

3 years ago

1.0.0-alpha.28

3 years ago

1.0.0-alpha.27

4 years ago

1.0.0-alpha.26

4 years ago

1.0.0-alpha.25

4 years ago

1.0.0-alpha.23

4 years ago

1.0.0-alpha.24

4 years ago

1.0.0-alpha.22

5 years ago

1.0.0-alpha.21

5 years ago

1.0.0-alpha.20

5 years ago

1.0.0-alpha.19

5 years ago

1.0.0-alpha.18

5 years ago

1.0.0-alpha.17

5 years ago

1.0.0-alpha.16

5 years ago

1.0.0-alpha.15

5 years ago

1.0.0-alpha.14

5 years ago

1.0.0-alpha.13

5 years ago

1.0.0-alpha.12

5 years ago

1.0.0-alpha.11

5 years ago

1.0.0-alpha.10

5 years ago

1.0.0-alpha.9

5 years ago

1.0.0-alpha.8

5 years ago

1.0.0-alpha.7

5 years ago

1.0.0-alpha.6

5 years ago

1.0.0-alpha.5

5 years ago

1.0.0-alpha.4

5 years ago

1.0.0-alpha.3

5 years ago

1.0.0-alpha.2

6 years ago

1.0.0-alpha.1

6 years ago