@photon-grove/evt 0.1.0 → 0.1.2

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 (96) hide show
  1. package/README.md +85 -9
  2. package/dist/aggregate.d.ts +26 -0
  3. package/dist/aggregate.d.ts.map +1 -1
  4. package/dist/aggregate.js.map +1 -1
  5. package/dist/dynamodb/client.d.ts +83 -0
  6. package/dist/dynamodb/client.d.ts.map +1 -0
  7. package/dist/dynamodb/client.js +6 -0
  8. package/dist/dynamodb/client.js.map +1 -0
  9. package/dist/dynamodb/index.d.ts +6 -1
  10. package/dist/dynamodb/index.d.ts.map +1 -1
  11. package/dist/dynamodb/index.js +8 -3
  12. package/dist/dynamodb/index.js.map +1 -1
  13. package/dist/dynamodb/item.d.ts +2 -0
  14. package/dist/dynamodb/item.d.ts.map +1 -1
  15. package/dist/dynamodb/item.js +5 -4
  16. package/dist/dynamodb/item.js.map +1 -1
  17. package/dist/dynamodb/marker.d.ts +11 -0
  18. package/dist/dynamodb/marker.d.ts.map +1 -0
  19. package/dist/dynamodb/marker.js +56 -0
  20. package/dist/dynamodb/marker.js.map +1 -0
  21. package/dist/dynamodb/repository.d.ts +61 -0
  22. package/dist/dynamodb/repository.d.ts.map +1 -0
  23. package/dist/dynamodb/repository.js +748 -0
  24. package/dist/dynamodb/repository.js.map +1 -0
  25. package/dist/dynamodb/requests.d.ts +68 -0
  26. package/dist/dynamodb/requests.d.ts.map +1 -0
  27. package/dist/dynamodb/requests.js +194 -0
  28. package/dist/dynamodb/requests.js.map +1 -0
  29. package/dist/dynamodb/views.d.ts +18 -0
  30. package/dist/dynamodb/views.d.ts.map +1 -0
  31. package/dist/dynamodb/views.js +56 -0
  32. package/dist/dynamodb/views.js.map +1 -0
  33. package/dist/errors.d.ts +109 -2
  34. package/dist/errors.d.ts.map +1 -1
  35. package/dist/errors.js +118 -2
  36. package/dist/errors.js.map +1 -1
  37. package/dist/features.d.ts +55 -0
  38. package/dist/features.d.ts.map +1 -0
  39. package/dist/features.js +126 -0
  40. package/dist/features.js.map +1 -0
  41. package/dist/index.d.ts +4 -3
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +2 -1
  44. package/dist/index.js.map +1 -1
  45. package/dist/memory/index.d.ts +1 -1
  46. package/dist/memory/index.d.ts.map +1 -1
  47. package/dist/memory/index.js +1 -1
  48. package/dist/memory/index.js.map +1 -1
  49. package/dist/memory/repository.d.ts +38 -2
  50. package/dist/memory/repository.d.ts.map +1 -1
  51. package/dist/memory/repository.js +95 -6
  52. package/dist/memory/repository.js.map +1 -1
  53. package/dist/postgres/client.d.ts +32 -0
  54. package/dist/postgres/client.d.ts.map +1 -0
  55. package/dist/postgres/client.js +4 -0
  56. package/dist/postgres/client.js.map +1 -0
  57. package/dist/postgres/index.d.ts +4 -0
  58. package/dist/postgres/index.d.ts.map +1 -0
  59. package/dist/postgres/index.js +6 -0
  60. package/dist/postgres/index.js.map +1 -0
  61. package/dist/postgres/repository.d.ts +55 -0
  62. package/dist/postgres/repository.d.ts.map +1 -0
  63. package/dist/postgres/repository.js +527 -0
  64. package/dist/postgres/repository.js.map +1 -0
  65. package/dist/postgres/schema.d.ts +81 -0
  66. package/dist/postgres/schema.d.ts.map +1 -0
  67. package/dist/postgres/schema.js +233 -0
  68. package/dist/postgres/schema.js.map +1 -0
  69. package/dist/repository.d.ts +56 -7
  70. package/dist/repository.d.ts.map +1 -1
  71. package/dist/repository.js +24 -9
  72. package/dist/repository.js.map +1 -1
  73. package/dist/store.d.ts +7 -5
  74. package/dist/store.d.ts.map +1 -1
  75. package/dist/store.js +34 -9
  76. package/dist/store.js.map +1 -1
  77. package/package.json +16 -3
  78. package/src/aggregate.ts +28 -0
  79. package/src/dynamodb/client.ts +102 -0
  80. package/src/dynamodb/index.ts +27 -2
  81. package/src/dynamodb/item.ts +6 -4
  82. package/src/dynamodb/marker.ts +64 -0
  83. package/src/dynamodb/repository.ts +1053 -0
  84. package/src/dynamodb/requests.ts +276 -0
  85. package/src/dynamodb/views.ts +67 -0
  86. package/src/errors.ts +176 -2
  87. package/src/features.ts +175 -0
  88. package/src/index.ts +28 -1
  89. package/src/memory/index.ts +9 -1
  90. package/src/memory/repository.ts +156 -6
  91. package/src/postgres/client.ts +37 -0
  92. package/src/postgres/index.ts +23 -0
  93. package/src/postgres/repository.ts +773 -0
  94. package/src/postgres/schema.ts +286 -0
  95. package/src/repository.ts +86 -13
  96. package/src/store.ts +61 -11
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@photon-grove/evt",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Event sourcing for TypeScript, wire-compatible with the Go and Rust evt implementations.",
5
5
  "keywords": [
6
6
  "event-sourcing",
@@ -38,20 +38,33 @@
38
38
  "types": "./dist/memory/index.d.ts",
39
39
  "default": "./dist/memory/index.js"
40
40
  },
