From 77f7f10921215ff919a4e743c5b11bec9aa243aa Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 18 Feb 2026 14:25:46 +0000 Subject: [PATCH] feat: add convex state functions for snapshot persistence --- README.md | 3 +++ convex/schema.ts | 9 +++++++ convex/state.ts | 45 +++++++++++++++++++++++++++++++++++ test/convex-functions.test.js | 14 +++++++++++ test/deployment.test.js | 8 +++++++ 5 files changed, 79 insertions(+) create mode 100644 convex/schema.ts create mode 100644 convex/state.ts create mode 100644 test/convex-functions.test.js diff --git a/README.md b/README.md index 431aa3e..b60b73a 100644 --- a/README.md +++ b/README.md @@ -451,5 +451,8 @@ Use `.env.example` as the source of truth. 1. Replace `/auth/dev-login` with direct Better Auth UI/OAuth sign-in for public launch. 2. Populate integration keys in Coolify environment for X, Polar, Qwen3 TTS, MinIO, and Convex. 3. Implement Convex functions named by `CONVEX_STATE_QUERY` and `CONVEX_STATE_MUTATION`. + - This repository includes `convex/state.ts` and `convex/schema.ts` for defaults: + - `state:getLatestSnapshot` + - `state:saveSnapshot` 4. Move Better Auth from memory adapter to a persistent production adapter. 5. Add tracing and external alerting. diff --git a/convex/schema.ts b/convex/schema.ts new file mode 100644 index 0000000..f6d5b6b --- /dev/null +++ b/convex/schema.ts @@ -0,0 +1,9 @@ +import { defineSchema, defineTable } from "convex/server"; +import { v } from "convex/values"; + +export default defineSchema({ + state_snapshots: defineTable({ + snapshot: v.any(), + updatedAt: v.string(), + }), +}); diff --git a/convex/state.ts b/convex/state.ts new file mode 100644 index 0000000..80e0f03 --- /dev/null +++ b/convex/state.ts @@ -0,0 +1,45 @@ +import { mutation, query } from "./_generated/server"; +import { v } from "convex/values"; + +export const getLatestSnapshot = query({ + args: {}, + handler: async (ctx) => { + const latest = await ctx.db + .query("state_snapshots") + .order("desc") + .first(); + + return latest + ? { + snapshot: latest.snapshot, + updatedAt: latest.updatedAt, + } + : null; + }, +}); + +export const saveSnapshot = mutation({ + args: { + snapshot: v.any(), + updatedAt: v.string(), + }, + handler: async (ctx, args) => { + const latest = await ctx.db + .query("state_snapshots") + .order("desc") + .first(); + + if (latest) { + await ctx.db.patch(latest._id, { + snapshot: args.snapshot, + updatedAt: args.updatedAt, + }); + return latest._id; + } + + return ctx.db.insert("state_snapshots", { + snapshot: args.snapshot, + updatedAt: args.updatedAt, + }); + }, +}); diff --git a/test/convex-functions.test.js b/test/convex-functions.test.js new file mode 100644 index 0000000..7cf43b2 --- /dev/null +++ b/test/convex-functions.test.js @@ -0,0 +1,14 @@ +"use strict"; + +const test = require("node:test"); +const assert = require("node:assert/strict"); +const fs = require("node:fs"); + +test("convex state functions are present for configured function names", () => { + const schema = fs.readFileSync("convex/schema.ts", "utf8"); + const state = fs.readFileSync("convex/state.ts", "utf8"); + + assert.match(schema, /state_snapshots/); + assert.match(state, /export const getLatestSnapshot = query/); + assert.match(state, /export const saveSnapshot = mutation/); +}); diff --git a/test/deployment.test.js b/test/deployment.test.js index b8d4ce7..618eb1b 100644 --- a/test/deployment.test.js +++ b/test/deployment.test.js @@ -35,3 +35,11 @@ test("env example includes required webhook and credit settings", () => { assert.match(envFile, /INCLUDED_CHARS=/); assert.match(envFile, /WEBHOOK_RPM=/); }); + +test("convex state function files exist", () => { + const schema = fs.readFileSync("convex/schema.ts", "utf8"); + const state = fs.readFileSync("convex/state.ts", "utf8"); + assert.match(schema, /defineSchema/); + assert.match(state, /getLatestSnapshot/); + assert.match(state, /saveSnapshot/); +});