Skip to content

Releases: boostercloud/booster

Sequenced Read Models and improved Read Model Before Hooks.

27 Aug 14:47
Compare
Choose a tag to compare

This release introduces two features:

Sequenced Read Models

This release introduces the ability to model read models as time sequenced (Or time series). This feature is useful to model a series of data objects that are indexed by the time they were logged. This might sound similar to events, but sequenced read models are also mutable, so they can change over time without losing their place in the sequence. An example use case could be a chat application, where all messages are identified by a specific channel ID and a timestamp of when they were sent, but individual messages can be edited or deleted.

Booster provides a special decorator to tag a specific property as a sequence key for a read model:

export class MessageReadModel {
  public constructor(
    readonly id: UUID, // A channel ID
    @sequencedBy readonly timestamp: string,
    readonly contents: string
  )

  @Projects(Message, 'id')
  public static projectMessage(entity: Message, currentReadModel: MessageReadModel): ProjectionResult<MessageReadModel> {
    return new MessageReadModel(entity.id, entity.timestamp, entity.contents)
  }
}

Adding a sequence key to a read model changes the behavior of its query, which now accepts the sequence key as an optional parameter:

query MessageReadModel(id: ID!, timestamp: string): [MessageReadModel]

In this query, when only the id is provided, you get an array of all the messages in the channel ordered by timestamp in ascending order (from older to newer). When you also provide a specific timestam, you still get an array, but it will only contain the specific message sent at that time.

You can read more about sequenced read models in the documentation.

Improved Read Model Before Hooks

Read Model Before Hooks allow you to set up one or more functions that are called before a read model request is processed, having the opportunity of altering or canceling the request. A common use case for read model before hooks is managing complex access restrictions policies that depend on the business logic, like allowing a user to access objects they own but not objects they should not have access to.

Starting from this release, before hooks will receive the entire request object received from GraphQL, and not only the filters, opening the door to more complex rewrites of specific queries. Before hook functions will receive and return ReadModelRequestEnvelope objects which have the following signature:

interface ReadModelRequestEnvelope<TReadModel> {
  currentUser?: UserEnvelope // The current authenticated user
  requestID: UUID // An ID assigned to this request
  key?: { // If present, contains the id and sequenceKey that identify a specific read model 
    id: UUID
    sequenceKey?: SequenceKey
  }
  className: string // The read model class name
  filters: ReadModelRequestProperties<TReadModel> // Filters set in the GraphQL query
  limit?: number // Query limit if set
  afterCursor?: unknown // For paginated requests, id to start reading from
}

Fixes in the GraphQL input types

10 Aug 13:14
Compare
Choose a tag to compare

In the latest releases, the GraphQL schema generation was improved by adding GraphQLNonNull to elements of arrays.

However, this caused arrays in input types to result in JSONObject instead of the specific type. (e9906b4)

What happened in the last release

Here's what happened

image

It also broke PR #890 which also fixes some JSONObject types, returning specific types instead.

[Booster]  Error: The type of UpsertDossierInput.parties must be Input Type but got: [DossierParty!].

With this release the input types are improved again, so they behave as expected:

image

Command handlers that return values

02 Aug 18:00
Compare
Choose a tag to compare

In this release we introduce the @Returns decorator that allows you to define the return type of a command handler.

