Skip to content

Commit

Permalink
Merge pull request #142 from castore-dev/create-http-event-storage-ad…
Browse files Browse the repository at this point in the history
…apter

feature: add HTTP event storage adapter
  • Loading branch information
anaisberg authored Nov 10, 2023
2 parents 80d0bba + 44acacc commit 18a3a71
Show file tree
Hide file tree
Showing 34 changed files with 1,702 additions and 56 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/release-to-npm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ jobs:
with:
token: ${{ secrets.NPM_TOKEN }}
package: ./packages/event-storage-adapter-redux/package.json
- uses: JS-DevTools/npm-publish@v2
with:
token: ${{ secrets.NPM_TOKEN }}
package: ./packages/event-storage-adapter-http/package.json
- uses: JS-DevTools/npm-publish@v2
with:
token: ${{ secrets.NPM_TOKEN }}
Expand Down
4 changes: 4 additions & 0 deletions castore.code-workspace
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@
"path": "packages/event-storage-adapter-dynamodb",
"name": "🔌 DynamoDB"
},
{
"path": "packages/event-storage-adapter-http",
"name": "🔌 HTTP"
},
{
"path": "packages/event-storage-adapter-redux",
"name": "🔌 Redux"
Expand Down
18 changes: 18 additions & 0 deletions demo/implementation/functions/getPokemonEvents/handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { EventDetail } from '@castore/core';

import { pokemonsEventStore } from '~/libs/eventStores/pokemons';
import { applyConsoleMiddleware } from '~/libs/middlewares/console';

import { Input, inputSchema } from './schema';

export const getPokemonEvents = async (
event: Input,
): Promise<{ events: EventDetail[] }> => {
const {
queryStringParameters: { aggregateId },
} = event;

return pokemonsEventStore.getEvents(aggregateId);
};

export const main = applyConsoleMiddleware(getPokemonEvents, { inputSchema });
13 changes: 13 additions & 0 deletions demo/implementation/functions/getPokemonEvents/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { AWS } from '@serverless/typescript';

export const getPokemonEvents: Exclude<AWS['functions'], undefined>[string] = {
handler: 'functions/getPokemonEvents/handler.main',
events: [
{
httpApi: {
method: 'GET',
path: '/events',
},
},
],
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,16 @@ import { uuidSchema } from '~/libs/schemas/uuid';
export const inputSchema = {
type: 'object',
properties: {
pokemonId: uuidSchema,
queryStringParameters: {
type: 'object',
properties: {
aggregateId: uuidSchema,
},
required: ['aggregateId'],
additionalProperties: false,
},
},
required: ['pokemonId'],
additionalProperties: false,
required: ['queryStringParameters'],
} as const;

export type Input = FromSchema<typeof inputSchema>;
8 changes: 4 additions & 4 deletions demo/implementation/functions/index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { catchPokemon } from './catchPokemon';
import { getPokemonEvents } from './getPokemonEvents';
import { levelUpPokemon } from './levelUpPokemon';
import { logPokemonEvents } from './logPokemonEvents';
import { logPokemonIds } from './logPokemonIds';
import { listPokemonAggregateIds } from './listPokemonAggregateIds/';
import { startPokemonGame } from './startPokemonGame';
import { wildPokemonAppear } from './wildPokemonAppear';

export const functions = {
catchPokemon,
getPokemonEvents,
levelUpPokemon,
logPokemonEvents,
logPokemonIds,
listPokemonAggregateIds,
startPokemonGame,
wildPokemonAppear,
};
13 changes: 13 additions & 0 deletions demo/implementation/functions/listPokemonAggregateIds/handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ListAggregateIdsOutput } from '@castore/core';

import { pokemonsEventStore } from '~/libs/eventStores/pokemons';
import { applyConsoleMiddleware } from '~/libs/middlewares/console';

export const listPokemonAggregateIds =
async (): Promise<ListAggregateIdsOutput> => {
const { aggregateIds } = await pokemonsEventStore.listAggregateIds();

return { aggregateIds };
};

export const main = applyConsoleMiddleware(listPokemonAggregateIds);
16 changes: 16 additions & 0 deletions demo/implementation/functions/listPokemonAggregateIds/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { AWS } from '@serverless/typescript';

export const listPokemonAggregateIds: Exclude<
AWS['functions'],
undefined
>[string] = {
handler: 'functions/listPokemonAggregateIds/handler.main',
events: [
{
httpApi: {
method: 'GET',
path: '/aggregateIds',
},
},
],
};
14 changes: 0 additions & 14 deletions demo/implementation/functions/logPokemonEvents/handler.ts

This file was deleted.

5 changes: 0 additions & 5 deletions demo/implementation/functions/logPokemonEvents/index.ts

This file was deleted.

10 changes: 0 additions & 10 deletions demo/implementation/functions/logPokemonIds/handler.ts

This file was deleted.

5 changes: 0 additions & 5 deletions demo/implementation/functions/logPokemonIds/index.ts

This file was deleted.

11 changes: 6 additions & 5 deletions demo/implementation/libs/eventStores/pokemons.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { pokemonsEventStore as $pokemonsEventStore } from '@castore/demo-blueprint';
import { LegacyDynamoDBEventStorageAdapter } from '@castore/event-storage-adapter-dynamodb';
import { DynamoDBSingleTableEventStorageAdapter } from '@castore/event-storage-adapter-dynamodb';

import { dynamoDBClient } from './client';

export const pokemonsEventStore = $pokemonsEventStore;

pokemonsEventStore.eventStorageAdapter = new LegacyDynamoDBEventStorageAdapter({
tableName: process.env.POKEMON_EVENTS_TABLE_NAME as string,
dynamoDBClient,
});
pokemonsEventStore.eventStorageAdapter =
new DynamoDBSingleTableEventStorageAdapter({
tableName: process.env.POKEMON_EVENTS_TABLE_NAME as string,
dynamoDBClient,
});
11 changes: 6 additions & 5 deletions demo/implementation/libs/eventStores/trainers.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { trainersEventStore as $trainersEventStore } from '@castore/demo-blueprint';
import { LegacyDynamoDBEventStorageAdapter } from '@castore/event-storage-adapter-dynamodb';
import { DynamoDBSingleTableEventStorageAdapter } from '@castore/event-storage-adapter-dynamodb';

import { dynamoDBClient } from './client';

export const trainersEventStore = $trainersEventStore;

trainersEventStore.eventStorageAdapter = new LegacyDynamoDBEventStorageAdapter({
tableName: process.env.TRAINER_EVENTS_TABLE_NAME as string,
dynamoDBClient,
});
trainersEventStore.eventStorageAdapter =
new DynamoDBSingleTableEventStorageAdapter({
tableName: process.env.TRAINER_EVENTS_TABLE_NAME as string,
dynamoDBClient,
});
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"@nrwl/tao": "^14.0.3",
"@nrwl/workspace": "^14.0.3",
"@trivago/prettier-plugin-sort-imports": "^3.3.0",
"@types/node": "*",
"@typescript-eslint/eslint-plugin": "^5.21.0",
"@typescript-eslint/parser": "^5.21.0",
"aws-sdk": "^2.1124.0",
Expand Down
1 change: 1 addition & 0 deletions packages/event-storage-adapter-http/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dist
2 changes: 2 additions & 0 deletions packages/event-storage-adapter-http/.lintstagedrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
const baseConfig = require('../../.lintstagedrc');
module.exports = baseConfig;
174 changes: 174 additions & 0 deletions packages/event-storage-adapter-http/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
# HTTP Event Storage Adapter

DRY Castore [`EventStorageAdapter`](https://github.com/castore-dev/castore/#--eventstorageadapter) implementation using a HTTP API.

This class is mainly useful when you already have the logic for events implemented and you want to expose your methods for a front-end to use them, eg.

## 📥 Installation

```bash
# npm
npm install @castore/event-storage-adapter-http

# yarn
yarn add @castore/event-storage-adapter-http
```

This package has `@castore/core` as peer dependency, so you will have to install it as well:

```bash
# npm
npm install @castore/core

# yarn
yarn add @castore/core
```

### 👩‍💻 Usage

```ts
import { HttpEventStorageAdapter } from '@castore/event-storage-adapter-http';
import { swagger } from './swagger.json'; // your swagger file


const pokemonHttpEventStorageAdapter = new HttpEventStorageAdapter({ swagger });

const pokemonEventStore = new EventStore({
...
eventStorageAdapter: pokemonHttpEventStorageAdapter,
});
```

### 🤔 How it works

You need to expose 2 API endpoints that will be used by the adapter. They need to return the data correctly formatted:

- getEvents: `(aggregateId: string) => { events: EventDetail[] }`
- listAggregateIds: `() => ListAggregateIdsOutput`

See [here](https://castore-dev.github.io/castore/docs/event-sourcing/events/) for more details about the EventDetails type.
For the `ListAggregateIdsOutput` type:

```typescript
type ListAggregateIdsOutput = {
aggregateIds: {
aggregateId: string;
initialEventTimestamp: string;
}[];
nextPageToken?: string;
};
```

Once your API is deployed, you can export is as an OpenAPI specification (swagger) and pass it to the adapter.
[Here](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-export.html) is how export an API gateway as a swagger.

This adapter uses the swagger passed in input to generate requests to you API endpoints.
For each method, it looks for the tags `operationId` in the swagger to generate the request.

The swagger should be typed like this, with at least the paths for the `getEvents` and `listAggregateIds` methods:

```typescript
type Swagger = {
openapi: string; // the OpenAPI version you are using. Ex: 3.0.1
info: {
title: string; // the title of your API
version: string; // timestamps
};
servers: {
url: string; // the base url of your API
variables: {
basePath: {
default: string; // the default value can be ''
};
};
}[];
paths: {
[path: string]: {
[verb: string]: {
operationId: string; // the operation id for the castore method (getEvents | listAggregateIds)
responses: {
[statusCode: string]: {
description: string;
content?: {
[type: string]: {
schema: {
$ref: string;
};
};
};
};
default: {
description: string;
};
};
parameters?: {
name: string;
in: string; // 'path' | 'query' | 'header' | 'cookie'
description: string;
required: boolean;
format: string;
}[];
};
};
};
};
```

### 📝 Examples

Example of swagger:

```json
{
"openapi": "3.0.1",
"info": {
"title": "event-store-api",
"version": "2023-10-27 14:58:17UTC"
},
"servers": [
{
"url": "https://yourApiGatewayId.execute-api.eu-west-1.amazonaws.com/{basePath}",
"variables": {
"basePath": {
"default": ""
}
}
}
],
"paths": {
"/aggregateIds": {
"get": {
"responses": {
"default": {
"description": "Default response for GET /aggregateIds"
}
},
"operationId": "listAggregateIds"
}
},
"/events?aggregateId={aggregateId}": {
"get": {
"responses": {
"default": {
"description": "Default response for GET /events"
}
},
"x-castore-operationId": "getEvents",
// you can alternatively use the operationId field
// "operationId": "getEvents",
"parameters": [
{
"name": "aggregateId",
"in": "path",
"description": "aggregateId of the event-trace we want to retrieve",
"required": true,
"format": "int64"
}
]
}
}
}
}
```

Note that if you don't specify the `x-castore-operationId` or the `operationId` field, then the adapter will not be able to find the method to call.
3 changes: 3 additions & 0 deletions packages/event-storage-adapter-http/babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const commonBabelConfig = require('../../commonConfiguration/babel.config');

module.exports = commonBabelConfig();
3 changes: 3 additions & 0 deletions packages/event-storage-adapter-http/dependency-cruiser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/** @type {import('dependency-cruiser').IConfiguration} */
const baseConfig = require('../../dependency-cruiser');
module.exports = baseConfig;
Loading

0 comments on commit 18a3a71

Please sign in to comment.