effect-cqrs 0.1.0 → 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 (59) hide show
  1. package/README.md +34 -6
  2. package/dist/Aggregate.d.ts +47 -6
  3. package/dist/Aggregate.d.ts.map +1 -1
  4. package/dist/Aggregate.js +45 -1
  5. package/dist/Aggregate.js.map +1 -1
  6. package/dist/AggregateService.d.ts +83 -11
  7. package/dist/AggregateService.d.ts.map +1 -1
  8. package/dist/AggregateService.js +261 -35
  9. package/dist/AggregateService.js.map +1 -1
  10. package/dist/Causation.d.ts +34 -0
  11. package/dist/Causation.d.ts.map +1 -0
  12. package/dist/Causation.js +32 -0
  13. package/dist/Causation.js.map +1 -0
  14. package/dist/EventStore.d.ts +22 -0
  15. package/dist/EventStore.d.ts.map +1 -1
  16. package/dist/EventStore.js +30 -10
  17. package/dist/EventStore.js.map +1 -1
  18. package/dist/EventVersions.d.ts +64 -0
  19. package/dist/EventVersions.d.ts.map +1 -0
  20. package/dist/EventVersions.js +55 -0
  21. package/dist/EventVersions.js.map +1 -0
  22. package/dist/Fixture.d.ts +40 -5
  23. package/dist/Fixture.d.ts.map +1 -1
  24. package/dist/Fixture.js +44 -3
  25. package/dist/Fixture.js.map +1 -1
  26. package/dist/Metadata.d.ts +42 -0
  27. package/dist/Metadata.d.ts.map +1 -0
  28. package/dist/Metadata.js +39 -0
  29. package/dist/Metadata.js.map +1 -0
  30. package/dist/PostgresEventStore.d.ts.map +1 -1
  31. package/dist/PostgresEventStore.js +22 -13
  32. package/dist/PostgresEventStore.js.map +1 -1
  33. package/dist/Saga.d.ts +105 -0
  34. package/dist/Saga.d.ts.map +1 -0
  35. package/dist/Saga.js +97 -0
  36. package/dist/Saga.js.map +1 -0
  37. package/dist/SqliteEventStore.d.ts.map +1 -1
  38. package/dist/SqliteEventStore.js +32 -13
  39. package/dist/SqliteEventStore.js.map +1 -1
  40. package/dist/ViewModel.d.ts.map +1 -1
  41. package/dist/ViewModel.js +3 -1
  42. package/dist/ViewModel.js.map +1 -1
  43. package/dist/index.d.ts +4 -0
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +4 -0
  46. package/dist/index.js.map +1 -1
  47. package/package.json +5 -3
  48. package/src/Aggregate.ts +81 -9
  49. package/src/AggregateService.ts +418 -55
  50. package/src/Causation.ts +48 -0
  51. package/src/EventStore.ts +47 -10
  52. package/src/EventVersions.ts +104 -0
  53. package/src/Fixture.ts +92 -7
  54. package/src/Metadata.ts +52 -0
  55. package/src/PostgresEventStore.ts +24 -13
  56. package/src/Saga.ts +176 -0
  57. package/src/SqliteEventStore.ts +43 -13
  58. package/src/ViewModel.ts +3 -1
  59. package/src/index.ts +4 -0
package/src/Aggregate.ts CHANGED
@@ -5,7 +5,8 @@
5
5
  * An aggregate is plain data. {@link AggregateService} gives it a store to run
6
6
  * against, and `Fixture` runs it in tests.
7
7
  */
8
- import type { DateTime, Effect, Schema } from "effect"
8
+ import { type DateTime, type Effect, Predicate, Schema } from "effect"
9
+ import * as EventVersions from "./EventVersions.ts"
9
10
 
10
11
  export interface Tagged {
11
12
  readonly _tag: string
@@ -29,6 +30,12 @@ export interface EventMeta {
29
30
  readonly version: number
30
31
  readonly position: number
31
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
32
39
  }
33
40
 
34
41
  export interface CommandContext {
@@ -62,17 +69,27 @@ export interface AnyCommand {
62
69
  readonly handler: CommandHandler<any, any, any, any, any>
63
70
  }
64
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
+
65
80
  export interface Aggregate<
66
81
  Name extends string,
67
82
  State,
68
83
  Events extends EventSchema<any>,
69
- Commands extends Record<string, AnyCommand>
84
+ Commands extends Record<string, AnyCommand>,
85
+ V extends Versions<Events["Type"]> = {}
70
86
  > {
71
87
  readonly name: Name
72
88
  readonly events: Events
73
89
  readonly initial: State
74
90
  readonly projection: Projection<State, Events["Type"]>
75
91
  readonly commands: Commands
92
+ readonly versions: V
76
93
  /** Returns a new aggregate that also accepts this command. */
77
94
  command<const CommandName extends string, Payload extends PayloadSchema, E = never, R = never>(
78
95
  name: CommandName,
@@ -80,10 +97,10 @@ export interface Aggregate<
80
97
  readonly payload: Payload
81
98
  readonly handler: CommandHandler<State, Payload["Type"], Events["Type"], E, R>
82
99
  }
83
- ): Aggregate<Name, State, Events, Commands & Record<CommandName, Command<Payload, State, Events["Type"], E, R>>>
100
+ ): Aggregate<Name, State, Events, Commands & Record<CommandName, Command<Payload, State, Events["Type"], E, R>>, V>
84
101
  }
85
102
 
86
- export type Any = Aggregate<string, any, EventSchema<any>, Record<string, AnyCommand>>
103
+ export type Any = Aggregate<string, any, EventSchema<any>, Record<string, AnyCommand>, any>
87
104
 
88
105
  export type Name<A extends Any> = A["name"]
89
106
  export type State<A extends Any> = A["initial"]
@@ -100,10 +117,14 @@ const build = <
100
117
  Name extends string,
101
118
  State,
102
119
  Events extends EventSchema<any>,
103
- Commands extends Record<string, AnyCommand>
120
+ Commands extends Record<string, AnyCommand>,
121
+ V extends Versions<Events["Type"]>
104
122
  >(
105
- definition: Pick<Aggregate<Name, State, Events, Commands>, "name" | "events" | "initial" | "projection" | "commands">
106
- ): Aggregate<Name, State, Events, Commands> => ({
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> => ({
107
128
  ...definition,
108
129
  command: (name, options) =>
109
130
  build({
@@ -117,12 +138,15 @@ const build = <
117
138
  * projection's return values are checked against it rather than widened.
118
139
  */
119
140
  export const make = <State>() =>
120
- <const Name extends string, Events extends EventSchema<any>>(options: {
141
+ <const Name extends string, Events extends EventSchema<any>, const V extends Versions<Events["Type"]> = {}>(options: {
121
142
  readonly name: Name
122
143
  readonly events: Events
123
144
  readonly initial: State
124
145
  readonly projection: Projection<State, Events["Type"]>
125
- }): Aggregate<Name, State, Events, {}> => build({ ...options, commands: {} })
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: {} })
126
150
 
127
151
  /** Folds one event into the state. */
128
152
  export const evolve = <A extends Any>(aggregate: A, state: State<A>, event: Event<A>, meta: EventMeta): State<A> =>
@@ -131,3 +155,51 @@ export const evolve = <A extends Any>(aggregate: A, state: State<A>, event: Even
131
155
  event,
132
156
  meta
133
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
+ }