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
@@ -16,7 +16,8 @@ export interface Options {
16
16
 
17
17
  const EventRow = Schema.Struct({
18
18
  ...EventStore.StoredEvent.fields,
19
- payload: Schema.fromJsonString(Schema.Json)
19
+ payload: Schema.fromJsonString(Schema.Json),
20
+ metadata: Schema.fromJsonString(Schema.JsonObject)
20
21
  })
21
22
 
22
23
  const decodeRows = Schema.decodeUnknownEffect(Schema.Array(EventRow))
@@ -25,6 +26,13 @@ const storeError = (operation: string) => (cause: unknown) => new EventStore.Eve
25
26
 
26
27
  const isUniqueViolation = (error: SqlError.SqlError) => error.reason._tag === "UniqueViolation"
27
28
 
29
+ const addedColumns: ReadonlyArray<readonly [string, string]> = [
30
+ ["schema_version", "INTEGER NOT NULL DEFAULT 1"],
31
+ ["correlation_id", "TEXT"],
32
+ ["causation_id", "TEXT"],
33
+ ["metadata", "TEXT NOT NULL DEFAULT '{}'"]
34
+ ]
35
+
28
36
  export const make = Effect.fnUntraced(function*(options?: Options) {
29
37
  const sql = yield* SqlClient.SqlClient
30
38
  const events = sql(options?.eventsTable ?? "cqrs_events")
@@ -37,11 +45,29 @@ export const make = Effect.fnUntraced(function*(options?: Options) {
37
45
  aggregate_id TEXT NOT NULL,
38
46
  version INTEGER NOT NULL,
39
47
  type TEXT NOT NULL,
48
+ schema_version INTEGER NOT NULL DEFAULT 1,
40
49
  payload TEXT NOT NULL,
41
50
  timestamp INTEGER NOT NULL,
51
+ correlation_id TEXT,
52
+ causation_id TEXT,
53
+ metadata TEXT NOT NULL DEFAULT '{}',
42
54
  UNIQUE (aggregate_name, aggregate_id, version)
43
55
  )
44
56
  `.pipe(Effect.mapError(storeError("initialize")))
57
+ // Tables created before these columns existed get them here. SQLite has no
58
+ // `ADD COLUMN IF NOT EXISTS`, so the existing columns are listed first.
59
+ const existing = yield* sql<{ readonly name: string }>`PRAGMA table_info(${events})`.pipe(
60
+ Effect.map((rows) => new Set(rows.map(({ name }) => name))),
61
+ Effect.mapError(storeError("initialize"))
62
+ )
63
+ yield* Effect.forEach(
64
+ addedColumns.filter(([name]) => !existing.has(name)),
65
+ ([name, definition]) =>
66
+ sql`ALTER TABLE ${events} ADD COLUMN ${sql(name)} ${sql.literal(definition)}`.pipe(
67
+ Effect.mapError(storeError("initialize"))
68
+ ),
69
+ { discard: true }
70
+ )
45
71
  yield* sql`
46
72
  CREATE INDEX IF NOT EXISTS ${sql(`${indexPrefix}_type_idx`)} ON ${events} (type, position)
47
73
  `.pipe(Effect.mapError(storeError("initialize")))
@@ -52,8 +78,12 @@ export const make = Effect.fnUntraced(function*(options?: Options) {
52
78
  aggregate_id AS "aggregateId",
53
79
  version,
54
80
  type,
81
+ schema_version AS "schemaVersion",
55
82
  payload,
56
- timestamp
83
+ timestamp,
84
+ correlation_id AS "correlationId",
85
+ causation_id AS "causationId",
86
+ metadata
57
87
  `)
58
88
 
59
89
  const append = (input: EventStore.AppendInput) => {
@@ -71,28 +101,28 @@ export const make = Effect.fnUntraced(function*(options?: Options) {
71
101
  return yield* conflict
72
102
  }
73
103
  const timestamp = yield* Clock.currentTimeMillis
104
+ const metadata = JSON.stringify(input.metadata ?? {})
74
105
  return yield* Effect.forEach(input.events, (event, index) =>
75
106
  sql<{ readonly position: number }>`
76
- INSERT INTO ${events} (aggregate_name, aggregate_id, version, type, payload, timestamp)
107
+ INSERT INTO ${events} (
108
+ aggregate_name, aggregate_id, version, type, schema_version, payload, timestamp, correlation_id, causation_id,
109
+ metadata
110
+ )
77
111
  VALUES (
78
112
  ${input.aggregateName},
79
113
  ${input.aggregateId},
80
114
  ${input.expectedVersion + index + 1},
81
115
  ${event.type},
116
+ ${event.schemaVersion ?? 1},
82
117
  ${JSON.stringify(event.payload)},
83
- ${timestamp}
118
+ ${timestamp},
119
+ ${input.correlationId ?? null},
120
+ ${input.causationId ?? null},
121
+ ${metadata}
84
122
  )
85
123
  RETURNING position
86
124
  `.pipe(
87
- Effect.map(([row]): EventStore.StoredEvent => ({
88
- position: row!.position,
89
- aggregateName: input.aggregateName,
90
- aggregateId: input.aggregateId,
91
- version: input.expectedVersion + index + 1,
92
- type: event.type,
93
- payload: event.payload,
94
- timestamp: DateTime.makeUnsafe(timestamp)
95
- }))
125
+ Effect.map(([row]) => EventStore.toStored(input, event, index, row!.position, DateTime.makeUnsafe(timestamp)))
96
126
  ))
97
127
  }).pipe(
98
128
  sql.withTransaction,
package/src/ViewModel.ts CHANGED
@@ -23,6 +23,7 @@ import {
23
23
  import type { DateTime } from "effect"
24
24
  import type * as Aggregate from "./Aggregate.ts"
25
25
  import { makeEventDecoder } from "./AggregateService.ts"
26
+ import * as Causation from "./Causation.ts"
26
27
  import { Checkpoints } from "./Checkpoints.ts"
27
28
  import { type EventFilter, EventStore, type EventStoreError, type StoredEvent } from "./EventStore.ts"
28
29
 
@@ -188,7 +189,8 @@ const makeRunner = Effect.fnUntraced(function*<V extends Any>(viewModel: V, tail
188
189
  const handle = (decodeEvent: (stored: StoredEvent) => Effect.Effect<unknown, unknown>) => (stored: StoredEvent) =>
189
190
  Effect.gen(function*() {
190
191
  const event = yield* decodeEvent(stored)
191
- yield* viewModel.handlers[stored.type]!(event as never, stored)
192
+ // Commands the handler runs record this event as their cause.
193
+ yield* viewModel.handlers[stored.type]!(event as never, stored).pipe(Causation.causedBy(stored))
192
194
  }).pipe(
193
195
  Effect.mapError(fail(stored.position)),
194
196
  Effect.catchDefect((defect) => Effect.fail(fail(stored.position)(defect)))
package/src/index.ts CHANGED
@@ -1,8 +1,12 @@
1
1
  export * as Aggregate from "./Aggregate.ts"
2
2
  export * as AggregateService from "./AggregateService.ts"
3
+ export * as Causation from "./Causation.ts"
3
4
  export * as Checkpoints from "./Checkpoints.ts"
4
5
  export * as EventStore from "./EventStore.ts"
6
+ export * as EventVersions from "./EventVersions.ts"
5
7
  export * as Fixture from "./Fixture.ts"
8
+ export * as Metadata from "./Metadata.ts"
6
9
  export * as PostgresEventStore from "./PostgresEventStore.ts"
10
+ export * as Saga from "./Saga.ts"
7
11
  export * as SqliteEventStore from "./SqliteEventStore.ts"
8
12
  export * as ViewModel from "./ViewModel.ts"