41
+ "./postgres": {
42
+ "types": "./dist/postgres/index.d.ts",
43
+ "default": "./dist/postgres/index.js"
44
+ },
41
45
  "./testing": {
42
46
  "types": "./dist/testing/index.d.ts",
43
47
  "default": "./dist/testing/index.js"
44
48
  },
45
49
  "./package.json": "./package.json"
46
50
  },
51
+ "peerDependencies": {
52
+ "@aws-sdk/client-dynamodb": "^3.0.0"
53
+ },
54
+ "peerDependenciesMeta": {
55
+ "@aws-sdk/client-dynamodb": {
56
+ "optional": true
57
+ }
58
+ },
47
59
  "scripts": {
48
60
  "build": "tsc",
49
61
  "check": "tsc --noEmit && tsc -p tsconfig.test.json && oxlint --deny-warnings && node scripts/check-imports.ts",
50
62
  "test": "vitest run"
51
63
  },
52
64
  "devDependencies": {
53
- "@types/node": "^25.9.9",
54
- "oxlint": "1.88.0",
65
+ "@aws-sdk/client-dynamodb": "3.1161.0",
66
+ "@types/node": "^26.6.6",
67
+ "oxlint": "1.90.0",
55
68
  "typescript": "^7.0.2",
56
69
  "vitest": "5.0.5"
57
70
  }
package/src/aggregate.ts CHANGED
@@ -5,6 +5,7 @@ import type {Codec} from './codec.js'
5
5
  import {EvtError, ReplayError, messageOf} from './errors.js'
6
6
  import type {SerializedEvent} from './event.js'
7
7
  import {type JsonValue, parseJson, stringifyJson} from './json.js'
8
+ import type {ProjectorGroup} from './repository.js'
8
9
  import {utf8Decode, utf8Encode} from './utf8.js'
9
10
 
10
11
  /**
@@ -103,6 +104,33 @@ export interface AggregateDefinition<S, C, D extends EventDefinitions> {
103
104
  * snapshots.
104
105
  */
105
106
  readonly snapshotInterval?: number | undefined
107
+ /**
108
+ * The projectors whose operations every commit of this type writes in the same transaction as
109
+ * its events, in this order, such as the DynamoDB backend's `viewProjector`.
110
+ */
111
+ readonly projectors?: readonly Projector<S>[] | undefined
112
+ }
113
+
114
+ /** What a projector sees of a commit: its entity, the state after its events, and the events. */
115
+ export interface Projection<S> {
116
+ readonly entityType: string
117
+ readonly entityId: string
118
+ /** The state after the commit's events. */
119
+ readonly state: S
120
+ /** The state as the aggregate's state codec encodes it, as a snapshot stores it. */
121
+ readonly encodedState: Uint8Array
122
+ /** Copies of the commit's events, as they are stored. */
123
+ readonly events: readonly Readonly<SerializedEvent>[]
124
+ }
125
+
126
+ /**
127
+ * Builds the operations a commit writes beside its events, in the same transaction, such as a
128
+ * view of the entity's state. Its group names the backend that writes it. `project` returns null
129
+ * when the commit needs none. It runs only for a commit with events, again on every retry, and it
130
+ * must not fail or change the state.
131
+ */
132
+ export interface Projector<S> {
133
+ project(projection: Projection<S>): ProjectorGroup | null
106
134
  }
107
135
 
108
136
  /** A checked aggregate type. */
