feat: update aggregate implementation
This commit is contained in:
60
README.md
60
README.md
@@ -11,66 +11,6 @@ repository to one or more distibuted services.
|
|||||||
|
|
||||||
The following provides a quick introduction on how to get started.
|
The following provides a quick introduction on how to get started.
|
||||||
|
|
||||||
### Configs
|
|
||||||
|
|
||||||
Events are defined in `json` configuration files which we print to a generated `events.ts` file that is used by the
|
|
||||||
event store instance we are using. To do this, start by creating a new folder that will house our event configurations.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
$ mkdir events
|
|
||||||
$ cd events
|
|
||||||
```
|
|
||||||
|
|
||||||
Now add a new event configuration file.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
$ touch user-created.json
|
|
||||||
```
|
|
||||||
|
|
||||||
Open the file and add the event details.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"event": {
|
|
||||||
"type": "user:created",
|
|
||||||
"data": {
|
|
||||||
"name": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"given": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"family": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"email": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"meta": {
|
|
||||||
"auditor": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Generate
|
|
||||||
|
|
||||||
To create our `events.ts` file we have to run our configurations through our event printer.
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import { printEvents } from "@valkyr/event-store";
|
|
||||||
|
|
||||||
await printEvents({
|
|
||||||
inputs: ["./configs/events"],
|
|
||||||
outputs: ["./generated/events.ts"],
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Event Store
|
### Event Store
|
||||||
|
|
||||||
Once we have defined our configs and printed our events we create a new event store instance. Currently we have support
|
Once we have defined our configs and printed our events we create a new event store instance. Currently we have support
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import z from "zod/v4";
|
import z from "zod";
|
||||||
|
|
||||||
import type { CollectionRegistrar } from "../types.ts";
|
import type { CollectionRegistrar } from "../types.ts";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import z from "zod/v4";
|
import z from "zod";
|
||||||
|
|
||||||
import type { CollectionRegistrar } from "../types.ts";
|
import type { CollectionRegistrar } from "../types.ts";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import z from "zod/v4";
|
import z from "zod";
|
||||||
|
|
||||||
import type { CollectionRegistrar } from "../types.ts";
|
import type { CollectionRegistrar } from "../types.ts";
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Db, WithId } from "mongodb";
|
import type { Db, WithId } from "mongodb";
|
||||||
import type { z, ZodObject } from "zod/v4";
|
import type { z, ZodObject } from "zod";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Take a list of records and run it through the given zod parser. This
|
* Take a list of records and run it through the given zod parser. This
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@valkyr/event-store",
|
"name": "@valkyr/event-store",
|
||||||
"version": "2.0.0-beta.4",
|
"version": "2.0.0-beta.5",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./mod.ts",
|
".": "./mod.ts",
|
||||||
"./browser": "./adapters/browser/adapter.ts",
|
"./browser": "./adapters/browser/adapter.ts",
|
||||||
|
|||||||
171
deno.lock
generated
171
deno.lock
generated
@@ -1,23 +1,23 @@
|
|||||||
{
|
{
|
||||||
"version": "5",
|
"version": "5",
|
||||||
"specifiers": {
|
"specifiers": {
|
||||||
"npm:@jsr/std__assert@1.0.13": "1.0.13",
|
"npm:@jsr/std__assert@1": "1.0.13",
|
||||||
"npm:@jsr/std__async@1.0.13": "1.0.13",
|
"npm:@jsr/std__async@1": "1.0.14",
|
||||||
"npm:@jsr/std__testing@1.0.14": "1.0.14",
|
"npm:@jsr/std__testing@1": "1.0.15",
|
||||||
"npm:@jsr/valkyr__testcontainers@2": "2.0.1",
|
"npm:@jsr/valkyr__testcontainers@2": "2.0.2",
|
||||||
"npm:@valkyr/db@1.0.1": "1.0.1",
|
"npm:@valkyr/db@1.0.1": "1.0.1",
|
||||||
"npm:eslint-plugin-simple-import-sort@12.1.1": "12.1.1_eslint@9.30.1",
|
"npm:eslint-plugin-simple-import-sort@12": "12.1.1_eslint@9.33.0",
|
||||||
"npm:eslint@9.30.1": "9.30.1",
|
"npm:eslint@9": "9.33.0",
|
||||||
"npm:fake-indexeddb@6.0.1": "6.0.1",
|
"npm:fake-indexeddb@6": "6.1.0",
|
||||||
"npm:mongodb@6": "6.17.0",
|
"npm:mongodb@6": "6.18.0",
|
||||||
"npm:nanoid@5": "5.1.5",
|
"npm:nanoid@5": "5.1.5",
|
||||||
"npm:postgres@3": "3.4.7",
|
"npm:postgres@3": "3.4.7",
|
||||||
"npm:prettier@3.6.2": "3.6.2",
|
"npm:prettier@3": "3.6.2",
|
||||||
"npm:typescript-eslint@8.35.1": "8.35.1_eslint@9.30.1_typescript@5.8.3_@typescript-eslint+parser@8.35.1__eslint@9.30.1__typescript@5.8.3",
|
"npm:typescript-eslint@8": "8.39.0_eslint@9.33.0_typescript@5.9.2_@typescript-eslint+parser@8.39.0__eslint@9.33.0__typescript@5.9.2",
|
||||||
"npm:zod@3.25": "3.25.75"
|
"npm:zod@4": "4.0.17"
|
||||||
},
|
},
|
||||||
"npm": {
|
"npm": {
|
||||||
"@eslint-community/eslint-utils@4.7.0_eslint@9.30.1": {
|
"@eslint-community/eslint-utils@4.7.0_eslint@9.33.0": {
|
||||||
"integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==",
|
"integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"eslint",
|
"eslint",
|
||||||
@@ -35,17 +35,11 @@
|
|||||||
"minimatch@3.1.2"
|
"minimatch@3.1.2"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@eslint/config-helpers@0.3.0": {
|
"@eslint/config-helpers@0.3.1": {
|
||||||
"integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw=="
|
"integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA=="
|
||||||
},
|
},
|
||||||
"@eslint/core@0.14.0": {
|
"@eslint/core@0.15.2": {
|
||||||
"integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==",
|
"integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==",
|
||||||
"dependencies": [
|
|
||||||
"@types/json-schema"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"@eslint/core@0.15.1": {
|
|
||||||
"integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==",
|
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@types/json-schema"
|
"@types/json-schema"
|
||||||
]
|
]
|
||||||
@@ -64,16 +58,16 @@
|
|||||||
"strip-json-comments"
|
"strip-json-comments"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@eslint/js@9.30.1": {
|
"@eslint/js@9.33.0": {
|
||||||
"integrity": "sha512-zXhuECFlyep42KZUhWjfvsmXGX39W8K8LFb8AWXM9gSV9dQB+MrJGLKvW6Zw0Ggnbpw0VHTtrhFXYe3Gym18jg=="
|
"integrity": "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A=="
|
||||||
},
|
},
|
||||||
"@eslint/object-schema@2.1.6": {
|
"@eslint/object-schema@2.1.6": {
|
||||||
"integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="
|
"integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="
|
||||||
},
|
},
|
||||||
"@eslint/plugin-kit@0.3.3": {
|
"@eslint/plugin-kit@0.3.5": {
|
||||||
"integrity": "sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==",
|
"integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@eslint/core@0.15.1",
|
"@eslint/core",
|
||||||
"levn"
|
"levn"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -103,13 +97,16 @@
|
|||||||
],
|
],
|
||||||
"tarball": "https://npm.jsr.io/~/11/@jsr/std__assert/1.0.13.tgz"
|
"tarball": "https://npm.jsr.io/~/11/@jsr/std__assert/1.0.13.tgz"
|
||||||
},
|
},
|
||||||
"@jsr/std__async@1.0.13": {
|
"@jsr/std__async@1.0.14": {
|
||||||
"integrity": "sha512-GEApyNtzauJ0kEZ/GxebSkdEN0t29qJtkw+WEvzYTwkL6fHX8cq3YWzRjCqHu+4jMl+rpHiwyr/lfitNInntzA==",
|
"integrity": "sha512-aIG8W3TOmW+lKdAJA5w56qASu9EiUmBXbhW6eAlSEUBid+KVESGqQygFFg+awt/c8K+qobVM6M/u3SbIy0NyUQ==",
|
||||||
"tarball": "https://npm.jsr.io/~/11/@jsr/std__async/1.0.13.tgz"
|
"tarball": "https://npm.jsr.io/~/11/@jsr/std__async/1.0.14.tgz"
|
||||||
},
|
},
|
||||||
"@jsr/std__data-structures@1.0.8": {
|
"@jsr/std__data-structures@1.0.9": {
|
||||||
"integrity": "sha512-7BHBUlBEJ/9w2zv9sNmyuQOINBTEP1erxLHMpIDBa7GMCV1Nxm6LvgC4R5cgN90FFKpoCFa9PPB66Hkeem9Q2g==",
|
"integrity": "sha512-+mT4Nll6fx+CPNqrlC+huhIOYNSMS+KUdJ4B8NujiQrh/bq++ds5PXpEsfV5EPR+YuWcuDGG0P1DE+Rednd7Wg==",
|
||||||
"tarball": "https://npm.jsr.io/~/11/@jsr/std__data-structures/1.0.8.tgz"
|
"dependencies": [
|
||||||
|
"@jsr/std__assert"
|
||||||
|
],
|
||||||
|
"tarball": "https://npm.jsr.io/~/11/@jsr/std__data-structures/1.0.9.tgz"
|
||||||
},
|
},
|
||||||
"@jsr/std__fs@1.0.19": {
|
"@jsr/std__fs@1.0.19": {
|
||||||
"integrity": "sha512-TEjyE8g+46jPlu7dJHLrwc8NMGl8zfG+JjWxyNQyDbxP0RtqZ4JmYZfR9vy4RWYWJQbLpw6Kbt2n+K/2zAO/JA==",
|
"integrity": "sha512-TEjyE8g+46jPlu7dJHLrwc8NMGl8zfG+JjWxyNQyDbxP0RtqZ4JmYZfR9vy4RWYWJQbLpw6Kbt2n+K/2zAO/JA==",
|
||||||
@@ -119,9 +116,9 @@
|
|||||||
],
|
],
|
||||||
"tarball": "https://npm.jsr.io/~/11/@jsr/std__fs/1.0.19.tgz"
|
"tarball": "https://npm.jsr.io/~/11/@jsr/std__fs/1.0.19.tgz"
|
||||||
},
|
},
|
||||||
"@jsr/std__internal@1.0.9": {
|
"@jsr/std__internal@1.0.10": {
|
||||||
"integrity": "sha512-s+f4qrJzZgPAy7XuFOtgaSaxyPLnnEmAfXGLvRXGxPTL76URLVHkF+hOzqXz+bmk8/awybF6BRsasxtAQOV23Q==",
|
"integrity": "sha512-fmD6yKep/sMnB2yPQU/REZG7Z4N9SZwcUBNnceo4QkXk67l3JEfxHoROQ/YHeVSOmq6x55Ra6nuMjz2ib3nj3g==",
|
||||||
"tarball": "https://npm.jsr.io/~/11/@jsr/std__internal/1.0.9.tgz"
|
"tarball": "https://npm.jsr.io/~/11/@jsr/std__internal/1.0.10.tgz"
|
||||||
},
|
},
|
||||||
"@jsr/std__net@1.0.4": {
|
"@jsr/std__net@1.0.4": {
|
||||||
"integrity": "sha512-KJGU8ZpQ70sMW2Zk+wU3wFUkggS9lTLfRFBygnV9VaK8KI+1ggiqtB06rH4a14CNRGM9y46Mn/ZCbQUd4Q45Jg==",
|
"integrity": "sha512-KJGU8ZpQ70sMW2Zk+wU3wFUkggS9lTLfRFBygnV9VaK8KI+1ggiqtB06rH4a14CNRGM9y46Mn/ZCbQUd4Q45Jg==",
|
||||||
@@ -134,8 +131,8 @@
|
|||||||
],
|
],
|
||||||
"tarball": "https://npm.jsr.io/~/11/@jsr/std__path/1.1.1.tgz"
|
"tarball": "https://npm.jsr.io/~/11/@jsr/std__path/1.1.1.tgz"
|
||||||
},
|
},
|
||||||
"@jsr/std__testing@1.0.14": {
|
"@jsr/std__testing@1.0.15": {
|
||||||
"integrity": "sha512-WQ2ctU3AmV0dcaVEahIUfz4e+3+Y3UMyqFLjCZ6JKeI40zkDpeMFBsTop7e7ptGE4wgHrQj+FETh9XAgEuBlZA==",
|
"integrity": "sha512-NgQuXxTEG4ecbh2fzYbkJWJoBgPXwbv6bdsrAYSOeLpX2d+TROEzpErbWQXHi/yxZy/FNn9IF548ZDAqMZxi/g==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@jsr/std__assert",
|
"@jsr/std__assert",
|
||||||
"@jsr/std__async",
|
"@jsr/std__async",
|
||||||
@@ -144,10 +141,10 @@
|
|||||||
"@jsr/std__internal",
|
"@jsr/std__internal",
|
||||||
"@jsr/std__path"
|
"@jsr/std__path"
|
||||||
],
|
],
|
||||||
"tarball": "https://npm.jsr.io/~/11/@jsr/std__testing/1.0.14.tgz"
|
"tarball": "https://npm.jsr.io/~/11/@jsr/std__testing/1.0.15.tgz"
|
||||||
},
|
},
|
||||||
"@jsr/valkyr__testcontainers@2.0.1": {
|
"@jsr/valkyr__testcontainers@2.0.2": {
|
||||||
"integrity": "sha512-HInqMkCDj1ICrcz+Led/3jyLa70mwncxdlly8v/5WepuPW3gszKftq5U2jbjy2THOYUP7ibBK2o0recg7qhvcw==",
|
"integrity": "sha512-YnmfraYFr3msoUGrIFeElm03nbQqXOaPu0QUT6JI3w6/mIYpVfzPxghkB7gn2RIc81QgrqjwKJE/AL3dltlR1w==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@jsr/std__async",
|
"@jsr/std__async",
|
||||||
"@jsr/std__fs",
|
"@jsr/std__fs",
|
||||||
@@ -155,7 +152,7 @@
|
|||||||
"mongodb",
|
"mongodb",
|
||||||
"postgres"
|
"postgres"
|
||||||
],
|
],
|
||||||
"tarball": "https://npm.jsr.io/~/11/@jsr/valkyr__testcontainers/2.0.1.tgz"
|
"tarball": "https://npm.jsr.io/~/11/@jsr/valkyr__testcontainers/2.0.2.tgz"
|
||||||
},
|
},
|
||||||
"@mongodb-js/saslprep@1.3.0": {
|
"@mongodb-js/saslprep@1.3.0": {
|
||||||
"integrity": "sha512-zlayKCsIjYb7/IdfqxorK5+xUMyi4vOKcFy10wKJYc63NSdKI8mNME+uJqfatkPmOSMMUiojrL58IePKBm3gvQ==",
|
"integrity": "sha512-zlayKCsIjYb7/IdfqxorK5+xUMyi4vOKcFy10wKJYc63NSdKI8mNME+uJqfatkPmOSMMUiojrL58IePKBm3gvQ==",
|
||||||
@@ -195,8 +192,8 @@
|
|||||||
"@types/webidl-conversions"
|
"@types/webidl-conversions"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@typescript-eslint/eslint-plugin@8.35.1_@typescript-eslint+parser@8.35.1__eslint@9.30.1__typescript@5.8.3_eslint@9.30.1_typescript@5.8.3": {
|
"@typescript-eslint/eslint-plugin@8.39.0_@typescript-eslint+parser@8.39.0__eslint@9.33.0__typescript@5.9.2_eslint@9.33.0_typescript@5.9.2": {
|
||||||
"integrity": "sha512-9XNTlo7P7RJxbVeICaIIIEipqxLKguyh+3UbXuT2XQuFp6d8VOeDEGuz5IiX0dgZo8CiI6aOFLg4e8cF71SFVg==",
|
"integrity": "sha512-bhEz6OZeUR+O/6yx9Jk6ohX6H9JSFTaiY0v9/PuKT3oGK0rn0jNplLmyFUGV+a9gfYnVNwGDwS/UkLIuXNb2Rw==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@eslint-community/regexpp",
|
"@eslint-community/regexpp",
|
||||||
"@typescript-eslint/parser",
|
"@typescript-eslint/parser",
|
||||||
@@ -212,8 +209,8 @@
|
|||||||
"typescript"
|
"typescript"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@typescript-eslint/parser@8.35.1_eslint@9.30.1_typescript@5.8.3": {
|
"@typescript-eslint/parser@8.39.0_eslint@9.33.0_typescript@5.9.2": {
|
||||||
"integrity": "sha512-3MyiDfrfLeK06bi/g9DqJxP5pV74LNv4rFTyvGDmT3x2p1yp1lOd+qYZfiRPIOf/oON+WRZR5wxxuF85qOar+w==",
|
"integrity": "sha512-g3WpVQHngx0aLXn6kfIYCZxM6rRJlWzEkVpqEFLT3SgEDsp9cpCbxxgwnE504q4H+ruSDh/VGS6nqZIDynP+vg==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@typescript-eslint/scope-manager",
|
"@typescript-eslint/scope-manager",
|
||||||
"@typescript-eslint/types",
|
"@typescript-eslint/types",
|
||||||
@@ -224,8 +221,8 @@
|
|||||||
"typescript"
|
"typescript"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@typescript-eslint/project-service@8.35.1_typescript@5.8.3": {
|
"@typescript-eslint/project-service@8.39.0_typescript@5.9.2": {
|
||||||
"integrity": "sha512-VYxn/5LOpVxADAuP3NrnxxHYfzVtQzLKeldIhDhzC8UHaiQvYlXvKuVho1qLduFbJjjy5U5bkGwa3rUGUb1Q6Q==",
|
"integrity": "sha512-CTzJqaSq30V/Z2Og9jogzZt8lJRR5TKlAdXmWgdu4hgcC9Kww5flQ+xFvMxIBWVNdxJO7OifgdOK4PokMIWPew==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@typescript-eslint/tsconfig-utils",
|
"@typescript-eslint/tsconfig-utils",
|
||||||
"@typescript-eslint/types",
|
"@typescript-eslint/types",
|
||||||
@@ -233,22 +230,23 @@
|
|||||||
"typescript"
|
"typescript"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@typescript-eslint/scope-manager@8.35.1": {
|
"@typescript-eslint/scope-manager@8.39.0": {
|
||||||
"integrity": "sha512-s/Bpd4i7ht2934nG+UoSPlYXd08KYz3bmjLEb7Ye1UVob0d1ENiT3lY8bsCmik4RqfSbPw9xJJHbugpPpP5JUg==",
|
"integrity": "sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@typescript-eslint/types",
|
"@typescript-eslint/types",
|
||||||
"@typescript-eslint/visitor-keys"
|
"@typescript-eslint/visitor-keys"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@typescript-eslint/tsconfig-utils@8.35.1_typescript@5.8.3": {
|
"@typescript-eslint/tsconfig-utils@8.39.0_typescript@5.9.2": {
|
||||||
"integrity": "sha512-K5/U9VmT9dTHoNowWZpz+/TObS3xqC5h0xAIjXPw+MNcKV9qg6eSatEnmeAwkjHijhACH0/N7bkhKvbt1+DXWQ==",
|
"integrity": "sha512-Fd3/QjmFV2sKmvv3Mrj8r6N8CryYiCS8Wdb/6/rgOXAWGcFuc+VkQuG28uk/4kVNVZBQuuDHEDUpo/pQ32zsIQ==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"typescript"
|
"typescript"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@typescript-eslint/type-utils@8.35.1_eslint@9.30.1_typescript@5.8.3": {
|
"@typescript-eslint/type-utils@8.39.0_eslint@9.33.0_typescript@5.9.2": {
|
||||||
"integrity": "sha512-HOrUBlfVRz5W2LIKpXzZoy6VTZzMu2n8q9C2V/cFngIC5U1nStJgv0tMV4sZPzdf4wQm9/ToWUFPMN9Vq9VJQQ==",
|
"integrity": "sha512-6B3z0c1DXVT2vYA9+z9axjtc09rqKUPRmijD5m9iv8iQpHBRYRMBcgxSiKTZKm6FwWw1/cI4v6em35OsKCiN5Q==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
|
"@typescript-eslint/types",
|
||||||
"@typescript-eslint/typescript-estree",
|
"@typescript-eslint/typescript-estree",
|
||||||
"@typescript-eslint/utils",
|
"@typescript-eslint/utils",
|
||||||
"debug",
|
"debug",
|
||||||
@@ -257,11 +255,11 @@
|
|||||||
"typescript"
|
"typescript"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@typescript-eslint/types@8.35.1": {
|
"@typescript-eslint/types@8.39.0": {
|
||||||
"integrity": "sha512-q/O04vVnKHfrrhNAscndAn1tuQhIkwqnaW+eu5waD5IPts2eX1dgJxgqcPx5BX109/qAz7IG6VrEPTOYKCNfRQ=="
|
"integrity": "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg=="
|
||||||
},
|
},
|
||||||
"@typescript-eslint/typescript-estree@8.35.1_typescript@5.8.3": {
|
"@typescript-eslint/typescript-estree@8.39.0_typescript@5.9.2": {
|
||||||
"integrity": "sha512-Vvpuvj4tBxIka7cPs6Y1uvM7gJgdF5Uu9F+mBJBPY4MhvjrjWGK4H0lVgLJd/8PWZ23FTqsaJaLEkBCFUk8Y9g==",
|
"integrity": "sha512-ndWdiflRMvfIgQRpckQQLiB5qAKQ7w++V4LlCHwp62eym1HLB/kw7D9f2e8ytONls/jt89TEasgvb+VwnRprsw==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@typescript-eslint/project-service",
|
"@typescript-eslint/project-service",
|
||||||
"@typescript-eslint/tsconfig-utils",
|
"@typescript-eslint/tsconfig-utils",
|
||||||
@@ -276,8 +274,8 @@
|
|||||||
"typescript"
|
"typescript"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@typescript-eslint/utils@8.35.1_eslint@9.30.1_typescript@5.8.3": {
|
"@typescript-eslint/utils@8.39.0_eslint@9.33.0_typescript@5.9.2": {
|
||||||
"integrity": "sha512-lhnwatFmOFcazAsUm3ZnZFpXSxiwoa1Lj50HphnDe1Et01NF4+hrdXONSUHIcbVu2eFb1bAf+5yjXkGVkXBKAQ==",
|
"integrity": "sha512-4GVSvNA0Vx1Ktwvf4sFE+exxJ3QGUorQG1/A5mRfRNZtkBT2xrA/BCO2H0eALx/PnvCS6/vmYwRdDA41EoffkQ==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@eslint-community/eslint-utils",
|
"@eslint-community/eslint-utils",
|
||||||
"@typescript-eslint/scope-manager",
|
"@typescript-eslint/scope-manager",
|
||||||
@@ -287,8 +285,8 @@
|
|||||||
"typescript"
|
"typescript"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@typescript-eslint/visitor-keys@8.35.1": {
|
"@typescript-eslint/visitor-keys@8.39.0": {
|
||||||
"integrity": "sha512-VRwixir4zBWCSTP/ljEo091lbpypz57PoeAQ9imjG+vbeof9LplljsL1mos4ccG6H9IjfrVGM359RozUnuFhpw==",
|
"integrity": "sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@typescript-eslint/types",
|
"@typescript-eslint/types",
|
||||||
"eslint-visitor-keys@4.2.1"
|
"eslint-visitor-keys@4.2.1"
|
||||||
@@ -407,7 +405,7 @@
|
|||||||
"escape-string-regexp@4.0.0": {
|
"escape-string-regexp@4.0.0": {
|
||||||
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="
|
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="
|
||||||
},
|
},
|
||||||
"eslint-plugin-simple-import-sort@12.1.1_eslint@9.30.1": {
|
"eslint-plugin-simple-import-sort@12.1.1_eslint@9.33.0": {
|
||||||
"integrity": "sha512-6nuzu4xwQtE3332Uz0to+TxDQYRLTKRESSc2hefVT48Zc8JthmN23Gx9lnYhu0FtkRSL1oxny3kJ2aveVhmOVA==",
|
"integrity": "sha512-6nuzu4xwQtE3332Uz0to+TxDQYRLTKRESSc2hefVT48Zc8JthmN23Gx9lnYhu0FtkRSL1oxny3kJ2aveVhmOVA==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"eslint"
|
"eslint"
|
||||||
@@ -426,14 +424,14 @@
|
|||||||
"eslint-visitor-keys@4.2.1": {
|
"eslint-visitor-keys@4.2.1": {
|
||||||
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="
|
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="
|
||||||
},
|
},
|
||||||
"eslint@9.30.1": {
|
"eslint@9.33.0": {
|
||||||
"integrity": "sha512-zmxXPNMOXmwm9E0yQLi5uqXHs7uq2UIiqEKo3Gq+3fwo1XrJ+hijAZImyF7hclW3E6oHz43Yk3RP8at6OTKflQ==",
|
"integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@eslint-community/eslint-utils",
|
"@eslint-community/eslint-utils",
|
||||||
"@eslint-community/regexpp",
|
"@eslint-community/regexpp",
|
||||||
"@eslint/config-array",
|
"@eslint/config-array",
|
||||||
"@eslint/config-helpers",
|
"@eslint/config-helpers",
|
||||||
"@eslint/core@0.14.0",
|
"@eslint/core",
|
||||||
"@eslint/eslintrc",
|
"@eslint/eslintrc",
|
||||||
"@eslint/js",
|
"@eslint/js",
|
||||||
"@eslint/plugin-kit",
|
"@eslint/plugin-kit",
|
||||||
@@ -493,8 +491,8 @@
|
|||||||
"esutils@2.0.3": {
|
"esutils@2.0.3": {
|
||||||
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="
|
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="
|
||||||
},
|
},
|
||||||
"fake-indexeddb@6.0.1": {
|
"fake-indexeddb@6.1.0": {
|
||||||
"integrity": "sha512-He2AjQGHe46svIFq5+L2Nx/eHDTI1oKgoevBP+TthnjymXiKkeJQ3+ITeWey99Y5+2OaPFbI1qEsx/5RsGtWnQ=="
|
"integrity": "sha512-gOzajWIhEug/CQHUIxigKT9Zilh5/I6WvUBez6/UdUtT/YVEHM9r572Os8wfvhp7TkmgBtRNdqSM7YoCXWMzZg=="
|
||||||
},
|
},
|
||||||
"fast-deep-equal@3.1.3": {
|
"fast-deep-equal@3.1.3": {
|
||||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
|
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
|
||||||
@@ -681,8 +679,8 @@
|
|||||||
"whatwg-url"
|
"whatwg-url"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"mongodb@6.17.0": {
|
"mongodb@6.18.0": {
|
||||||
"integrity": "sha512-neerUzg/8U26cgruLysKEjJvoNSXhyID3RvzvdcpsIi2COYM3FS3o9nlH7fxFtefTb942dX3W9i37oPfCVj4wA==",
|
"integrity": "sha512-fO5ttN9VC8P0F5fqtQmclAkgXZxbIkYRTUi1j8JO6IYwvamkhtYDilJr35jOPELR49zqCJgXZWwCtW7B+TM8vQ==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@mongodb-js/saslprep",
|
"@mongodb-js/saslprep",
|
||||||
"bson",
|
"bson",
|
||||||
@@ -818,7 +816,7 @@
|
|||||||
"punycode"
|
"punycode"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"ts-api-utils@2.1.0_typescript@5.8.3": {
|
"ts-api-utils@2.1.0_typescript@5.9.2": {
|
||||||
"integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
|
"integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"typescript"
|
"typescript"
|
||||||
@@ -836,18 +834,19 @@
|
|||||||
"type-fest@3.13.1": {
|
"type-fest@3.13.1": {
|
||||||
"integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g=="
|
"integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g=="
|
||||||
},
|
},
|
||||||
"typescript-eslint@8.35.1_eslint@9.30.1_typescript@5.8.3_@typescript-eslint+parser@8.35.1__eslint@9.30.1__typescript@5.8.3": {
|
"typescript-eslint@8.39.0_eslint@9.33.0_typescript@5.9.2_@typescript-eslint+parser@8.39.0__eslint@9.33.0__typescript@5.9.2": {
|
||||||
"integrity": "sha512-xslJjFzhOmHYQzSB/QTeASAHbjmxOGEP6Coh93TXmUBFQoJ1VU35UHIDmG06Jd6taf3wqqC1ntBnCMeymy5Ovw==",
|
"integrity": "sha512-lH8FvtdtzcHJCkMOKnN73LIn6SLTpoojgJqDAxPm1jCR14eWSGPX8ul/gggBdPMk/d5+u9V854vTYQ8T5jF/1Q==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@typescript-eslint/eslint-plugin",
|
"@typescript-eslint/eslint-plugin",
|
||||||
"@typescript-eslint/parser",
|
"@typescript-eslint/parser",
|
||||||
|
"@typescript-eslint/typescript-estree",
|
||||||
"@typescript-eslint/utils",
|
"@typescript-eslint/utils",
|
||||||
"eslint",
|
"eslint",
|
||||||
"typescript"
|
"typescript"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"typescript@5.8.3": {
|
"typescript@5.9.2": {
|
||||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
||||||
"bin": true
|
"bin": true
|
||||||
},
|
},
|
||||||
"uri-js@4.4.1": {
|
"uri-js@4.4.1": {
|
||||||
@@ -879,27 +878,27 @@
|
|||||||
"yocto-queue@0.1.0": {
|
"yocto-queue@0.1.0": {
|
||||||
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="
|
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="
|
||||||
},
|
},
|
||||||
"zod@3.25.75": {
|
"zod@4.0.17": {
|
||||||
"integrity": "sha512-OhpzAmVzabPOL6C3A3gpAifqr9MqihV/Msx3gor2b2kviCgcb+HM9SEOpMWwwNp9MRunWnhtAKUoo0AHhjyPPg=="
|
"integrity": "sha512-1PHjlYRevNxxdy2JZ8JcNAw7rX8V9P1AKkP+x/xZfxB0K5FYfuV+Ug6P/6NVSR2jHQ+FzDDoDHS04nYUsOIyLQ=="
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"workspace": {
|
"workspace": {
|
||||||
"packageJson": {
|
"packageJson": {
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"npm:@jsr/std__assert@1.0.13",
|
"npm:@jsr/std__assert@1",
|
||||||
"npm:@jsr/std__async@1.0.13",
|
"npm:@jsr/std__async@1",
|
||||||
"npm:@jsr/std__testing@1.0.14",
|
"npm:@jsr/std__testing@1",
|
||||||
"npm:@jsr/valkyr__testcontainers@2",
|
"npm:@jsr/valkyr__testcontainers@2",
|
||||||
"npm:@valkyr/db@1.0.1",
|
"npm:@valkyr/db@1.0.1",
|
||||||
"npm:eslint-plugin-simple-import-sort@12.1.1",
|
"npm:eslint-plugin-simple-import-sort@12",
|
||||||
"npm:eslint@9.30.1",
|
"npm:eslint@9",
|
||||||
"npm:fake-indexeddb@6.0.1",
|
"npm:fake-indexeddb@6",
|
||||||
"npm:mongodb@6",
|
"npm:mongodb@6",
|
||||||
"npm:nanoid@5",
|
"npm:nanoid@5",
|
||||||
"npm:postgres@3",
|
"npm:postgres@3",
|
||||||
"npm:prettier@3.6.2",
|
"npm:prettier@3",
|
||||||
"npm:typescript-eslint@8.35.1",
|
"npm:typescript-eslint@8",
|
||||||
"npm:zod@3.25"
|
"npm:zod@4"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
import { AggregateRootClass } from "./aggregate.ts";
|
|
||||||
import { EventFactory } from "./event-factory.ts";
|
|
||||||
import { AnyEventStore } from "./event-store.ts";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indexes a list of event factories for use with aggregates and event stores
|
|
||||||
* when generating or accessing event functionality.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
*
|
|
||||||
* ```ts
|
|
||||||
* import { AggregateRoot, AggregateFactory } from "@valkyr/event-store";
|
|
||||||
* import z from "zod";
|
|
||||||
*
|
|
||||||
* class User extends AggregateRoot {}
|
|
||||||
*
|
|
||||||
* const factory = new AggregateFactory([User]);
|
|
||||||
*
|
|
||||||
* export type Aggregates = typeof factory.$aggregates;
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export class AggregateFactory<
|
|
||||||
const TEventFactory extends EventFactory = EventFactory,
|
|
||||||
const TAggregates extends AggregateRootClass<TEventFactory>[] = AggregateRootClass<TEventFactory>[],
|
|
||||||
> {
|
|
||||||
/**
|
|
||||||
* Optimized aggregate lookup index.
|
|
||||||
*/
|
|
||||||
readonly #index = new Map<TAggregates[number]["name"], TAggregates[number]>();
|
|
||||||
|
|
||||||
aggregates: TAggregates;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inferred type of the aggregates registered with the factory.
|
|
||||||
*/
|
|
||||||
declare readonly $aggregates: TAggregates;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Instantiate a new AggregateFactory with given list of supported aggregates.
|
|
||||||
*
|
|
||||||
* @param aggregates - Aggregates to register with the factory.
|
|
||||||
*/
|
|
||||||
constructor(aggregates: TAggregates) {
|
|
||||||
this.aggregates = aggregates;
|
|
||||||
for (const aggregate of aggregates) {
|
|
||||||
this.#index.set(aggregate.name, aggregate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attaches the given store to all the aggregates registered with this instance.
|
|
||||||
*
|
|
||||||
* If the factory is passed into multiple event stores, the aggregates will be
|
|
||||||
* overriden by the last execution. Its recommended to create individual instances
|
|
||||||
* for each list of aggregates.
|
|
||||||
*
|
|
||||||
* @param store - Event store to attach to the aggregates.
|
|
||||||
*/
|
|
||||||
withStore(store: AnyEventStore): this {
|
|
||||||
for (const aggregate of this.aggregates) {
|
|
||||||
aggregate.$store = store;
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a registered aggregate from the factory.
|
|
||||||
*
|
|
||||||
* @param name - Aggregate to retrieve.
|
|
||||||
*/
|
|
||||||
get<TName extends TAggregates[number]["name"]>(name: TName): Extract<TAggregates[number], { name: TName }> {
|
|
||||||
return this.#index.get(name) as Extract<TAggregates[number], { name: TName }>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import type { AnyEventStore, EventsInsertSettings } from "../libraries/event-store.ts";
|
import type { AnyEventStore, EventsInsertSettings } from "../libraries/event-store.ts";
|
||||||
import type { Unknown } from "../types/common.ts";
|
import type { Unknown } from "../types/common.ts";
|
||||||
|
import { AggregateSnapshotViolation, AggregateStreamViolation } from "./errors.ts";
|
||||||
import { EventFactory } from "./event-factory.ts";
|
import { EventFactory } from "./event-factory.ts";
|
||||||
|
import { makeAggregateReducer } from "./reducer.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents an aggregate root in an event-sourced system.
|
* Represents an aggregate root in an event-sourced system.
|
||||||
@@ -18,39 +20,43 @@ export abstract class AggregateRoot<TEventFactory extends EventFactory> {
|
|||||||
*/
|
*/
|
||||||
static readonly name: string;
|
static readonly name: string;
|
||||||
|
|
||||||
|
readonly #store: AnyEventStore;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Event store to transact against.
|
* Primary unique identifier for the stream the aggregate belongs to.
|
||||||
*/
|
*/
|
||||||
protected static _store?: AnyEventStore;
|
#stream?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of pending records to push to the parent event store.
|
* List of pending records to push to the parent event store.
|
||||||
*/
|
*/
|
||||||
#pending: TEventFactory["$events"][number]["$record"][] = [];
|
#pending: TEventFactory["$events"][number]["$record"][] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiate a new AggregateRoot with a given event store instance.
|
||||||
|
*
|
||||||
|
* @param store - Store this aggregate instance acts against.
|
||||||
|
*/
|
||||||
|
constructor(store: AnyEventStore) {
|
||||||
|
this.#store = store;
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Accessors
|
// Accessors
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
static get $store(): AnyEventStore {
|
set id(value: string) {
|
||||||
if (this._store === undefined) {
|
if (this.#stream !== undefined) {
|
||||||
throw new Error(`Aggregate Root > Failed to retrieve store for '${this.name}', no store has been attached.`);
|
throw new AggregateStreamViolation(this.constructor.name);
|
||||||
}
|
}
|
||||||
return this._store;
|
this.#stream = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
static set $store(store: AnyEventStore) {
|
get id() {
|
||||||
// if (this._store !== undefined) {
|
if (this.#stream === undefined) {
|
||||||
// throw new Error(`Aggregate '${this.constructor.name}' already has store assigned`);
|
this.#stream = crypto.randomUUID();
|
||||||
// }
|
}
|
||||||
this._store = store;
|
return this.#stream;
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get store instance attached to the static aggregate.
|
|
||||||
*/
|
|
||||||
get $store(): AnyEventStore {
|
|
||||||
return (this.constructor as any).$store;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -74,9 +80,10 @@ export abstract class AggregateRoot<TEventFactory extends EventFactory> {
|
|||||||
*/
|
*/
|
||||||
static from<TEventFactory extends EventFactory, TAggregateRoot extends typeof AggregateRoot<TEventFactory>>(
|
static from<TEventFactory extends EventFactory, TAggregateRoot extends typeof AggregateRoot<TEventFactory>>(
|
||||||
this: TAggregateRoot,
|
this: TAggregateRoot,
|
||||||
|
store: AnyEventStore,
|
||||||
snapshot?: Unknown,
|
snapshot?: Unknown,
|
||||||
): InstanceType<TAggregateRoot> {
|
): InstanceType<TAggregateRoot> {
|
||||||
const instance = new (this as any)();
|
const instance = new (this as any)(store);
|
||||||
if (snapshot !== undefined) {
|
if (snapshot !== undefined) {
|
||||||
Object.assign(instance, snapshot);
|
Object.assign(instance, snapshot);
|
||||||
}
|
}
|
||||||
@@ -109,7 +116,7 @@ export abstract class AggregateRoot<TEventFactory extends EventFactory> {
|
|||||||
push<TType extends TEventFactory["$events"][number]["state"]["type"]>(
|
push<TType extends TEventFactory["$events"][number]["state"]["type"]>(
|
||||||
record: { type: TType } & Extract<TEventFactory["$events"][number], { state: { type: TType } }>["$payload"],
|
record: { type: TType } & Extract<TEventFactory["$events"][number], { state: { type: TType } }>["$payload"],
|
||||||
): this {
|
): this {
|
||||||
const pending = this.$store.event(record);
|
const pending = this.#store.event(record);
|
||||||
this.#pending.push(pending);
|
this.#pending.push(pending);
|
||||||
this.with(pending);
|
this.with(pending);
|
||||||
return this;
|
return this;
|
||||||
@@ -136,13 +143,25 @@ export abstract class AggregateRoot<TEventFactory extends EventFactory> {
|
|||||||
if (this.isDirty === false) {
|
if (this.isDirty === false) {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
await this.$store.pushManyEvents(this.#pending, settings);
|
await this.#store.pushManyEvents(this.#pending, settings);
|
||||||
if (flush === true) {
|
if (flush === true) {
|
||||||
this.flush();
|
this.flush();
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async snapshot() {
|
||||||
|
const stream = this.#stream;
|
||||||
|
if (stream === undefined) {
|
||||||
|
throw new AggregateSnapshotViolation((this.constructor as typeof AggregateRoot<TEventFactory>).name);
|
||||||
|
}
|
||||||
|
await this.#store.createSnapshot({
|
||||||
|
name: this.constructor.name,
|
||||||
|
stream,
|
||||||
|
reducer: makeAggregateReducer(this.#store, this.constructor as typeof AggregateRoot<TEventFactory>),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes all events from the aggregate #pending list.
|
* Removes all events from the aggregate #pending list.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,3 +1,42 @@
|
|||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------------
|
||||||
|
| Aggregate Errors
|
||||||
|
|--------------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error thrown when stream assignment on the aggregate has already been set.
|
||||||
|
*
|
||||||
|
* @property name - Name of the aggregate throwing the error.
|
||||||
|
*/
|
||||||
|
export class AggregateStreamViolation extends Error {
|
||||||
|
readonly type = "AggregateStreamAlreadySet";
|
||||||
|
|
||||||
|
constructor(name: string) {
|
||||||
|
super(`EventStore Error: Aggregate '${name}' already has a stream assigned, overriding not supported.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error thrown when attempting to snapshot an aggregate without a resolved
|
||||||
|
* stream.
|
||||||
|
*
|
||||||
|
* @property name - Name of the aggregate throwing the error.
|
||||||
|
*/
|
||||||
|
export class AggregateSnapshotViolation extends Error {
|
||||||
|
readonly type = "AggregateSnapshotViolation";
|
||||||
|
|
||||||
|
constructor(name: string) {
|
||||||
|
super(`EventStore Error: Aggregate '${name}' has no stream assigned, snapshot generation cannot be executed.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------------
|
||||||
|
| Event Errors
|
||||||
|
|--------------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Error thrown when an expected event is missing from the event store.
|
* Error thrown when an expected event is missing from the event store.
|
||||||
*
|
*
|
||||||
@@ -14,12 +53,6 @@ export class EventMissingError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------------
|
|
||||||
| Event Errors
|
|
||||||
|--------------------------------------------------------------------------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Error thrown when an event fails validation checks.
|
* Error thrown when an event fails validation checks.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -35,8 +35,7 @@
|
|||||||
import { EventStoreAdapter } from "../types/adapter.ts";
|
import { EventStoreAdapter } from "../types/adapter.ts";
|
||||||
import type { Unknown } from "../types/common.ts";
|
import type { Unknown } from "../types/common.ts";
|
||||||
import type { EventReadOptions, ReduceQuery } from "../types/query.ts";
|
import type { EventReadOptions, ReduceQuery } from "../types/query.ts";
|
||||||
import type { AggregateRoot } from "./aggregate.ts";
|
import { AggregateRootClass } from "./aggregate.ts";
|
||||||
import { AggregateFactory } from "./aggregate-factory.ts";
|
|
||||||
import { EventInsertionError, EventMissingError, EventValidationError } from "./errors.ts";
|
import { EventInsertionError, EventMissingError, EventValidationError } from "./errors.ts";
|
||||||
import { EventStatus } from "./event.ts";
|
import { EventStatus } from "./event.ts";
|
||||||
import { EventFactory } from "./event-factory.ts";
|
import { EventFactory } from "./event-factory.ts";
|
||||||
@@ -53,24 +52,21 @@ import { makeAggregateReducer, makeReducer } from "./reducer.ts";
|
|||||||
* Provides a common interface to interact with a event storage solution. Its built
|
* Provides a common interface to interact with a event storage solution. Its built
|
||||||
* on an adapter pattern to allow for multiple different storage drivers.
|
* on an adapter pattern to allow for multiple different storage drivers.
|
||||||
*/
|
*/
|
||||||
export class EventStore<
|
export class EventStore<TEventFactory extends EventFactory, TEventStoreAdapter extends EventStoreAdapter<any>> {
|
||||||
TEventFactory extends EventFactory,
|
readonly uuid: string;
|
||||||
TAggregateFactory extends AggregateFactory<TEventFactory>,
|
|
||||||
TEventStoreAdapter extends EventStoreAdapter<any>,
|
|
||||||
> {
|
|
||||||
readonly #adapter: TEventStoreAdapter;
|
readonly #adapter: TEventStoreAdapter;
|
||||||
readonly #events: TEventFactory;
|
readonly #events: TEventFactory;
|
||||||
readonly #aggregates: TAggregateFactory;
|
|
||||||
readonly #snapshot: "manual" | "auto";
|
readonly #snapshot: "manual" | "auto";
|
||||||
readonly #hooks: EventStoreHooks<TEventFactory>;
|
readonly #hooks: EventStoreHooks<TEventFactory>;
|
||||||
|
|
||||||
declare readonly $events: TEventFactory["$events"];
|
declare readonly $events: TEventFactory["$events"];
|
||||||
declare readonly $records: TEventFactory["$events"][number]["$record"][];
|
declare readonly $records: TEventFactory["$events"][number]["$record"][];
|
||||||
|
|
||||||
constructor(config: EventStoreConfig<TEventFactory, TAggregateFactory, TEventStoreAdapter>) {
|
constructor(config: EventStoreConfig<TEventFactory, TEventStoreAdapter>) {
|
||||||
|
this.uuid = crypto.randomUUID();
|
||||||
this.#adapter = config.adapter;
|
this.#adapter = config.adapter;
|
||||||
this.#events = config.events;
|
this.#events = config.events;
|
||||||
this.#aggregates = config.aggregates.withStore(this);
|
|
||||||
this.#snapshot = config.snapshot ?? "manual";
|
this.#snapshot = config.snapshot ?? "manual";
|
||||||
this.#hooks = config.hooks ?? {};
|
this.#hooks = config.hooks ?? {};
|
||||||
}
|
}
|
||||||
@@ -113,55 +109,82 @@ export class EventStore<
|
|||||||
|--------------------------------------------------------------------------------
|
|--------------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
readonly aggregate = {
|
||||||
* Get aggregate uninstantiated class.
|
/**
|
||||||
*
|
* Takes a list of aggregates and commits any pending events to the event store.
|
||||||
* @param name - Aggregate name to retrieve.
|
* Events are committed in order so its important to ensure that the aggregates
|
||||||
*/
|
* are placed in the correct index position of the array.
|
||||||
aggregate<TName extends TAggregateFactory["$aggregates"][number]["name"]>(
|
*
|
||||||
name: TName,
|
* This method allows for a simpler way to commit many events over many
|
||||||
): Extract<TAggregateFactory["$aggregates"][number], { name: TName }> {
|
* aggregates in a single transaction. Ensuring atomicity of a larger group
|
||||||
return this.#aggregates.get(name) as Extract<TAggregateFactory["$aggregates"][number], { name: TName }>;
|
* of events.
|
||||||
}
|
*
|
||||||
|
* @param aggregates - Aggregates to push events from.
|
||||||
|
* @param settings - Event settings which can modify insertion behavior.
|
||||||
|
*/
|
||||||
|
push: async (
|
||||||
|
aggregates: InstanceType<AggregateRootClass<TEventFactory>>[],
|
||||||
|
settings?: EventsInsertSettings,
|
||||||
|
): Promise<void> => {
|
||||||
|
const events: this["$events"][number]["$record"][] = [];
|
||||||
|
for (const aggregate of aggregates) {
|
||||||
|
events.push(...aggregate.toPending());
|
||||||
|
}
|
||||||
|
await this.pushManyEvents(events, settings);
|
||||||
|
for (const aggregate of aggregates) {
|
||||||
|
aggregate.flush();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes in an aggregate and commits any pending events to the event store.
|
* Get a new aggregate instance by a given stream.
|
||||||
*
|
*
|
||||||
* @param aggregate - Aggregate to push events from.
|
* @param name - Aggregate to instantiate.
|
||||||
* @param settings - Event settings which can modify insertion behavior.
|
* @param stream - Stream to retrieve snapshot from.
|
||||||
*/
|
*/
|
||||||
async pushAggregate(
|
getByStream: async <TAggregate extends AggregateRootClass<TEventFactory>>(
|
||||||
aggregate: InstanceType<TAggregateFactory["$aggregates"][number]>,
|
aggregate: TAggregate,
|
||||||
settings?: EventsInsertSettings,
|
stream: string,
|
||||||
): Promise<void> {
|
): Promise<InstanceType<TAggregate> | undefined> => {
|
||||||
await aggregate.save(settings);
|
const reducer = makeAggregateReducer(this, aggregate);
|
||||||
}
|
const snapshot = await this.reduce({ name: aggregate.name, stream, reducer });
|
||||||
|
if (snapshot === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return aggregate.from(this, snapshot as Unknown);
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes a list of aggregates and commits any pending events to the event store.
|
* Get a new aggregate instance by a given relation.
|
||||||
* Events are committed in order so its important to ensure that the aggregates
|
*
|
||||||
* are placed in the correct index position of the array.
|
* @param name - Aggregate to instantiate.
|
||||||
*
|
* @param relation - Relation to retrieve snapshot from.
|
||||||
* This method allows for a simpler way to commit many events over many
|
*/
|
||||||
* aggregates in a single transaction. Ensuring atomicity of a larger group
|
getByRelation: async <TAggregate extends AggregateRootClass<TEventFactory>>(
|
||||||
* of events.
|
aggregate: TAggregate,
|
||||||
*
|
relation: string,
|
||||||
* @param aggregates - Aggregates to push events from.
|
): Promise<InstanceType<TAggregate> | undefined> => {
|
||||||
* @param settings - Event settings which can modify insertion behavior.
|
const reducer = makeAggregateReducer(this, aggregate);
|
||||||
*/
|
const snapshot = await this.reduce({ name: aggregate.name, relation, reducer });
|
||||||
async pushManyAggregates(
|
if (snapshot === undefined) {
|
||||||
aggregates: InstanceType<TAggregateFactory["$aggregates"][number]>[],
|
return undefined;
|
||||||
settings?: EventsInsertSettings,
|
}
|
||||||
): Promise<void> {
|
return aggregate.from(this, snapshot as Unknown);
|
||||||
const events: this["$events"][number]["$record"][] = [];
|
},
|
||||||
for (const aggregate of aggregates) {
|
|
||||||
events.push(...aggregate.toPending());
|
/**
|
||||||
}
|
* Instantiate a new aggreate.
|
||||||
await this.pushManyEvents(events, settings);
|
*
|
||||||
for (const aggregate of aggregates) {
|
* @param aggregate - Aggregate to instantiate.
|
||||||
aggregate.flush();
|
* @param snapshot - Optional snapshot to instantiate aggregate with.
|
||||||
}
|
*/
|
||||||
}
|
from: <TAggregate extends AggregateRootClass<TEventFactory>>(
|
||||||
|
aggregate: TAggregate,
|
||||||
|
snapshot?: Unknown,
|
||||||
|
): InstanceType<TAggregate> => {
|
||||||
|
return aggregate.from(this, snapshot);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------------
|
|--------------------------------------------------------------------------------
|
||||||
@@ -341,43 +364,6 @@ export class EventStore<
|
|||||||
return makeReducer<TEventFactory, TState>(foldFn, stateFn);
|
return makeReducer<TEventFactory, TState>(foldFn, stateFn);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Make a new event reducer based on the events registered with the event store.
|
|
||||||
*
|
|
||||||
* @param aggregate - Aggregate class to create instance from.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* class Foo extends AggregateRoot<Event> {
|
|
||||||
* name: string = "";
|
|
||||||
*
|
|
||||||
* static #reducer = makeAggregateReducer(Foo);
|
|
||||||
*
|
|
||||||
* static async getById(fooId: string): Promise<Foo | undefined> {
|
|
||||||
* return eventStore.reduce({
|
|
||||||
* name: "foo",
|
|
||||||
* stream: "stream-id",
|
|
||||||
* reducer: this.#reducer,
|
|
||||||
* });
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* with(event) {
|
|
||||||
* switch (event.type) {
|
|
||||||
* case "FooCreated": {
|
|
||||||
* this.name = event.data.name;
|
|
||||||
* break;
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* });
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
makeAggregateReducer<TAggregateRoot extends typeof AggregateRoot<TEventFactory>>(
|
|
||||||
aggregate: TAggregateRoot,
|
|
||||||
): Reducer<TEventFactory, InstanceType<TAggregateRoot>> {
|
|
||||||
return makeAggregateReducer<TEventFactory, TAggregateRoot>(aggregate);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reduce events in the given stream to a entity state.
|
* Reduce events in the given stream to a entity state.
|
||||||
*
|
*
|
||||||
@@ -540,14 +526,9 @@ export class EventStore<
|
|||||||
|--------------------------------------------------------------------------------
|
|--------------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
type EventStoreConfig<
|
type EventStoreConfig<TEventFactory extends EventFactory, TEventStoreAdapter extends EventStoreAdapter<any>> = {
|
||||||
TEventFactory extends EventFactory,
|
|
||||||
TAggregateFactory extends AggregateFactory<TEventFactory>,
|
|
||||||
TEventStoreAdapter extends EventStoreAdapter<any>,
|
|
||||||
> = {
|
|
||||||
adapter: TEventStoreAdapter;
|
adapter: TEventStoreAdapter;
|
||||||
events: TEventFactory;
|
events: TEventFactory;
|
||||||
aggregates: TAggregateFactory;
|
|
||||||
snapshot?: "manual" | "auto";
|
snapshot?: "manual" | "auto";
|
||||||
hooks?: EventStoreHooks<TEventFactory>;
|
hooks?: EventStoreHooks<TEventFactory>;
|
||||||
};
|
};
|
||||||
@@ -588,4 +569,4 @@ export type EventStoreHooks<TEventFactory extends EventFactory> = Partial<{
|
|||||||
onError(error: unknown): Promise<void>;
|
onError(error: unknown): Promise<void>;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type AnyEventStore = EventStore<any, any, any>;
|
export type AnyEventStore = EventStore<any, any>;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import z, { ZodType } from "zod/v4";
|
import z, { ZodType } from "zod";
|
||||||
|
|
||||||
import { EventValidationError } from "./errors.ts";
|
import { EventValidationError } from "./errors.ts";
|
||||||
import { makeId } from "./nanoid.ts";
|
import { makeId } from "./nanoid.ts";
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { AggregateRoot } from "../libraries/aggregate.ts";
|
import type { AggregateRoot } from "../libraries/aggregate.ts";
|
||||||
import type { Unknown } from "../types/common.ts";
|
import type { Unknown } from "../types/common.ts";
|
||||||
import { EventFactory } from "./event-factory.ts";
|
import { EventFactory } from "./event-factory.ts";
|
||||||
|
import type { AnyEventStore } from "./event-store.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make an event reducer that produces a aggregate instance from resolved
|
* Make an event reducer that produces a aggregate instance from resolved
|
||||||
@@ -11,13 +12,13 @@ import { EventFactory } from "./event-factory.ts";
|
|||||||
export function makeAggregateReducer<
|
export function makeAggregateReducer<
|
||||||
TEventFactory extends EventFactory,
|
TEventFactory extends EventFactory,
|
||||||
TAggregateRoot extends typeof AggregateRoot<TEventFactory>,
|
TAggregateRoot extends typeof AggregateRoot<TEventFactory>,
|
||||||
>(aggregate: TAggregateRoot): Reducer<TEventFactory, InstanceType<TAggregateRoot>> {
|
>(store: AnyEventStore, aggregate: TAggregateRoot): Reducer<TEventFactory, InstanceType<TAggregateRoot>> {
|
||||||
return {
|
return {
|
||||||
from(snapshot: Unknown) {
|
from(snapshot: Unknown) {
|
||||||
return aggregate.from(snapshot);
|
return aggregate.from(store, snapshot);
|
||||||
},
|
},
|
||||||
reduce(events: TEventFactory["$events"][number]["$record"][], snapshot?: Unknown) {
|
reduce(events: TEventFactory["$events"][number]["$record"][], snapshot?: Unknown) {
|
||||||
const instance = aggregate.from(snapshot);
|
const instance = aggregate.from(store, snapshot);
|
||||||
for (const event of events) {
|
for (const event of events) {
|
||||||
instance.with(event);
|
instance.with(event);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ZodError } from "zod/v4";
|
import { ZodError } from "zod";
|
||||||
|
|
||||||
export function toPrettyErrorLines(error: ZodError, padding: number = 0): string[] {
|
export function toPrettyErrorLines(error: ZodError, padding: number = 0): string[] {
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
|
|||||||
1
mod.ts
1
mod.ts
@@ -1,5 +1,4 @@
|
|||||||
export * from "./libraries/aggregate.ts";
|
export * from "./libraries/aggregate.ts";
|
||||||
export * from "./libraries/aggregate-factory.ts";
|
|
||||||
export * from "./libraries/errors.ts";
|
export * from "./libraries/errors.ts";
|
||||||
export * from "./libraries/event.ts";
|
export * from "./libraries/event.ts";
|
||||||
export * from "./libraries/event-factory.ts";
|
export * from "./libraries/event-factory.ts";
|
||||||
|
|||||||
18
package.json
18
package.json
@@ -4,17 +4,17 @@
|
|||||||
"mongodb": "6",
|
"mongodb": "6",
|
||||||
"nanoid": "5",
|
"nanoid": "5",
|
||||||
"postgres": "3",
|
"postgres": "3",
|
||||||
"zod": "3.25"
|
"zod": "4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@std/async": "npm:@jsr/std__async@1.0.13",
|
"@std/async": "npm:@jsr/std__async@1",
|
||||||
"@std/assert": "npm:@jsr/std__assert@1.0.13",
|
"@std/assert": "npm:@jsr/std__assert@1",
|
||||||
"@std/testing": "npm:@jsr/std__testing@1.0.14",
|
"@std/testing": "npm:@jsr/std__testing@1",
|
||||||
"@valkyr/testcontainers": "npm:@jsr/valkyr__testcontainers@2",
|
"@valkyr/testcontainers": "npm:@jsr/valkyr__testcontainers@2",
|
||||||
"eslint": "9.30.1",
|
"eslint": "9",
|
||||||
"eslint-plugin-simple-import-sort": "12.1.1",
|
"eslint-plugin-simple-import-sort": "12",
|
||||||
"fake-indexeddb": "6.0.1",
|
"fake-indexeddb": "6",
|
||||||
"prettier": "3.6.2",
|
"prettier": "3",
|
||||||
"typescript-eslint": "8.35.1"
|
"typescript-eslint": "8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ import { afterAll, describe } from "@std/testing/bdd";
|
|||||||
import { BrowserAdapter } from "../adapters/browser/adapter.ts";
|
import { BrowserAdapter } from "../adapters/browser/adapter.ts";
|
||||||
import { EventStore, EventStoreHooks } from "../libraries/event-store.ts";
|
import { EventStore, EventStoreHooks } from "../libraries/event-store.ts";
|
||||||
import { Projector } from "../libraries/projector.ts";
|
import { Projector } from "../libraries/projector.ts";
|
||||||
import { aggregates } from "./mocks/aggregates.ts";
|
import { Events, events } from "./mocks/events.ts";
|
||||||
import { events, EventStoreFactory } from "./mocks/events.ts";
|
|
||||||
import testAddEvent from "./store/add-event.ts";
|
import testAddEvent from "./store/add-event.ts";
|
||||||
import testCreateSnapshot from "./store/create-snapshot.ts";
|
import testCreateSnapshot from "./store/create-snapshot.ts";
|
||||||
import testMakeAggregateReducer from "./store/make-aggregate-reducer.ts";
|
import testMakeAggregateReducer from "./store/make-aggregate-reducer.ts";
|
||||||
@@ -18,7 +17,7 @@ import testPushManyAggregates from "./store/push-many-aggregates.ts";
|
|||||||
import testReduce from "./store/reduce.ts";
|
import testReduce from "./store/reduce.ts";
|
||||||
import testReplayEvents from "./store/replay-events.ts";
|
import testReplayEvents from "./store/replay-events.ts";
|
||||||
|
|
||||||
const eventStoreFn = async (options: { hooks?: EventStoreHooks<EventStoreFactory> } = {}) => getEventStore(options);
|
const eventStoreFn = async (options: { hooks?: EventStoreHooks<Events> } = {}) => getEventStore(options);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------------
|
|--------------------------------------------------------------------------------
|
||||||
@@ -44,7 +43,6 @@ describe("Adapter > Browser (IndexedDb)", () => {
|
|||||||
testReplayEvents(eventStoreFn);
|
testReplayEvents(eventStoreFn);
|
||||||
testReduce(eventStoreFn);
|
testReduce(eventStoreFn);
|
||||||
testOnceProjection(eventStoreFn);
|
testOnceProjection(eventStoreFn);
|
||||||
|
|
||||||
testPushAggregate(eventStoreFn);
|
testPushAggregate(eventStoreFn);
|
||||||
testPushManyAggregates(eventStoreFn);
|
testPushManyAggregates(eventStoreFn);
|
||||||
});
|
});
|
||||||
@@ -55,15 +53,14 @@ describe("Adapter > Browser (IndexedDb)", () => {
|
|||||||
|--------------------------------------------------------------------------------
|
|--------------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function getEventStore({ hooks = {} }: { hooks?: EventStoreHooks<EventStoreFactory> }) {
|
function getEventStore({ hooks = {} }: { hooks?: EventStoreHooks<Events> }) {
|
||||||
const store = new EventStore({
|
const store = new EventStore({
|
||||||
adapter: new BrowserAdapter("indexeddb"),
|
adapter: new BrowserAdapter("indexeddb"),
|
||||||
events,
|
events,
|
||||||
aggregates,
|
|
||||||
hooks,
|
hooks,
|
||||||
});
|
});
|
||||||
|
|
||||||
const projector = new Projector<EventStoreFactory>();
|
const projector = new Projector<Events>();
|
||||||
|
|
||||||
if (hooks.onEventsInserted === undefined) {
|
if (hooks.onEventsInserted === undefined) {
|
||||||
store.onEventsInserted(async (records, { batch }) => {
|
store.onEventsInserted(async (records, { batch }) => {
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ import { describe } from "@std/testing/bdd";
|
|||||||
import { BrowserAdapter } from "../adapters/browser/adapter.ts";
|
import { BrowserAdapter } from "../adapters/browser/adapter.ts";
|
||||||
import { EventStore, EventStoreHooks } from "../libraries/event-store.ts";
|
import { EventStore, EventStoreHooks } from "../libraries/event-store.ts";
|
||||||
import { Projector } from "../libraries/projector.ts";
|
import { Projector } from "../libraries/projector.ts";
|
||||||
import { aggregates } from "./mocks/aggregates.ts";
|
import { Events, events } from "./mocks/events.ts";
|
||||||
import { events, EventStoreFactory } from "./mocks/events.ts";
|
|
||||||
import testAddEvent from "./store/add-event.ts";
|
import testAddEvent from "./store/add-event.ts";
|
||||||
import testCreateSnapshot from "./store/create-snapshot.ts";
|
import testCreateSnapshot from "./store/create-snapshot.ts";
|
||||||
import testMakeAggregateReducer from "./store/make-aggregate-reducer.ts";
|
import testMakeAggregateReducer from "./store/make-aggregate-reducer.ts";
|
||||||
@@ -17,7 +16,7 @@ import testPushManyAggregates from "./store/push-many-aggregates.ts";
|
|||||||
import testReduce from "./store/reduce.ts";
|
import testReduce from "./store/reduce.ts";
|
||||||
import testReplayEvents from "./store/replay-events.ts";
|
import testReplayEvents from "./store/replay-events.ts";
|
||||||
|
|
||||||
const eventStoreFn = async (options: { hooks?: EventStoreHooks<EventStoreFactory> } = {}) => getEventStore(options);
|
const eventStoreFn = async (options: { hooks?: EventStoreHooks<Events> } = {}) => getEventStore(options);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------------
|
|--------------------------------------------------------------------------------
|
||||||
@@ -33,7 +32,6 @@ describe("Adapter > Browser (memory)", () => {
|
|||||||
testReplayEvents(eventStoreFn);
|
testReplayEvents(eventStoreFn);
|
||||||
testReduce(eventStoreFn);
|
testReduce(eventStoreFn);
|
||||||
testOnceProjection(eventStoreFn);
|
testOnceProjection(eventStoreFn);
|
||||||
|
|
||||||
testPushAggregate(eventStoreFn);
|
testPushAggregate(eventStoreFn);
|
||||||
testPushManyAggregates(eventStoreFn);
|
testPushManyAggregates(eventStoreFn);
|
||||||
});
|
});
|
||||||
@@ -44,15 +42,14 @@ describe("Adapter > Browser (memory)", () => {
|
|||||||
|--------------------------------------------------------------------------------
|
|--------------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function getEventStore({ hooks = {} }: { hooks?: EventStoreHooks<EventStoreFactory> }) {
|
function getEventStore({ hooks = {} }: { hooks?: EventStoreHooks<Events> }) {
|
||||||
const store = new EventStore({
|
const store = new EventStore({
|
||||||
adapter: new BrowserAdapter("memorydb"),
|
adapter: new BrowserAdapter("memorydb"),
|
||||||
events,
|
events,
|
||||||
aggregates,
|
|
||||||
hooks,
|
hooks,
|
||||||
});
|
});
|
||||||
|
|
||||||
const projector = new Projector<EventStoreFactory>();
|
const projector = new Projector<Events>();
|
||||||
|
|
||||||
if (hooks.onEventsInserted === undefined) {
|
if (hooks.onEventsInserted === undefined) {
|
||||||
store.onEventsInserted(async (records, { batch }) => {
|
store.onEventsInserted(async (records, { batch }) => {
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import { AggregateRoot } from "../../libraries/aggregate.ts";
|
import { AggregateRoot } from "../../libraries/aggregate.ts";
|
||||||
import { AggregateFactory } from "../../libraries/aggregate-factory.ts";
|
import { Events } from "./events.ts";
|
||||||
import { makeId } from "../../libraries/nanoid.ts";
|
|
||||||
import { makeAggregateReducer } from "../../libraries/reducer.ts";
|
|
||||||
import { EventStoreFactory } from "./events.ts";
|
|
||||||
|
|
||||||
export class User extends AggregateRoot<EventStoreFactory> {
|
export class User extends AggregateRoot<Events> {
|
||||||
static override readonly name = "user";
|
static override readonly name = "user";
|
||||||
|
|
||||||
id: string = "";
|
|
||||||
name: Name = {
|
name: Name = {
|
||||||
given: "",
|
given: "",
|
||||||
family: "",
|
family: "",
|
||||||
@@ -19,40 +15,12 @@ export class User extends AggregateRoot<EventStoreFactory> {
|
|||||||
count: 0,
|
count: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
// Factories
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
static reducer = makeAggregateReducer(User);
|
|
||||||
|
|
||||||
static create(name: Name, email: string): User {
|
|
||||||
const user = new User();
|
|
||||||
user.push({
|
|
||||||
type: "user:created",
|
|
||||||
stream: makeId(),
|
|
||||||
data: { name, email },
|
|
||||||
meta: { auditor: "foo" },
|
|
||||||
});
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async getById(userId: string): Promise<User | undefined> {
|
|
||||||
return this.$store.reduce({ name: "user", stream: userId, reducer: this.reducer });
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Reducer
|
// Reducer
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
with(event: EventStoreFactory["$events"][number]["$record"]) {
|
with(event: Events["$events"][number]["$record"]) {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case "user:created": {
|
|
||||||
this.id = event.stream;
|
|
||||||
this.name.given = event.data.name?.given ?? "";
|
|
||||||
this.name.family = event.data.name?.family ?? "";
|
|
||||||
this.email = event.data.email;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "user:name:given-set": {
|
case "user:name:given-set": {
|
||||||
this.name.given = event.data;
|
this.name.given = event.data;
|
||||||
break;
|
break;
|
||||||
@@ -107,11 +75,6 @@ export class User extends AggregateRoot<EventStoreFactory> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async snapshot(): Promise<this> {
|
|
||||||
await this.$store.createSnapshot({ name: "user", stream: this.id, reducer: User.reducer });
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Helpers
|
// Helpers
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
@@ -121,8 +84,6 @@ export class User extends AggregateRoot<EventStoreFactory> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const aggregates = new AggregateFactory([User]);
|
|
||||||
|
|
||||||
type Name = {
|
type Name = {
|
||||||
given: string;
|
given: string;
|
||||||
family: string;
|
family: string;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import z from "zod/v4";
|
import z from "zod";
|
||||||
|
|
||||||
import { event } from "../../libraries/event.ts";
|
import { event } from "../../libraries/event.ts";
|
||||||
import { EventFactory } from "../../libraries/event-factory.ts";
|
import { EventFactory } from "../../libraries/event-factory.ts";
|
||||||
@@ -32,4 +32,4 @@ export const events = new EventFactory([
|
|||||||
event.type("post:removed").meta(auditor),
|
event.type("post:removed").meta(auditor),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export type EventStoreFactory = typeof events;
|
export type Events = typeof events;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { makeReducer } from "../../libraries/reducer.ts";
|
import { makeReducer } from "../../libraries/reducer.ts";
|
||||||
import { EventStoreFactory } from "./events.ts";
|
import { Events } from "./events.ts";
|
||||||
|
|
||||||
export const userPostReducer = makeReducer<EventStoreFactory, UserPostState>(
|
export const userPostReducer = makeReducer<Events, UserPostState>(
|
||||||
(state, event) => {
|
(state, event) => {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case "post:created": {
|
case "post:created": {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { makeReducer } from "../../libraries/reducer.ts";
|
import { makeReducer } from "../../libraries/reducer.ts";
|
||||||
import { EventStoreFactory } from "./events.ts";
|
import { Events } from "./events.ts";
|
||||||
|
|
||||||
export const userReducer = makeReducer<EventStoreFactory, UserState>(
|
export const userReducer = makeReducer<Events, UserState>(
|
||||||
(state, event) => {
|
(state, event) => {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case "user:created": {
|
case "user:created": {
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ import { MongoTestContainer } from "@valkyr/testcontainers/mongodb";
|
|||||||
import { MongoAdapter, register } from "../adapters/mongo/adapter.ts";
|
import { MongoAdapter, register } from "../adapters/mongo/adapter.ts";
|
||||||
import { EventStore, type EventStoreHooks } from "../libraries/event-store.ts";
|
import { EventStore, type EventStoreHooks } from "../libraries/event-store.ts";
|
||||||
import { Projector } from "../libraries/projector.ts";
|
import { Projector } from "../libraries/projector.ts";
|
||||||
import { aggregates } from "./mocks/aggregates.ts";
|
import { type Events, events } from "./mocks/events.ts";
|
||||||
import { events, type EventStoreFactory } from "./mocks/events.ts";
|
|
||||||
import testAddEvent from "./store/add-event.ts";
|
import testAddEvent from "./store/add-event.ts";
|
||||||
import testAddManyEvents from "./store/add-many-events.ts";
|
import testAddManyEvents from "./store/add-many-events.ts";
|
||||||
import testCreateSnapshot from "./store/create-snapshot.ts";
|
import testCreateSnapshot from "./store/create-snapshot.ts";
|
||||||
@@ -23,7 +22,7 @@ const DB_NAME = "sandbox";
|
|||||||
|
|
||||||
const container = await MongoTestContainer.start();
|
const container = await MongoTestContainer.start();
|
||||||
|
|
||||||
const eventStoreFn = async (options: { hooks?: EventStoreHooks<EventStoreFactory> } = {}) => getEventStore(options);
|
const eventStoreFn = async (options: { hooks?: EventStoreHooks<Events> } = {}) => getEventStore(options);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------------
|
|--------------------------------------------------------------------------------
|
||||||
@@ -66,7 +65,6 @@ describe("Adapter > MongoDb", () => {
|
|||||||
testReplayEvents(eventStoreFn);
|
testReplayEvents(eventStoreFn);
|
||||||
testReduce(eventStoreFn);
|
testReduce(eventStoreFn);
|
||||||
testOnceProjection(eventStoreFn);
|
testOnceProjection(eventStoreFn);
|
||||||
|
|
||||||
testPushAggregate(eventStoreFn);
|
testPushAggregate(eventStoreFn);
|
||||||
testPushManyAggregates(eventStoreFn);
|
testPushManyAggregates(eventStoreFn);
|
||||||
});
|
});
|
||||||
@@ -77,15 +75,14 @@ describe("Adapter > MongoDb", () => {
|
|||||||
|--------------------------------------------------------------------------------
|
|--------------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
async function getEventStore({ hooks = {} }: { hooks?: EventStoreHooks<EventStoreFactory> }) {
|
async function getEventStore({ hooks = {} }: { hooks?: EventStoreHooks<Events> }) {
|
||||||
const store = new EventStore({
|
const store = new EventStore({
|
||||||
adapter: new MongoAdapter(() => container.client, DB_NAME),
|
adapter: new MongoAdapter(() => container.client, DB_NAME),
|
||||||
events,
|
events,
|
||||||
aggregates,
|
|
||||||
hooks,
|
hooks,
|
||||||
});
|
});
|
||||||
|
|
||||||
const projector = new Projector<EventStoreFactory>();
|
const projector = new Projector<Events>();
|
||||||
|
|
||||||
if (hooks.onEventsInserted === undefined) {
|
if (hooks.onEventsInserted === undefined) {
|
||||||
store.onEventsInserted(async (records, { batch }) => {
|
store.onEventsInserted(async (records, { batch }) => {
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ import { PostgresAdapter } from "../adapters/postgres/adapter.ts";
|
|||||||
import type { PostgresConnection } from "../adapters/postgres/connection.ts";
|
import type { PostgresConnection } from "../adapters/postgres/connection.ts";
|
||||||
import { EventStore, type EventStoreHooks } from "../libraries/event-store.ts";
|
import { EventStore, type EventStoreHooks } from "../libraries/event-store.ts";
|
||||||
import { Projector } from "../libraries/projector.ts";
|
import { Projector } from "../libraries/projector.ts";
|
||||||
import { aggregates } from "./mocks/aggregates.ts";
|
import { Events, events } from "./mocks/events.ts";
|
||||||
import { events, EventStoreFactory } from "./mocks/events.ts";
|
|
||||||
import testAddEvent from "./store/add-event.ts";
|
import testAddEvent from "./store/add-event.ts";
|
||||||
import testAddManyEvents from "./store/add-many-events.ts";
|
import testAddManyEvents from "./store/add-many-events.ts";
|
||||||
import testCreateSnapshot from "./store/create-snapshot.ts";
|
import testCreateSnapshot from "./store/create-snapshot.ts";
|
||||||
@@ -26,8 +25,7 @@ const DB_NAME = "sandbox";
|
|||||||
const container = await PostgresTestContainer.start("postgres:17");
|
const container = await PostgresTestContainer.start("postgres:17");
|
||||||
const sql = postgres(container.url(DB_NAME));
|
const sql = postgres(container.url(DB_NAME));
|
||||||
|
|
||||||
const eventStoreFn = async (options: { hooks?: EventStoreHooks<EventStoreFactory> } = {}) =>
|
const eventStoreFn = async (options: { hooks?: EventStoreHooks<Events> } = {}) => getEventStore(sql, options);
|
||||||
getEventStore(sql, options);
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------------
|
|--------------------------------------------------------------------------------
|
||||||
@@ -103,7 +101,6 @@ describe("Adapter > Postgres", () => {
|
|||||||
testReplayEvents(eventStoreFn);
|
testReplayEvents(eventStoreFn);
|
||||||
testReduce(eventStoreFn);
|
testReduce(eventStoreFn);
|
||||||
testOnceProjection(eventStoreFn);
|
testOnceProjection(eventStoreFn);
|
||||||
|
|
||||||
testPushAggregate(eventStoreFn);
|
testPushAggregate(eventStoreFn);
|
||||||
testPushManyAggregates(eventStoreFn);
|
testPushManyAggregates(eventStoreFn);
|
||||||
});
|
});
|
||||||
@@ -114,18 +111,14 @@ describe("Adapter > Postgres", () => {
|
|||||||
|--------------------------------------------------------------------------------
|
|--------------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
async function getEventStore(
|
async function getEventStore(connection: PostgresConnection, { hooks = {} }: { hooks?: EventStoreHooks<Events> }) {
|
||||||
connection: PostgresConnection,
|
|
||||||
{ hooks = {} }: { hooks?: EventStoreHooks<EventStoreFactory> },
|
|
||||||
) {
|
|
||||||
const store = new EventStore({
|
const store = new EventStore({
|
||||||
adapter: new PostgresAdapter(connection, { schema: "event_store" }),
|
adapter: new PostgresAdapter(connection, { schema: "event_store" }),
|
||||||
events,
|
events,
|
||||||
aggregates,
|
|
||||||
hooks,
|
hooks,
|
||||||
});
|
});
|
||||||
|
|
||||||
const projector = new Projector<EventStoreFactory>();
|
const projector = new Projector<Events>();
|
||||||
|
|
||||||
if (hooks.onEventsInserted === undefined) {
|
if (hooks.onEventsInserted === undefined) {
|
||||||
store.onEventsInserted(async (records, { batch }) => {
|
store.onEventsInserted(async (records, { batch }) => {
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import { it } from "@std/testing/bdd";
|
|||||||
|
|
||||||
import { EventInsertionError, EventValidationError } from "../../libraries/errors.ts";
|
import { EventInsertionError, EventValidationError } from "../../libraries/errors.ts";
|
||||||
import { makeId } from "../../libraries/nanoid.ts";
|
import { makeId } from "../../libraries/nanoid.ts";
|
||||||
import type { EventStoreFactory } from "../mocks/events.ts";
|
import type { Events } from "../mocks/events.ts";
|
||||||
import { describe } from "../utilities/describe.ts";
|
import { describe } from "../utilities/describe.ts";
|
||||||
|
|
||||||
export default describe<EventStoreFactory>(".addEvent", (getEventStore) => {
|
export default describe<Events>(".addEvent", (getEventStore) => {
|
||||||
it("should throw a 'EventValidationError' when providing bad event data", async () => {
|
it("should throw a 'EventValidationError' when providing bad event data", async () => {
|
||||||
const { store } = await getEventStore();
|
const { store } = await getEventStore();
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import { it } from "@std/testing/bdd";
|
|||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
import { EventValidationError } from "../../mod.ts";
|
import { EventValidationError } from "../../mod.ts";
|
||||||
import type { EventStoreFactory } from "../mocks/events.ts";
|
import type { Events } from "../mocks/events.ts";
|
||||||
import { userReducer } from "../mocks/user-reducer.ts";
|
import { userReducer } from "../mocks/user-reducer.ts";
|
||||||
import { describe } from "../utilities/describe.ts";
|
import { describe } from "../utilities/describe.ts";
|
||||||
|
|
||||||
export default describe<EventStoreFactory>(".addSequence", (getEventStore) => {
|
export default describe<Events>(".addSequence", (getEventStore) => {
|
||||||
it("should insert 'user:created', 'user:name:given-set', and 'user:email-set' in a sequence of events", async () => {
|
it("should insert 'user:created', 'user:name:given-set', and 'user:email-set' in a sequence of events", async () => {
|
||||||
const { store } = await getEventStore();
|
const { store } = await getEventStore();
|
||||||
const stream = nanoid();
|
const stream = nanoid();
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import { assertEquals, assertNotEquals, assertObjectMatch } from "@std/assert";
|
|||||||
import { it } from "@std/testing/bdd";
|
import { it } from "@std/testing/bdd";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
import type { EventStoreFactory } from "../mocks/events.ts";
|
import type { Events } from "../mocks/events.ts";
|
||||||
import { userReducer } from "../mocks/user-reducer.ts";
|
import { userReducer } from "../mocks/user-reducer.ts";
|
||||||
import { describe } from "../utilities/describe.ts";
|
import { describe } from "../utilities/describe.ts";
|
||||||
|
|
||||||
export default describe<EventStoreFactory>(".createSnapshot", (getEventStore) => {
|
export default describe<Events>(".createSnapshot", (getEventStore) => {
|
||||||
it("should create a new snapshot", async () => {
|
it("should create a new snapshot", async () => {
|
||||||
const { store } = await getEventStore();
|
const { store } = await getEventStore();
|
||||||
const stream = nanoid();
|
const stream = nanoid();
|
||||||
|
|||||||
@@ -1,24 +1,26 @@
|
|||||||
import { assertEquals } from "@std/assert";
|
import { assertEquals } from "@std/assert";
|
||||||
import { it } from "@std/testing/bdd";
|
import { it } from "@std/testing/bdd";
|
||||||
|
|
||||||
import type { EventStoreFactory } from "../mocks/events.ts";
|
import { User } from "../mocks/aggregates.ts";
|
||||||
|
import type { Events } from "../mocks/events.ts";
|
||||||
import { describe } from "../utilities/describe.ts";
|
import { describe } from "../utilities/describe.ts";
|
||||||
|
|
||||||
export default describe<EventStoreFactory>(".makeAggregateReducer", (getEventStore) => {
|
export default describe<Events>(".makeAggregateReducer", (getEventStore) => {
|
||||||
it("should reduce a user", async () => {
|
it("should reduce a user", async () => {
|
||||||
const { store } = await getEventStore();
|
const { store } = await getEventStore();
|
||||||
|
|
||||||
const userA = await store
|
const userA = await store.aggregate
|
||||||
.aggregate("user")
|
.from(User)
|
||||||
.create({ given: "John", family: "Doe" }, "john.doe@fixture.none")
|
|
||||||
.setGivenName("Jane")
|
.setGivenName("Jane")
|
||||||
|
.setFamilyName("Doe")
|
||||||
|
.setEmail("john.doe@fixture.none", "auditor")
|
||||||
.save();
|
.save();
|
||||||
|
|
||||||
await userA.snapshot();
|
await userA.snapshot();
|
||||||
|
|
||||||
await userA.setFamilyName("Smith").setEmail("jane.smith@fixture.none", "system").save();
|
await userA.setFamilyName("Smith").setEmail("jane.smith@fixture.none", "system").save();
|
||||||
|
|
||||||
const userB = await store.aggregate("user").getById(userA.id);
|
const userB = await store.aggregate.getByStream(User, userA.id);
|
||||||
if (userB === undefined) {
|
if (userB === undefined) {
|
||||||
throw new Error("Expected user to exist");
|
throw new Error("Expected user to exist");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import { assertEquals, assertLess } from "@std/assert";
|
|||||||
import { it } from "@std/testing/bdd";
|
import { it } from "@std/testing/bdd";
|
||||||
|
|
||||||
import { RelationPayload } from "../../types/adapter.ts";
|
import { RelationPayload } from "../../types/adapter.ts";
|
||||||
import type { EventStoreFactory } from "../mocks/events.ts";
|
import type { Events } from "../mocks/events.ts";
|
||||||
import { describe } from "../utilities/describe.ts";
|
import { describe } from "../utilities/describe.ts";
|
||||||
|
|
||||||
export default describe<EventStoreFactory>(".makeEvent", (getEventStore) => {
|
export default describe<Events>(".makeEvent", (getEventStore) => {
|
||||||
it("should make and performantly batch insert a list of events directly", async () => {
|
it("should make and performantly batch insert a list of events directly", async () => {
|
||||||
const { store } = await getEventStore();
|
const { store } = await getEventStore();
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ import { it } from "@std/testing/bdd";
|
|||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
import { makeId } from "../../libraries/nanoid.ts";
|
import { makeId } from "../../libraries/nanoid.ts";
|
||||||
import type { EventStoreFactory } from "../mocks/events.ts";
|
import type { Events } from "../mocks/events.ts";
|
||||||
import { userPostReducer } from "../mocks/user-posts-reducer.ts";
|
import { userPostReducer } from "../mocks/user-posts-reducer.ts";
|
||||||
import { userReducer } from "../mocks/user-reducer.ts";
|
import { userReducer } from "../mocks/user-reducer.ts";
|
||||||
import { describe } from "../utilities/describe.ts";
|
import { describe } from "../utilities/describe.ts";
|
||||||
|
|
||||||
export default describe<EventStoreFactory>(".makeReducer", (getEventStore) => {
|
export default describe<Events>(".makeReducer", (getEventStore) => {
|
||||||
it("should create a 'user' reducer and only reduce filtered events", async () => {
|
it("should create a 'user' reducer and only reduce filtered events", async () => {
|
||||||
const { store } = await getEventStore();
|
const { store } = await getEventStore();
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import { assertEquals, assertObjectMatch } from "@std/assert";
|
|||||||
import { it } from "@std/testing/bdd";
|
import { it } from "@std/testing/bdd";
|
||||||
|
|
||||||
import { makeId } from "../../libraries/nanoid.ts";
|
import { makeId } from "../../libraries/nanoid.ts";
|
||||||
import type { EventStoreFactory } from "../mocks/events.ts";
|
import type { Events } from "../mocks/events.ts";
|
||||||
import { describe } from "../utilities/describe.ts";
|
import { describe } from "../utilities/describe.ts";
|
||||||
|
|
||||||
export default describe<EventStoreFactory>("projector.once", (getEventStore) => {
|
export default describe<Events>("projector.once", (getEventStore) => {
|
||||||
it("should handle successfull projection", async () => {
|
it("should handle successfull projection", async () => {
|
||||||
const { store, projector } = await getEventStore();
|
const { store, projector } = await getEventStore();
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import { assertEquals } from "@std/assert";
|
|||||||
import { it } from "@std/testing/bdd";
|
import { it } from "@std/testing/bdd";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
import type { EventStoreFactory } from "../../mocks/events.ts";
|
import type { Events } from "../../mocks/events.ts";
|
||||||
import { describe } from "../../utilities/describe.ts";
|
import { describe } from "../../utilities/describe.ts";
|
||||||
|
|
||||||
export default describe<EventStoreFactory>("relations", (getEventStore) => {
|
export default describe<Events>("relations", (getEventStore) => {
|
||||||
it("should create a new relation", async () => {
|
it("should create a new relation", async () => {
|
||||||
const { store } = await getEventStore();
|
const { store } = await getEventStore();
|
||||||
|
|
||||||
|
|||||||
@@ -1,36 +1,38 @@
|
|||||||
import { assertEquals, assertObjectMatch } from "@std/assert";
|
import { assertEquals, assertObjectMatch } from "@std/assert";
|
||||||
import { it } from "@std/testing/bdd";
|
import { it } from "@std/testing/bdd";
|
||||||
|
|
||||||
import type { EventStoreFactory } from "../mocks/events.ts";
|
import { User } from "../mocks/aggregates.ts";
|
||||||
|
import type { Events } from "../mocks/events.ts";
|
||||||
import { userReducer } from "../mocks/user-reducer.ts";
|
import { userReducer } from "../mocks/user-reducer.ts";
|
||||||
import { describe } from "../utilities/describe.ts";
|
import { describe } from "../utilities/describe.ts";
|
||||||
|
|
||||||
export default describe<EventStoreFactory>(".pushAggregate", (getEventStore) => {
|
export default describe<Events>(".pushAggregate", (getEventStore) => {
|
||||||
it("should successfully commit pending aggregate events to the event store", async () => {
|
it("should successfully commit pending aggregate events to the event store", async () => {
|
||||||
const { store } = await getEventStore();
|
const { store } = await getEventStore();
|
||||||
|
|
||||||
const user = store
|
const user = store.aggregate
|
||||||
.aggregate("user")
|
.from(User)
|
||||||
.create({ given: "Jane", family: "Doe" }, "jane.doe@fixture.none")
|
.setGivenName("Jane")
|
||||||
|
.setFamilyName("Doe")
|
||||||
|
.setEmail("jane.doe@fixture.none", "admin")
|
||||||
.setGivenName("John")
|
.setGivenName("John")
|
||||||
.setEmail("john.doe@fixture.none", "admin");
|
.setEmail("john.doe@fixture.none", "admin");
|
||||||
|
|
||||||
assertEquals(user.toPending().length, 3);
|
assertEquals(user.toPending().length, 5);
|
||||||
|
|
||||||
await store.pushAggregate(user);
|
await user.save();
|
||||||
|
|
||||||
assertEquals(user.toPending().length, 0);
|
assertEquals(user.toPending().length, 0);
|
||||||
|
|
||||||
const records = await store.getEventsByStreams([user.id]);
|
const records = await store.getEventsByStreams([user.id]);
|
||||||
|
|
||||||
assertEquals(records.length, 3);
|
assertEquals(records.length, 5);
|
||||||
|
|
||||||
assertObjectMatch(records[0], {
|
assertObjectMatch(records[0], { stream: user.id, data: "Jane" });
|
||||||
stream: user.id,
|
assertObjectMatch(records[1], { stream: user.id, data: "Doe" });
|
||||||
data: { name: { given: "Jane", family: "Doe" }, email: "jane.doe@fixture.none" },
|
assertObjectMatch(records[2], { stream: user.id, data: "jane.doe@fixture.none", meta: { auditor: "admin" } });
|
||||||
});
|
assertObjectMatch(records[3], { stream: user.id, data: "John" });
|
||||||
assertObjectMatch(records[1], { stream: user.id, data: "John" });
|
assertObjectMatch(records[4], { stream: user.id, data: "john.doe@fixture.none", meta: { auditor: "admin" } });
|
||||||
assertObjectMatch(records[2], { stream: user.id, data: "john.doe@fixture.none", meta: { auditor: "admin" } });
|
|
||||||
|
|
||||||
const state = await store.reduce({ name: "user", stream: user.id, reducer: userReducer });
|
const state = await store.reduce({ name: "user", stream: user.id, reducer: userReducer });
|
||||||
|
|
||||||
|
|||||||
@@ -1,50 +1,53 @@
|
|||||||
import { assertEquals, assertObjectMatch } from "@std/assert";
|
import { assertEquals, assertObjectMatch } from "@std/assert";
|
||||||
import { it } from "@std/testing/bdd";
|
import { it } from "@std/testing/bdd";
|
||||||
|
|
||||||
import type { EventStoreFactory } from "../mocks/events.ts";
|
import { User } from "../mocks/aggregates.ts";
|
||||||
|
import type { Events } from "../mocks/events.ts";
|
||||||
import { userReducer } from "../mocks/user-reducer.ts";
|
import { userReducer } from "../mocks/user-reducer.ts";
|
||||||
import { describe } from "../utilities/describe.ts";
|
import { describe } from "../utilities/describe.ts";
|
||||||
|
|
||||||
export default describe<EventStoreFactory>(".pushManyAggregates", (getEventStore) => {
|
export default describe<Events>(".pushManyAggregates", (getEventStore) => {
|
||||||
it("should successfully commit pending aggregates events to the event store", async () => {
|
it("should successfully commit pending aggregates events to the event store", async () => {
|
||||||
const { store } = await getEventStore();
|
const { store } = await getEventStore();
|
||||||
|
|
||||||
const userA = store
|
const userA = store.aggregate
|
||||||
.aggregate("user")
|
.from(User)
|
||||||
.create({ given: "Jane", family: "Doe" }, "jane.doe@fixture.none")
|
.setGivenName("Jane")
|
||||||
|
.setFamilyName("Doe")
|
||||||
|
.setEmail("jane.doe@fixture.none", "admin")
|
||||||
.setGivenName("John")
|
.setGivenName("John")
|
||||||
.setEmail("john.doe@fixture.none", "admin");
|
.setEmail("john.doe@fixture.none", "admin");
|
||||||
|
|
||||||
const userB = store
|
const userB = store.aggregate
|
||||||
.aggregate("user")
|
.from(User)
|
||||||
.create({ given: "Peter", family: "Doe" }, "peter.doe@fixture.none")
|
.setGivenName("Peter")
|
||||||
|
.setFamilyName("Doe")
|
||||||
|
.setEmail("peter.doe@fixture.none", "admin")
|
||||||
.setGivenName("Barry")
|
.setGivenName("Barry")
|
||||||
.setEmail("barry.doe@fixture.none", "admin");
|
.setEmail("barry.doe@fixture.none", "admin");
|
||||||
|
|
||||||
assertEquals(userA.toPending().length, 3);
|
assertEquals(userA.toPending().length, 5);
|
||||||
assertEquals(userB.toPending().length, 3);
|
assertEquals(userB.toPending().length, 5);
|
||||||
|
|
||||||
await store.pushManyAggregates([userA, userB]);
|
await store.aggregate.push([userA, userB]);
|
||||||
|
|
||||||
assertEquals(userA.toPending().length, 0);
|
assertEquals(userA.toPending().length, 0);
|
||||||
assertEquals(userB.toPending().length, 0);
|
assertEquals(userB.toPending().length, 0);
|
||||||
|
|
||||||
const records = await store.getEventsByStreams([userA.id, userB.id]);
|
const records = await store.getEventsByStreams([userA.id, userB.id]);
|
||||||
|
|
||||||
assertEquals(records.length, 6);
|
assertEquals(records.length, 10);
|
||||||
|
|
||||||
assertObjectMatch(records[0], {
|
assertObjectMatch(records[0], { stream: userA.id, data: "Jane" });
|
||||||
stream: userA.id,
|
assertObjectMatch(records[1], { stream: userA.id, data: "Doe" });
|
||||||
data: { name: { given: "Jane", family: "Doe" }, email: "jane.doe@fixture.none" },
|
assertObjectMatch(records[2], { stream: userA.id, data: "jane.doe@fixture.none", meta: { auditor: "admin" } });
|
||||||
});
|
assertObjectMatch(records[3], { stream: userA.id, data: "John" });
|
||||||
assertObjectMatch(records[1], { stream: userA.id, data: "John" });
|
assertObjectMatch(records[4], { stream: userA.id, data: "john.doe@fixture.none", meta: { auditor: "admin" } });
|
||||||
assertObjectMatch(records[2], { stream: userA.id, data: "john.doe@fixture.none", meta: { auditor: "admin" } });
|
assertObjectMatch(records[5], { stream: userB.id, data: "Peter" });
|
||||||
assertObjectMatch(records[3], {
|
assertObjectMatch(records[6], { stream: userB.id, data: "Doe" });
|
||||||
stream: userB.id,
|
assertObjectMatch(records[7], { stream: userB.id, data: "peter.doe@fixture.none", meta: { auditor: "admin" } });
|
||||||
data: { name: { given: "Peter", family: "Doe" }, email: "peter.doe@fixture.none" },
|
assertObjectMatch(records[8], { stream: userB.id, data: "Barry" });
|
||||||
});
|
assertObjectMatch(records[9], { stream: userB.id, data: "barry.doe@fixture.none", meta: { auditor: "admin" } });
|
||||||
assertObjectMatch(records[4], { stream: userB.id, data: "Barry" });
|
|
||||||
assertObjectMatch(records[5], { stream: userB.id, data: "barry.doe@fixture.none", meta: { auditor: "admin" } });
|
|
||||||
|
|
||||||
const stateA = await store.reduce({ name: "user", stream: userA.id, reducer: userReducer });
|
const stateA = await store.reduce({ name: "user", stream: userA.id, reducer: userReducer });
|
||||||
const stateB = await store.reduce({ name: "user", stream: userB.id, reducer: userReducer });
|
const stateB = await store.reduce({ name: "user", stream: userB.id, reducer: userReducer });
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import { assertEquals } from "@std/assert";
|
|||||||
import { it } from "@std/testing/bdd";
|
import { it } from "@std/testing/bdd";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
import type { EventStoreFactory } from "../mocks/events.ts";
|
import type { Events } from "../mocks/events.ts";
|
||||||
import { userReducer } from "../mocks/user-reducer.ts";
|
import { userReducer } from "../mocks/user-reducer.ts";
|
||||||
import { describe } from "../utilities/describe.ts";
|
import { describe } from "../utilities/describe.ts";
|
||||||
|
|
||||||
export default describe<EventStoreFactory>(".reduce", (getEventStore) => {
|
export default describe<Events>(".reduce", (getEventStore) => {
|
||||||
it("should return reduced state", async () => {
|
it("should return reduced state", async () => {
|
||||||
const { store } = await getEventStore();
|
const { store } = await getEventStore();
|
||||||
const stream = nanoid();
|
const stream = nanoid();
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import { assertObjectMatch } from "@std/assert";
|
|||||||
import { it } from "@std/testing/bdd";
|
import { it } from "@std/testing/bdd";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
import { EventStoreFactory } from "../mocks/events.ts";
|
import type { Events } from "../mocks/events.ts";
|
||||||
import { describe } from "../utilities/describe.ts";
|
import { describe } from "../utilities/describe.ts";
|
||||||
|
|
||||||
export default describe<EventStoreFactory>(".replayEvents", (getEventStore) => {
|
export default describe<Events>(".replayEvents", (getEventStore) => {
|
||||||
it("should replay events", async () => {
|
it("should replay events", async () => {
|
||||||
const { store, projector } = await getEventStore();
|
const { store, projector } = await getEventStore();
|
||||||
const stream = nanoid();
|
const stream = nanoid();
|
||||||
|
|||||||
@@ -14,6 +14,6 @@ export function describe<TEventFactory extends EventFactory>(
|
|||||||
type EventStoreFn<TEventFactory extends EventFactory> = (options?: {
|
type EventStoreFn<TEventFactory extends EventFactory> = (options?: {
|
||||||
hooks?: EventStoreHooks<TEventFactory>;
|
hooks?: EventStoreHooks<TEventFactory>;
|
||||||
}) => Promise<{
|
}) => Promise<{
|
||||||
store: EventStore<TEventFactory, any, any>;
|
store: EventStore<TEventFactory, any>;
|
||||||
projector: Projector<TEventFactory>;
|
projector: Projector<TEventFactory>;
|
||||||
}>;
|
}>;
|
||||||
|
|||||||
Reference in New Issue
Block a user