effect-cqrs 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +326 -2
  3. package/dist/Aggregate.d.ts +127 -0
  4. package/dist/Aggregate.d.ts.map +1 -0
  5. package/dist/Aggregate.js +59 -0
  6. package/dist/Aggregate.js.map +1 -0
  7. package/dist/AggregateService.d.ts +154 -0
  8. package/dist/AggregateService.d.ts.map +1 -0
  9. package/dist/AggregateService.js +407 -0
  10. package/dist/AggregateService.js.map +1 -0
  11. package/dist/Causation.d.ts +34 -0
  12. package/dist/Causation.d.ts.map +1 -0
  13. package/dist/Causation.js +32 -0
  14. package/dist/Causation.js.map +1 -0
  15. package/dist/Checkpoints.d.ts +19 -0
  16. package/dist/Checkpoints.d.ts.map +1 -0
  17. package/dist/Checkpoints.js +18 -0
  18. package/dist/Checkpoints.js.map +1 -0
  19. package/dist/EventStore.d.ts +132 -0
  20. package/dist/EventStore.d.ts.map +1 -0
  21. package/dist/EventStore.js +156 -0
  22. package/dist/EventStore.js.map +1 -0
  23. package/dist/EventVersions.d.ts +64 -0
  24. package/dist/EventVersions.d.ts.map +1 -0
  25. package/dist/EventVersions.js +55 -0
  26. package/dist/EventVersions.js.map +1 -0
  27. package/dist/Fixture.d.ts +106 -0
  28. package/dist/Fixture.d.ts.map +1 -0
  29. package/dist/Fixture.js +149 -0
  30. package/dist/Fixture.js.map +1 -0
  31. package/dist/Metadata.d.ts +42 -0
  32. package/dist/Metadata.d.ts.map +1 -0
  33. package/dist/Metadata.js +39 -0
  34. package/dist/Metadata.js.map +1 -0
  35. package/dist/PostgresEventStore.d.ts +30 -0
  36. package/dist/PostgresEventStore.d.ts.map +1 -0
  37. package/dist/PostgresEventStore.js +153 -0
  38. package/dist/PostgresEventStore.js.map +1 -0
  39. package/dist/Saga.d.ts +105 -0
  40. package/dist/Saga.d.ts.map +1 -0
  41. package/dist/Saga.js +97 -0
  42. package/dist/Saga.js.map +1 -0
  43. package/dist/SqliteEventStore.d.ts +25 -0
  44. package/dist/SqliteEventStore.d.ts.map +1 -0
  45. package/dist/SqliteEventStore.js +142 -0
  46. package/dist/SqliteEventStore.js.map +1 -0
  47. package/dist/ViewModel.d.ts +90 -0
  48. package/dist/ViewModel.d.ts.map +1 -0
  49. package/dist/ViewModel.js +172 -0
  50. package/dist/ViewModel.js.map +1 -0
  51. package/dist/index.d.ts +13 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +13 -0
  54. package/dist/index.js.map +1 -0
  55. package/package.json +89 -3
  56. package/src/Aggregate.ts +205 -0
  57. package/src/AggregateService.ts +705 -0
  58. package/src/Causation.ts +48 -0
  59. package/src/Checkpoints.ts +23 -0
  60. package/src/EventStore.ts +270 -0
  61. package/src/EventVersions.ts +104 -0
  62. package/src/Fixture.ts +298 -0
  63. package/src/Metadata.ts +52 -0
  64. package/src/PostgresEventStore.ts +199 -0
  65. package/src/Saga.ts +176 -0
  66. package/src/SqliteEventStore.ts +196 -0
  67. package/src/ViewModel.ts +350 -0
  68. package/src/index.ts +12 -0
