diff --git a/libs/checkpoint-supabase/.gitignore b/libs/checkpoint-supabase/.gitignore new file mode 100644 index 00000000..c10034e2 --- /dev/null +++ b/libs/checkpoint-supabase/.gitignore @@ -0,0 +1,7 @@ +index.cjs +index.js +index.d.ts +index.d.cts +node_modules +dist +.yarn diff --git a/libs/checkpoint-supabase/README.md b/libs/checkpoint-supabase/README.md new file mode 100644 index 00000000..cc531d05 --- /dev/null +++ b/libs/checkpoint-supabase/README.md @@ -0,0 +1,110 @@ +# @langchain/langgraph-checkpoint-supabase + +Implementation of a [LangGraph.js](https://github.com/langchain-ai/langgraphjs) CheckpointSaver that uses the Supabase JS SDK. + +## Setup + +Create the following tables in your Supabase database, you can change the table names if required when also setting the `checkPointTable` and `writeTable` options of the `SupabaseSaver` class. + +> [!CAUTION] +> Make sure to enable RLS policies on the tables! + +```sql +create table + public.langgraph_checkpoints ( + thread_id text not null, + created_at timestamp with time zone not null default now(), + checkpoint_ns text not null default '', + checkpoint_id text not null, + parent_checkpoint_id text null, + type text null, + checkpoint jsonb null, + metadata jsonb null, + constraint langgraph_checkpoints_pkey primary key (thread_id, checkpoint_ns, checkpoint_id) + ) tablespace pg_default; + +create table + public.langgraph_writes ( + thread_id text not null, + created_at timestamp with time zone not null default now(), + checkpoint_ns text not null default '', + checkpoint_id text not null, + task_id text not null, + idx bigint not null, + channel text not null, + type text null, + value jsonb null, + constraint langgraph_writes_pkey primary key ( + thread_id, + checkpoint_ns, + checkpoint_id, + task_id, + idx + ) + ) tablespace pg_default; + +--- Important to disable public access to the tables! +alter table "langgraph_checkpoints" enable row level security; +alter table "langgraph_writes" enable row level security; +``` + +## Usage + +```ts +import { SqliteSaver } from "@langchain/langgraph-checkpoint-supabase"; + +const writeConfig = { + configurable: { + thread_id: "1", + checkpoint_ns: "" + } +}; +const readConfig = { + configurable: { + thread_id: "1" + } +}; + +const supabaseClient = createClient(SUPABASE_URL, SUPABASE_KEY); +const checkpointer = new SupabaseSaver(supabaseClient, { + checkPointTable: "langgraph_checkpoints", + writeTable: "langgraph_writes", +}); + +const checkpoint = { + v: 1, + ts: "2024-07-31T20:14:19.804150+00:00", + id: "1ef4f797-8335-6428-8001-8a1503f9b875", + channel_values: { + my_key: "meow", + node: "node" + }, + channel_versions: { + __start__: 2, + my_key: 3, + start:node: 3, + node: 3 + }, + versions_seen: { + __input__: {}, + __start__: { + __start__: 1 + }, + node: { + start:node: 2 + } + }, + pending_sends: [], +} + +// store checkpoint +await checkpointer.put(writeConfig, checkpoint, {}, {}) + +// load checkpoint +await checkpointer.get(readConfig) + +// list checkpoints +for await (const checkpoint of checkpointer.list(readConfig)) { + console.log(checkpoint); +} +``` diff --git a/libs/checkpoint-supabase/jest.config.cjs b/libs/checkpoint-supabase/jest.config.cjs new file mode 100644 index 00000000..385d19f6 --- /dev/null +++ b/libs/checkpoint-supabase/jest.config.cjs @@ -0,0 +1,20 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: "ts-jest/presets/default-esm", + testEnvironment: "./jest.env.cjs", + modulePathIgnorePatterns: ["dist/"], + moduleNameMapper: { + "^(\\.{1,2}/.*)\\.js$": "$1", + }, + transform: { + "^.+\\.tsx?$": ["@swc/jest"], + }, + transformIgnorePatterns: [ + "/node_modules/", + "\\.pnp\\.[^\\/]+$", + "./scripts/jest-setup-after-env.js", + ], + setupFiles: ["dotenv/config"], + testTimeout: 20_000, + passWithNoTests: true, +}; diff --git a/libs/checkpoint-supabase/jest.env.cjs b/libs/checkpoint-supabase/jest.env.cjs new file mode 100644 index 00000000..2ccedccb --- /dev/null +++ b/libs/checkpoint-supabase/jest.env.cjs @@ -0,0 +1,12 @@ +const { TestEnvironment } = require("jest-environment-node"); + +class AdjustedTestEnvironmentToSupportFloat32Array extends TestEnvironment { + constructor(config, context) { + // Make `instanceof Float32Array` return true in tests + // to avoid https://github.com/xenova/transformers.js/issues/57 and https://github.com/jestjs/jest/issues/2549 + super(config, context); + this.global.Float32Array = Float32Array; + } +} + +module.exports = AdjustedTestEnvironmentToSupportFloat32Array; diff --git a/libs/checkpoint-supabase/langchain.config.js b/libs/checkpoint-supabase/langchain.config.js new file mode 100644 index 00000000..fe70c345 --- /dev/null +++ b/libs/checkpoint-supabase/langchain.config.js @@ -0,0 +1,21 @@ +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +/** + * @param {string} relativePath + * @returns {string} + */ +function abs(relativePath) { + return resolve(dirname(fileURLToPath(import.meta.url)), relativePath); +} + +export const config = { + internals: [/node\:/, /@langchain\/core\//, /async_hooks/], + entrypoints: { + index: "index" + }, + tsConfigPath: resolve("./tsconfig.json"), + cjsSource: "./dist-cjs", + cjsDestination: "./dist", + abs, +}; diff --git a/libs/checkpoint-supabase/package.json b/libs/checkpoint-supabase/package.json new file mode 100644 index 00000000..3b094ad1 --- /dev/null +++ b/libs/checkpoint-supabase/package.json @@ -0,0 +1,91 @@ +{ + "name": "@langchain/langgraph-checkpoint-supabase", + "version": "0.1.2", + "description": "LangGraph", + "type": "module", + "engines": { + "node": ">=18" + }, + "main": "./index.js", + "types": "./index.d.ts", + "repository": { + "type": "git", + "url": "git@github.com:langchain-ai/langgraphjs.git" + }, + "scripts": { + "build": "yarn turbo:command build:internal --filter=@langchain/langgraph-checkpoint-supabase", + "build:internal": "yarn clean && yarn lc_build --create-entrypoints --pre --tree-shaking", + "clean": "rm -rf dist/ dist-cjs/ .turbo/", + "lint:eslint": "NODE_OPTIONS=--max-old-space-size=4096 eslint --cache --ext .ts,.js src/", + "lint:dpdm": "dpdm --exit-code circular:1 --no-warning --no-tree src/*.ts src/**/*.ts", + "lint": "yarn lint:eslint && yarn lint:dpdm", + "lint:fix": "yarn lint:eslint --fix && yarn lint:dpdm", + "prepack": "yarn build", + "test": "NODE_OPTIONS=--experimental-vm-modules jest --testPathIgnorePatterns=\\.int\\.test.ts --testTimeout 30000 --maxWorkers=50%", + "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch --testPathIgnorePatterns=\\.int\\.test.ts", + "test:single": "NODE_OPTIONS=--experimental-vm-modules yarn run jest --config jest.config.cjs --testTimeout 100000", + "test:int": "NODE_OPTIONS=--experimental-vm-modules jest --testPathPattern=\\.int\\.test.ts --testTimeout 100000 --maxWorkers=50%", + "format": "prettier --config .prettierrc --write \"src\"", + "format:check": "prettier --config .prettierrc --check \"src\"" + }, + "author": "LangChain", + "license": "MIT", + "dependencies": { + "@supabase/supabase-js": "^2.45.6" + }, + "peerDependencies": { + "@langchain/core": ">=0.2.31 <0.4.0", + "@langchain/langgraph-checkpoint": "~0.0.6" + }, + "devDependencies": { + "@jest/globals": "^29.5.0", + "@langchain/langgraph-checkpoint": "workspace:*", + "@langchain/scripts": ">=0.1.3 <0.2.0", + "@swc/core": "^1.3.90", + "@swc/jest": "^0.2.29", + "@tsconfig/recommended": "^1.0.3", + "@types/uuid": "^10", + "@typescript-eslint/eslint-plugin": "^6.12.0", + "@typescript-eslint/parser": "^6.12.0", + "dotenv": "^16.3.1", + "dpdm": "^3.12.0", + "eslint": "^8.33.0", + "eslint-config-airbnb-base": "^15.0.0", + "eslint-config-prettier": "^8.6.0", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-jest": "^28.8.0", + "eslint-plugin-no-instanceof": "^1.0.1", + "eslint-plugin-prettier": "^4.2.1", + "jest": "^29.5.0", + "jest-environment-node": "^29.6.4", + "prettier": "^2.8.3", + "release-it": "^17.6.0", + "rollup": "^4.23.0", + "ts-jest": "^29.1.0", + "tsx": "^4.7.0", + "typescript": "^4.9.5 || ^5.4.5" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "exports": { + ".": { + "types": { + "import": "./index.d.ts", + "require": "./index.d.cts", + "default": "./index.d.ts" + }, + "import": "./index.js", + "require": "./index.cjs" + }, + "./package.json": "./package.json" + }, + "files": [ + "dist/", + "index.cjs", + "index.js", + "index.d.ts", + "index.d.cts" + ] +} diff --git a/libs/checkpoint-supabase/src/index.ts b/libs/checkpoint-supabase/src/index.ts new file mode 100644 index 00000000..d75347bc --- /dev/null +++ b/libs/checkpoint-supabase/src/index.ts @@ -0,0 +1,320 @@ +import type { SupabaseClient } from "@supabase/supabase-js"; + +import type { RunnableConfig } from "@langchain/core/runnables"; +import { + BaseCheckpointSaver, + type Checkpoint, + type CheckpointListOptions, + type CheckpointMetadata, + type CheckpointTuple, + type PendingWrite, + type SerializerProtocol, +} from "@langchain/langgraph-checkpoint"; + +interface CheckpointRow { + checkpoint: string; + metadata: string; + parent_checkpoint_id?: string; + thread_id: string; + checkpoint_id: string; + checkpoint_ns?: string; + type?: string; +} + +interface WritesRow { + thread_id: string; + checkpoint_ns: string; + checkpoint_id: string; + task_id: string; + idx: number; + channel: string; + type?: string; + value?: string; +} + +const DEFAULT_TYPE = 'json' as const; +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; +} + +const validCheckpointMetadataKeys = validateKeys< + CheckpointMetadata, + typeof checkpointMetadataKeys +>(checkpointMetadataKeys); + +export class SupabaseSaver extends BaseCheckpointSaver { + + private options: { + checkPointTable: string; + writeTable: string; + } = { + checkPointTable: "langgraph_checkpoints", + writeTable: "langgraph_writes", + }; + + constructor(private client: SupabaseClient, config?: { + checkPointTable?: string; + writeTable?: string; + },serde?: SerializerProtocol) { + super(serde); + + // Apply config + if (config) { + this.options = { + ...this.options, + ...config, + }; + } + } + + protected _dumpMetadata(metadata: CheckpointMetadata): unknown { + const [, serializedMetadata] = this.serde.dumpsTyped(metadata); + return this.parseAndCleanJson(serializedMetadata); + } + + private parseAndCleanJson(data: Uint8Array): unknown { + return JSON.parse( + new TextDecoder().decode(data).replace(/\0/g, "") + ); + } + + private validateConfig(config: RunnableConfig): asserts config is Required { + if (!config.configurable?.thread_id || !config.configurable?.checkpoint_id) { + throw new Error("Missing required config: thread_id or checkpoint_id"); + } + } + + async getTuple(config: RunnableConfig): Promise { + const { thread_id, checkpoint_ns = "", checkpoint_id } = config.configurable ?? {}; + + const query = this.client + .from(this.options.checkPointTable) + .select() + .eq("thread_id", thread_id) + .eq("checkpoint_ns", checkpoint_ns); + + const res = await (checkpoint_id + ? query.eq("checkpoint_id", checkpoint_id) + : query.order("checkpoint_id", { ascending: false }) + ).throwOnError(); + + const [row] = res.data as CheckpointRow[]; + if (!row) return undefined; + + const finalConfig = !checkpoint_id ? { + configurable: { + thread_id: row.thread_id, + checkpoint_ns, + checkpoint_id: row.checkpoint_id, + }, + } : config; + + this.validateConfig(finalConfig); + + const pendingWrites = await this.fetchPendingWrites( + finalConfig.configurable.thread_id, + checkpoint_ns, + finalConfig.configurable.checkpoint_id + ); + + return { + config: finalConfig, + checkpoint: await this.deserializeField(row.type, row.checkpoint) as Checkpoint, + metadata: await this.deserializeField(row.type, row.metadata) as CheckpointMetadata, + parentConfig: row.parent_checkpoint_id ? { + configurable: { + thread_id: row.thread_id, + checkpoint_ns, + checkpoint_id: row.parent_checkpoint_id, + }, + } : undefined, + pendingWrites, + }; + } + + private async deserializeField(type: string | undefined, value: string): Promise { + return this.serde.loadsTyped( + type ?? DEFAULT_TYPE, + JSON.stringify(value) + ); + } + + private async fetchPendingWrites( + threadId: string, + checkpointNs: string, + checkpointId: string + ): Promise<[string, string, unknown][]> { + const { data } = await this.client + .from(this.options.writeTable) + .select() + .eq("thread_id", threadId) + .eq("checkpoint_ns", checkpointNs) + .eq("checkpoint_id", checkpointId) + .throwOnError(); + + const rows = data as WritesRow[]; + return Promise.all( + rows.map(async (row) => [ + row.task_id, + row.channel, + await this.deserializeField(row.type, row.value ?? ""), + ]) + ); + } + + async *list( + config: RunnableConfig, + options?: CheckpointListOptions + ): AsyncGenerator { + const { limit, before, filter } = options ?? {}; + const thread_id = config.configurable?.thread_id; + const checkpoint_ns = config.configurable?.checkpoint_ns; + + let query = this.client.from(this.options.checkPointTable).select("*"); + + if (thread_id !== undefined && thread_id !== null) { + query = query.eq("thread_id", thread_id); + } + + if (checkpoint_ns !== undefined && checkpoint_ns !== null) { + query = query.eq("checkpoint_ns", checkpoint_ns); + } + + if (before?.configurable?.checkpoint_id !== undefined) { + query = query.lt("checkpoint_id", before.configurable.checkpoint_id); + } + + const sanitizedFilter = Object.fromEntries( + Object.entries(filter ?? {}).filter( + ([key, value]) => + value !== undefined && + validCheckpointMetadataKeys.includes(key as keyof CheckpointMetadata) + ) + ); + + for (const [key, value] of Object.entries(sanitizedFilter)) { + let searchObject = {} as any; + searchObject[key] = value; + query = query.eq(`metadata->>${key}`, value); + } + + query = query.order("checkpoint_id", { ascending: false }); + + if (limit) { + query = query.limit(parseInt(limit as any, 10)); + } + + const { data: rows } = await query.throwOnError(); + + if (rows === null) { + throw new Error("Unexpected error listing checkpoints"); + } + + if (rows) { + for (const row of rows) { + yield { + config: { + configurable: { + thread_id: row.thread_id, + checkpoint_ns: row.checkpoint_ns, + checkpoint_id: row.checkpoint_id, + }, + }, + checkpoint: (await this.serde.loadsTyped( + row.type ?? "json", + JSON.stringify(row.checkpoint) + )) as Checkpoint, + metadata: (await this.serde.loadsTyped( + row.type ?? "json", + JSON.stringify(row.metadata) + )) as CheckpointMetadata, + parentConfig: row.parent_checkpoint_id + ? { + configurable: { + thread_id: row.thread_id, + checkpoint_ns: row.checkpoint_ns, + checkpoint_id: row.parent_checkpoint_id, + }, + } + : undefined, + }; + } + } + } + + async put( + config: RunnableConfig, + checkpoint: Checkpoint, + metadata: CheckpointMetadata + ): Promise { + await this.client + .from(this.options.checkPointTable) + .upsert( + { + thread_id: config.configurable?.thread_id, + checkpoint_ns: config.configurable?.checkpoint_ns, + checkpoint_id: checkpoint.id, + parent_checkpoint_id: config.configurable?.checkpoint_id, + type: "json", + checkpoint: checkpoint, + metadata: metadata, + } + ) + .throwOnError(); + + return { + configurable: { + thread_id: config.configurable?.thread_id, + checkpoint_ns: config.configurable?.checkpoint_ns ?? "", + checkpoint_id: checkpoint.id, + }, + }; + } + + async putWrites( + config: RunnableConfig, + writes: PendingWrite[], + taskId: string + ): Promise { + const thread_id = config.configurable?.thread_id; + const checkpoint_id = config.configurable?.checkpoint_id; + const checkpoint_ns = config.configurable?.checkpoint_ns; + + // Process writes sequentially + for (const [idx, write] of writes.entries()) { + const [, serializedWrite] = this.serde.dumpsTyped(write[1]); + + await this.client + .from(this.options.writeTable) + .upsert( + [ + { + thread_id, + checkpoint_ns, + checkpoint_id, + task_id: taskId, + idx, + channel: write[0], + type: "json", + value: JSON.parse( + new TextDecoder().decode(serializedWrite).replace(/\0/g, "") + ), + }, + ] + ) + .throwOnError(); + } + } +} \ No newline at end of file diff --git a/libs/checkpoint-supabase/tsconfig.cjs.json b/libs/checkpoint-supabase/tsconfig.cjs.json new file mode 100644 index 00000000..3b7026ea --- /dev/null +++ b/libs/checkpoint-supabase/tsconfig.cjs.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "declaration": false + }, + "exclude": ["node_modules", "dist", "docs", "**/tests"] +} diff --git a/libs/checkpoint-supabase/tsconfig.json b/libs/checkpoint-supabase/tsconfig.json new file mode 100644 index 00000000..bc85d83b --- /dev/null +++ b/libs/checkpoint-supabase/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "@tsconfig/recommended", + "compilerOptions": { + "outDir": "../dist", + "rootDir": "./src", + "target": "ES2021", + "lib": ["ES2021", "ES2022.Object", "DOM"], + "module": "ES2020", + "moduleResolution": "nodenext", + "esModuleInterop": true, + "declaration": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "useDefineForClassFields": true, + "strictPropertyInitialization": false, + "allowJs": true, + "strict": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "docs"] +} diff --git a/libs/checkpoint-supabase/turbo.json b/libs/checkpoint-supabase/turbo.json new file mode 100644 index 00000000..d1bb60a7 --- /dev/null +++ b/libs/checkpoint-supabase/turbo.json @@ -0,0 +1,11 @@ +{ + "extends": ["//"], + "tasks": { + "build": { + "outputs": ["**/dist/**"] + }, + "build:internal": { + "dependsOn": ["^build:internal"] + } + } +} diff --git a/libs/checkpoint-validation/jest.config.cjs b/libs/checkpoint-validation/jest.config.cjs index ab56a4c3..418f44e6 100644 --- a/libs/checkpoint-validation/jest.config.cjs +++ b/libs/checkpoint-validation/jest.config.cjs @@ -11,6 +11,7 @@ module.exports = { "/libs/checkpoint-mongodb/src/index.ts", "/libs/checkpoint-postgres/src/index.ts", "/libs/checkpoint-sqlite/src/index.ts", + "/libs/checkpoint-supabase/src/index.ts", ], coveragePathIgnorePatterns: [ diff --git a/libs/checkpoint-validation/package.json b/libs/checkpoint-validation/package.json index a0c6cd51..8bed386e 100644 --- a/libs/checkpoint-validation/package.json +++ b/libs/checkpoint-validation/package.json @@ -51,7 +51,9 @@ "@langchain/langgraph-checkpoint-mongodb": "workspace:*", "@langchain/langgraph-checkpoint-postgres": "workspace:*", "@langchain/langgraph-checkpoint-sqlite": "workspace:*", + "@langchain/langgraph-checkpoint-supabase": "workspace:*", "@langchain/scripts": ">=0.1.3 <0.2.0", + "@supabase/supabase-js": "^2.46.1", "@testcontainers/mongodb": "^10.13.2", "@testcontainers/postgresql": "^10.13.2", "@tsconfig/recommended": "^1.0.3", diff --git a/libs/checkpoint-validation/src/spec/list.ts b/libs/checkpoint-validation/src/spec/list.ts index 41995bed..35032379 100644 --- a/libs/checkpoint-validation/src/spec/list.ts +++ b/libs/checkpoint-validation/src/spec/list.ts @@ -116,11 +116,15 @@ export function listTests( } else { expect(actualTuplesMap.size).toEqual(expectedTuplesMap.size); for (const [key, value] of actualTuplesMap.entries()) { - // TODO: MongoDBSaver doesn't return pendingWrites on list, so we need to special case them + // TODO: MongoDBSaver, SQLiteSaver And SupabaseSaver don't return pendingWrites on list, so we need to special case them // see: https://github.com/langchain-ai/langgraphjs/issues/589 const checkpointerIncludesPendingWritesOnList = initializer.checkpointerName !== - "@langchain/langgraph-checkpoint-mongodb"; + "@langchain/langgraph-checkpoint-mongodb" && + initializer.checkpointerName !== + "@langchain/langgraph-checkpoint-sqlite" && + initializer.checkpointerName !== + "@langchain/langgraph-checkpoint-supabase"; const expectedTuple = expectedTuplesMap.get(key); if (!checkpointerIncludesPendingWritesOnList) { @@ -169,7 +173,7 @@ export function listTests( // 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" + "@langchain/langgraph-checkpoint-mongodb" ? [undefined] : [undefined, {}, { source: "input" }, { source: "loop" }], }; @@ -191,17 +195,17 @@ export function listTests( tuple.config.configurable?.thread_id === thread_id) && (checkpoint_ns === undefined || tuple.config.configurable?.checkpoint_ns === - checkpoint_ns) && + checkpoint_ns) && (before === undefined || tuple.checkpoint.id < - before.configurable?.checkpoint_id) && + before.configurable?.checkpoint_id) && (filter === undefined || Object.entries(filter).every( ([key, value]) => ( tuple.metadata as - | Record - | undefined + | Record + | undefined )?.[key] === value )) ) @@ -319,9 +323,8 @@ export function listTests( const descriptionWhen = descriptionWhenParts.length > 1 - ? `${descriptionWhenParts.slice(0, -1).join(", ")}, and ${ - descriptionWhenParts[descriptionWhenParts.length - 1] - }` + ? `${descriptionWhenParts.slice(0, -1).join(", ")}, and ${descriptionWhenParts[descriptionWhenParts.length - 1] + }` : descriptionWhenParts[0]; return `should return ${descriptionTupleCount} when ${descriptionWhen}`; diff --git a/libs/checkpoint-validation/src/spec/put.ts b/libs/checkpoint-validation/src/spec/put.ts index 71c7d203..96ffb373 100644 --- a/libs/checkpoint-validation/src/spec/put.ts +++ b/libs/checkpoint-validation/src/spec/put.ts @@ -1,3 +1,4 @@ +import { RunnableConfig } from "@langchain/core/runnables"; import { Checkpoint, CheckpointMetadata, @@ -5,13 +6,12 @@ import { uuid6, type BaseCheckpointSaver, } from "@langchain/langgraph-checkpoint"; -import { RunnableConfig } from "@langchain/core/runnables"; -import { CheckpointerTestInitializer } from "../types.js"; import { initialCheckpointTuple, it_skipForSomeModules, putTuples, } from "../test_utils.js"; +import { CheckpointerTestInitializer } from "../types.js"; export function putTests( initializer: CheckpointerTestInitializer @@ -176,7 +176,7 @@ export function putTests( }); }); }); - + it("should default to empty namespace if the checkpoint namespace is missing from config.configurable", async () => { const missingNamespaceConfig: RunnableConfig = { configurable: { thread_id }, @@ -211,6 +211,8 @@ export function putTests( "TODO: MongoDBSaver doesn't store channel deltas", "@langchain/langgraph-checkpoint-sqlite": "TODO: SQLiteSaver doesn't store channel deltas", + "@langchain/langgraph-checkpoint-supabase": + "TODO: SupabaseSaver doesn't store channel deltas", })( "should only store channel_values that have changed (based on newVersions)", async () => { diff --git a/libs/checkpoint-validation/src/tests/supabase.spec.ts b/libs/checkpoint-validation/src/tests/supabase.spec.ts new file mode 100644 index 00000000..84813940 --- /dev/null +++ b/libs/checkpoint-validation/src/tests/supabase.spec.ts @@ -0,0 +1,5 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import { specTest } from "../spec/index.js"; +import { initializer } from "./supabase_initializer.js"; + +specTest(initializer); diff --git a/libs/checkpoint-validation/src/tests/supabase_initializer.ts b/libs/checkpoint-validation/src/tests/supabase_initializer.ts new file mode 100644 index 00000000..cfd9899b --- /dev/null +++ b/libs/checkpoint-validation/src/tests/supabase_initializer.ts @@ -0,0 +1,33 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import { SupabaseSaver } from "@langchain/langgraph-checkpoint-supabase"; +import { createClient } from "@supabase/supabase-js"; +import { CheckpointerTestInitializer } from "../types.js"; + +const SUPABASE_URL = process.env.SUPABASE_URL!; +const SUPABASE_KEY = process.env.SUPABASE_KEY!; + +export const initializer: CheckpointerTestInitializer = { + checkpointerName: "@langchain/langgraph-checkpoint-supabase", + + async createCheckpointer() { + const client = createClient(SUPABASE_URL, SUPABASE_KEY); + return new SupabaseSaver(client); + }, + + // Reset the tables between groups of tests + async destroyCheckpointer() { + const client = createClient(SUPABASE_URL, SUPABASE_KEY); + await client + .from("langgraph_checkpoints") + .delete() + .neq("thread_id", "filter-needs-a-value") + .throwOnError() + await client + .from("langgraph_writes") + .delete() + .neq("thread_id", "filter-needs-a-value") + .throwOnError() + } +}; + +export default initializer; diff --git a/yarn.lock b/yarn.lock index 416899cf..95e8e851 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1747,6 +1747,43 @@ __metadata: languageName: unknown linkType: soft +"@langchain/langgraph-checkpoint-supabase@workspace:*, @langchain/langgraph-checkpoint-supabase@workspace:libs/checkpoint-supabase": + version: 0.0.0-use.local + resolution: "@langchain/langgraph-checkpoint-supabase@workspace:libs/checkpoint-supabase" + dependencies: + "@jest/globals": ^29.5.0 + "@langchain/langgraph-checkpoint": "workspace:*" + "@langchain/scripts": ">=0.1.3 <0.2.0" + "@supabase/supabase-js": ^2.45.6 + "@swc/core": ^1.3.90 + "@swc/jest": ^0.2.29 + "@tsconfig/recommended": ^1.0.3 + "@types/uuid": ^10 + "@typescript-eslint/eslint-plugin": ^6.12.0 + "@typescript-eslint/parser": ^6.12.0 + dotenv: ^16.3.1 + dpdm: ^3.12.0 + eslint: ^8.33.0 + eslint-config-airbnb-base: ^15.0.0 + eslint-config-prettier: ^8.6.0 + eslint-plugin-import: ^2.29.1 + eslint-plugin-jest: ^28.8.0 + eslint-plugin-no-instanceof: ^1.0.1 + eslint-plugin-prettier: ^4.2.1 + jest: ^29.5.0 + jest-environment-node: ^29.6.4 + prettier: ^2.8.3 + release-it: ^17.6.0 + rollup: ^4.23.0 + ts-jest: ^29.1.0 + tsx: ^4.7.0 + typescript: ^4.9.5 || ^5.4.5 + peerDependencies: + "@langchain/core": ">=0.2.31 <0.4.0" + "@langchain/langgraph-checkpoint": ~0.0.6 + languageName: unknown + linkType: soft + "@langchain/langgraph-checkpoint-validation@workspace:libs/checkpoint-validation": version: 0.0.0-use.local resolution: "@langchain/langgraph-checkpoint-validation@workspace:libs/checkpoint-validation" @@ -1757,7 +1794,10 @@ __metadata: "@langchain/langgraph-checkpoint-mongodb": "workspace:*" "@langchain/langgraph-checkpoint-postgres": "workspace:*" "@langchain/langgraph-checkpoint-sqlite": "workspace:*" + "@langchain/langgraph-checkpoint-supabase": "workspace:*" "@langchain/scripts": ">=0.1.3 <0.2.0" + "@supabase/supabase-js": ^2.46.1 + "@swc-node/register": ^1.10.9 "@swc/core": ^1.3.90 "@swc/jest": ^0.2.29 "@testcontainers/mongodb": ^10.13.2 @@ -2753,7 +2793,129 @@ __metadata: checksum: 614d30cb4d5201550c940945d44c9e0b6d64a888ff2cd5b357f95ad6721070d6b8839cd10e15b76bf5e14af0bcc1d8f9ec00d49a46318f1f669a4bec1d7f3148 languageName: node linkType: hard + +"@supabase/auth-js@npm:2.65.1": + version: 2.65.1 + resolution: "@supabase/auth-js@npm:2.65.1" + dependencies: + "@supabase/node-fetch": ^2.6.14 + checksum: 5e4a9c4d94b5d8d3e4c6ea113eb4adf84d5bf0b187c775e4577693d18bfba4ffa6fdf9ef236e1f7a2cebf1696948cba1ec8cafd705a6493b63ecb7807cee86ac + languageName: node + linkType: hard + +"@supabase/functions-js@npm:2.4.3": + version: 2.4.3 + resolution: "@supabase/functions-js@npm:2.4.3" + dependencies: + "@supabase/node-fetch": ^2.6.14 + checksum: 1c2d58b498c19bd0c8984407f1d4c207ac6816df5e38c52f0d009a9ae55cfd80cc3b74b66414b386c3dc5c972b7db99452aeed545f9f5d6472ebb631274261a8 + languageName: node + linkType: hard + +"@supabase/node-fetch@npm:2.6.15, @supabase/node-fetch@npm:^2.6.14": + version: 2.6.15 + resolution: "@supabase/node-fetch@npm:2.6.15" + dependencies: + whatwg-url: ^5.0.0 + checksum: 9673b49236a56df49eb7ea5cb789cf4e8b1393069b84b4964ac052995e318a34872f428726d128f232139e17c3375a531e45e99edd3e96a25cce60d914b53879 + languageName: node + linkType: hard + +"@supabase/postgrest-js@npm:1.16.3": + version: 1.16.3 + resolution: "@supabase/postgrest-js@npm:1.16.3" + dependencies: + "@supabase/node-fetch": ^2.6.14 + checksum: e89f3d75b8d7253de19356c9f57ca1674cd09a62a5229bf80705450bebf0cbe0ca667333a5e349c13eb10a74dfcbb316d574399b734591d96389d5e2ff2f0801 + languageName: node + linkType: hard + +"@supabase/realtime-js@npm:2.10.7": + version: 2.10.7 + resolution: "@supabase/realtime-js@npm:2.10.7" + dependencies: + "@supabase/node-fetch": ^2.6.14 + "@types/phoenix": ^1.5.4 + "@types/ws": ^8.5.10 + ws: ^8.14.2 + checksum: fd0a39a096c691782732eac5a08f5b150c7fbb0b8d73e91c0d7a4df9accd5835a760d3ed984c4640e3c7a72e4e8ece31ce1bbeedd47e4aacb3476c5a53e95791 + languageName: node + linkType: hard + +"@supabase/storage-js@npm:2.7.1": + version: 2.7.1 + resolution: "@supabase/storage-js@npm:2.7.1" + dependencies: + "@supabase/node-fetch": ^2.6.14 + checksum: ed8f3a3178856c331b36588f4fff5cbb7f2f89977fff9716ab20b1977d13816bda5a887a316638f2a05ac35fdef46e18eab8a543d6113de76d3a06b15bf9ae8e + languageName: node + linkType: hard + +"@supabase/supabase-js@npm:^2.45.6": + version: 2.45.6 + resolution: "@supabase/supabase-js@npm:2.45.6" + dependencies: + "@supabase/auth-js": 2.65.1 + "@supabase/functions-js": 2.4.3 + "@supabase/node-fetch": 2.6.15 + "@supabase/postgrest-js": 1.16.3 + "@supabase/realtime-js": 2.10.7 + "@supabase/storage-js": 2.7.1 + checksum: d97c18180a7e4725615e6d22ab322eb6f68de0fe8b5bb9cdd921544c16274917b5d7594ff46fb4401d175329e768e82a9ad8dd3c362a6c5ab2231afb021481b9 + languageName: node + linkType: hard + +"@supabase/supabase-js@npm:^2.46.1": + version: 2.46.1 + resolution: "@supabase/supabase-js@npm:2.46.1" + dependencies: + "@supabase/auth-js": 2.65.1 + "@supabase/functions-js": 2.4.3 + "@supabase/node-fetch": 2.6.15 + "@supabase/postgrest-js": 1.16.3 + "@supabase/realtime-js": 2.10.7 + "@supabase/storage-js": 2.7.1 + checksum: 5f8c143124adab36a145c78a1c9799e0fd80598d64904e99e907efa3c6ae1a6f3c95c97b2c9a493690d6761169d9fe138f885c7f3885e376a5928676c616c5fc + languageName: node + linkType: hard + +"@swc-node/core@npm:^1.13.3": + version: 1.13.3 + resolution: "@swc-node/core@npm:1.13.3" + peerDependencies: + "@swc/core": ">= 1.4.13" + "@swc/types": ">= 0.1" + checksum: 9bad56479b2e980af8cfbcc1f040b95a928e38ead40ee3f980cd5718814cdaa6dc93d3a1d4a584e9fb1105af9a8f06ee2d5d82c6465ac364e6febe637f6139d7 + languageName: node + linkType: hard +"@swc-node/register@npm:^1.10.9": + version: 1.10.9 + resolution: "@swc-node/register@npm:1.10.9" + dependencies: + "@swc-node/core": ^1.13.3 + "@swc-node/sourcemap-support": ^0.5.1 + colorette: ^2.0.20 + debug: ^4.3.5 + oxc-resolver: ^1.10.2 + pirates: ^4.0.6 + tslib: ^2.6.3 + peerDependencies: + "@swc/core": ">= 1.4.13" + typescript: ">= 4.3" + checksum: 147998eaca7b12dbaf17f937d849f615b8f541bada136a6619b79610ec2fc509599ac48fe61f04cfbe6f459cf9773b87119845b77b30c5d31ebe450a527c6566 + languageName: node + linkType: hard + +"@swc-node/sourcemap-support@npm:^0.5.1": + version: 0.5.1 + resolution: "@swc-node/sourcemap-support@npm:0.5.1" + dependencies: + source-map-support: ^0.5.21 + tslib: ^2.6.3 + checksum: 307be2a52c10f3899871dc316190584e7a6e48375de5b84638cd0ca96681c4ce89891b9f7e86dedb93aac106dea7eff42ac2192f443ac1a1242a206ec93d0caf + languageName: node + linkType: hard "@swc/core-darwin-arm64@npm:1.4.16": version: 1.4.16 resolution: "@swc/core-darwin-arm64@npm:1.4.16" @@ -3473,6 +3635,13 @@ __metadata: languageName: node linkType: hard +"@types/phoenix@npm:^1.5.4": + version: 1.6.5 + resolution: "@types/phoenix@npm:1.6.5" + checksum: b87416393159f0ba2812875fc2721914a3284cde8b1f263dfcd46f4149dae7f4efc2bfa062d558c8bbfb7ae2a9d802487b0dd4744ff08799386cbc49c19368f0 + languageName: node + linkType: hard + "@types/qs@npm:^6.9.15": version: 6.9.15 resolution: "@types/qs@npm:6.9.15" @@ -3561,6 +3730,15 @@ __metadata: languageName: node linkType: hard +"@types/ws@npm:^8.5.10": + version: 8.5.12 + resolution: "@types/ws@npm:8.5.12" + dependencies: + "@types/node": "*" + checksum: ddefb6ad1671f70ce73b38a5f47f471d4d493864fca7c51f002a86e5993d031294201c5dced6d5018fb8905ad46888d65c7f20dd54fc165910b69f42fba9a6d0 + languageName: node + linkType: hard + "@types/yargs-parser@npm:*": version: 21.0.3 resolution: "@types/yargs-parser@npm:21.0.3" @@ -12810,6 +12988,21 @@ __metadata: languageName: node linkType: hard +"ws@npm:^8.14.2": + version: 8.18.0 + resolution: "ws@npm:8.18.0" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 91d4d35bc99ff6df483bdf029b9ea4bfd7af1f16fc91231a96777a63d263e1eabf486e13a2353970efc534f9faa43bdbf9ee76525af22f4752cbc5ebda333975 + languageName: node + linkType: hard + "xdg-basedir@npm:^5.0.1, xdg-basedir@npm:^5.1.0": version: 5.1.0 resolution: "xdg-basedir@npm:5.1.0"