@durable-streams/state 0.2.9 → 0.3.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.
- package/README.md +12 -2
- package/dist/db.cjs +561 -0
- package/dist/db.d.cts +152 -0
- package/dist/db.d.ts +152 -0
- package/dist/db.js +364 -0
- package/dist/index-CqdIsdQy.d.cts +173 -0
- package/dist/index-D6Nak3Wl.d.ts +173 -0
- package/dist/index.cjs +5 -758
- package/dist/index.d.cts +2 -315
- package/dist/index.d.ts +2 -315
- package/dist/index.js +2 -561
- package/dist/src-AIE5IYwJ.cjs +228 -0
- package/dist/src-VTyL9Eij.js +203 -0
- package/package.json +16 -3
- package/skills/stream-db/SKILL.md +3 -3
- package/src/db.ts +61 -0
- package/src/index.ts +12 -55
- package/src/schema.ts +265 -0
- package/src/stream-db.ts +13 -254
package/src/schema.ts
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from "@standard-schema/spec"
|
|
2
|
+
import type { ChangeEvent } from "./types"
|
|
3
|
+
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// Schema Definitions
|
|
6
|
+
//
|
|
7
|
+
// This module is intentionally free of any @tanstack/db dependency: it covers
|
|
8
|
+
// the producer side of the state protocol (defining schemas and constructing
|
|
9
|
+
// validated change events). The reactive, TanStack DB-backed surface lives in
|
|
10
|
+
// ./stream-db and is published under the `@durable-streams/state/db` subpath.
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Definition for a single collection in the stream state
|
|
15
|
+
*/
|
|
16
|
+
export interface CollectionDefinition<T = unknown> {
|
|
17
|
+
/** Standard Schema for validating values */
|
|
18
|
+
schema: StandardSchemaV1<T>
|
|
19
|
+
/** The type field value in change events that map to this collection */
|
|
20
|
+
type: string
|
|
21
|
+
/** The property name in T that serves as the primary key */
|
|
22
|
+
primaryKey: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Helper methods for creating change events for a collection
|
|
27
|
+
*/
|
|
28
|
+
export interface CollectionEventHelpers<T> {
|
|
29
|
+
/**
|
|
30
|
+
* Create an insert change event
|
|
31
|
+
*/
|
|
32
|
+
insert: (params: {
|
|
33
|
+
key?: string
|
|
34
|
+
value: T
|
|
35
|
+
headers?: Omit<Record<string, string>, `operation`>
|
|
36
|
+
}) => ChangeEvent<T>
|
|
37
|
+
/**
|
|
38
|
+
* Create an update change event
|
|
39
|
+
*/
|
|
40
|
+
update: (params: {
|
|
41
|
+
key?: string
|
|
42
|
+
value: T
|
|
43
|
+
oldValue?: T
|
|
44
|
+
headers?: Omit<Record<string, string>, `operation`>
|
|
45
|
+
}) => ChangeEvent<T>
|
|
46
|
+
/**
|
|
47
|
+
* Create a delete change event
|
|
48
|
+
*/
|
|
49
|
+
delete: (params: {
|
|
50
|
+
key?: string
|
|
51
|
+
oldValue?: T
|
|
52
|
+
headers?: Omit<Record<string, string>, `operation`>
|
|
53
|
+
}) => ChangeEvent<T>
|
|
54
|
+
/**
|
|
55
|
+
* Create an upsert change event (insert or update)
|
|
56
|
+
*/
|
|
57
|
+
upsert: (params: {
|
|
58
|
+
key?: string
|
|
59
|
+
value: T
|
|
60
|
+
headers?: Omit<Record<string, string>, `operation`>
|
|
61
|
+
}) => ChangeEvent<T>
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Collection definition enhanced with event creation helpers
|
|
66
|
+
*/
|
|
67
|
+
export type CollectionWithHelpers<T = unknown> = CollectionDefinition<T> &
|
|
68
|
+
CollectionEventHelpers<T>
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Stream state definition containing all collections
|
|
72
|
+
*/
|
|
73
|
+
export type StreamStateDefinition = Record<string, CollectionDefinition>
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Stream state schema with helper methods for creating change events
|
|
77
|
+
*/
|
|
78
|
+
export type StateSchema<T extends Record<string, CollectionDefinition>> = {
|
|
79
|
+
[K in keyof T]: CollectionWithHelpers<
|
|
80
|
+
T[K] extends CollectionDefinition<infer U> ? U : unknown
|
|
81
|
+
>
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Reserved collection names that would collide with StreamDB properties
|
|
86
|
+
* (collections are now namespaced, but we still prevent internal name collisions)
|
|
87
|
+
*/
|
|
88
|
+
const RESERVED_COLLECTION_NAMES = new Set([
|
|
89
|
+
`collections`,
|
|
90
|
+
`preload`,
|
|
91
|
+
`close`,
|
|
92
|
+
`utils`,
|
|
93
|
+
`actions`,
|
|
94
|
+
])
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Create helper functions for a collection
|
|
98
|
+
*/
|
|
99
|
+
function createCollectionHelpers<T>(
|
|
100
|
+
eventType: string,
|
|
101
|
+
primaryKey: string,
|
|
102
|
+
schema: StandardSchemaV1<T>
|
|
103
|
+
): CollectionEventHelpers<T> {
|
|
104
|
+
return {
|
|
105
|
+
insert: ({ key, value, headers }): ChangeEvent<T> => {
|
|
106
|
+
// Validate value
|
|
107
|
+
const result = schema[`~standard`].validate(value)
|
|
108
|
+
if (`issues` in result) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
`Validation failed for ${eventType} insert: ${result.issues?.map((i) => i.message).join(`, `) ?? `Unknown validation error`}`
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Derive key from value if not explicitly provided
|
|
115
|
+
const derived = (value as any)[primaryKey]
|
|
116
|
+
const finalKey =
|
|
117
|
+
key ?? (derived != null && derived !== `` ? String(derived) : undefined)
|
|
118
|
+
if (finalKey == null || finalKey === ``) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
`Cannot create ${eventType} insert event: must provide either 'key' or a value with a non-empty '${primaryKey}' field`
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
type: eventType,
|
|
126
|
+
key: finalKey,
|
|
127
|
+
value,
|
|
128
|
+
headers: { ...headers, operation: `insert` },
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
update: ({ key, value, oldValue, headers }): ChangeEvent<T> => {
|
|
132
|
+
// Validate value
|
|
133
|
+
const result = schema[`~standard`].validate(value)
|
|
134
|
+
if (`issues` in result) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`Validation failed for ${eventType} update: ${result.issues?.map((i) => i.message).join(`, `) ?? `Unknown validation error`}`
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Optionally validate oldValue if provided
|
|
141
|
+
if (oldValue !== undefined) {
|
|
142
|
+
const oldResult = schema[`~standard`].validate(oldValue)
|
|
143
|
+
if (`issues` in oldResult) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`Validation failed for ${eventType} update (oldValue): ${oldResult.issues?.map((i) => i.message).join(`, `) ?? `Unknown validation error`}`
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Derive key from value if not explicitly provided
|
|
151
|
+
const derived = (value as any)[primaryKey]
|
|
152
|
+
const finalKey =
|
|
153
|
+
key ?? (derived != null && derived !== `` ? String(derived) : undefined)
|
|
154
|
+
if (finalKey == null || finalKey === ``) {
|
|
155
|
+
throw new Error(
|
|
156
|
+
`Cannot create ${eventType} update event: must provide either 'key' or a value with a non-empty '${primaryKey}' field`
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
type: eventType,
|
|
162
|
+
key: finalKey,
|
|
163
|
+
value,
|
|
164
|
+
old_value: oldValue,
|
|
165
|
+
headers: { ...headers, operation: `update` },
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
delete: ({ key, oldValue, headers }): ChangeEvent<T> => {
|
|
169
|
+
// Optionally validate oldValue if provided
|
|
170
|
+
if (oldValue !== undefined) {
|
|
171
|
+
const result = schema[`~standard`].validate(oldValue)
|
|
172
|
+
if (`issues` in result) {
|
|
173
|
+
throw new Error(
|
|
174
|
+
`Validation failed for ${eventType} delete (oldValue): ${result.issues?.map((i) => i.message).join(`, `) ?? `Unknown validation error`}`
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Ensure we have either key or oldValue to derive the key from
|
|
180
|
+
const finalKey =
|
|
181
|
+
key ?? (oldValue ? String((oldValue as any)[primaryKey]) : undefined)
|
|
182
|
+
if (!finalKey) {
|
|
183
|
+
throw new Error(
|
|
184
|
+
`Cannot create ${eventType} delete event: must provide either 'key' or 'oldValue' with a ${primaryKey} field`
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
type: eventType,
|
|
190
|
+
key: finalKey,
|
|
191
|
+
old_value: oldValue,
|
|
192
|
+
headers: { ...headers, operation: `delete` },
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
upsert: ({ key, value, headers }): ChangeEvent<T> => {
|
|
196
|
+
// Validate value
|
|
197
|
+
const result = schema[`~standard`].validate(value)
|
|
198
|
+
if (`issues` in result) {
|
|
199
|
+
throw new Error(
|
|
200
|
+
`Validation failed for ${eventType} upsert: ${result.issues?.map((i) => i.message).join(`, `) ?? `Unknown validation error`}`
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Derive key from value if not explicitly provided
|
|
205
|
+
const derived = (value as any)[primaryKey]
|
|
206
|
+
const finalKey =
|
|
207
|
+
key ?? (derived != null && derived !== `` ? String(derived) : undefined)
|
|
208
|
+
if (finalKey == null || finalKey === ``) {
|
|
209
|
+
throw new Error(
|
|
210
|
+
`Cannot create ${eventType} upsert event: must provide either 'key' or a value with a non-empty '${primaryKey}' field`
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
type: eventType,
|
|
216
|
+
key: finalKey,
|
|
217
|
+
value,
|
|
218
|
+
headers: { ...headers, operation: `upsert` },
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Create a state schema definition with typed collections and event helpers
|
|
226
|
+
*/
|
|
227
|
+
export function createStateSchema<
|
|
228
|
+
T extends Record<string, CollectionDefinition>,
|
|
229
|
+
>(collections: T): StateSchema<T> {
|
|
230
|
+
// Validate no reserved collection names
|
|
231
|
+
for (const name of Object.keys(collections)) {
|
|
232
|
+
if (RESERVED_COLLECTION_NAMES.has(name)) {
|
|
233
|
+
throw new Error(
|
|
234
|
+
`Reserved collection name "${name}" - this would collide with StreamDB properties (${Array.from(RESERVED_COLLECTION_NAMES).join(`, `)})`
|
|
235
|
+
)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Validate no duplicate event types
|
|
240
|
+
const typeToCollection = new Map<string, string>()
|
|
241
|
+
for (const [collectionName, def] of Object.entries(collections)) {
|
|
242
|
+
const existing = typeToCollection.get(def.type)
|
|
243
|
+
if (existing) {
|
|
244
|
+
throw new Error(
|
|
245
|
+
`Duplicate event type "${def.type}" - used by both "${existing}" and "${collectionName}" collections`
|
|
246
|
+
)
|
|
247
|
+
}
|
|
248
|
+
typeToCollection.set(def.type, collectionName)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Enhance collections with helper methods
|
|
252
|
+
const enhancedCollections: any = {}
|
|
253
|
+
for (const [name, collectionDef] of Object.entries(collections)) {
|
|
254
|
+
enhancedCollections[name] = {
|
|
255
|
+
...collectionDef,
|
|
256
|
+
...createCollectionHelpers(
|
|
257
|
+
collectionDef.type,
|
|
258
|
+
collectionDef.primaryKey,
|
|
259
|
+
collectionDef.schema
|
|
260
|
+
),
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return enhancedCollections
|
|
265
|
+
}
|
package/src/stream-db.ts
CHANGED
|
@@ -15,82 +15,24 @@ import type {
|
|
|
15
15
|
LiveMode,
|
|
16
16
|
StreamResponse,
|
|
17
17
|
} from "@durable-streams/client"
|
|
18
|
+
import type { CollectionDefinition, StreamStateDefinition } from "./schema"
|
|
19
|
+
|
|
20
|
+
// Schema definitions and event construction are db-free and live in ./schema.
|
|
21
|
+
// Re-export them here so the TanStack-backed `@durable-streams/state/db`
|
|
22
|
+
// surface stays a superset of the db-free main entry.
|
|
23
|
+
export { createStateSchema } from "./schema"
|
|
24
|
+
export type {
|
|
25
|
+
CollectionDefinition,
|
|
26
|
+
CollectionEventHelpers,
|
|
27
|
+
CollectionWithHelpers,
|
|
28
|
+
StreamStateDefinition,
|
|
29
|
+
StateSchema,
|
|
30
|
+
} from "./schema"
|
|
18
31
|
|
|
19
32
|
// ============================================================================
|
|
20
33
|
// Type Definitions
|
|
21
34
|
// ============================================================================
|
|
22
35
|
|
|
23
|
-
/**
|
|
24
|
-
* Definition for a single collection in the stream state
|
|
25
|
-
*/
|
|
26
|
-
export interface CollectionDefinition<T = unknown> {
|
|
27
|
-
/** Standard Schema for validating values */
|
|
28
|
-
schema: StandardSchemaV1<T>
|
|
29
|
-
/** The type field value in change events that map to this collection */
|
|
30
|
-
type: string
|
|
31
|
-
/** The property name in T that serves as the primary key */
|
|
32
|
-
primaryKey: string
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Helper methods for creating change events for a collection
|
|
37
|
-
*/
|
|
38
|
-
export interface CollectionEventHelpers<T> {
|
|
39
|
-
/**
|
|
40
|
-
* Create an insert change event
|
|
41
|
-
*/
|
|
42
|
-
insert: (params: {
|
|
43
|
-
key?: string
|
|
44
|
-
value: T
|
|
45
|
-
headers?: Omit<Record<string, string>, `operation`>
|
|
46
|
-
}) => ChangeEvent<T>
|
|
47
|
-
/**
|
|
48
|
-
* Create an update change event
|
|
49
|
-
*/
|
|
50
|
-
update: (params: {
|
|
51
|
-
key?: string
|
|
52
|
-
value: T
|
|
53
|
-
oldValue?: T
|
|
54
|
-
headers?: Omit<Record<string, string>, `operation`>
|
|
55
|
-
}) => ChangeEvent<T>
|
|
56
|
-
/**
|
|
57
|
-
* Create a delete change event
|
|
58
|
-
*/
|
|
59
|
-
delete: (params: {
|
|
60
|
-
key?: string
|
|
61
|
-
oldValue?: T
|
|
62
|
-
headers?: Omit<Record<string, string>, `operation`>
|
|
63
|
-
}) => ChangeEvent<T>
|
|
64
|
-
/**
|
|
65
|
-
* Create an upsert change event (insert or update)
|
|
66
|
-
*/
|
|
67
|
-
upsert: (params: {
|
|
68
|
-
key?: string
|
|
69
|
-
value: T
|
|
70
|
-
headers?: Omit<Record<string, string>, `operation`>
|
|
71
|
-
}) => ChangeEvent<T>
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Collection definition enhanced with event creation helpers
|
|
76
|
-
*/
|
|
77
|
-
export type CollectionWithHelpers<T = unknown> = CollectionDefinition<T> &
|
|
78
|
-
CollectionEventHelpers<T>
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Stream state definition containing all collections
|
|
82
|
-
*/
|
|
83
|
-
export type StreamStateDefinition = Record<string, CollectionDefinition>
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Stream state schema with helper methods for creating change events
|
|
87
|
-
*/
|
|
88
|
-
export type StateSchema<T extends Record<string, CollectionDefinition>> = {
|
|
89
|
-
[K in keyof T]: CollectionWithHelpers<
|
|
90
|
-
T[K] extends CollectionDefinition<infer U> ? U : unknown
|
|
91
|
-
>
|
|
92
|
-
}
|
|
93
|
-
|
|
94
36
|
/**
|
|
95
37
|
* Definition for a single action that can be passed to createOptimisticAction
|
|
96
38
|
*/
|
|
@@ -619,189 +561,6 @@ function createStreamSyncConfig<T extends object>(
|
|
|
619
561
|
// Main Implementation
|
|
620
562
|
// ============================================================================
|
|
621
563
|
|
|
622
|
-
/**
|
|
623
|
-
* Reserved collection names that would collide with StreamDB properties
|
|
624
|
-
* (collections are now namespaced, but we still prevent internal name collisions)
|
|
625
|
-
*/
|
|
626
|
-
const RESERVED_COLLECTION_NAMES = new Set([
|
|
627
|
-
`collections`,
|
|
628
|
-
`preload`,
|
|
629
|
-
`close`,
|
|
630
|
-
`utils`,
|
|
631
|
-
`actions`,
|
|
632
|
-
])
|
|
633
|
-
|
|
634
|
-
/**
|
|
635
|
-
* Create helper functions for a collection
|
|
636
|
-
*/
|
|
637
|
-
function createCollectionHelpers<T>(
|
|
638
|
-
eventType: string,
|
|
639
|
-
primaryKey: string,
|
|
640
|
-
schema: StandardSchemaV1<T>
|
|
641
|
-
): CollectionEventHelpers<T> {
|
|
642
|
-
return {
|
|
643
|
-
insert: ({ key, value, headers }): ChangeEvent<T> => {
|
|
644
|
-
// Validate value
|
|
645
|
-
const result = schema[`~standard`].validate(value)
|
|
646
|
-
if (`issues` in result) {
|
|
647
|
-
throw new Error(
|
|
648
|
-
`Validation failed for ${eventType} insert: ${result.issues?.map((i) => i.message).join(`, `) ?? `Unknown validation error`}`
|
|
649
|
-
)
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
// Derive key from value if not explicitly provided
|
|
653
|
-
const derived = (value as any)[primaryKey]
|
|
654
|
-
const finalKey =
|
|
655
|
-
key ?? (derived != null && derived !== `` ? String(derived) : undefined)
|
|
656
|
-
if (finalKey == null || finalKey === ``) {
|
|
657
|
-
throw new Error(
|
|
658
|
-
`Cannot create ${eventType} insert event: must provide either 'key' or a value with a non-empty '${primaryKey}' field`
|
|
659
|
-
)
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
return {
|
|
663
|
-
type: eventType,
|
|
664
|
-
key: finalKey,
|
|
665
|
-
value,
|
|
666
|
-
headers: { ...headers, operation: `insert` },
|
|
667
|
-
}
|
|
668
|
-
},
|
|
669
|
-
update: ({ key, value, oldValue, headers }): ChangeEvent<T> => {
|
|
670
|
-
// Validate value
|
|
671
|
-
const result = schema[`~standard`].validate(value)
|
|
672
|
-
if (`issues` in result) {
|
|
673
|
-
throw new Error(
|
|
674
|
-
`Validation failed for ${eventType} update: ${result.issues?.map((i) => i.message).join(`, `) ?? `Unknown validation error`}`
|
|
675
|
-
)
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
// Optionally validate oldValue if provided
|
|
679
|
-
if (oldValue !== undefined) {
|
|
680
|
-
const oldResult = schema[`~standard`].validate(oldValue)
|
|
681
|
-
if (`issues` in oldResult) {
|
|
682
|
-
throw new Error(
|
|
683
|
-
`Validation failed for ${eventType} update (oldValue): ${oldResult.issues?.map((i) => i.message).join(`, `) ?? `Unknown validation error`}`
|
|
684
|
-
)
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
// Derive key from value if not explicitly provided
|
|
689
|
-
const derived = (value as any)[primaryKey]
|
|
690
|
-
const finalKey =
|
|
691
|
-
key ?? (derived != null && derived !== `` ? String(derived) : undefined)
|
|
692
|
-
if (finalKey == null || finalKey === ``) {
|
|
693
|
-
throw new Error(
|
|
694
|
-
`Cannot create ${eventType} update event: must provide either 'key' or a value with a non-empty '${primaryKey}' field`
|
|
695
|
-
)
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
return {
|
|
699
|
-
type: eventType,
|
|
700
|
-
key: finalKey,
|
|
701
|
-
value,
|
|
702
|
-
old_value: oldValue,
|
|
703
|
-
headers: { ...headers, operation: `update` },
|
|
704
|
-
}
|
|
705
|
-
},
|
|
706
|
-
delete: ({ key, oldValue, headers }): ChangeEvent<T> => {
|
|
707
|
-
// Optionally validate oldValue if provided
|
|
708
|
-
if (oldValue !== undefined) {
|
|
709
|
-
const result = schema[`~standard`].validate(oldValue)
|
|
710
|
-
if (`issues` in result) {
|
|
711
|
-
throw new Error(
|
|
712
|
-
`Validation failed for ${eventType} delete (oldValue): ${result.issues?.map((i) => i.message).join(`, `) ?? `Unknown validation error`}`
|
|
713
|
-
)
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
// Ensure we have either key or oldValue to derive the key from
|
|
718
|
-
const finalKey =
|
|
719
|
-
key ?? (oldValue ? String((oldValue as any)[primaryKey]) : undefined)
|
|
720
|
-
if (!finalKey) {
|
|
721
|
-
throw new Error(
|
|
722
|
-
`Cannot create ${eventType} delete event: must provide either 'key' or 'oldValue' with a ${primaryKey} field`
|
|
723
|
-
)
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
return {
|
|
727
|
-
type: eventType,
|
|
728
|
-
key: finalKey,
|
|
729
|
-
old_value: oldValue,
|
|
730
|
-
headers: { ...headers, operation: `delete` },
|
|
731
|
-
}
|
|
732
|
-
},
|
|
733
|
-
upsert: ({ key, value, headers }): ChangeEvent<T> => {
|
|
734
|
-
// Validate value
|
|
735
|
-
const result = schema[`~standard`].validate(value)
|
|
736
|
-
if (`issues` in result) {
|
|
737
|
-
throw new Error(
|
|
738
|
-
`Validation failed for ${eventType} upsert: ${result.issues?.map((i) => i.message).join(`, `) ?? `Unknown validation error`}`
|
|
739
|
-
)
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
// Derive key from value if not explicitly provided
|
|
743
|
-
const derived = (value as any)[primaryKey]
|
|
744
|
-
const finalKey =
|
|
745
|
-
key ?? (derived != null && derived !== `` ? String(derived) : undefined)
|
|
746
|
-
if (finalKey == null || finalKey === ``) {
|
|
747
|
-
throw new Error(
|
|
748
|
-
`Cannot create ${eventType} upsert event: must provide either 'key' or a value with a non-empty '${primaryKey}' field`
|
|
749
|
-
)
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
return {
|
|
753
|
-
type: eventType,
|
|
754
|
-
key: finalKey,
|
|
755
|
-
value,
|
|
756
|
-
headers: { ...headers, operation: `upsert` },
|
|
757
|
-
}
|
|
758
|
-
},
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
/**
|
|
763
|
-
* Create a state schema definition with typed collections and event helpers
|
|
764
|
-
*/
|
|
765
|
-
export function createStateSchema<
|
|
766
|
-
T extends Record<string, CollectionDefinition>,
|
|
767
|
-
>(collections: T): StateSchema<T> {
|
|
768
|
-
// Validate no reserved collection names
|
|
769
|
-
for (const name of Object.keys(collections)) {
|
|
770
|
-
if (RESERVED_COLLECTION_NAMES.has(name)) {
|
|
771
|
-
throw new Error(
|
|
772
|
-
`Reserved collection name "${name}" - this would collide with StreamDB properties (${Array.from(RESERVED_COLLECTION_NAMES).join(`, `)})`
|
|
773
|
-
)
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
// Validate no duplicate event types
|
|
778
|
-
const typeToCollection = new Map<string, string>()
|
|
779
|
-
for (const [collectionName, def] of Object.entries(collections)) {
|
|
780
|
-
const existing = typeToCollection.get(def.type)
|
|
781
|
-
if (existing) {
|
|
782
|
-
throw new Error(
|
|
783
|
-
`Duplicate event type "${def.type}" - used by both "${existing}" and "${collectionName}" collections`
|
|
784
|
-
)
|
|
785
|
-
}
|
|
786
|
-
typeToCollection.set(def.type, collectionName)
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
// Enhance collections with helper methods
|
|
790
|
-
const enhancedCollections: any = {}
|
|
791
|
-
for (const [name, collectionDef] of Object.entries(collections)) {
|
|
792
|
-
enhancedCollections[name] = {
|
|
793
|
-
...collectionDef,
|
|
794
|
-
...createCollectionHelpers(
|
|
795
|
-
collectionDef.type,
|
|
796
|
-
collectionDef.primaryKey,
|
|
797
|
-
collectionDef.schema
|
|
798
|
-
),
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
return enhancedCollections
|
|
803
|
-
}
|
|
804
|
-
|
|
805
564
|
/**
|
|
806
565
|
* Create a stream-backed database with TanStack DB collections
|
|
807
566
|
*
|