This value will be returned to the client through GraphQL, instead of just returning true (that's left for the case of void).

As an example:

@Command({
  authorize: 'all',
})
export class CreateProduct {
  public constructor(readonly sku: string, readonly price: number) {}

  @Returns(String)
  public static async handle(command: CreateProduct, register: Register): Promise<string> {
    return "Product created!"
  }
}

Learn more in the Booster documentation

Introducing GraphQL Pagination on queries

08 Jul 15:46
Compare
Choose a tag to compare

From now on the Booster GraphQL API includes a type for your read models that stands for List<YourReadModelName>, which is the official way to work with pagination. Alternatively, and for compatibility with previous versions, there is still the type without the List prefix, which will be deprecated in future versions.

The new Read Model List type includes some new parameters that can be used on queries:

  • limit; an integer that specifies the maximum number of items to be returned.
  • afterCursor; a parameter to set the cursor property returned by the previous query, if not null.

Example:

query {
  ListProductReadModels
  (
    limit: 1,
    afterCursor: { id: "last-page-item"}
  ) {
    id
    sku
    availability
    price
  }
}

Besides the parameters, this type also returns a type {your-read-model-name}Connection, it includes the following properties:

  • cursor; if there are more results to paginate, it will return the object to pass to the afterCursor parameter on the next query. If there aren't more items to be shown, it will be undefined.
  • items; the list of items returned by the query, if there aren't any, it will be an empty list.

Local provider enabling and lazy provider loading

07 Jul 13:10
Compare
Choose a tag to compare

From now on, setting a provider is done by specifying config.providerPackage, rather than the old config.provider.
In this new way, instead of importing the Provider object and assigning it in the config.provider field, you simply specify the name of the package as a string:

  // Old way – Now deprecated
  import * as AWS from '@boostercloud/framework-provider-aws'

  Booster.configure(environment.name, (config: BoosterConfig) => {
    config.appName = 'my-cool-app'
    config.provider = AWSProvider()
  })

  // New way
  Booster.configure(environment.name, (config: BoosterConfig) => {
    config.appName = 'my-cool-app'
    config.providerPackage = '@boostercloud/framework-provider-aws'

    // New optional field for specifying rockets
    config.rockets = [/* your rockets here */]
  })

With this addition, now Booster provider libraries are loaded on runtime when they are needed, meaning that if you want to deploy the same application to different providers (e.g. AWS, and Azure) you won't get any runtime errors complaining that the SDK for the cloud provider is missing.

Commands' before hooks

05 Jul 15:01
Compare
Choose a tag to compare

This time we're adding before hooks to command handlers! They work the same as the Read Models ones, except that you can modify inputs instead of filters.

Here's an example in case you're curious, where we just check if the cartUserId is equal to the currentUser.id, which is the user id extracted from the auth token:

@Command({
  authorize: [User],
  before: [beforeFn],
})
export class ChangeCartItem {
  public constructor(readonly cartId: UUID, readonly productId: UUID, readonly quantity: number) {
  }
}

function beforeFn(input: CommandInput, currentUser?: UserEnvelope): CommandInput {
  if (input.cartUserId !== currentUser.id) {
    throw NonAuthorizedUserException() // We don't let this user to trigger the command
  }
  return input
}

This way, we can throw an exception and avoid this user calling this command.

Bug fix: Create classes instances of entities, read models and events in the Booster class

05 Jul 14:44
Compare
Choose a tag to compare

🐞 Bug Fixes

Until now, when you get entities, read models and events using the Booster class, you get a raw javascript object instead of an instance of their respective classes.
To fix this bug, a proper instance has been created for entities, read models and events of the Booster class.

For example, in the case of entities, the original code called the deprecated fetchEntitySnapshot method:

public static entity<TEntity extends EntityInterface>(
    entityClass: Class<TEntity>,
    entityID: UUID
  ): Promise<TEntity | undefined> {
    return fetchEntitySnapshot(this.config, this.logger, entityClass, entityID)
  }

And now, the deprecated fetchEntitySnapshot method has been removed and we call a new method called createInstance to fix it.

public static async entity<TEntity extends EntityInterface>(
    entityClass: Class<TEntity>,
    entityID: UUID
  ): Promise<TEntity | undefined> {
    const eventStore = new EventStore(this.config, this.logger)
    const entitySnapshotEnvelope = await eventStore.fetchEntitySnapshot(entityClass.name, entityID)
    return entitySnapshotEnvelope ? createInstance(entityClass, entitySnapshotEnvelope) : undefined
  }
export function createInstance<T>(instanceClass: Class<T>, rawObject: Record<string, any>): T {
  const instance = new instanceClass()
  Object.assign(instance, rawObject)
  return instance
}

Read models' before hooks

01 Jul 13:21
Compare
Choose a tag to compare

With this new version, we can now add before hooks to our @ReadModel annotations. This can become really handy to either:

  • Validate parameters
  • Change read model filters on the fly with programmatic logic
  • Deny access to specific users

For example, we could deny user's access when he/she's not the Cart's owner:

@ReadModel({
  authorize: [User],
  before: [validateUser],
})
export class CartReadModel {
  public constructor(
    readonly id: UUID,
    readonly userId: UUID
  ) {}
  // Your projections go here
}

function validateUser(filter: FilterFor<CartReadModel>, currentUser?: UserEnvelope): FilterFor<CartReadModel> {
  if (filter?.userId !== currentUser.id) throw NotAuthorizedError("...")
  return filter
}

You can also chain these before functions to split your logic:

import { changeFilters } from '../../filters-helper' // You can also use external functions!

@ReadModel({
  authorize: [User],
  before: [validateUser, validateEmail, changeFilters],
})
export class CartReadModel {
  public constructor(
    readonly id: UUID,
    readonly userId: UUID
  ) {}

  // Your projections go here
}

function validateUser(filter: FilterFor<CartReadModel>, currentUser?: UserEnvelope): FilterFor<CartReadModel> {
  if (filter.userId !== currentUser.id) throw NotAuthorizedError("...")
  return filter // This filter will be passed as a parameter to the validateEmail function
}

function validateEmail(filter: FilterFor<CartReadModel>, currentUser?: UserEnvelope): FilterFor<CartReadModel> {
  if (!filter.email.includes('myCompanyDomain.com')) throw NotAuthorizedError("...")
  return filter
}

As a side note, remember that the order in which filters are specified matters.

New GraphQL Schema and complex filters

03 May 11:11
Compare
Choose a tag to compare

Finally, one of the most requested features is here! Complex filters on the GraphQL API. This change will allow users to filter read models like never before, for instance:

query {
  ProductReadModels(filter: { price: { gt: 200 } }) {
    id
    sku
    availability
    price
  }
}

Now you can filter by specific types, complex properties, and even use filter combinators! You can find the complete set of filters in the documentation.

⚠ Breaking Changes

To perform these filters, the GraphQL schema has been redesigned, which introduces breaking changes for existing Booster applications with versions lower than v0.16.0.

Those are the required steps to migrate your app to use Booster v0.16.0 or later successfully.

Update package.json file.

Include the following dependencies to your package.json file on the devDependencies section.

"ttypescript": "1.5.12",
"metadata-booster": "0.3.1"

Also, update the TypeScript dependency version to 4.1.5 a ts-node to 9.1.1.

"typescript": "4.1.5",
"ts-node": "9.1.1",

Finally, change the compile script to use ttsc without npx instead of using npx tsc, it should look like this:

"compile": "ttsc -b tsconfig.json",

Update tsconfig.json file.

Remove the "emitDecoratorMetadata": true line from the tsconfig.json file, and add the following property.

"plugins": [
      { "transform": "metadata-booster" }
 ]

This property allows the application to use the metadata-booster plugin when compiling TypeScript code, allowing the compiled JavaScript files to know the properties of every type created. Ensure you have the following properties:

"experimentalDecorators": true,
"plugins": [
    { "transform": "metadata-booster" }
]

Note: remember to delete the emitDecoratorMetadata property to avoid getting compilation errors.

Upgrade npm

To be able to run the npm scripts properly, especially the compilation one, you will need to upgrade at least to npm v7 or later.

And that's it! 🎉 Your Booster application is ready to upgrade and use the new GraphQL API filters! 🚀

API for reading events and huge reliability improvements on high loads

19 Apr 15:41
Compare
Choose a tag to compare

It's been a lot going on in Booster lately so expect some very exciting news listed here!

⚠ Breaking Changes

  • In version 0.12.0 there was a change in the AWS Provider event store (DynamoDB) to create two new indexes. If you are updating from version 0.11.* or older, you will get an error when deploying your app because AWS limits the number of new indexes per deployment to just one. The solution is simple but involves doing two deploys. Follow these steps:
    1. Update all your Booster dependencies (core, types, etc) to this new version, "0.14.4", except the framework-provider-aws-infrastructure package.
    2. For that package, use the following version: "0.14.4-pre12step.1". This version is exactly the same as the "0.14.4" but it only creates one index. The package.json file should show something similar to this:
      image
    3. Deploy your application with boost deploy. You should not see any error now.
    4. When finished, change the version of framework-provider-aws-infrastructure to the final one, "0.14.4". The package.json file should now show something like this:
      image
    5. Do another deployment and you are done.

⚰️ Deprecated

  • Method Booster.fetchEntitySnapshot is deprecated and will be deleted in the next major version. Use Booster.entity instead.

🚀 Features & Improvements

  • Booster now generates a GraphQL API to directly read events (besides your read models). Documentation is being created (issue #790), but you can take a look at this PR description to know what is it about
  • You can also query events in your command or event handlers by using the Booster.events method
  • The storage and processing of events is now much more reliable on extremely high loads (thousands of events per second)
  • Added experimental support for Azure provider 🎉
  • Added experimental support for Kubernetes provider 🎉

🩹 Fixes

  • Fixed a bug caused by a race condition on huge loads when projecting read models. Now optimistic concurrency is used to ensure data is not corrupted.
  • Fixed a bug that was causing some events to be overwritten on huge loads.
  • Many, many, many more bug fixes

🐛 Known bug

  • We are working to provide full support to filter read models by any property. Some of those filters, although not documented, were available using a "beta" structure. If you were one of those adventurous users that were using them, you need to know that they are broken in this version. In the next version we will release full support for those filters, so stay tuned!