From dfb038a1d9b6c54731ccfe95c7f0f04720fb6e0d Mon Sep 17 00:00:00 2001 From: Ben Burns <803016+benjamincburns@users.noreply.github.com> Date: Mon, 21 Oct 2024 23:54:33 +1300 Subject: [PATCH] fix(checkpoint-mongodb): apply filters correctly in list method fixes #581 --- libs/checkpoint-mongodb/package.json | 2 + libs/checkpoint-mongodb/src/index.ts | 162 ++++++++++++++-- .../src/migrations/1_object_metadata.ts | 110 +++++++++++ .../checkpoint-mongodb/src/migrations/base.ts | 73 ++++++++ .../src/migrations/index.ts | 21 +++ .../migrations/1_object_metadata.test.ts | 176 ++++++++++++++++++ libs/checkpoint-sqlite/src/index.ts | 28 +-- libs/checkpoint-validation/package.json | 3 +- libs/checkpoint-validation/src/spec/list.ts | 8 +- libs/checkpoint/src/index.ts | 1 + libs/checkpoint/src/types.ts | 23 +++ yarn.lock | 15 +- 12 files changed, 563 insertions(+), 59 deletions(-) create mode 100644 libs/checkpoint-mongodb/src/migrations/1_object_metadata.ts create mode 100644 libs/checkpoint-mongodb/src/migrations/base.ts create mode 100644 libs/checkpoint-mongodb/src/migrations/index.ts create mode 100644 libs/checkpoint-mongodb/src/tests/migrations/1_object_metadata.test.ts diff --git a/libs/checkpoint-mongodb/package.json b/libs/checkpoint-mongodb/package.json index 4c16f852..acbfc02c 100644 --- a/libs/checkpoint-mongodb/package.json +++ b/libs/checkpoint-mongodb/package.json @@ -43,6 +43,7 @@ "@langchain/scripts": ">=0.1.3 <0.2.0", "@swc/core": "^1.3.90", "@swc/jest": "^0.2.29", + "@testcontainers/mongodb": "^10.13.2", "@tsconfig/recommended": "^1.0.3", "@types/better-sqlite3": "^7.6.9", "@types/uuid": "^10", @@ -62,6 +63,7 @@ "prettier": "^2.8.3", "release-it": "^17.6.0", "rollup": "^4.23.0", + "testcontainers": "^10.13.2", "ts-jest": "^29.1.0", "tsx": "^4.7.0", "typescript": "^4.9.5 || ^5.4.5" diff --git a/libs/checkpoint-mongodb/src/index.ts b/libs/checkpoint-mongodb/src/index.ts index 94e4eddf..eba6c50f 100644 --- a/libs/checkpoint-mongodb/src/index.ts +++ b/libs/checkpoint-mongodb/src/index.ts @@ -9,13 +9,21 @@ import { type PendingWrite, type CheckpointMetadata, CheckpointPendingWrite, + validCheckpointMetadataKeys, } from "@langchain/langgraph-checkpoint"; +import { applyMigrations, needsMigration } from "./migrations/index.js"; + +export * from "./migrations/index.js"; + +// increment this whenever the structure of the database changes in a way that would require a migration +const CURRENT_SCHEMA_VERSION = 1; export type MongoDBSaverParams = { client: MongoClient; dbName?: string; checkpointCollectionName?: string; checkpointWritesCollectionName?: string; + schemaVersionCollectionName?: string; }; /** @@ -26,16 +34,21 @@ export class MongoDBSaver extends BaseCheckpointSaver { protected db: MongoDatabase; + private setupPromise: Promise | undefined; + checkpointCollectionName = "checkpoints"; checkpointWritesCollectionName = "checkpoint_writes"; + schemaVersionCollectionName = "schema_version"; + constructor( { client, dbName, checkpointCollectionName, checkpointWritesCollectionName, + schemaVersionCollectionName, }: MongoDBSaverParams, serde?: SerializerProtocol ) { @@ -46,6 +59,118 @@ export class MongoDBSaver extends BaseCheckpointSaver { checkpointCollectionName ?? this.checkpointCollectionName; this.checkpointWritesCollectionName = checkpointWritesCollectionName ?? this.checkpointWritesCollectionName; + this.schemaVersionCollectionName = + schemaVersionCollectionName ?? this.schemaVersionCollectionName; + } + + /** + * Runs async setup tasks if they haven't been run yet. + */ + async setup(): Promise { + if (this.setupPromise) { + return this.setupPromise; + } + this.setupPromise = this.initializeSchemaVersion(); + return this.setupPromise; + } + + private async isDatabaseEmpty(): Promise { + const results = await Promise.all( + [this.checkpointCollectionName, this.checkpointWritesCollectionName].map( + async (collectionName) => { + const collection = this.db.collection(collectionName); + // set a limit of 1 to stop scanning if any documents are found + const count = await collection.countDocuments({}, { limit: 1 }); + return count === 0; + } + ) + ); + + return results.every((result) => result); + } + + private async initializeSchemaVersion(): Promise { + const schemaVersionCollection = this.db.collection( + this.schemaVersionCollectionName + ); + + // empty database, no migrations needed - just set the schema version and move on + if (await this.isDatabaseEmpty()) { + const schemaVersionCollection = this.db.collection( + this.schemaVersionCollectionName + ); + + const versionDoc = await schemaVersionCollection.findOne({}); + if (!versionDoc) { + await schemaVersionCollection.insertOne({ + version: CURRENT_SCHEMA_VERSION, + }); + } + } else { + // non-empty database, check if migrations are needed + const dbNeedsMigration = await needsMigration({ + client: this.client, + dbName: this.db.databaseName, + checkpointCollectionName: this.checkpointCollectionName, + checkpointWritesCollectionName: this.checkpointWritesCollectionName, + schemaVersionCollectionName: this.schemaVersionCollectionName, + serializer: this.serde, + currentSchemaVersion: CURRENT_SCHEMA_VERSION, + }); + + if (dbNeedsMigration) { + throw new Error( + `Database needs migration. Call the migrate() method to migrate the database.` + ); + } + + // always defined if dbNeedsMigration is false + const versionDoc = (await schemaVersionCollection.findOne({}))!; + + if (versionDoc.version == null) { + throw new Error( + `BUG: Database schema version is corrupt. Manual intervention required.` + ); + } + + if (versionDoc.version > CURRENT_SCHEMA_VERSION) { + throw new Error( + `Database created with newer version of checkpoint-mongodb. This version supports schema version ` + + `${CURRENT_SCHEMA_VERSION} but the database was created with schema version ${versionDoc.version}.` + ); + } + + if (versionDoc.version < CURRENT_SCHEMA_VERSION) { + throw new Error( + `BUG: Schema version ${versionDoc.version} is outdated (should be >= ${CURRENT_SCHEMA_VERSION}), but no ` + + `migration wants to execute.` + ); + } + } + } + + async migrate() { + if ( + await needsMigration({ + client: this.client, + dbName: this.db.databaseName, + checkpointCollectionName: this.checkpointCollectionName, + checkpointWritesCollectionName: this.checkpointWritesCollectionName, + schemaVersionCollectionName: this.schemaVersionCollectionName, + serializer: this.serde, + currentSchemaVersion: CURRENT_SCHEMA_VERSION, + }) + ) { + await applyMigrations({ + client: this.client, + dbName: this.db.databaseName, + checkpointCollectionName: this.checkpointCollectionName, + checkpointWritesCollectionName: this.checkpointWritesCollectionName, + schemaVersionCollectionName: this.schemaVersionCollectionName, + serializer: this.serde, + currentSchemaVersion: CURRENT_SCHEMA_VERSION, + }); + } } /** @@ -55,6 +180,8 @@ export class MongoDBSaver extends BaseCheckpointSaver { * for the given thread ID is retrieved. */ async getTuple(config: RunnableConfig): Promise { + await this.setup(); + const { thread_id, checkpoint_ns = "", @@ -109,10 +236,7 @@ export class MongoDBSaver extends BaseCheckpointSaver { config: { configurable: configurableValues }, checkpoint, pendingWrites, - metadata: (await this.serde.loadsTyped( - doc.type, - doc.metadata.value() - )) as CheckpointMetadata, + metadata: doc.metadata as CheckpointMetadata, parentConfig: doc.parent_checkpoint_id != null ? { @@ -135,6 +259,8 @@ export class MongoDBSaver extends BaseCheckpointSaver { config: RunnableConfig, options?: CheckpointListOptions ): AsyncGenerator { + await this.setup(); + const { limit, before, filter } = options ?? {}; const query: Record = {}; @@ -150,9 +276,16 @@ export class MongoDBSaver extends BaseCheckpointSaver { } if (filter) { - Object.entries(filter).forEach(([key, value]) => { - query[`metadata.${key}`] = value; - }); + Object.entries(filter) + .filter( + ([key, value]) => + validCheckpointMetadataKeys.includes( + key as keyof CheckpointMetadata + ) && value !== undefined + ) + .forEach(([key, value]) => { + query[`metadata.${key}`] = value; + }); } if (before) { @@ -173,10 +306,7 @@ export class MongoDBSaver extends BaseCheckpointSaver { doc.type, doc.checkpoint.value() )) as Checkpoint; - const metadata = (await this.serde.loadsTyped( - doc.type, - doc.metadata.value() - )) as CheckpointMetadata; + const metadata = doc.metadata as CheckpointMetadata; yield { config: { @@ -210,6 +340,8 @@ export class MongoDBSaver extends BaseCheckpointSaver { checkpoint: Checkpoint, metadata: CheckpointMetadata ): Promise { + await this.setup(); + const thread_id = config.configurable?.thread_id; const checkpoint_ns = config.configurable?.checkpoint_ns ?? ""; const checkpoint_id = checkpoint.id; @@ -220,15 +352,11 @@ export class MongoDBSaver extends BaseCheckpointSaver { } const [checkpointType, serializedCheckpoint] = this.serde.dumpsTyped(checkpoint); - const [metadataType, serializedMetadata] = this.serde.dumpsTyped(metadata); - if (checkpointType !== metadataType) { - throw new Error("Mismatched checkpoint and metadata types."); - } const doc = { parent_checkpoint_id: config.configurable?.checkpoint_id, type: checkpointType, checkpoint: serializedCheckpoint, - metadata: serializedMetadata, + metadata, }; const upsertQuery = { thread_id, @@ -259,6 +387,8 @@ export class MongoDBSaver extends BaseCheckpointSaver { writes: PendingWrite[], taskId: string ): Promise { + await this.setup(); + const thread_id = config.configurable?.thread_id; const checkpoint_ns = config.configurable?.checkpoint_ns; const checkpoint_id = config.configurable?.checkpoint_id; diff --git a/libs/checkpoint-mongodb/src/migrations/1_object_metadata.ts b/libs/checkpoint-mongodb/src/migrations/1_object_metadata.ts new file mode 100644 index 00000000..f3ecf304 --- /dev/null +++ b/libs/checkpoint-mongodb/src/migrations/1_object_metadata.ts @@ -0,0 +1,110 @@ +import { Binary, ObjectId, Collection, Document, WithId } from "mongodb"; +import { CheckpointMetadata } from "@langchain/langgraph-checkpoint"; +import { Migration, MigrationParams } from "./base.js"; + +const BULK_WRITE_SIZE = 100; + +interface OldCheckpointDocument { + parent_checkpoint_id: string | undefined; + type: string; + checkpoint: Binary; + metadata: Binary; + thread_id: string; + checkpoint_ns: string | undefined; + checkpoint_id: string; +} + +interface NewCheckpointDocument { + parent_checkpoint_id: string | undefined; + type: string; + checkpoint: Binary; + metadata: CheckpointMetadata; + thread_id: string; + checkpoint_ns: string | undefined; + checkpoint_id: string; +} + +export class Migration1ObjectMetadata extends Migration { + version = 1; + + constructor(params: MigrationParams) { + super(params); + } + + override async apply() { + const db = this.client.db(this.dbName); + const checkpointCollection = db.collection(this.checkpointCollectionName); + const schemaVersionCollection = db.collection( + this.schemaVersionCollectionName + ); + + // Fetch all documents from the checkpoints collection + const cursor = checkpointCollection.find({}); + + let updateBatch: { + id: string; + newDoc: NewCheckpointDocument; + }[] = []; + + for await (const doc of cursor) { + // already migrated + if (!(doc.metadata._bsontype && doc.metadata._bsontype === "Binary")) { + continue; + } + + const oldDoc = doc as WithId; + + const metadata: CheckpointMetadata = await this.serializer.loadsTyped( + oldDoc.type, + oldDoc.metadata.value() + ); + + const newDoc: NewCheckpointDocument = { + ...oldDoc, + metadata, + }; + + updateBatch.push({ + id: doc._id.toString(), + newDoc, + }); + + if (updateBatch.length >= BULK_WRITE_SIZE) { + await this.flushBatch(updateBatch, checkpointCollection); + updateBatch = []; + } + } + + if (updateBatch.length > 0) { + await this.flushBatch(updateBatch, checkpointCollection); + } + + // Update schema version to 1 + await schemaVersionCollection.updateOne( + {}, + { $set: { version: 1 } }, + { upsert: true } + ); + } + + private async flushBatch( + updateBatch: { + id: string; + newDoc: NewCheckpointDocument; + }[], + checkpointCollection: Collection + ) { + if (updateBatch.length === 0) { + throw new Error("No updates to apply"); + } + + const bulkOps = updateBatch.map(({ id, newDoc: newCheckpoint }) => ({ + updateOne: { + filter: { _id: new ObjectId(id) }, + update: { $set: newCheckpoint }, + }, + })); + + await checkpointCollection.bulkWrite(bulkOps); + } +} diff --git a/libs/checkpoint-mongodb/src/migrations/base.ts b/libs/checkpoint-mongodb/src/migrations/base.ts new file mode 100644 index 00000000..a98c3092 --- /dev/null +++ b/libs/checkpoint-mongodb/src/migrations/base.ts @@ -0,0 +1,73 @@ +import { SerializerProtocol } from "@langchain/langgraph-checkpoint"; +import { Db, MongoClient } from "mongodb"; + +export interface MigrationParams { + client: MongoClient; + dbName: string; + checkpointCollectionName: string; + checkpointWritesCollectionName: string; + schemaVersionCollectionName: string; + serializer: SerializerProtocol; + currentSchemaVersion: number; +} + +export abstract class Migration { + abstract version: number; + + protected client: MongoClient; + + protected dbName: string; + + protected checkpointCollectionName: string; + + protected checkpointWritesCollectionName: string; + + protected schemaVersionCollectionName: string; + + protected serializer: SerializerProtocol; + + protected currentSchemaVersion: number; + + private db: Db; + + constructor({ + client, + dbName, + checkpointCollectionName, + checkpointWritesCollectionName, + schemaVersionCollectionName, + serializer, + currentSchemaVersion, + }: MigrationParams) { + this.client = client; + this.dbName = dbName; + this.checkpointCollectionName = checkpointCollectionName; + this.checkpointWritesCollectionName = checkpointWritesCollectionName; + this.schemaVersionCollectionName = schemaVersionCollectionName; + this.serializer = serializer; + this.currentSchemaVersion = currentSchemaVersion; + this.db = this.client.db(this.dbName); + } + + abstract apply(): Promise; + + async isApplicable(): Promise { + const versionDoc = await this.db + .collection(this.schemaVersionCollectionName) + .findOne({}); + + if (!versionDoc || versionDoc.version === undefined) { + return true; + } + + const version = versionDoc.version as number; + + if (version < this.version) { + return true; + } + + return false; + } +} + +export class MigrationError extends Error {} diff --git a/libs/checkpoint-mongodb/src/migrations/index.ts b/libs/checkpoint-mongodb/src/migrations/index.ts new file mode 100644 index 00000000..3cb988d2 --- /dev/null +++ b/libs/checkpoint-mongodb/src/migrations/index.ts @@ -0,0 +1,21 @@ +import { Migration1ObjectMetadata } from "./1_object_metadata.js"; +import { MigrationParams } from "./base.js"; + +function _getMigrations(params: MigrationParams) { + const migrations = [Migration1ObjectMetadata]; + return migrations.map((MigrationClass) => new MigrationClass(params)); +} + +export async function needsMigration(params: MigrationParams) { + const migrations = _getMigrations(params); + return migrations.some((migration) => migration.isApplicable()); +} + +export async function applyMigrations(params: MigrationParams) { + const migrations = _getMigrations(params); + for (const migration of migrations) { + if (await migration.isApplicable()) { + await migration.apply(); + } + } +} diff --git a/libs/checkpoint-mongodb/src/tests/migrations/1_object_metadata.test.ts b/libs/checkpoint-mongodb/src/tests/migrations/1_object_metadata.test.ts new file mode 100644 index 00000000..b2dbdcd1 --- /dev/null +++ b/libs/checkpoint-mongodb/src/tests/migrations/1_object_metadata.test.ts @@ -0,0 +1,176 @@ +import { + beforeAll, + describe, + it, + expect, + afterAll, + beforeEach, +} from "@jest/globals"; +import { + MongoDBContainer, + StartedMongoDBContainer, +} from "@testcontainers/mongodb"; +import { Binary, MongoClient } from "mongodb"; +import { + Checkpoint, + CheckpointMetadata, + JsonPlusSerializer, + uuid6, +} from "@langchain/langgraph-checkpoint"; +import { Migration1ObjectMetadata } from "../../migrations/1_object_metadata.js"; + +describe("1_object_metadata", () => { + const dbName = "test_db"; + let container: StartedMongoDBContainer; + let client: MongoClient; + + beforeAll(async () => { + container = await new MongoDBContainer("mongo:6.0.1").start(); + const connectionString = `mongodb://127.0.0.1:${container.getMappedPort( + 27017 + )}/${dbName}?directConnection=true`; + client = new MongoClient(connectionString); + }); + + afterAll(async () => { + await client.close(); + await container.stop(); + }); + + describe("isApplicable", () => { + // MongoDBSaver handles this automatically in initializeSchemaVersion + it("should want to apply on empty database", async () => { + // ensure database is empty + const db = client.db(dbName); + await db.dropDatabase(); + + const migration = new Migration1ObjectMetadata({ + client, + dbName, + checkpointCollectionName: "checkpoints", + checkpointWritesCollectionName: "checkpoint_writes", + schemaVersionCollectionName: "schema_version", + serializer: new JsonPlusSerializer(), + currentSchemaVersion: 1, + }); + expect(await migration.isApplicable()).toBe(true); + }); + + it("should not want to apply on database with schema version of 1", async () => { + const db = client.db(dbName); + await db.dropDatabase(); + await db.createCollection("schema_version"); + await db.collection("schema_version").insertOne({ version: 1 }); + + const migration = new Migration1ObjectMetadata({ + client, + dbName, + checkpointCollectionName: "checkpoints", + checkpointWritesCollectionName: "checkpoint_writes", + schemaVersionCollectionName: "schema_version", + serializer: new JsonPlusSerializer(), + currentSchemaVersion: 1, + }); + expect(await migration.isApplicable()).toBe(false); + }); + }); + + describe("apply", () => { + const expectedCheckpoints: Record< + string, + { + parent_checkpoint_id?: string; + checkpoint: Binary; + type: string; + metadata: CheckpointMetadata; + thread_id: string; + checkpoint_ns: string; + checkpoint_id: string; + } + > = {}; + + beforeEach(async () => { + const serde = new JsonPlusSerializer(); + const dropDb = client.db(dbName); + await dropDb.dropDatabase(); + const db = client.db(dbName); + await db.createCollection("checkpoints"); + await db.createCollection("schema_version"); + + for (let i = 0; i < 10; i += 1) { + const checkpoint_id = uuid6(-3); + const thread_id = uuid6(-3); + const checkpoint_ns = ""; + + const checkpoint: Checkpoint = { + v: 1, + id: checkpoint_id, + ts: new Date().toISOString(), + channel_values: {}, + channel_versions: {}, + versions_seen: {}, + pending_sends: [], + }; + + const metadata: CheckpointMetadata = { + source: "update", + step: -1, + writes: {}, + parents: {}, + }; + + const [checkpointType, serializedCheckpoint] = + serde.dumpsTyped(checkpoint); + const serializedMetadata = serde.dumpsTyped(metadata)[1]; + + await db.collection("checkpoints").insertOne({ + type: checkpointType, + checkpoint: serializedCheckpoint, + metadata: serializedMetadata, + thread_id, + checkpoint_ns, + checkpoint_id, + }); + + expectedCheckpoints[checkpoint_id] = { + checkpoint: new Binary(serializedCheckpoint), + type: checkpointType, + metadata, + thread_id, + checkpoint_ns, + checkpoint_id, + }; + } + }); + + it("should migrate all checkpoints", async () => { + const migration = new Migration1ObjectMetadata({ + client, + dbName, + checkpointCollectionName: "checkpoints", + checkpointWritesCollectionName: "checkpoint_writes", + schemaVersionCollectionName: "schema_version", + serializer: new JsonPlusSerializer(), + currentSchemaVersion: 1, + }); + await migration.apply(); + + const db = client.db(dbName); + const cursor = await db.collection("checkpoints").find({}); + + let docCount = 0; + for await (const actual of cursor) { + docCount += 1; + const expected = expectedCheckpoints[actual.checkpoint_id]; + expect(actual.parent_checkpoint_id).toBe(expected.parent_checkpoint_id); + expect(actual.type).toBe(expected.type); + expect(actual.checkpoint).toEqual(expected.checkpoint); + expect(actual.metadata).toEqual(expected.metadata); + expect(actual.thread_id).toBe(expected.thread_id); + expect(actual.checkpoint_ns).toBe(expected.checkpoint_ns); + expect(actual.checkpoint_id).toBe(expected.checkpoint_id); + } + expect(docCount).toBe(10); + }); + }); +}); diff --git a/libs/checkpoint-sqlite/src/index.ts b/libs/checkpoint-sqlite/src/index.ts index 6740bb22..0957a500 100644 --- a/libs/checkpoint-sqlite/src/index.ts +++ b/libs/checkpoint-sqlite/src/index.ts @@ -8,6 +8,7 @@ import { type SerializerProtocol, type PendingWrite, type CheckpointMetadata, + validCheckpointMetadataKeys, } from "@langchain/langgraph-checkpoint"; interface CheckpointRow { @@ -31,33 +32,6 @@ interface WritesRow { value?: string; } -// In the `SqliteSaver.list` method, we need to sanitize the `options.filter` argument to ensure it only contains keys -// that are part of the `CheckpointMetadata` type. The lines below ensure that we get compile-time errors if the list -// of keys that we use is out of sync with the `CheckpointMetadata` type. -const checkpointMetadataKeys = ["source", "step", "writes", "parents"] as const; - -type CheckKeys = [K[number]] extends [ - keyof T -] - ? [keyof T] extends [K[number]] - ? K - : never - : never; - -function validateKeys( - keys: CheckKeys -): K { - return keys; -} - -// If this line fails to compile, the list of keys that we use in the `SqliteSaver.list` method is out of sync with the -// `CheckpointMetadata` type. In that case, just update `checkpointMetadataKeys` to contain all the keys in -// `CheckpointMetadata` -const validCheckpointMetadataKeys = validateKeys< - CheckpointMetadata, - typeof checkpointMetadataKeys ->(checkpointMetadataKeys); - export class SqliteSaver extends BaseCheckpointSaver { db: DatabaseType; diff --git a/libs/checkpoint-validation/package.json b/libs/checkpoint-validation/package.json index bc07827a..36396cfe 100644 --- a/libs/checkpoint-validation/package.json +++ b/libs/checkpoint-validation/package.json @@ -39,8 +39,7 @@ "jest": "^29.5.0", "jest-environment-node": "^29.6.4", "uuid": "^10.0.0", - "yargs": "^17.7.2", - "zod": "^3.23.8" + "yargs": "^17.7.2" }, "peerDependencies": { "@langchain/core": ">=0.2.31 <0.4.0", diff --git a/libs/checkpoint-validation/src/spec/list.ts b/libs/checkpoint-validation/src/spec/list.ts index d9f63097..bb00f590 100644 --- a/libs/checkpoint-validation/src/spec/list.ts +++ b/libs/checkpoint-validation/src/spec/list.ts @@ -168,13 +168,7 @@ export function listTests( parentTupleInDefaultNamespace.config, childTupleInDefaultNamespace.config, ], - filter: - // TODO: MongoDBSaver support for filter is broken and can't be fixed without a breaking change - // see: https://github.com/langchain-ai/langgraphjs/issues/581 - initializer.checkpointerName === - "@langchain/langgraph-checkpoint-mongodb" - ? [undefined] - : [undefined, {}, { source: "input" }, { source: "loop" }], + filter: [undefined, {}, { source: "input" }, { source: "loop" }], }; } diff --git a/libs/checkpoint/src/index.ts b/libs/checkpoint/src/index.ts index 82872011..9d88f534 100644 --- a/libs/checkpoint/src/index.ts +++ b/libs/checkpoint/src/index.ts @@ -4,4 +4,5 @@ export * from "./id.js"; export * from "./types.js"; export * from "./serde/base.js"; export * from "./serde/types.js"; +export * from "./serde/jsonplus.js"; export * from "./store/index.js"; diff --git a/libs/checkpoint/src/types.ts b/libs/checkpoint/src/types.ts index a9edc47a..a801a235 100644 --- a/libs/checkpoint/src/types.ts +++ b/libs/checkpoint/src/types.ts @@ -36,3 +36,26 @@ export interface CheckpointMetadata { */ parents: Record; } + +const checkpointMetadataKeys = ["source", "step", "writes", "parents"] as const; + +type CheckKeys = [K[number]] extends [ + keyof T +] + ? [keyof T] extends [K[number]] + ? K + : never + : never; + +function validateKeys( + keys: CheckKeys +): K { + return keys; +} + +// Used by checkpoint list methods to sanitize the `options.filter` argument. If this line fails to compile, update +// `checkpointMetadataKeys` to contain all the keys in `CheckpointMetadata`. +export const validCheckpointMetadataKeys = validateKeys< + CheckpointMetadata, + typeof checkpointMetadataKeys +>(checkpointMetadataKeys); diff --git a/yarn.lock b/yarn.lock index e8d385ea..f4885bf2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1663,6 +1663,7 @@ __metadata: "@langchain/scripts": ">=0.1.3 <0.2.0" "@swc/core": ^1.3.90 "@swc/jest": ^0.2.29 + "@testcontainers/mongodb": ^10.13.2 "@tsconfig/recommended": ^1.0.3 "@types/better-sqlite3": ^7.6.9 "@types/uuid": ^10 @@ -1683,6 +1684,7 @@ __metadata: prettier: ^2.8.3 release-it: ^17.6.0 rollup: ^4.23.0 + testcontainers: ^10.13.2 ts-jest: ^29.1.0 tsx: ^4.7.0 typescript: ^4.9.5 || ^5.4.5 @@ -1811,7 +1813,6 @@ __metadata: typescript: ^4.9.5 || ^5.4.5 uuid: ^10.0.0 yargs: ^17.7.2 - zod: ^3.23.8 peerDependencies: "@langchain/core": ">=0.2.31 <0.4.0" "@langchain/langgraph-checkpoint": ~0.0.6 @@ -9702,11 +9703,11 @@ __metadata: linkType: hard "nan@npm:^2.19.0, nan@npm:^2.20.0": - version: 2.20.0 - resolution: "nan@npm:2.20.0" + version: 2.22.0 + resolution: "nan@npm:2.22.0" dependencies: node-gyp: latest - checksum: eb09286e6c238a3582db4d88c875db73e9b5ab35f60306090acd2f3acae21696c9b653368b4a0e32abcef64ee304a923d6223acaddd16169e5eaaf5c508fb533 + checksum: 222e3a090e326c72f6782d948f44ee9b81cfb2161d5fe53216f04426a273fd094deee9dcc6813096dd2397689a2b10c1a92d3885d2e73fd2488a51547beb2929 languageName: node linkType: hard @@ -13058,11 +13059,11 @@ __metadata: linkType: hard "yaml@npm:^2.2.2": - version: 2.5.1 - resolution: "yaml@npm:2.5.1" + version: 2.6.0 + resolution: "yaml@npm:2.6.0" bin: yaml: bin.mjs - checksum: 31275223863fbd0b47ba9d2b248fbdf085db8d899e4ca43fff8a3a009497c5741084da6871d11f40e555d61360951c4c910b98216c1325d2c94753c0036d8172 + checksum: e5e74fd75e01bde2c09333d529af9fbb5928c5f7f01bfdefdcb2bf753d4ef489a45cab4deac01c9448f55ca27e691612b81fe3c3a59bb8cb5b0069da0f92cf0b languageName: node linkType: hard