@@ -0,0 +1,102 @@
1
+ // The DynamoDB calls the repository makes, as a structural interface. The AWS SDK's aggregated
2
+ // `DynamoDB` client (`@aws-sdk/client-dynamodb`) satisfies it, so this package imports nothing
3
+ // from the SDK. Requests and responses use the low-level `AttributeValue` maps, never the document
4
+ // client's marshalling, because the storage format pins every attribute's type.
5
+
6
+ import type {Item} from './item.js'
7
+
8
+ /** The options the repository passes with every call. */
9
+ export interface RequestOptions {
10
+ abortSignal?: AbortSignal
11
+ }
12
+
13
+ /** One action of a `TransactWriteItems` call. */
14
+ export interface TransactItem {
15
+ ConditionCheck?: ConditionCheckAction
16
+ Put?: PutAction
17
+ Update?: UpdateAction
18
+ Delete?: DeleteAction
19
+ }
20
+
21
+ /** The expression parts an action or a query may carry. */
22
+ export interface Expressions {
23
+ ConditionExpression?: string
24
+ ExpressionAttributeNames?: Record<string, string>
25
+ ExpressionAttributeValues?: Item
26
+ }
27
+
28
+ export interface ConditionCheckAction extends Expressions {
29
+ TableName: string
30
+ Key: Item
31
+ ConditionExpression: string
32
+ }
33
+
34
+ export interface PutAction extends Expressions {
35
+ TableName: string
36
+ Item: Item
37
+ }
38
+
39
+ export interface UpdateAction extends Expressions {
40
+ TableName: string
41
+ Key: Item
42
+ UpdateExpression: string
43
+ }
44
+
45
+ export interface DeleteAction extends Expressions {
46
+ TableName: string
47
+ Key: Item
48
+ }
49
+
50
+ export interface GetItemInput {
51
+ TableName: string
52
+ Key: Item
53
+ ConsistentRead?: boolean
54
+ ProjectionExpression?: string
55
+ ExpressionAttributeNames?: Record<string, string>
56
+ }
57
+
58
+ export interface PutItemInput extends Expressions {
59
+ TableName: string
60
+ Item: Item
61
+ }
62
+
63
+ export interface QueryInput {
64
+ TableName: string
65
+ KeyConditionExpression: string
66
+ ExpressionAttributeNames?: Record<string, string>
67
+ ExpressionAttributeValues: Item
68
+ ConsistentRead?: boolean
69
+ ProjectionExpression?: string
70
+ ScanIndexForward?: boolean
71
+ Limit?: number
72
+ ExclusiveStartKey?: Item
73
+ }
74
+
75
+ export interface ScanInput {
76
+ TableName: string
77
+ ConsistentRead?: boolean
78
+ ProjectionExpression?: string
79
+ Limit?: number
80
+ ExclusiveStartKey?: Item
81
+ }
82
+
83
+ /** A page of a query or a scan. Items come back as the SDK decodes them. */
84
+ export interface Page {
85
+ Items?: Record<string, unknown>[] | undefined
86
+ LastEvaluatedKey?: Record<string, unknown> | undefined
87
+ }
88
+
89
+ /** The calls {@link DynamoRepository} makes: the SDK's `DynamoDB` client has them all. */
90
+ export interface DynamoClient {
91
+ getItem(
92
+ input: GetItemInput,
93
+ options?: RequestOptions,
94
+ ): Promise<{Item?: Record<string, unknown> | undefined}>
95
+ putItem(input: PutItemInput, options?: RequestOptions): Promise<unknown>
96
+ query(input: QueryInput, options?: RequestOptions): Promise<Page>
97
+ scan(input: ScanInput, options?: RequestOptions): Promise<Page>
98
+ transactWriteItems(
99
+ input: {TransactItems: TransactItem[]},
100
+ options?: RequestOptions,
101
+ ): Promise<unknown>
102
+ }
@@ -1,5 +1,5 @@
1
- // @photon-grove/evt/dynamodb: the DynamoDB backend. So far it holds the item codec, which imports
2
- // nothing from the AWS SDK.
1
+ // @photon-grove/evt/dynamodb: the DynamoDB backend. It imports nothing from the AWS SDK: the
2
+ // repository takes the SDK's `DynamoDB` client, or anything shaped like it, as a DynamoClient.
3
3
 
