@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.
- package/README.md +85 -9
- package/dist/aggregate.d.ts +26 -0
- package/dist/aggregate.d.ts.map +1 -1
- package/dist/aggregate.js.map +1 -1
- package/dist/dynamodb/client.d.ts +83 -0
- package/dist/dynamodb/client.d.ts.map +1 -0
- package/dist/dynamodb/client.js +6 -0
- package/dist/dynamodb/client.js.map +1 -0
- package/dist/dynamodb/index.d.ts +6 -1
- package/dist/dynamodb/index.d.ts.map +1 -1
- package/dist/dynamodb/index.js +8 -3
- package/dist/dynamodb/index.js.map +1 -1
- package/dist/dynamodb/item.d.ts +2 -0
- package/dist/dynamodb/item.d.ts.map +1 -1
- package/dist/dynamodb/item.js +5 -4
- package/dist/dynamodb/item.js.map +1 -1
- package/dist/dynamodb/marker.d.ts +11 -0
- package/dist/dynamodb/marker.d.ts.map +1 -0
- package/dist/dynamodb/marker.js +56 -0
- package/dist/dynamodb/marker.js.map +1 -0
- package/dist/dynamodb/repository.d.ts +61 -0
- package/dist/dynamodb/repository.d.ts.map +1 -0
- package/dist/dynamodb/repository.js +748 -0
- package/dist/dynamodb/repository.js.map +1 -0
- package/dist/dynamodb/requests.d.ts +68 -0
- package/dist/dynamodb/requests.d.ts.map +1 -0
- package/dist/dynamodb/requests.js +194 -0
- package/dist/dynamodb/requests.js.map +1 -0
- package/dist/dynamodb/views.d.ts +18 -0
- package/dist/dynamodb/views.d.ts.map +1 -0
- package/dist/dynamodb/views.js +56 -0
- package/dist/dynamodb/views.js.map +1 -0
- package/dist/errors.d.ts +109 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +118 -2
- package/dist/errors.js.map +1 -1
- package/dist/features.d.ts +55 -0
- package/dist/features.d.ts.map +1 -0
- package/dist/features.js +126 -0
- package/dist/features.js.map +1 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/memory/index.d.ts +1 -1
- package/dist/memory/index.d.ts.map +1 -1
- package/dist/memory/index.js +1 -1
- package/dist/memory/index.js.map +1 -1
- package/dist/memory/repository.d.ts +38 -2
- package/dist/memory/repository.d.ts.map +1 -1
- package/dist/memory/repository.js +95 -6
- package/dist/memory/repository.js.map +1 -1
- package/dist/postgres/client.d.ts +32 -0
- package/dist/postgres/client.d.ts.map +1 -0
- package/dist/postgres/client.js +4 -0
- package/dist/postgres/client.js.map +1 -0
- package/dist/postgres/index.d.ts +4 -0
- package/dist/postgres/index.d.ts.map +1 -0
- package/dist/postgres/index.js +6 -0
- package/dist/postgres/index.js.map +1 -0
- package/dist/postgres/repository.d.ts +55 -0
- package/dist/postgres/repository.d.ts.map +1 -0
- package/dist/postgres/repository.js +527 -0
- package/dist/postgres/repository.js.map +1 -0
- package/dist/postgres/schema.d.ts +81 -0
- package/dist/postgres/schema.d.ts.map +1 -0
- package/dist/postgres/schema.js +233 -0
- package/dist/postgres/schema.js.map +1 -0
- package/dist/repository.d.ts +56 -7
- package/dist/repository.d.ts.map +1 -1
- package/dist/repository.js +24 -9
- package/dist/repository.js.map +1 -1
- package/dist/store.d.ts +7 -5
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +34 -9
- package/dist/store.js.map +1 -1
- package/package.json +16 -3
- package/src/aggregate.ts +28 -0
- package/src/dynamodb/client.ts +102 -0
- package/src/dynamodb/index.ts +27 -2
- package/src/dynamodb/item.ts +6 -4
- package/src/dynamodb/marker.ts +64 -0
- package/src/dynamodb/repository.ts +1053 -0
- package/src/dynamodb/requests.ts +276 -0
- package/src/dynamodb/views.ts +67 -0
- package/src/errors.ts +176 -2
- package/src/features.ts +175 -0
- package/src/index.ts +28 -1
- package/src/memory/index.ts +9 -1
- package/src/memory/repository.ts +156 -6
- package/src/postgres/client.ts +37 -0
- package/src/postgres/index.ts +23 -0
- package/src/postgres/repository.ts +773 -0
- package/src/postgres/schema.ts +286 -0
- package/src/repository.ts +86 -13
- 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.
|
|
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
|
-
"@
|
|
54
|
-
"
|
|
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
|
+
}
|
package/src/dynamodb/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// @photon-grove/evt/dynamodb: the DynamoDB backend.
|
|
2
|
-
//
|
|
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'
|
package/src/dynamodb/item.ts
CHANGED
|
@@ -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
|
+
}
|