package/package.json CHANGED
@@ -1,6 +1,92 @@
1
1
  {
2
2
  "name": "effect-cqrs",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.0",
4
+ "description": "CQRS and event sourcing for Effect v4, a port of moleculer-cqrs",
5
+ "keywords": [
6
+ "effect",
7
+ "cqrs",
8
+ "event-sourcing",
9
+ "event-store",
10
+ "aggregate",
11
+ "projection",
12
+ "sqlite",
13
+ "postgres"
14
+ ],
15
+ "homepage": "https://davidnussio.github.io/effect-cqrs/",
16
+ "bugs": {
17
+ "url": "https://github.com/davidnussio/effect-cqrs/issues"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/davidnussio/effect-cqrs.git"
22
+ },
23
+ "type": "module",
24
+ "license": "MIT",
25
+ "author": "David Nussio",
26
+ "engines": {
27
+ "node": ">=24"
28
+ },
29
+ "exports": {
30
+ ".": {
31
+ "types": "./dist/index.d.ts",
32
+ "default": "./dist/index.js"
33
+ },
34
+ "./*": {
35
+ "types": "./dist/*.d.ts",
36
+ "default": "./dist/*.js"
37
+ }
38
+ },
39
+ "files": [
40
+ "dist",
41
+ "src",
42
+ "!**/*.tsbuildinfo"
43
+ ],
44
+ "sideEffects": false,
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "peerDependencies": {
49
+ "effect": "^4.0.6"
50
+ },
51
+ "devDependencies": {
52
+ "@astrojs/starlight": "^0.42.7",
53
+ "@changesets/cli": "^2.31.1",
54
+ "@effect/platform-node": "^4.0.6",
55
+ "@effect/sql-pg": "^4.0.6",
56
+ "@effect/sql-sqlite-node": "^4.0.6",
57
+ "@effect/vitest": "^4.0.6",
58
+ "@tailwindcss/vite": "^4.3.3",
59
+ "@types/node": "^24.0.0",
60
+ "@types/react": "^19.3.0",
61
+ "@types/react-dom": "^19.3.0",
62
+ "@vitejs/plugin-react": "^6.1.2",
63
+ "astro": "^7.4.4",
64
+ "effect": "^4.0.6",
65
+ "motion": "^14.7.1",
66
+ "react": "^19.3.1",
67
+ "react-dom": "^19.3.1",
68
+ "tailwindcss": "^4.3.3",
69
+ "typescript": "^7.0.2",
70
+ "vite": "^8.3.5",
71
+ "vitest": "^5.0.5"
72
+ },
73
+ "scripts": {
74
+ "check": "tsc --noEmit && tsc --noEmit -p examples/bank-ui && tsc --noEmit -p examples/shop-ui",
75
+ "build": "rm -rf dist && tsc -p tsconfig.build.json",
76
+ "test": "vitest run",
77
+ "test:watch": "vitest",
78
+ "example": "node examples/todo/main.ts",
79
+ "tutorial": "node examples/tutorial/main.ts",
80
+ "ui": "vite examples/bank-ui",
81
+ "ui:shop": "vite examples/shop-ui",
82
+ "docs": "astro dev --root docs",
83
+ "docs:build": "vite build examples/bank-ui --base \"${DOCS_BASE:-/effect-cqrs}/demo/\" --outDir ../../docs/public/demo --emptyOutDir && vite build examples/shop-ui --base \"${DOCS_BASE:-/effect-cqrs}/shop-demo/\" --outDir ../../docs/public/shop-demo --emptyOutDir && astro build --root docs",
84
+ "docs:preview": "astro preview --root docs",
85
+ "bench": "node bench/performance.ts",
86
+ "stress": "node --expose-gc bench/stress.ts",
87
+ "changeset": "changeset",
88
+ "version-packages": "changeset version"
89
+ },
90
+ "main": "./dist/index.js",
91
+ "types": "./dist/index.d.ts"
6
92
  }