4
4
  export {
5
5
  type AttributeValue,
@@ -17,5 +17,30 @@ export {
17
17
  snapshotFromItem,
18
18
  snapshotItem,
19
19
  sortKey,
20
+ ttlFromItem,
20
21
  viewItem,
21
22
  } from './item.js'
23
+ export {
24
+ type ConditionCheckAction,
25
+ type DeleteAction,
26
+ type DynamoClient,
27
+ type Expressions,
28
+ type GetItemInput,
29
+ type Page,
30
+ type PutAction,
31
+ type PutItemInput,
32
+ type QueryInput,
33
+ type RequestOptions,
34
+ type ScanInput,
35
+ type TransactItem,
36
+ type UpdateAction,
37
+ } from './client.js'
38
+ export {FEATURES_PK, REBUILD_LOCK_PK, manifestFromItem, markerItem} from './marker.js'
39
+ export {DynamoRepository, type DynamoRepositoryOptions} from './repository.js'
40
+ export {DYNAMODB_BACKEND, transactItemSize, viewProjector} from './views.js'
41
+ export {
42
+ type CommitTtl,
43
+ type SnapshotTtl,
44
+ commitTransaction,
45
+ snapshotTransaction,
46
+ } from './requests.js'
@@ -151,12 +151,14 @@ export function snapshotFromItem(item: Item): SerializedSnapshot {
151
151
 
152
152
  /** Decodes a head item into its head and its `ttl`, if it has one. */
153
153
  export function headFromItem(item: Item): {head: bigint; ttl: number | undefined} {
154
+ return {head: requiredNumber(item, 'head'), ttl: ttlFromItem(item)}
155
+ }
156
+
157
+ /** An item's `ttl` in Unix seconds, or undefined when it has none. */
158
+ export function ttlFromItem(item: Item): number | undefined {
154
159
  const ttl = optionalNumber(item, 'ttl')
155
160
 
156
- return {
157
- head: requiredNumber(item, 'head'),
158
- ttl: ttl === undefined ? undefined : safeInteger(ttl, 'ttl'),
159
- }
161
+ return ttl === undefined ? undefined : safeInteger(ttl, 'ttl')
160
162
  }
161
163
 
162
164
  /** Whether an item is a stream's inline snapshot (`sk = 0`). */
@@ -0,0 +1,64 @@
1
+ // The DynamoDB item of the feature-marker row. The control rows live in the reserved SYSTEM#
2
+ // namespace at sort key 0.
3
+ //
4
+ // | Attribute | Type | Feature-marker item (`pk = SYSTEM#FEATURES`, `sk = 0`) |
5
+ // | ------------------ | ---- | ------------------------------------------------------------ |
6
+ // | `version` | N | bumped on every change to the manifest |
7
+ // | `features` | S | JSON object, feature name → stored-format version, sorted |
8
+ // | `historyDependent` | S | JSON array of entity types, sorted. Absent when empty |
9
+ // | `retention` | S | JSON array of entity types, sorted. Absent when empty |
10
+
11
+ import {checkInt64, invalid} from '../fields.js'
12
+ import {type FeatureManifest, decodeManifest, encodeManifest} from '../features.js'
13
+ import {type Item, SNAPSHOT_SORT_KEY} from './item.js'
14
+
15
+ /** The partition key of the feature-marker row. */
16
+ export const FEATURES_PK = 'SYSTEM#FEATURES'
17
+
18
+ /** The partition key of the rebuild-lock row, which every commit checks is absent. */
19
+ export const REBUILD_LOCK_PK = 'SYSTEM#REBUILD'
20
+
21
+ /** Encodes a feature manifest as its marker item. Empty entity-type sets are left out. */
22
+ export function markerItem(manifest: FeatureManifest): Item {
23
+ const texts = encodeManifest(manifest)
24
+ const item: Item = {
25
+ pk: {S: FEATURES_PK},
26
+ sk: {N: SNAPSHOT_SORT_KEY.toString()},
27
+ version: {N: manifest.version.toString()},
28
+ features: {S: texts.features},
29
+ }
30
+ if (manifest.historyDependent.length > 0) {
31
+ item['historyDependent'] = {S: texts.historyDependent}
32
+ }
33
+ if (manifest.retention.length > 0) {
34
+ item['retention'] = {S: texts.retention}
35
+ }
36
+
37
+ return item
38
+ }
39
+
40
+ /** Decodes a marker item. An absent text attribute is an empty map or set. */
41
+ export function manifestFromItem(item: Item): FeatureManifest {
42
+ const version = item['version']
43
+ if (version === undefined || !('N' in version) || !/^\d+$/.test(version.N)) {
44
+ throw invalid('the feature marker has no version')
45
+ }
46
+
47
+ return decodeManifest(checkInt64(BigInt(version.N), 'the feature marker version'), {
48
+ features: text(item, 'features'),
49
+ historyDependent: text(item, 'historyDependent'),
50
+ retention: text(item, 'retention'),
51
+ })
52
+ }
53
+
54
+ function text(item: Item, name: string): string {
55
+ const value = item[name]
56
+ if (value === undefined) {
57
+ return ''
58
+ }
59
+ if (!('S' in value)) {
60
+ throw invalid(`the feature marker's ${name} attribute is not a string`)
61
+ }
62
+
63
+ return value.S
64
+ }