diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 5f0a34af..c4c254a1 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -38,7 +38,7 @@ jobs: run: npm run lint - name: Build - run: npm run build:ts + run: npm run build - name: Test run: npm run test diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml new file mode 100644 index 00000000..ddec128b --- /dev/null +++ b/.github/workflows/deploy-docs.yml @@ -0,0 +1,66 @@ +# Sample workflow for building and deploying a VitePress site to GitHub Pages +# +name: Deploy VitePress site to Pages + +on: + # Runs on pushes targeting the `main` branch. Change this to `master` if you're + # using the `master` branch as the default branch. + push: + branches: [main] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: pages + cancel-in-progress: false + +jobs: + # Build job + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Not needed if lastUpdated is not enabled + # - uses: pnpm/action-setup@v2 # Uncomment this if you're using pnpm + # - uses: oven-sh/setup-bun@v1 # Uncomment this if you're using Bun + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm # or pnpm / yarn + - name: Setup Pages + uses: actions/configure-pages@v4 + - name: Install dependencies + run: npm ci # or pnpm install / yarn install / bun install + - name: Build with VitePress + run: | + npm run docs:build # or pnpm docs:build / yarn docs:build / bun run docs:build + touch docs/.vitepress/dist/.nojekyll + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: docs/.vitepress/dist + + # Deployment job + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + needs: build + runs-on: ubuntu-latest + name: Deploy + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/README.md b/README.md index 32a486af..a65e6069 100644 --- a/README.md +++ b/README.md @@ -2,15 +2,15 @@ ![](./docs/logo.png) -# Emmett - a Node.js Event Store +# Emmett - vent Sourcing development made simple Nowadays, storage is cheap, but the information is priceless. Event Sourcing, contrary to the standard approach, keeps all the facts that happened in our system. To do that, it needs an event store: a database designed for its needs. -**This project aims to deliver an opinionated event store based on my experience working on [Marten](martendb.io/) and [EventStoreDB](https://developers.eventstore.com/).** +This project aims to experiment with an opinionated Event Sourcing framework based on my experience working on [Marten](martendb.io/) and [EventStoreDB](https://developers.eventstore.com/). -Check my inspirations and what I'm up to in [Reference materials](./ReferenceMaterials.md). +**Take your event-driven applications back to the future!** ## FAQ diff --git a/ReferenceMaterials.md b/ReferenceMaterials.md index 55e53801..3ddaa2ff 100644 --- a/ReferenceMaterials.md +++ b/ReferenceMaterials.md @@ -40,6 +40,7 @@ I put here links that either are useful or may be useful for my implementation. ## NPM Packaging - [Orhun Özer - How to bundle a tree-shakable typescript library with tsup and publish with npm](https://dev.to/orabazu/how-to-bundle-a-tree-shakable-typescript-library-with-tsup-and-publish-with-npm-3c46) +- [Andrea Stagi - Publish to NPM using GitHub Actions](https://dev.to/astagi/publish-to-npm-using-github-actions-23fn) - [Colin Diesh - You may not need a bundler for your NPM library](https://cmdcolin.github.io/posts/2022-05-27-youmaynotneedabundler) ## Frontend diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index c2163d77..ea8a7793 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -1,7 +1,10 @@ import { defineConfig } from 'vitepress'; +const env = process.env.NODE_ENV; + // https://vitepress.dev/reference/site-config export default defineConfig({ + base: env === 'production' ? '/emmett/' : '/', title: 'Emmett', description: 'Event Sourcing made simple', themeConfig: { @@ -9,21 +12,21 @@ export default defineConfig({ // https://vitepress.dev/reference/default-theme-config nav: [ { text: 'Home', link: '/' }, - { text: 'Examples', link: '/markdown-examples' }, + { text: 'Getting Started', link: '/getting-started' }, ], sidebar: [ { text: 'Examples', items: [ - { text: 'Markdown Examples', link: '/markdown-examples' }, - { text: 'Runtime API Examples', link: '/api-examples' }, + { text: 'Getting Started', link: '/getting-started' }, + { text: 'API Examples', link: '/api-examples' }, ], }, ], socialLinks: [ - { icon: 'github', link: 'https://github.com/vuejs/vitepress' }, + { icon: 'github', link: 'https://github.com/event-driven-io/emmett' }, ], }, }); diff --git a/docs/.vitepress/theme/custom.css b/docs/.vitepress/theme/custom.css new file mode 100644 index 00000000..fb443418 --- /dev/null +++ b/docs/.vitepress/theme/custom.css @@ -0,0 +1,8 @@ +--vp-c-brand: #2b0f54; +--vp-c-brand-light: #ab1f65; +--vp-c-brand-dark: #2b0f54; +--vp-home-hero-name-color: var(--vp-c-brand); +--vp-button-brand-bg: var(--vp-c-brand); +--vp-button-brand-border: #c9b8a9; +--vp-button-brand-hover-border: var(--vp-button-brand-border); +--vp-button-brand-hover-bg: var(--vp-c-brand-light); diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts new file mode 100644 index 00000000..be401193 --- /dev/null +++ b/docs/.vitepress/theme/index.ts @@ -0,0 +1,6 @@ +import DefaultTheme from 'vitepress/theme'; +import './custom.css'; + +export default { + ...DefaultTheme, +}; diff --git a/docs/api-docs.md b/docs/api-docs.md new file mode 100644 index 00000000..7879ce20 --- /dev/null +++ b/docs/api-docs.md @@ -0,0 +1,5 @@ +--- +outline: deep +--- + +# API docs diff --git a/docs/api-examples.md b/docs/api-examples.md deleted file mode 100644 index 691df9cc..00000000 --- a/docs/api-examples.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -outline: deep ---- - -# Runtime API Examples - -This page demonstrates usage of some of the runtime APIs provided by VitePress. - -The main `useData()` API can be used to access site, theme, and page data for the current page. It works in both `.md` and `.vue` files: - -```md - - -## Results - -### Theme Data - -
{{ theme }}
- -### Page Data - -
{{ page }}
- -### Page Frontmatter - -
{{ frontmatter }}
-``` - - - -## Results - -### Theme Data - -
{{ theme }}
- -### Page Data - -
{{ page }}
- -### Page Frontmatter - -
{{ frontmatter }}
- -## More - -Check out the documentation for the [full list of runtime APIs](https://vitepress.dev/reference/runtime-api#usedata). diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 00000000..6b166075 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,11 @@ +**Event Sourcing keeps all the facts that happened in our system, and that's powerful!** Facts are stored as events that can be used to make decisions, fine-tune read models, integrate our systems, and enhance our analytics and tracking. All in one package, wash and go! + +Yet, some say that's complex and complicated; Emmett aims to prove that it doesn't have to be like that. We cut the boilerplate and layered madness, letting you focus on delivery. We're opinionated but focus on composition, not magic. Let me show you how. + +## Events + +Events are the centrepiece of event-sourced systems. They represent both critical points of the business process but are also used as the state. That enables you to reflect your business into the code better, getting the synergy. Let's model a simple business process: a shopping cart. You can open it, add or remove the product from it and confirm or cancel. + +We could define it as follows: + +<<< @/snippets/shoppingCart.ts#getting-started-events diff --git a/docs/index.md b/docs/index.md index fc496466..23a8b359 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,21 +4,21 @@ layout: home hero: name: 'Emmett' - text: 'Event Sourcing made simple' - tagline: My great project tagline + text: 'Event Sourcing development made simple' + tagline: Take your event-driven applications back to the future! actions: - theme: brand - text: Markdown Examples - link: /markdown-examples + text: Getting Started + link: /getting-started - theme: alt - text: API Examples - link: /api-examples + text: API Docs + link: /api-docs features: - - title: Feature A - details: Lorem ipsum dolor sit amet, consectetur adipiscing elit - - title: Feature B - details: Lorem ipsum dolor sit amet, consectetur adipiscing elit - - title: Feature C - details: Lorem ipsum dolor sit amet, consectetur adipiscing elit + - title: DevExperience as prime goal + details: Reduce the boilerplate, and focus on delivery with accessible tooling + - title: Gain insights from your data + details: Unleash the power of your data with Event Sourcing capabilities + - title: All patterns in one place + details: Use Decider, Workflow and other event-driven best practices seamlessly --- diff --git a/docs/markdown-examples.md b/docs/markdown-examples.md deleted file mode 100644 index f9258a55..00000000 --- a/docs/markdown-examples.md +++ /dev/null @@ -1,85 +0,0 @@ -# Markdown Extension Examples - -This page demonstrates some of the built-in markdown extensions provided by VitePress. - -## Syntax Highlighting - -VitePress provides Syntax Highlighting powered by [Shiki](https://github.com/shikijs/shiki), with additional features like line-highlighting: - -**Input** - -````md -```js{4} -export default { - data () { - return { - msg: 'Highlighted!' - } - } -} -``` -```` - -**Output** - -```js{4} -export default { - data () { - return { - msg: 'Highlighted!' - } - } -} -``` - -## Custom Containers - -**Input** - -```md -::: info -This is an info box. -::: - -::: tip -This is a tip. -::: - -::: warning -This is a warning. -::: - -::: danger -This is a dangerous warning. -::: - -::: details -This is a details block. -::: -``` - -**Output** - -::: info -This is an info box. -::: - -::: tip -This is a tip. -::: - -::: warning -This is a warning. -::: - -::: danger -This is a dangerous warning. -::: - -::: details -This is a details block. -::: - -## More - -Check out the documentation for the [full list of markdown extensions](https://vitepress.dev/guide/markdown). diff --git a/docs/snippets/shoppingCart.ts b/docs/snippets/shoppingCart.ts new file mode 100644 index 00000000..63db3ff3 --- /dev/null +++ b/docs/snippets/shoppingCart.ts @@ -0,0 +1,48 @@ +// #region getting-started-events +export type ShoppingCartEvent = + | { + type: 'ShoppingCartOpened'; + data: { + shoppingCartId: string; + clientId: string; + openedAt: string; + }; + } + | { + type: 'ProductItemAddedToShoppingCart'; + data: { + shoppingCartId: string; + productItem: PricedProductItem; + }; + } + | { + type: 'ProductItemRemovedFromShoppingCart'; + data: { + shoppingCartId: string; + productItem: PricedProductItem; + }; + } + | { + type: 'ShoppingCartConfirmed'; + data: { + shoppingCartId: string; + confirmedAt: string; + }; + } + | { + type: 'ShoppingCartCanceled'; + data: { + shoppingCartId: string; + canceledAt: string; + }; + }; + +export interface ProductItem { + productId: string; + quantity: number; +} + +export type PricedProductItem = ProductItem & { + price: number; +}; +// #endregion getting-started-events diff --git a/package-lock.json b/package-lock.json index 37608135..0736a899 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,19 @@ { - "name": "emmett", - "version": "0.0.1", + "name": "@event-driven-io/emmett", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "emmett", - "version": "0.0.1", + "name": "@event-driven-io/emmett", + "version": "0.1.0", + "dependencies": { + "express": "4.18.2", + "express-async-errors": "3.1.1" + }, "devDependencies": { "@faker-js/faker": "8.4.1", + "@types/express": "4.17.21", "@types/jest": "29.5.0", "@types/node": "20.11.17", "@typescript-eslint/eslint-plugin": "6.21.0", @@ -2314,12 +2319,55 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, + "node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.43", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.43.tgz", + "integrity": "sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -2329,6 +2377,12 @@ "@types/node": "*" } }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", @@ -2391,6 +2445,12 @@ "integrity": "sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==", "dev": true }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true + }, "node_modules/@types/node": { "version": "20.11.17", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.17.tgz", @@ -2400,12 +2460,45 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/qs": { + "version": "6.9.11", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.11.tgz", + "integrity": "sha512-oGk0gmhnEJK4Yyk+oI7EfXsLayXatCWPHary1MtcmbAifkobT9cM9yutG/hZKIseOU0MqbIwQ/u2nn/Gb+ltuQ==", + "dev": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true + }, "node_modules/@types/semver": { "version": "7.5.6", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", "dev": true }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz", + "integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==", + "dev": true, + "dependencies": { + "@types/http-errors": "*", + "@types/mime": "*", + "@types/node": "*" + } + }, "node_modules/@types/stack-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", @@ -2983,6 +3076,18 @@ } } }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", @@ -3146,6 +3251,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, "node_modules/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -3285,6 +3395,42 @@ "node": ">=8" } }, + "node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -3377,6 +3523,14 @@ "esbuild": ">=0.17" } }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -3390,7 +3544,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, "dependencies": { "function-bind": "^1.1.1", "get-intrinsic": "^1.0.2" @@ -3599,12 +3752,44 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, "node_modules/cookiejar": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", @@ -3729,6 +3914,23 @@ "node": ">=0.4.0" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -3796,6 +3998,11 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, "node_modules/electron-to-chromium": { "version": "1.4.335", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.335.tgz", @@ -3820,6 +4027,14 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -3967,6 +4182,11 @@ "node": ">=6" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -4182,6 +4402,14 @@ "node": ">=0.10.0" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -4230,6 +4458,68 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express-async-errors": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/express-async-errors/-/express-async-errors-3.1.1.tgz", + "integrity": "sha512-h6aK1da4tpqWSbyCa3FxB/V6Ehd4EEB15zyQq9qe75OZBp0krinNKuH4rAY+S/U/2I36vdLAUFSjQJ+TFmODng==", + "peerDependencies": { + "express": "^4.16.2" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4330,6 +4620,36 @@ "node": ">=8" } }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -4440,6 +4760,22 @@ "url": "https://ko-fi.com/tunnckoCore/commissions" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -4463,8 +4799,7 @@ "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "node_modules/function.prototype.name": { "version": "1.1.5", @@ -4515,7 +4850,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", - "dev": true, "dependencies": { "function-bind": "^1.1.1", "has": "^1.0.3", @@ -4672,7 +5006,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, "dependencies": { "function-bind": "^1.1.1" }, @@ -4726,7 +5059,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -4776,6 +5108,21 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -4785,6 +5132,17 @@ "node": ">=10.17.0" } }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -4851,8 +5209,7 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/internal-slot": { "version": "1.0.5", @@ -4868,6 +5225,14 @@ "node": ">= 0.4" } }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-array-buffer": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", @@ -6063,6 +6428,14 @@ "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==", "dev": true }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/memorystream": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", @@ -6072,6 +6445,11 @@ "node": ">= 0.10.0" } }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -6091,7 +6469,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -6109,11 +6486,21 @@ "node": ">=8.6" } }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -6122,7 +6509,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "dependencies": { "mime-db": "1.52.0" }, @@ -6222,6 +6608,14 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", @@ -6458,7 +6852,6 @@ "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -6490,6 +6883,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -6600,6 +7004,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -6658,6 +7070,11 @@ "node": "14 || >=16.14" } }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -6933,6 +7350,18 @@ "node": ">= 6" } }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -6962,7 +7391,6 @@ "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dev": true, "dependencies": { "side-channel": "^1.0.4" }, @@ -6993,6 +7421,28 @@ } ] }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -7205,6 +7655,25 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/safe-regex-test": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", @@ -7219,6 +7688,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, "node_modules/search-insights": { "version": "2.13.0", "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.13.0.tgz", @@ -7259,6 +7733,66 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -7302,7 +7836,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dev": true, "dependencies": { "call-bind": "^1.0.0", "get-intrinsic": "^1.0.2", @@ -7429,6 +7962,14 @@ "node": ">=8" } }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -7830,6 +8371,14 @@ "node": ">=8.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, "node_modules/tr46": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", @@ -8093,6 +8642,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typed-array-length": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", @@ -8141,6 +8702,14 @@ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "dev": true }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/update-browserslist-db": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", @@ -8176,6 +8745,14 @@ "punycode": "^2.1.0" } }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -8206,6 +8783,14 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/vite": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.1.tgz", diff --git a/package.json b/package.json index 4cb6ff19..7b3f4b51 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@event-driven-io/emmett", - "version": "0.0.1", - "description": "Emmett - Event Sourcing made simple", + "version": "0.1.0", + "description": "Emmett - Event Sourcing development made simple", "scripts": { "setup": "cat .nvmrc | nvm install; nvm use", "build": "tsup", @@ -37,18 +37,22 @@ "bugs": { "url": "https://github.com/event-driven-io/emmett/issues" }, - "homepage": "https://github.com/event-driven-io/emmett#readme", + "homepage": "https://event-driven-io.github.io/emmett/", "main": "./dist/index.js", "module": "./dist/index.mjs", "types": "./dist/index.d.ts", "files": [ "dist" ], - "dependencies": {}, + "dependencies": { + "express": "4.18.2", + "express-async-errors": "3.1.1" + }, "devDependencies": { "@faker-js/faker": "8.4.1", "@types/jest": "29.5.0", "@types/node": "20.11.17", + "@types/express": "4.17.21", "@typescript-eslint/eslint-plugin": "6.21.0", "@typescript-eslint/parser": "6.21.0", "eslint": "8.56.0", diff --git a/src/commandHandling/handleCommand.ts b/src/commandHandling/handleCommand.ts new file mode 100644 index 00000000..01f82c87 --- /dev/null +++ b/src/commandHandling/handleCommand.ts @@ -0,0 +1,27 @@ +import type { EventStore } from '../eventStore'; +import type { Event } from '../typing'; + +export const CommandHandler = + ( + evolve: (state: State, event: StreamEvent) => State, + getInitialState: () => State, + mapToStreamId: (id: string) => string, + ) => + async ( + eventStore: EventStore, + id: string, + handle: (state: State) => StreamEvent | StreamEvent[], + ) => { + const streamName = mapToStreamId(id); + + const state = await eventStore.aggregateStream(streamName, { + evolve, + getInitialState, + }); + + const result = handle(state ?? getInitialState()); + + if (Array.isArray(result)) + return eventStore.appendToStream(streamName, ...result); + else return eventStore.appendToStream(streamName, result); + }; diff --git a/src/commandHandling/handleCommandWithDecider.ts b/src/commandHandling/handleCommandWithDecider.ts new file mode 100644 index 00000000..28287ceb --- /dev/null +++ b/src/commandHandling/handleCommandWithDecider.ts @@ -0,0 +1,27 @@ +import type { EventStore } from '../eventStore'; +import type { Command, Event } from '../typing'; +import type { Decider } from '../typing/decider'; + +export const DeciderCommandHandler = + ( + { + decide, + evolve, + getInitialState, + }: Decider, + mapToStreamId: (id: string) => string, + ) => + async (eventStore: EventStore, id: string, command: CommandType) => { + const streamName = mapToStreamId(id); + + const state = await eventStore.aggregateStream(streamName, { + evolve, + getInitialState, + }); + + const result = decide(command, state ?? getInitialState()); + + if (Array.isArray(result)) + return eventStore.appendToStream(streamName, ...result); + else return eventStore.appendToStream(streamName, result); + }; diff --git a/src/commandHandling/index.ts b/src/commandHandling/index.ts new file mode 100644 index 00000000..e5153204 --- /dev/null +++ b/src/commandHandling/index.ts @@ -0,0 +1,2 @@ +export * from './handleCommand'; +export * from './handleCommandWithDecider'; diff --git a/src/eventStore/eventStore.ts b/src/eventStore/eventStore.ts new file mode 100644 index 00000000..9cc7101d --- /dev/null +++ b/src/eventStore/eventStore.ts @@ -0,0 +1,18 @@ +import type { Event } from '../typing'; + +export interface EventStore { + aggregateStream( + streamName: string, + options: { + evolve: (currentState: Entity, event: E) => Entity; + getInitialState: () => Entity; + }, + ): Promise; + + readStream(streamName: string): Promise; + + appendToStream( + streamId: string, + ...events: E[] + ): Promise; +} diff --git a/src/eventStore/index.ts b/src/eventStore/index.ts new file mode 100644 index 00000000..7790fae0 --- /dev/null +++ b/src/eventStore/index.ts @@ -0,0 +1 @@ +export * from './eventStore'; diff --git a/src/immutable/index.ts b/src/immutable/index.ts new file mode 100644 index 00000000..38f29768 --- /dev/null +++ b/src/immutable/index.ts @@ -0,0 +1 @@ +export * from './merge'; diff --git a/src/immutable/merge.ts b/src/immutable/merge.ts new file mode 100644 index 00000000..35e69177 --- /dev/null +++ b/src/immutable/merge.ts @@ -0,0 +1,37 @@ +export const merge = ( + array: T[], + item: T, + where: (current: T) => boolean, + onExisting: (current: T) => T, + onNotFound: () => T | undefined = () => undefined, +) => { + let wasFound = false; + + const result = array + // merge the existing item if matches condition + .map((p: T) => { + if (!where(p)) return p; + + wasFound = true; + return onExisting(p); + }) + // filter out item if undefined was returned + // for cases of removal + .filter((p) => p !== undefined) + // make TypeScript happy + .map((p) => { + if (!p) throw Error('That should not happen'); + + return p; + }); + + // if item was not found and onNotFound action is defined + // try to generate new item + if (!wasFound) { + const result = onNotFound(); + + if (result !== undefined) return [...array, item]; + } + + return result; +}; diff --git a/src/index.ts b/src/index.ts index 2a96ab0f..2a7acaec 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1 @@ -export interface EventStore { - appendEvents: () => Promise; -} +export * from './typing'; diff --git a/src/serialization/json/JSONParser.ts b/src/serialization/json/JSONParser.ts new file mode 100644 index 00000000..1f849303 --- /dev/null +++ b/src/serialization/json/JSONParser.ts @@ -0,0 +1,53 @@ +export class ParseError extends Error { + constructor(text: string) { + super(`Cannot parse! ${text}`); + } +} + +export type Mapper = + | ((value: unknown) => To) + | ((value: Partial) => To) + | ((value: From) => To) + | ((value: Partial) => To) + | ((value: To) => To) + | ((value: Partial) => To) + | ((value: To | From) => To); + +export type MapperArgs = Partial & + From & + Partial & + To; + +export type ParseOptions = { + reviver?: (key: string, value: unknown) => unknown; + map?: Mapper; + typeCheck?: (value: unknown) => value is To; +}; + +export type StringifyOptions = { + map?: Mapper; +}; + +export const JSONParser = { + stringify: ( + value: From, + options?: StringifyOptions, + ) => { + return JSON.stringify( + options?.map ? options.map(value as MapperArgs) : value, + ); + }, + parse: ( + text: string, + options?: ParseOptions, + ): To | undefined => { + const parsed: unknown = JSON.parse(text, options?.reviver); + + if (options?.typeCheck && !options?.typeCheck(parsed)) + throw new ParseError(text); + + return options?.map + ? options.map(parsed as MapperArgs) + : (parsed as To | undefined); + }, +}; diff --git a/src/serialization/json/index.ts b/src/serialization/json/index.ts new file mode 100644 index 00000000..ccb7fe01 --- /dev/null +++ b/src/serialization/json/index.ts @@ -0,0 +1 @@ +export * from './JSONParser'; diff --git a/src/testing/deciderSpecification.ts b/src/testing/deciderSpecification.ts new file mode 100644 index 00000000..6de3b813 --- /dev/null +++ b/src/testing/deciderSpecification.ts @@ -0,0 +1,63 @@ +import assert from 'assert'; + +export type DeciderSpecfication = ( + givenEvents: Event | Event[], +) => { + when: (command: Command) => { + then: (expectedEvents: Event | Event[]) => void; + thenThrows: (assert: (error: Error) => boolean) => void; + }; +}; + +export const DeciderSpecification = { + for: (decider: { + decide: (command: Command, state: State) => Event | Event[]; + evolve: (state: State, event: Event) => State; + initialState: () => State; + }): DeciderSpecfication => { + { + return (givenEvents: Event | Event[]) => { + return { + when: (command: Command) => { + const handle = () => { + const existingEvents = Array.isArray(givenEvents) + ? givenEvents + : [givenEvents]; + + const currentState = existingEvents.reduce( + decider.evolve, + decider.initialState(), + ); + + return decider.decide(command, currentState); + }; + + return { + then: (expectedEvents: Event | Event[]): void => { + const resultEvents = handle(); + + const resultEventsArray = Array.isArray(resultEvents) + ? resultEvents + : [resultEvents]; + + const expectedEventsArray = Array.isArray(expectedEvents) + ? expectedEvents + : [expectedEvents]; + + assert.deepEqual(resultEventsArray, expectedEventsArray); + }, + thenThrows: (check: (error: Error) => boolean): void => { + try { + handle(); + assert.fail('Handler did not fail as expected'); + } catch (error) { + assert.ok(check(error as Error)); + } + }, + }; + }, + }; + }; + } + }, +}; diff --git a/src/testing/index.ts b/src/testing/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/typing/command.ts b/src/typing/command.ts new file mode 100644 index 00000000..a2c66e87 --- /dev/null +++ b/src/typing/command.ts @@ -0,0 +1,14 @@ +import type { Flavour } from './'; + +export type Command< + CommandType extends string = string, + CommandData extends Record = Record, + CommandMetaData extends Record = Record, +> = Flavour< + Readonly<{ + type: CommandType; + data: Readonly; + metadata?: CommandMetaData | undefined; + }>, + 'Command' +>; diff --git a/src/typing/decider.ts b/src/typing/decider.ts new file mode 100644 index 00000000..75b94945 --- /dev/null +++ b/src/typing/decider.ts @@ -0,0 +1,11 @@ +import type { Command, Event } from './'; + +export type Decider< + State, + CommandType extends Command, + StreamEvent extends Event, +> = { + decide: (command: CommandType, state: State) => StreamEvent | StreamEvent[]; + evolve: (currentState: State, event: StreamEvent) => State; + getInitialState: () => State; +}; diff --git a/src/typing/event.ts b/src/typing/event.ts new file mode 100644 index 00000000..7cd96386 --- /dev/null +++ b/src/typing/event.ts @@ -0,0 +1,14 @@ +import type { Flavour } from './'; + +export type Event< + EventType extends string = string, + EventData extends Record = Record, + EventMetaData extends Record = Record, +> = Flavour< + Readonly<{ + type: EventType; + data: Readonly; + metadata?: EventMetaData | undefined; + }>, + 'Event' +>; diff --git a/src/typing/index.ts b/src/typing/index.ts new file mode 100644 index 00000000..9e3119cb --- /dev/null +++ b/src/typing/index.ts @@ -0,0 +1,8 @@ +export * from './command'; +export * from './event'; + +export * from './decider'; +export * from './workflow'; + +export type Brand = K & { readonly __brand: T }; +export type Flavour = K & { readonly __brand?: T }; diff --git a/src/typing/workflow.ts b/src/typing/workflow.ts new file mode 100644 index 00000000..2468eb74 --- /dev/null +++ b/src/typing/workflow.ts @@ -0,0 +1,108 @@ +import type { Command } from './command'; +import type { Event } from './event'; + +/// Inspired by https://blog.bittacklr.be/the-workflow-pattern.html + +export type Workflow< + Input extends Event | Command, + State, + Output extends Event | Command, +> = { + decide: (command: Input, state: State) => WorkflowOutput[]; + evolve: (currentState: State, event: WorkflowEvent) => State; + getInitialState: () => State; +}; + +export type WorkflowEvent = Extract< + Output, + { __brand?: 'Event' } +>; + +export type WorkflowCommand = Extract< + Output, + { __brand?: 'Command' } +>; + +export type WorkflowOutput = + | { kind: 'Reply'; message: TOutput } + | { kind: 'Send'; message: WorkflowCommand } + | { kind: 'Publish'; message: WorkflowEvent } + | { + kind: 'Schedule'; + message: TOutput; + when: { afterInMs: number } | { at: Date }; + } + | { kind: 'Complete' } + | { kind: 'Accept' } + | { kind: 'Ignore'; reason: string } + | { kind: 'Error'; reason: string }; + +export const reply = ( + message: TOutput, +): WorkflowOutput => { + return { + kind: 'Reply', + message, + }; +}; + +export const send = ( + message: WorkflowCommand, +): WorkflowOutput => { + return { + kind: 'Send', + message, + }; +}; + +export const publish = ( + message: WorkflowEvent, +): WorkflowOutput => { + return { + kind: 'Publish', + message, + }; +}; + +export const schedule = ( + message: TOutput, + when: { afterInMs: number } | { at: Date }, +): WorkflowOutput => { + return { + kind: 'Schedule', + message, + when, + }; +}; + +export const complete = < + TOutput extends Command | Event, +>(): WorkflowOutput => { + return { + kind: 'Complete', + }; +}; + +export const ignore = ( + reason: string, +): WorkflowOutput => { + return { + kind: 'Ignore', + reason, + }; +}; + +export const error = ( + reason: string, +): WorkflowOutput => { + return { + kind: 'Error', + reason, + }; +}; + +export const accept = < + TOutput extends Command | Event, +>(): WorkflowOutput => { + return { kind: 'Accept' }; +}; diff --git a/src/validation/index.ts b/src/validation/index.ts new file mode 100644 index 00000000..71c65f92 --- /dev/null +++ b/src/validation/index.ts @@ -0,0 +1,27 @@ +export const enum ValidationErrors { + NOT_A_NONEMPTY_STRING = 'NOT_A_NONEMPTY_STRING', + NOT_A_POSITIVE_NUMBER = 'NOT_A_POSITIVE_NUMBER', + NOT_AN_UNSIGNED_BIGINT = 'NOT_AN_UNSIGNED_BIGINT', +} + +export const assertNotEmptyString = (value: unknown): string => { + if (typeof value !== 'string' || value.length === 0) { + throw new Error(ValidationErrors.NOT_A_NONEMPTY_STRING); + } + return value; +}; + +export const assertPositiveNumber = (value: unknown): number => { + if (typeof value !== 'number' || value <= 0) { + throw new Error(ValidationErrors.NOT_A_POSITIVE_NUMBER); + } + return value; +}; + +export const assertUnsignedBigInt = (value: string): bigint => { + const number = BigInt(value); + if (number < 0) { + throw new Error(ValidationErrors.NOT_AN_UNSIGNED_BIGINT); + } + return number; +};