mirror of
https://github.com/standardnotes/server
synced 2026-01-16 20:04:32 -05:00
feat: add event store package
This commit is contained in:
126
.github/workflows/event-store.release.yml
vendored
Normal file
126
.github/workflows/event-store.release.yml
vendored
Normal file
@@ -0,0 +1,126 @@
|
||||
name: Event Store
|
||||
|
||||
concurrency:
|
||||
group: event-store
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*standardnotes/event-store*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
node-version-file: '.nvmrc'
|
||||
- run: yarn build
|
||||
- run: yarn lint:event-store
|
||||
- run: yarn test:event-store
|
||||
|
||||
publish-aws-ecr:
|
||||
needs: test
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Build locally
|
||||
run: yarn build
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v1
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
aws-region: us-east-1
|
||||
- name: Login to Amazon ECR
|
||||
id: login-ecr
|
||||
uses: aws-actions/amazon-ecr-login@v1
|
||||
- name: Build, tag, and push image to Amazon ECR
|
||||
id: build-image
|
||||
env:
|
||||
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
|
||||
ECR_REPOSITORY: event-store
|
||||
IMAGE_TAG: ${{ github.sha }}
|
||||
run: |
|
||||
yarn docker build @standardnotes/event-store -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
|
||||
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
|
||||
docker tag $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG $ECR_REGISTRY/$ECR_REPOSITORY:latest
|
||||
docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest
|
||||
|
||||
publish-docker-hub:
|
||||
needs: test
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Build locally
|
||||
run: yarn build
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Build, tag, and push image to Docker Hub
|
||||
run: |
|
||||
yarn docker build @standardnotes/event-store -t standardnotes/event-store:${{ github.sha }}
|
||||
docker push standardnotes/event-store:${{ github.sha }}
|
||||
docker tag standardnotes/event-store:${{ github.sha }} standardnotes/event-store:latest
|
||||
docker push standardnotes/event-store:latest
|
||||
|
||||
deploy-worker:
|
||||
needs: publish-aws-ecr
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v1
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
aws-region: us-east-1
|
||||
- name: PROD - Download task definition
|
||||
run: |
|
||||
aws ecs describe-task-definition --task-definition event-store-prod --query taskDefinition > task-definition.json
|
||||
- name: PROD - Fill in the new version in the Amazon ECS task definition
|
||||
run: |
|
||||
jq '(.containerDefinitions[] | select(.name=="event-store-prod") | .environment[] | select(.name=="VERSION")).value = "${{ github.sha }}"' task-definition.json > tmp.json && mv tmp.json task-definition.json
|
||||
- name: PROD - Fill in the new image ID in the Amazon ECS task definition
|
||||
id: task-def-prod
|
||||
uses: aws-actions/amazon-ecs-render-task-definition@v1
|
||||
with:
|
||||
task-definition: task-definition.json
|
||||
container-name: event-store-prod
|
||||
image: ${{ secrets.AWS_ECR_REGISTRY }}/event-store:${{ github.sha }}
|
||||
- name: PROD - Deploy Amazon ECS task definition
|
||||
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
|
||||
with:
|
||||
task-definition: ${{ steps.task-def-prod.outputs.task-definition }}
|
||||
service: event-store-prod
|
||||
cluster: prod
|
||||
wait-for-service-stability: true
|
||||
|
||||
newrelic:
|
||||
needs: [ deploy-worker ]
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Create New Relic deployment marker for Worker
|
||||
uses: newrelic/deployment-marker-action@v1
|
||||
with:
|
||||
accountId: ${{ secrets.NEW_RELIC_ACCOUNT_ID }}
|
||||
apiKey: ${{ secrets.NEW_RELIC_API_KEY }}
|
||||
applicationId: ${{ secrets.NEW_RELIC_APPLICATION_ID_EVENT_STORE_PROD }}
|
||||
revision: "${{ github.sha }}"
|
||||
description: "Automated Deployment via Github Actions"
|
||||
user: "${{ github.actor }}"
|
||||
45
.pnp.cjs
generated
45
.pnp.cjs
generated
@@ -44,6 +44,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
"name": "@standardnotes/domain-events-infra",\
|
||||
"reference": "workspace:packages/domain-events-infra"\
|
||||
},\
|
||||
{\
|
||||
"name": "@standardnotes/event-store",\
|
||||
"reference": "workspace:packages/event-store"\
|
||||
},\
|
||||
{\
|
||||
"name": "@standardnotes/files-server",\
|
||||
"reference": "workspace:packages/files"\
|
||||
@@ -86,6 +90,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
["@standardnotes/common", ["workspace:packages/common"]],\
|
||||
["@standardnotes/domain-events", ["workspace:packages/domain-events"]],\
|
||||
["@standardnotes/domain-events-infra", ["workspace:packages/domain-events-infra"]],\
|
||||
["@standardnotes/event-store", ["workspace:packages/event-store"]],\
|
||||
["@standardnotes/files-server", ["workspace:packages/files"]],\
|
||||
["@standardnotes/predicates", ["workspace:packages/predicates"]],\
|
||||
["@standardnotes/scheduler-server", ["workspace:packages/scheduler"]],\
|
||||
@@ -2895,6 +2900,36 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@standardnotes/event-store", [\
|
||||
["workspace:packages/event-store", {\
|
||||
"packageLocation": "./packages/event-store/",\
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/event-store", "workspace:packages/event-store"],\
|
||||
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
||||
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
|
||||
["@standardnotes/time", "workspace:packages/time"],\
|
||||
["@types/ioredis", "npm:4.28.10"],\
|
||||
["@types/jest", "npm:28.1.4"],\
|
||||
["@types/newrelic", "npm:7.0.3"],\
|
||||
["@types/nodemailer", "npm:6.4.4"],\
|
||||
["@typescript-eslint/eslint-plugin", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:5.30.5"],\
|
||||
["aws-sdk", "npm:2.1168.0"],\
|
||||
["dotenv", "npm:8.2.0"],\
|
||||
["eslint", "npm:8.19.0"],\
|
||||
["eslint-plugin-prettier", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:4.2.1"],\
|
||||
["inversify", "npm:6.0.1"],\
|
||||
["ioredis", "npm:5.1.0"],\
|
||||
["jest", "virtual:e1128e9ebb31076ea8e955c00397fd108ee8bf0fb2df3b2a603c510b7014a507cfa360bccf848efc1ec8c431656aa94c5ad08bcec32950bdf1278d01cd890e4f#npm:28.1.2"],\
|
||||
["mysql2", "npm:2.3.3"],\
|
||||
["newrelic", "npm:8.14.1"],\
|
||||
["reflect-metadata", "npm:0.1.13"],\
|
||||
["ts-jest", "virtual:e1128e9ebb31076ea8e955c00397fd108ee8bf0fb2df3b2a603c510b7014a507cfa360bccf848efc1ec8c431656aa94c5ad08bcec32950bdf1278d01cd890e4f#npm:28.0.5"],\
|
||||
["typeorm", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:0.3.7"],\
|
||||
["winston", "npm:3.3.3"]\
|
||||
],\
|
||||
"linkType": "SOFT"\
|
||||
}]\
|
||||
]],\
|
||||
["@standardnotes/features", [\
|
||||
["npm:1.50.0", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-features-npm-1.50.0-dd65714983-b61b50695b.zip/node_modules/@standardnotes/features/",\
|
||||
@@ -3609,6 +3644,16 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@types/nodemailer", [\
|
||||
["npm:6.4.4", {\
|
||||
"packageLocation": "./.yarn/cache/@types-nodemailer-npm-6.4.4-c5c500abe2-16ed1bad2c.zip/node_modules/@types/nodemailer/",\
|
||||
"packageDependencies": [\
|
||||
["@types/nodemailer", "npm:6.4.4"],\
|
||||
["@types/node", "npm:18.0.3"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@types/normalize-package-data", [\
|
||||
["npm:2.4.1", {\
|
||||
"packageLocation": "./.yarn/cache/@types-normalize-package-data-npm-2.4.1-c31c56ae6a-e87bccbf11.zip/node_modules/@types/normalize-package-data/",\
|
||||
|
||||
BIN
.yarn/cache/@types-nodemailer-npm-6.4.4-c5c500abe2-16ed1bad2c.zip
vendored
Normal file
BIN
.yarn/cache/@types-nodemailer-npm-6.4.4-c5c500abe2-16ed1bad2c.zip
vendored
Normal file
Binary file not shown.
@@ -17,11 +17,13 @@
|
||||
"lint:syncing-server": "yarn workspace @standardnotes/syncing-server lint",
|
||||
"lint:files": "yarn workspace @standardnotes/files-server lint",
|
||||
"lint:api-gateway": "yarn workspace @standardnotes/api-gateway lint",
|
||||
"lint:event-store": "yarn workspace @standardnotes/event-store lint",
|
||||
"test": "yarn workspaces foreach -p -j 10 --verbose run test",
|
||||
"test:auth": "yarn workspace @standardnotes/auth-server test",
|
||||
"test:scheduler": "yarn workspace @standardnotes/scheduler-server test",
|
||||
"test:syncing-server": "yarn workspace @standardnotes/syncing-server test",
|
||||
"test:files": "yarn workspace @standardnotes/files-server test",
|
||||
"test:event-store": "yarn workspace @standardnotes/event-store test",
|
||||
"clean": "yarn workspaces foreach -p --verbose run clean",
|
||||
"setup:env": "cp .env.sample .env && yarn workspaces foreach -p --verbose run setup:env",
|
||||
"build": "yarn workspaces foreach -pt -j 10 --verbose run build",
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
"description": "Auth Server",
|
||||
"main": "dist/src/index.js",
|
||||
"typings": "dist/src/index.d.ts",
|
||||
"repository": "git@github.com:standardnotes/auth.git",
|
||||
"author": "Karol Sójko <karolsojko@standardnotes.com>",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"scripts": {
|
||||
|
||||
24
packages/event-store/.env.sample
Normal file
24
packages/event-store/.env.sample
Normal file
@@ -0,0 +1,24 @@
|
||||
LOG_LEVEL=debug
|
||||
NODE_ENV=development
|
||||
VERSION=development
|
||||
|
||||
DB_HOST=127.0.0.1
|
||||
DB_REPLICA_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_USERNAME=store
|
||||
DB_PASSWORD=changeme123
|
||||
DB_DATABASE=store
|
||||
DB_DEBUG_LEVEL=all # "all" | "query" | "schema" | "error" | "warn" | "info" | "log" | "migration"
|
||||
DB_MIGRATIONS_PATH=dist/migrations/*.js
|
||||
|
||||
SQS_QUEUE_URL=
|
||||
SQS_AWS_REGION=
|
||||
|
||||
# (Optional) New Relic Setup
|
||||
NEW_RELIC_ENABLED=false
|
||||
NEW_RELIC_APP_NAME="Event Store"
|
||||
NEW_RELIC_LICENSE_KEY=
|
||||
NEW_RELIC_NO_CONFIG_FILE=true
|
||||
NEW_RELIC_DISTRIBUTED_TRACING_ENABLED=false
|
||||
NEW_RELIC_LOG_ENABLED=false
|
||||
NEW_RELIC_LOG_LEVEL=info
|
||||
2
packages/event-store/.eslintignore
Normal file
2
packages/event-store/.eslintignore
Normal file
@@ -0,0 +1,2 @@
|
||||
dist
|
||||
test-setup.ts
|
||||
6
packages/event-store/.eslintrc
Normal file
6
packages/event-store/.eslintrc
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "../../.eslintrc",
|
||||
"parserOptions": {
|
||||
"project": "./linter.tsconfig.json"
|
||||
}
|
||||
}
|
||||
27
packages/event-store/Dockerfile
Normal file
27
packages/event-store/Dockerfile
Normal file
@@ -0,0 +1,27 @@
|
||||
FROM node:16.15.1-alpine AS builder
|
||||
|
||||
# Install dependencies for building native libraries
|
||||
RUN apk add --update git openssh-client python3 alpine-sdk
|
||||
|
||||
WORKDIR /workspace
|
||||
|
||||
# docker-build plugin copies everything needed for `yarn install` to `manifests` folder.
|
||||
COPY manifests ./
|
||||
|
||||
RUN yarn install --immutable
|
||||
|
||||
FROM node:16.15.1-alpine
|
||||
|
||||
RUN apk add --update curl
|
||||
|
||||
WORKDIR /workspace
|
||||
|
||||
# Copy the installed dependencies from the previous stage.
|
||||
COPY --from=builder /workspace ./
|
||||
|
||||
# docker-build plugin runs `yarn pack` in all workspace dependencies and copies them to `packs` folder.
|
||||
COPY packs ./
|
||||
|
||||
ENTRYPOINT [ "/workspace/packages/event-store/docker/entrypoint.sh" ]
|
||||
|
||||
CMD [ "start-worker" ]
|
||||
25
packages/event-store/bin/worker.ts
Normal file
25
packages/event-store/bin/worker.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import 'newrelic'
|
||||
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
|
||||
import TYPES from '../src/Bootstrap/Types'
|
||||
import { Env } from '../src/Bootstrap/Env'
|
||||
import { DomainEventSubscriberFactoryInterface } from '@standardnotes/domain-events'
|
||||
|
||||
const container = new ContainerConfigLoader()
|
||||
void container.load().then((container) => {
|
||||
const env: Env = new Env()
|
||||
env.load()
|
||||
|
||||
const logger: Logger = container.get(TYPES.Logger)
|
||||
|
||||
logger.info('Starting worker...')
|
||||
|
||||
const subscriberFactory: DomainEventSubscriberFactoryInterface = container.get(TYPES.DomainEventSubscriberFactory)
|
||||
subscriberFactory.create().start()
|
||||
|
||||
setInterval(() => logger.info('Alive and kicking!'), 20 * 60 * 1000)
|
||||
})
|
||||
17
packages/event-store/docker/entrypoint.sh
Executable file
17
packages/event-store/docker/entrypoint.sh
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
COMMAND=$1 && shift 1
|
||||
|
||||
case "$COMMAND" in
|
||||
'start-worker' )
|
||||
echo "Starting Worker..."
|
||||
yarn workspace @standardnotes/event-store worker
|
||||
;;
|
||||
|
||||
* )
|
||||
echo "Unknown command"
|
||||
;;
|
||||
esac
|
||||
|
||||
exec "$@"
|
||||
17
packages/event-store/jest.config.js
Normal file
17
packages/event-store/jest.config.js
Normal file
@@ -0,0 +1,17 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const base = require('../../jest.config');
|
||||
|
||||
module.exports = {
|
||||
...base,
|
||||
globals: {
|
||||
'ts-jest': {
|
||||
tsconfig: 'tsconfig.json',
|
||||
},
|
||||
},
|
||||
coveragePathIgnorePatterns: [
|
||||
'/Bootstrap/'
|
||||
],
|
||||
setupFilesAfterEnv: [
|
||||
'./test-setup.ts'
|
||||
]
|
||||
};
|
||||
4
packages/event-store/linter.tsconfig.json
Normal file
4
packages/event-store/linter.tsconfig.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["dist", "test-setup.ts"]
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class initDatabase1639394147420 implements MigrationInterface {
|
||||
name = 'initDatabase1639394147420'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
'CREATE TABLE `events` (`uuid` varchar(36) NOT NULL, `user_identifier` varchar(255) NOT NULL, `user_identifier_type` varchar(255) NOT NULL, `event_type` varchar(255) NOT NULL, `event_payload` text NOT NULL, `timestamp` bigint NOT NULL, INDEX `index_events_on_user_identifier` (`user_identifier`), PRIMARY KEY (`uuid`)) ENGINE=InnoDB',
|
||||
)
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('DROP INDEX `index_events_on_user_identifier` ON `events`')
|
||||
await queryRunner.query('DROP TABLE `events`')
|
||||
}
|
||||
}
|
||||
47
packages/event-store/package.json
Normal file
47
packages/event-store/package.json
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "@standardnotes/event-store",
|
||||
"version": "1.0.0",
|
||||
"description": "Event Store Service",
|
||||
"private": true,
|
||||
"main": "dist/src/index.js",
|
||||
"typings": "dist/src/index.d.ts",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rm -fr dist",
|
||||
"prebuild": "yarn clean",
|
||||
"build": "tsc --rootDir ./",
|
||||
"lint": "eslint . --ext .ts",
|
||||
"pretest": "yarn lint && yarn build",
|
||||
"test": "jest --coverage --config=./jest.config.js --maxWorkers=50%",
|
||||
"worker": "yarn node dist/bin/worker.js"
|
||||
},
|
||||
"author": "Karol Sójko <karolsojko@standardnotes.com>",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"devDependencies": {
|
||||
"@types/ioredis": "^4.28.10",
|
||||
"@types/jest": "^28.1.3",
|
||||
"@types/newrelic": "^7.0.3",
|
||||
"@types/nodemailer": "^6.4.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.30.5",
|
||||
"eslint": "^8.14.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"jest": "^28.1.1",
|
||||
"ts-jest": "^28.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@standardnotes/domain-events": "workspace:*",
|
||||
"@standardnotes/domain-events-infra": "workspace:*",
|
||||
"@standardnotes/time": "workspace:*",
|
||||
"aws-sdk": "^2.1159.0",
|
||||
"dotenv": "8.2.0",
|
||||
"inversify": "^6.0.1",
|
||||
"ioredis": "^5.0.6",
|
||||
"mysql2": "^2.3.3",
|
||||
"newrelic": "^8.14.1",
|
||||
"reflect-metadata": "0.1.13",
|
||||
"typeorm": "^0.3.6",
|
||||
"winston": "3.3.3"
|
||||
}
|
||||
}
|
||||
97
packages/event-store/src/Bootstrap/Container.ts
Normal file
97
packages/event-store/src/Bootstrap/Container.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import * as AWS from 'aws-sdk'
|
||||
import * as winston from 'winston'
|
||||
import { Container } from 'inversify'
|
||||
import { Event } from '../Domain/Event/Event'
|
||||
import { Env } from './Env'
|
||||
import TYPES from './Types'
|
||||
import {
|
||||
DomainEventHandlerInterface,
|
||||
DomainEventMessageHandlerInterface,
|
||||
DomainEventSubscriberFactoryInterface,
|
||||
} from '@standardnotes/domain-events'
|
||||
import {
|
||||
SQSDomainEventSubscriberFactory,
|
||||
SQSEventMessageHandler,
|
||||
SQSNewRelicEventMessageHandler,
|
||||
} from '@standardnotes/domain-events-infra'
|
||||
import { Timer, TimerInterface } from '@standardnotes/time'
|
||||
import { EventHandler } from '../Domain/Handler/EventHandler'
|
||||
import { AppDataSource } from './DataSource'
|
||||
import { Repository } from 'typeorm'
|
||||
|
||||
export class ContainerConfigLoader {
|
||||
async load(): Promise<Container> {
|
||||
const env: Env = new Env()
|
||||
env.load()
|
||||
|
||||
const container = new Container()
|
||||
|
||||
await AppDataSource.initialize()
|
||||
|
||||
container.bind<AWS.SQS>(TYPES.SQS).toConstantValue(
|
||||
new AWS.SQS({
|
||||
apiVersion: 'latest',
|
||||
region: env.get('SQS_AWS_REGION'),
|
||||
}),
|
||||
)
|
||||
|
||||
const logger = winston.createLogger({
|
||||
level: env.get('LOG_LEVEL') || 'info',
|
||||
format: winston.format.combine(winston.format.splat(), winston.format.json()),
|
||||
transports: [new winston.transports.Console({ level: env.get('LOG_LEVEL') || 'info' })],
|
||||
})
|
||||
container.bind<winston.Logger>(TYPES.Logger).toConstantValue(logger)
|
||||
|
||||
container.bind<TimerInterface>(TYPES.Timer).toConstantValue(new Timer())
|
||||
|
||||
// env vars
|
||||
container.bind(TYPES.SQS_AWS_REGION).toConstantValue(env.get('SQS_AWS_REGION'))
|
||||
container.bind(TYPES.SQS_QUEUE_URL).toConstantValue(env.get('SQS_QUEUE_URL'))
|
||||
|
||||
// Handlers
|
||||
container.bind<EventHandler>(TYPES.EventHandler).to(EventHandler)
|
||||
|
||||
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
|
||||
['USER_REGISTERED', container.get(TYPES.EventHandler)],
|
||||
['ACCOUNT_DELETION_REQUESTED', container.get(TYPES.EventHandler)],
|
||||
['SUBSCRIPTION_PURCHASED', container.get(TYPES.EventHandler)],
|
||||
['SUBSCRIPTION_CANCELLED', container.get(TYPES.EventHandler)],
|
||||
['SUBSCRIPTION_RENEWED', container.get(TYPES.EventHandler)],
|
||||
['SUBSCRIPTION_REFUNDED', container.get(TYPES.EventHandler)],
|
||||
['SUBSCRIPTION_SYNC_REQUESTED', container.get(TYPES.EventHandler)],
|
||||
['SUBSCRIPTION_EXPIRED', container.get(TYPES.EventHandler)],
|
||||
['EXTENSION_KEY_GRANTED', container.get(TYPES.EventHandler)],
|
||||
['SUBSCRIPTION_REASSIGNED', container.get(TYPES.EventHandler)],
|
||||
['USER_EMAIL_CHANGED', container.get(TYPES.EventHandler)],
|
||||
['FILE_UPLOADED', container.get(TYPES.EventHandler)],
|
||||
['FILE_REMOVED', container.get(TYPES.EventHandler)],
|
||||
['LISTED_ACCOUNT_REQUESTED', container.get(TYPES.EventHandler)],
|
||||
['LISTED_ACCOUNT_CREATED', container.get(TYPES.EventHandler)],
|
||||
['LISTED_ACCOUNT_DELETED', container.get(TYPES.EventHandler)],
|
||||
['USER_SIGNED_IN', container.get(TYPES.EventHandler)],
|
||||
['SHARED_SUBSCRIPTION_INVITATION_CREATED', container.get(TYPES.EventHandler)],
|
||||
])
|
||||
|
||||
// ORM
|
||||
container.bind<Repository<Event>>(TYPES.ORMEventRepository).toConstantValue(AppDataSource.getRepository(Event))
|
||||
|
||||
container
|
||||
.bind<DomainEventMessageHandlerInterface>(TYPES.DomainEventMessageHandler)
|
||||
.toConstantValue(
|
||||
env.get('NEW_RELIC_ENABLED', true) === 'true'
|
||||
? new SQSNewRelicEventMessageHandler(eventHandlers, container.get(TYPES.Logger))
|
||||
: new SQSEventMessageHandler(eventHandlers, container.get(TYPES.Logger)),
|
||||
)
|
||||
container
|
||||
.bind<DomainEventSubscriberFactoryInterface>(TYPES.DomainEventSubscriberFactory)
|
||||
.toConstantValue(
|
||||
new SQSDomainEventSubscriberFactory(
|
||||
container.get(TYPES.SQS),
|
||||
container.get(TYPES.SQS_QUEUE_URL),
|
||||
container.get(TYPES.DomainEventMessageHandler),
|
||||
),
|
||||
)
|
||||
|
||||
return container
|
||||
}
|
||||
}
|
||||
40
packages/event-store/src/Bootstrap/DataSource.ts
Normal file
40
packages/event-store/src/Bootstrap/DataSource.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { DataSource, LoggerOptions } from 'typeorm'
|
||||
import { Event } from '../Domain/Event/Event'
|
||||
import { Env } from './Env'
|
||||
|
||||
const env: Env = new Env()
|
||||
env.load()
|
||||
|
||||
const maxQueryExecutionTime = env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
|
||||
? +env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
|
||||
: 45_000
|
||||
|
||||
export const AppDataSource = new DataSource({
|
||||
type: 'mysql',
|
||||
supportBigNumbers: true,
|
||||
bigNumberStrings: false,
|
||||
maxQueryExecutionTime,
|
||||
replication: {
|
||||
master: {
|
||||
host: env.get('DB_HOST'),
|
||||
port: parseInt(env.get('DB_PORT')),
|
||||
username: env.get('DB_USERNAME'),
|
||||
password: env.get('DB_PASSWORD'),
|
||||
database: env.get('DB_DATABASE'),
|
||||
},
|
||||
slaves: [
|
||||
{
|
||||
host: env.get('DB_REPLICA_HOST'),
|
||||
port: parseInt(env.get('DB_PORT')),
|
||||
username: env.get('DB_USERNAME'),
|
||||
password: env.get('DB_PASSWORD'),
|
||||
database: env.get('DB_DATABASE'),
|
||||
},
|
||||
],
|
||||
removeNodeErrorCount: 10,
|
||||
},
|
||||
entities: [Event],
|
||||
migrations: [env.get('DB_MIGRATIONS_PATH')],
|
||||
migrationsRun: true,
|
||||
logging: <LoggerOptions>env.get('DB_DEBUG_LEVEL'),
|
||||
})
|
||||
24
packages/event-store/src/Bootstrap/Env.ts
Normal file
24
packages/event-store/src/Bootstrap/Env.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { config, DotenvParseOutput } from 'dotenv'
|
||||
import { injectable } from 'inversify'
|
||||
|
||||
@injectable()
|
||||
export class Env {
|
||||
private env?: DotenvParseOutput
|
||||
|
||||
public load(): void {
|
||||
const output = config()
|
||||
this.env = <DotenvParseOutput>output.parsed
|
||||
}
|
||||
|
||||
public get(key: string, optional = false): string {
|
||||
if (!this.env) {
|
||||
this.load()
|
||||
}
|
||||
|
||||
if (!process.env[key] && !optional) {
|
||||
throw new Error(`Environment variable ${key} not set`)
|
||||
}
|
||||
|
||||
return <string>process.env[key]
|
||||
}
|
||||
}
|
||||
17
packages/event-store/src/Bootstrap/Types.ts
Normal file
17
packages/event-store/src/Bootstrap/Types.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
const TYPES = {
|
||||
Logger: Symbol.for('Logger'),
|
||||
SQS: Symbol.for('SQS'),
|
||||
// env vars
|
||||
SQS_QUEUE_URL: Symbol.for('SQS_QUEUE_URL'),
|
||||
SQS_AWS_REGION: Symbol.for('SQS_AWS_REGION'),
|
||||
// Handlers
|
||||
DomainEventSubscriberFactory: Symbol.for('DomainEventSubscriberFactory'),
|
||||
DomainEventMessageHandler: Symbol.for('DomainEventMessageHandler'),
|
||||
EventHandler: Symbol.for('EventHandler'),
|
||||
// ORM
|
||||
ORMEventRepository: Symbol.for('ORMEventRepository'),
|
||||
// Services
|
||||
Timer: Symbol.for('Timer'),
|
||||
}
|
||||
|
||||
export default TYPES
|
||||
38
packages/event-store/src/Domain/Event/Event.ts
Normal file
38
packages/event-store/src/Domain/Event/Event.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'
|
||||
|
||||
@Entity({ name: 'events' })
|
||||
export class Event {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
declare uuid: string
|
||||
|
||||
@Column({
|
||||
name: 'user_identifier',
|
||||
length: 255,
|
||||
})
|
||||
@Index('index_events_on_user_identifier')
|
||||
declare userIdentifier: string
|
||||
|
||||
@Column({
|
||||
name: 'user_identifier_type',
|
||||
length: 255,
|
||||
})
|
||||
declare userIdentifierType: string
|
||||
|
||||
@Column({
|
||||
name: 'event_type',
|
||||
length: 255,
|
||||
})
|
||||
declare eventType: string
|
||||
|
||||
@Column({
|
||||
name: 'event_payload',
|
||||
type: 'text',
|
||||
})
|
||||
declare eventPayload: string
|
||||
|
||||
@Column({
|
||||
name: 'timestamp',
|
||||
type: 'bigint',
|
||||
})
|
||||
declare timestamp: number
|
||||
}
|
||||
76
packages/event-store/src/Domain/Handler/EventHandler.spec.ts
Normal file
76
packages/event-store/src/Domain/Handler/EventHandler.spec.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { Repository } from 'typeorm'
|
||||
import { EventHandler } from './EventHandler'
|
||||
import { Event } from '../Event/Event'
|
||||
import { Logger } from 'winston'
|
||||
import { DomainEventInterface } from '@standardnotes/domain-events'
|
||||
|
||||
describe('EventHandler', () => {
|
||||
let timer: TimerInterface
|
||||
let repository: Repository<Event>
|
||||
let logger: Logger
|
||||
|
||||
const createHandler = () => new EventHandler(timer, repository, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.convertStringDateToMicroseconds = jest.fn().mockReturnValue(1)
|
||||
|
||||
repository = {} as jest.Mocked<Repository<Event>>
|
||||
repository.save = jest.fn()
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.debug = jest.fn()
|
||||
logger.error = jest.fn()
|
||||
})
|
||||
|
||||
it('should persist as event in the store', async () => {
|
||||
const event = {
|
||||
type: 'test',
|
||||
createdAt: new Date(2),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: '1-2-3',
|
||||
userIdentifierType: 'uuid',
|
||||
},
|
||||
},
|
||||
payload: {
|
||||
foo: 'bar',
|
||||
},
|
||||
} as jest.Mocked<DomainEventInterface>
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(repository.save).toHaveBeenCalledWith({
|
||||
eventType: 'test',
|
||||
timestamp: 1,
|
||||
userIdentifier: '1-2-3',
|
||||
userIdentifierType: 'uuid',
|
||||
eventPayload: '{"foo":"bar"}',
|
||||
})
|
||||
})
|
||||
|
||||
it('should inform about failure to saven the event in the store', async () => {
|
||||
const event = {
|
||||
type: 'test',
|
||||
createdAt: new Date(2),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: '1-2-3',
|
||||
userIdentifierType: 'uuid',
|
||||
},
|
||||
},
|
||||
payload: {
|
||||
foo: 'bar',
|
||||
},
|
||||
} as jest.Mocked<DomainEventInterface>
|
||||
repository.save = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Ooops')
|
||||
})
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(logger.error).toHaveBeenCalledWith('Could not store event %O in the event store: %s', event, 'Ooops')
|
||||
})
|
||||
})
|
||||
33
packages/event-store/src/Domain/Handler/EventHandler.ts
Normal file
33
packages/event-store/src/Domain/Handler/EventHandler.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { DomainEventHandlerInterface, DomainEventInterface } from '@standardnotes/domain-events'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Repository } from 'typeorm'
|
||||
import { Logger } from 'winston'
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { Event } from '../Event/Event'
|
||||
|
||||
@injectable()
|
||||
export class EventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
@inject(TYPES.Timer) private timer: TimerInterface,
|
||||
@inject(TYPES.ORMEventRepository) private eventRepository: Repository<Event>,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
) {}
|
||||
|
||||
async handle(event: DomainEventInterface): Promise<void> {
|
||||
this.logger.debug('Handling event: %O', event)
|
||||
|
||||
try {
|
||||
const storedEvent = new Event()
|
||||
storedEvent.eventType = event.type
|
||||
storedEvent.userIdentifier = event.meta.correlation.userIdentifier
|
||||
storedEvent.userIdentifierType = event.meta.correlation.userIdentifierType
|
||||
storedEvent.eventPayload = JSON.stringify(event.payload)
|
||||
storedEvent.timestamp = this.timer.convertStringDateToMicroseconds(event.createdAt.toString())
|
||||
|
||||
await this.eventRepository.save(storedEvent)
|
||||
} catch (error) {
|
||||
this.logger.error('Could not store event %O in the event store: %s', event, (error as Error).message)
|
||||
}
|
||||
}
|
||||
}
|
||||
0
packages/event-store/test-setup.ts
Normal file
0
packages/event-store/test-setup.ts
Normal file
13
packages/event-store/tsconfig.json
Normal file
13
packages/event-store/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"outDir": "./dist",
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"bin/**/*",
|
||||
"migrations/**/*",
|
||||
],
|
||||
"references": []
|
||||
}
|
||||
@@ -40,6 +40,9 @@
|
||||
{
|
||||
"path": "./packages/domain-events-infra"
|
||||
},
|
||||
{
|
||||
"path": "./packages/event-store"
|
||||
},
|
||||
{
|
||||
"path": "./packages/files"
|
||||
},
|
||||
|
||||
39
yarn.lock
39
yarn.lock
@@ -2159,6 +2159,34 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@standardnotes/event-store@workspace:packages/event-store":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@standardnotes/event-store@workspace:packages/event-store"
|
||||
dependencies:
|
||||
"@standardnotes/domain-events": "workspace:*"
|
||||
"@standardnotes/domain-events-infra": "workspace:*"
|
||||
"@standardnotes/time": "workspace:*"
|
||||
"@types/ioredis": ^4.28.10
|
||||
"@types/jest": ^28.1.3
|
||||
"@types/newrelic": ^7.0.3
|
||||
"@types/nodemailer": ^6.4.1
|
||||
"@typescript-eslint/eslint-plugin": ^5.30.5
|
||||
aws-sdk: ^2.1159.0
|
||||
dotenv: 8.2.0
|
||||
eslint: ^8.14.0
|
||||
eslint-plugin-prettier: ^4.2.1
|
||||
inversify: ^6.0.1
|
||||
ioredis: ^5.0.6
|
||||
jest: ^28.1.1
|
||||
mysql2: ^2.3.3
|
||||
newrelic: ^8.14.1
|
||||
reflect-metadata: 0.1.13
|
||||
ts-jest: ^28.0.1
|
||||
typeorm: ^0.3.6
|
||||
winston: 3.3.3
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@standardnotes/features@npm:1.50.0, @standardnotes/features@npm:^1.36.3, @standardnotes/features@npm:^1.47.0":
|
||||
version: 1.50.0
|
||||
resolution: "@standardnotes/features@npm:1.50.0"
|
||||
@@ -2796,6 +2824,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/nodemailer@npm:^6.4.1":
|
||||
version: 6.4.4
|
||||
resolution: "@types/nodemailer@npm:6.4.4"
|
||||
dependencies:
|
||||
"@types/node": "*"
|
||||
checksum: 16ed1bad2cd8471fd3b026471e234da33ba3b65935dc44b31be3145eff7bdb067eb4d08ec4b41d23339b988075299abc1a0c0fe77b99f04ca235827bca95af81
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/normalize-package-data@npm:^2.4.0":
|
||||
version: 2.4.1
|
||||
resolution: "@types/normalize-package-data@npm:2.4.1"
|
||||
@@ -2912,7 +2949,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/eslint-plugin@npm:^5.12.1, @typescript-eslint/eslint-plugin@npm:^5.29.0, @typescript-eslint/eslint-plugin@npm:^5.30.0":
|
||||
"@typescript-eslint/eslint-plugin@npm:^5.12.1, @typescript-eslint/eslint-plugin@npm:^5.29.0, @typescript-eslint/eslint-plugin@npm:^5.30.0, @typescript-eslint/eslint-plugin@npm:^5.30.5":
|
||||
version: 5.30.5
|
||||
resolution: "@typescript-eslint/eslint-plugin@npm:5.30.5"
|
||||
dependencies:
|
||||
|
||||
Reference in New Issue
Block a user