@@ -9,13 +9,21 @@ import {
9
9
type PendingWrite ,
10
10
type CheckpointMetadata ,
11
11
CheckpointPendingWrite ,
12
+ validCheckpointMetadataKeys ,
12
13
} from "@langchain/langgraph-checkpoint" ;
14
+ import { applyMigrations , needsMigration } from "./migrations/index.js" ;
15
+
16
+ export * from "./migrations/index.js" ;
17
+
18
+ // increment this whenever the structure of the database changes in a way that would require a migration
19
+ const CURRENT_SCHEMA_VERSION = 1 ;
13
20
14
21
export type MongoDBSaverParams = {
15
22
client : MongoClient ;
16
23
dbName ?: string ;
17
24
checkpointCollectionName ?: string ;
18
25
checkpointWritesCollectionName ?: string ;
26
+ schemaVersionCollectionName ?: string ;
19
27
} ;
20
28
21
29
/**
@@ -26,16 +34,21 @@ export class MongoDBSaver extends BaseCheckpointSaver {
26
34
27
35
protected db : MongoDatabase ;
28
36
37
+ private setupPromise : Promise < void > | undefined ;
38
+
29
39
checkpointCollectionName = "checkpoints" ;
30
40
31
41
checkpointWritesCollectionName = "checkpoint_writes" ;
32
42
43
+ schemaVersionCollectionName = "schema_version" ;
44
+
33
45
constructor (
34
46
{
35
47
client,
36
48
dbName,
37
49
checkpointCollectionName,
38
50
checkpointWritesCollectionName,
51
+ schemaVersionCollectionName,
39
52
} : MongoDBSaverParams ,
40
53
serde ?: SerializerProtocol
41
54
) {
@@ -46,6 +59,118 @@ export class MongoDBSaver extends BaseCheckpointSaver {
46
59
checkpointCollectionName ?? this . checkpointCollectionName ;
47
60
this . checkpointWritesCollectionName =
48
61
checkpointWritesCollectionName ?? this . checkpointWritesCollectionName ;
62
+ this . schemaVersionCollectionName =
63
+ schemaVersionCollectionName ?? this . schemaVersionCollectionName ;
64
+ }
65
+
66
+ /**
67
+ * Runs async setup tasks if they haven't been run yet.
68
+ */
69
+ async setup ( ) : Promise < void > {
70
+ if ( this . setupPromise ) {
71
+ return this . setupPromise ;
72
+ }
73
+ this . setupPromise = this . initializeSchemaVersion ( ) ;
74
+ return this . setupPromise ;
75
+ }
76
+
77
+ private async isDatabaseEmpty ( ) : Promise < boolean > {
78
+ const results = await Promise . all (
79
+ [ this . checkpointCollectionName , this . checkpointWritesCollectionName ] . map (
80
+ async ( collectionName ) => {
81
+ const collection = this . db . collection ( collectionName ) ;
82
+ // set a limit of 1 to stop scanning if any documents are found
83
+ const count = await collection . countDocuments ( { } , { limit : 1 } ) ;
84
+ return count === 0 ;
85
+ }
86
+ )
87
+ ) ;
88
+
89
+ return results . every ( ( result ) => result ) ;
90
+ }
91
+
92
+ private async initializeSchemaVersion ( ) : Promise < void > {
93
+ const schemaVersionCollection = this . db . collection (
94
+ this . schemaVersionCollectionName
95
+ ) ;
96
+
97
+ // empty database, no migrations needed - just set the schema version and move on
98
+ if ( await this . isDatabaseEmpty ( ) ) {
99
+ const schemaVersionCollection = this . db . collection (
100
+ this . schemaVersionCollectionName
101
+ ) ;
102
+
103
+ const versionDoc = await schemaVersionCollection . findOne ( { } ) ;
104
+ if ( ! versionDoc ) {
105
+ await schemaVersionCollection . insertOne ( {
106
+ version : CURRENT_SCHEMA_VERSION ,
107
+ } ) ;
108
+ }
109
+ } else {
110
+ // non-empty database, check if migrations are needed
111
+ const dbNeedsMigration = await needsMigration ( {
112
+ client : this . client ,
113
+ dbName : this . db . databaseName ,
114
+ checkpointCollectionName : this . checkpointCollectionName ,
115
+ checkpointWritesCollectionName : this . checkpointWritesCollectionName ,
116
+ schemaVersionCollectionName : this . schemaVersionCollectionName ,
117
+ serializer : this . serde ,
118
+ currentSchemaVersion : CURRENT_SCHEMA_VERSION ,
119
+ } ) ;
120
+
121
+ if ( dbNeedsMigration ) {
122
+ throw new Error (
123
+ `Database needs migration. Call the migrate() method to migrate the database.`
124
+ ) ;
125
+ }
126
+
127
+ // always defined if dbNeedsMigration is false
128
+ const versionDoc = ( await schemaVersionCollection . findOne ( { } ) ) ! ;
129
+
130
+ if ( versionDoc . version == null ) {
131
+ throw new Error (
132
+ `BUG: Database schema version is corrupt. Manual intervention required.`
133
+ ) ;
134
+ }
135
+
136
+ if ( versionDoc . version > CURRENT_SCHEMA_VERSION ) {
137
+ throw new Error (
138
+ `Database created with newer version of checkpoint-mongodb. This version supports schema version ` +
139
+ `${ CURRENT_SCHEMA_VERSION } but the database was created with schema version ${ versionDoc . version } .`
140
+ ) ;
141
+ }
142
+
143
+ if ( versionDoc . version < CURRENT_SCHEMA_VERSION ) {
144
+ throw new Error (
145
+ `BUG: Schema version ${ versionDoc . version } is outdated (should be >= ${ CURRENT_SCHEMA_VERSION } ), but no ` +
146
+ `migration wants to execute.`
147
+ ) ;
148
+ }
149
+ }
150
+ }
151
+
152
+ async migrate ( ) {
153
+ if (
154
+ await needsMigration ( {
155
+ client : this . client ,
156
+ dbName : this . db . databaseName ,
157
+ checkpointCollectionName : this . checkpointCollectionName ,
158
+ checkpointWritesCollectionName : this . checkpointWritesCollectionName ,
159
+ schemaVersionCollectionName : this . schemaVersionCollectionName ,
160
+ serializer : this . serde ,
161
+ currentSchemaVersion : CURRENT_SCHEMA_VERSION ,
162
+ } )
163
+ ) {
164
+ await applyMigrations ( {
165
+ client : this . client ,
166
+ dbName : this . db . databaseName ,
167
+ checkpointCollectionName : this . checkpointCollectionName ,
168
+ checkpointWritesCollectionName : this . checkpointWritesCollectionName ,
169
+ schemaVersionCollectionName : this . schemaVersionCollectionName ,
170
+ serializer : this . serde ,
171
+ currentSchemaVersion : CURRENT_SCHEMA_VERSION ,
172
+ } ) ;
173
+ }
49
174
}
50
175
51
176
/**
@@ -55,6 +180,8 @@ export class MongoDBSaver extends BaseCheckpointSaver {
55
180
* for the given thread ID is retrieved.
56
181
*/
57
182
async getTuple ( config : RunnableConfig ) : Promise < CheckpointTuple | undefined > {
183
+ await this . setup ( ) ;
184
+
58
185
const {
59
186
thread_id,
60
187
checkpoint_ns = "" ,
@@ -109,10 +236,7 @@ export class MongoDBSaver extends BaseCheckpointSaver {
109
236
config : { configurable : configurableValues } ,
110
237
checkpoint,
111
238
pendingWrites,
112
- metadata : ( await this . serde . loadsTyped (
113
- doc . type ,
114
- doc . metadata . value ( )
115
- ) ) as CheckpointMetadata ,
239
+ metadata : doc . metadata as CheckpointMetadata ,
116
240
parentConfig :
117
241
doc . parent_checkpoint_id != null
118
242
? {
@@ -135,6 +259,8 @@ export class MongoDBSaver extends BaseCheckpointSaver {
135
259
config : RunnableConfig ,
136
260
options ?: CheckpointListOptions
137
261
) : AsyncGenerator < CheckpointTuple > {
262
+ await this . setup ( ) ;
263
+
138
264
const { limit, before, filter } = options ?? { } ;
139
265
const query : Record < string , unknown > = { } ;
140
266
@@ -150,9 +276,16 @@ export class MongoDBSaver extends BaseCheckpointSaver {
150
276
}
151
277
152
278
if ( filter ) {
153
- Object . entries ( filter ) . forEach ( ( [ key , value ] ) => {
154
- query [ `metadata.${ key } ` ] = value ;
155
- } ) ;
279
+ Object . entries ( filter )
280
+ . filter (
281
+ ( [ key , value ] ) =>
282
+ validCheckpointMetadataKeys . includes (
283
+ key as keyof CheckpointMetadata
284
+ ) && value !== undefined
285
+ )
286
+ . forEach ( ( [ key , value ] ) => {
287
+ query [ `metadata.${ key } ` ] = value ;
288
+ } ) ;
156
289
}
157
290
158
291
if ( before ) {
@@ -173,10 +306,7 @@ export class MongoDBSaver extends BaseCheckpointSaver {
173
306
doc . type ,
174
307
doc . checkpoint . value ( )
175
308
) ) as Checkpoint ;
176
- const metadata = ( await this . serde . loadsTyped (
177
- doc . type ,
178
- doc . metadata . value ( )
179
- ) ) as CheckpointMetadata ;
309
+ const metadata = doc . metadata as CheckpointMetadata ;
180
310
181
311
yield {
182
312
config : {
@@ -210,6 +340,8 @@ export class MongoDBSaver extends BaseCheckpointSaver {
210
340
checkpoint : Checkpoint ,
211
341
metadata : CheckpointMetadata
212
342
) : Promise < RunnableConfig > {
343
+ await this . setup ( ) ;
344
+
213
345
const thread_id = config . configurable ?. thread_id ;
214
346
const checkpoint_ns = config . configurable ?. checkpoint_ns ?? "" ;
215
347
const checkpoint_id = checkpoint . id ;
@@ -220,15 +352,11 @@ export class MongoDBSaver extends BaseCheckpointSaver {
220
352
}
221
353
const [ checkpointType , serializedCheckpoint ] =
222
354
this . serde . dumpsTyped ( checkpoint ) ;
223
- const [ metadataType , serializedMetadata ] = this . serde . dumpsTyped ( metadata ) ;
224
- if ( checkpointType !== metadataType ) {
225
- throw new Error ( "Mismatched checkpoint and metadata types." ) ;
226
- }
227
355
const doc = {
228
356
parent_checkpoint_id : config . configurable ?. checkpoint_id ,
229
357
type : checkpointType ,
230
358
checkpoint : serializedCheckpoint ,
231
- metadata : serializedMetadata ,
359
+ metadata,
232
360
} ;
233
361
const upsertQuery = {
234
362
thread_id,
@@ -259,6 +387,8 @@ export class MongoDBSaver extends BaseCheckpointSaver {
259
387
writes : PendingWrite [ ] ,
260
388
taskId : string
261
389
) : Promise < void > {
390
+ await this . setup ( ) ;
391
+
262
392
const thread_id = config . configurable ?. thread_id ;
263
393
const checkpoint_ns = config . configurable ?. checkpoint_ns ;
264
394
const checkpoint_id = config . configurable ?. checkpoint_id ;
0 commit comments