@@ -0,0 +1,205 @@
1
+ /**
2
+ * The write side of the domain: a schema of events, a projection that folds them
3
+ * into state, and commands that inspect that state and decide on new events.
4
+ *
5
+ * An aggregate is plain data. {@link AggregateService} gives it a store to run
6
+ * against, and `Fixture` runs it in tests.
7
+ */
8
+ import { type DateTime, type Effect, Predicate, Schema } from "effect"
9
+ import * as EventVersions from "./EventVersions.ts"
10
+
11
+ export interface Tagged {
12
+ readonly _tag: string
13
+ }
14
+
15
+ export type EventSchema<Event extends Tagged = Tagged> = Schema.Top & {
16
+ readonly Type: Event
17
+ readonly DecodingServices: never
18
+ readonly EncodingServices: never
19
+ }
20
+
21
+ export type PayloadSchema = Schema.Top & {
22
+ readonly DecodingServices: never
23
+ readonly EncodingServices: never
24
+ }
25
+
26
+ /** Where an event sits in its stream and in the store. */
27
+ export interface EventMeta {
28
+ readonly aggregateName: string
29
+ readonly aggregateId: string
30
+ readonly version: number
31
+ readonly position: number
32
+ readonly timestamp: DateTime.Utc
33
+ /** Shared by everything that follows from one original request, see `Causation`. */
34
+ readonly correlationId: string | null
35
+ /** What directly caused the command that recorded the event. */
36
+ readonly causationId: string | null
37
+ /** What the command carried beside its payload, such as the software version, see `Metadata`. */
38
+ readonly metadata: Schema.JsonObject
39
+ }
40
+
41
+ export interface CommandContext {
42
+ readonly aggregateId: string
43
+ readonly version: number
44
+ }
45
+
46
+ /** Exhaustive by event tag, so adding an event to the schema without folding it does not compile. */
47
+ export type Projection<State, Event extends Tagged> = {
48
+ readonly [Tag in Event["_tag"]]: (state: State, event: Extract<Event, { readonly _tag: Tag }>, meta: EventMeta) => State
49
+ }
50
+
51
+ /**
52
+ * Decides on the events a command produces. It may run several times when
53
+ * concurrent writers collide, so it should only decide and leave side effects to
54
+ * view models. Returning an empty array records nothing.
55
+ */
56
+ export type CommandHandler<State, Payload, Event, E, R> = (
57
+ state: State,
58
+ payload: Payload,
59
+ context: CommandContext
60
+ ) => Effect.Effect<Event | ReadonlyArray<Event>, E, R>
61
+
62
+ export interface Command<Payload extends PayloadSchema, State, Event, E, R> {
63
+ readonly payload: Payload
64
+ readonly handler: CommandHandler<State, Payload["Type"], Event, E, R>
65
+ }
66
+
67
+ export interface AnyCommand {
68
+ readonly payload: PayloadSchema
69
+ readonly handler: CommandHandler<any, any, any, any, any>
70
+ }
71
+
72
+ /**
73
+ * The older shapes of some events, keyed by tag. Each history has to end in the
74
+ * event's current shape.
75
+ */
76
+ export type Versions<Event extends Tagged> = {
77
+ readonly [Tag in Event["_tag"]]?: EventVersions.Any<Extract<Event, { readonly _tag: Tag }>>
78
+ }
79
+
80
+ export interface Aggregate<
81
+ Name extends string,
82
+ State,
83
+ Events extends EventSchema<any>,
84
+ Commands extends Record<string, AnyCommand>,
85
+ V extends Versions<Events["Type"]> = {}
86
+ > {
87
+ readonly name: Name
88
+ readonly events: Events
89
+ readonly initial: State
90
+ readonly projection: Projection<State, Events["Type"]>
91
+ readonly commands: Commands
92
+ readonly versions: V
93
+ /** Returns a new aggregate that also accepts this command. */
94
+ command<const CommandName extends string, Payload extends PayloadSchema, E = never, R = never>(
95
+ name: CommandName,
96
+ options: {
97
+ readonly payload: Payload
98
+ readonly handler: CommandHandler<State, Payload["Type"], Events["Type"], E, R>
99
+ }
100
+ ): Aggregate<Name, State, Events, Commands & Record<CommandName, Command<Payload, State, Events["Type"], E, R>>, V>
101
+ }
102
+
103
+ export type Any = Aggregate<string, any, EventSchema<any>, Record<string, AnyCommand>, any>
104
+
105
+ export type Name<A extends Any> = A["name"]
106
+ export type State<A extends Any> = A["initial"]
107
+ export type Event<A extends Any> = A["events"]["Type"]
108
+ export type Commands<A extends Any> = A["commands"]
109
+ export type CommandName<A extends Any> = keyof A["commands"] & string
110
+ export type Payload<A extends Any, K extends CommandName<A>> = A["commands"][K]["payload"]["Type"]
111
+ export type CommandError<A extends Any, K extends CommandName<A> = CommandName<A>> = A["commands"][K] extends
112
+ Command<any, any, any, infer E, any> ? E : never
113
+ export type Context<A extends Any> = A["commands"][CommandName<A>] extends Command<any, any, any, any, infer R> ? R
114
+ : never
115
+
116
+ const build = <
117
+ Name extends string,
118
+ State,
119
+ Events extends EventSchema<any>,
120
+ Commands extends Record<string, AnyCommand>,
121
+ V extends Versions<Events["Type"]>
122
+ >(
123
+ definition: Pick<
124
+ Aggregate<Name, State, Events, Commands, V>,
125
+ "name" | "events" | "initial" | "projection" | "commands" | "versions"
126
+ >
127
+ ): Aggregate<Name, State, Events, Commands, V> => ({
128
+ ...definition,
129
+ command: (name, options) =>
130
+ build({
131
+ ...definition,
132
+ commands: { ...definition.commands, [name]: options } as any
133
+ })
134
+ })
135
+
136
+ /**
137
+ * The state is given explicitly, `Aggregate.make<State>()({ ... })`, so that the
138
+ * projection's return values are checked against it rather than widened.
139
+ */
140
+ export const make = <State>() =>
141
+ <const Name extends string, Events extends EventSchema<any>, const V extends Versions<Events["Type"]> = {}>(options: {
142
+ readonly name: Name
143
+ readonly events: Events
144
+ readonly initial: State
145
+ readonly projection: Projection<State, Events["Type"]>
146
+ /** Older shapes of events already in the store, see `EventVersions`. */
147
+ readonly versions?: V | undefined
148
+ }): Aggregate<Name, State, Events, {}, V> =>
149
+ build({ ...options, versions: options.versions ?? ({} as V), commands: {} })
150
+
151
+ /** Folds one event into the state. */
152
+ export const evolve = <A extends Any>(aggregate: A, state: State<A>, event: Event<A>, meta: EventMeta): State<A> =>
153
+ (aggregate.projection as Record<string, (state: State<A>, event: Event<A>, meta: EventMeta) => State<A>>)[event._tag]!(
154
+ state,
155
+ event,
156
+ meta
157
+ )
158
+
159
+ /** The schema version new events with this tag are written with. */
160
+ export const schemaVersion = (aggregate: Any, tag: string): number =>
161
+ EventVersions.current((aggregate.versions as Record<string, EventVersions.Any | undefined>)[tag])
162
+
163
+ /**
164
+ * An event as it was stored in the past, in an older version of its schema.
165
+ * Construct one directly to replay JSON taken from a database.
166
+ */
167
+ export class Historical {
168
+ readonly type: string
169
+ readonly schemaVersion: number
170
+ readonly payload: Schema.Json
171
+ constructor(options: { readonly type: string; readonly schemaVersion: number; readonly payload: Schema.Json }) {
172
+ this.type = options.type
173
+ this.schemaVersion = options.schemaVersion
174
+ this.payload = options.payload
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Encodes an event in an older version of its schema, as code from that time
180
+ * would have stored it. The value is checked against that version, so a test
181
+ * can't seed a shape that never existed.
182
+ */
183
+ export const historical = <
184
+ A extends Any,
185
+ const Tag extends keyof A["versions"] & string,
186
+ const Version extends number
187
+ >(
188
+ aggregate: A,
189
+ tag: Tag,
190
+ version: Version,
191
+ value: EventVersions.SchemaAt<NonNullable<A["versions"][Tag]>, Version>["Type"]
192
+ ): Historical => {
193
+ const versions = (aggregate.versions as Record<string, EventVersions.Any | undefined>)[tag]
194
+ const schema = versions?.schemas[version - 1]
195
+ if (schema === undefined) {
196
+ throw new Error(`${aggregate.name} has no version ${version} of ${tag}`)
197
+ }
198
+ const payload = Schema.encodeSync(Schema.toCodecJson(schema))(value) as Schema.Json
199
+ return new Historical({
200
+ type: tag,
201
+ schemaVersion: version,
202
+ // Events were always stored with their tag, whatever the schema of the time said.
203
+ payload: Predicate.isObject(payload) ? { ...(payload as object), _tag: tag } as Schema.Json : payload
204
+ })
205
+ }