@abdssamie/convex-checkpoints 0.1.1
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/LICENSE +201 -0
- package/README.md +188 -0
- package/dist/client/_generated/_ignore.d.ts +1 -0
- package/dist/client/_generated/_ignore.d.ts.map +1 -0
- package/dist/client/_generated/_ignore.js +3 -0
- package/dist/client/_generated/_ignore.js.map +1 -0
- package/dist/client/index.d.ts +62 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +177 -0
- package/dist/client/index.js.map +1 -0
- package/dist/component/_generated/api.d.ts +36 -0
- package/dist/component/_generated/api.d.ts.map +1 -0
- package/dist/component/_generated/api.js +31 -0
- package/dist/component/_generated/api.js.map +1 -0
- package/dist/component/_generated/component.d.ts +73 -0
- package/dist/component/_generated/component.d.ts.map +1 -0
- package/dist/component/_generated/component.js +11 -0
- package/dist/component/_generated/component.js.map +1 -0
- package/dist/component/_generated/dataModel.d.ts +46 -0
- package/dist/component/_generated/dataModel.d.ts.map +1 -0
- package/dist/component/_generated/dataModel.js +11 -0
- package/dist/component/_generated/dataModel.js.map +1 -0
- package/dist/component/_generated/server.d.ts +121 -0
- package/dist/component/_generated/server.d.ts.map +1 -0
- package/dist/component/_generated/server.js +78 -0
- package/dist/component/_generated/server.js.map +1 -0
- package/dist/component/checkpointDispatcher.d.ts +14 -0
- package/dist/component/checkpointDispatcher.d.ts.map +1 -0
- package/dist/component/checkpointDispatcher.js +21 -0
- package/dist/component/checkpointDispatcher.js.map +1 -0
- package/dist/component/convex.config.d.ts +3 -0
- package/dist/component/convex.config.d.ts.map +1 -0
- package/dist/component/convex.config.js +3 -0
- package/dist/component/convex.config.js.map +1 -0
- package/dist/component/lib.d.ts +61 -0
- package/dist/component/lib.d.ts.map +1 -0
- package/dist/component/lib.js +91 -0
- package/dist/component/lib.js.map +1 -0
- package/dist/component/schema.d.ts +23 -0
- package/dist/component/schema.d.ts.map +1 -0
- package/dist/component/schema.js +16 -0
- package/dist/component/schema.js.map +1 -0
- package/package.json +94 -0
- package/src/client/_generated/_ignore.ts +1 -0
- package/src/client/index.test.ts +112 -0
- package/src/client/index.ts +264 -0
- package/src/client/setup.test.ts +26 -0
- package/src/component/_generated/api.ts +52 -0
- package/src/component/_generated/component.ts +89 -0
- package/src/component/_generated/dataModel.ts +60 -0
- package/src/component/_generated/server.ts +156 -0
- package/src/component/checkpointDispatcher.test.ts +72 -0
- package/src/component/checkpointDispatcher.ts +66 -0
- package/src/component/convex.config.ts +3 -0
- package/src/component/lib.test.ts +50 -0
- package/src/component/lib.ts +101 -0
- package/src/component/schema.ts +16 -0
- package/src/component/setup.test.ts +11 -0
- package/src/test.ts +18 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
import { internalQuery, mutation, query } from "./_generated/server.js";
|
|
3
|
+
import schema from "./schema.js";
|
|
4
|
+
const checkpointValidator = schema.tables.checkpoints.validator.extend({
|
|
5
|
+
_id: v.id("checkpoints"),
|
|
6
|
+
_creationTime: v.number(),
|
|
7
|
+
});
|
|
8
|
+
export const record = mutation({
|
|
9
|
+
args: {
|
|
10
|
+
name: v.string(),
|
|
11
|
+
userId: v.optional(v.string()),
|
|
12
|
+
payload: v.optional(v.any()),
|
|
13
|
+
idempotencyKey: v.optional(v.string()),
|
|
14
|
+
reachedAt: v.optional(v.number()),
|
|
15
|
+
},
|
|
16
|
+
returns: v.object({
|
|
17
|
+
checkpointId: v.id("checkpoints"),
|
|
18
|
+
created: v.boolean(),
|
|
19
|
+
}),
|
|
20
|
+
handler: async (ctx, args) => {
|
|
21
|
+
if (args.idempotencyKey !== undefined) {
|
|
22
|
+
const existing = await ctx.db
|
|
23
|
+
.query("checkpoints")
|
|
24
|
+
.withIndex("by_idempotencyKey", (q) => q.eq("idempotencyKey", args.idempotencyKey))
|
|
25
|
+
.unique();
|
|
26
|
+
if (existing !== null) {
|
|
27
|
+
return { checkpointId: existing._id, created: false };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const now = Date.now();
|
|
31
|
+
const checkpointId = await ctx.db.insert("checkpoints", {
|
|
32
|
+
name: args.name,
|
|
33
|
+
userId: args.userId,
|
|
34
|
+
payload: args.payload,
|
|
35
|
+
idempotencyKey: args.idempotencyKey,
|
|
36
|
+
reachedAt: args.reachedAt ?? now,
|
|
37
|
+
receivedAt: now,
|
|
38
|
+
});
|
|
39
|
+
return { checkpointId, created: true };
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
export const listRecent = query({
|
|
43
|
+
args: {
|
|
44
|
+
limit: v.optional(v.number()),
|
|
45
|
+
},
|
|
46
|
+
returns: v.array(checkpointValidator),
|
|
47
|
+
handler: async (ctx, args) => {
|
|
48
|
+
return await ctx.db
|
|
49
|
+
.query("checkpoints")
|
|
50
|
+
.order("desc")
|
|
51
|
+
.take(args.limit ?? 100);
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
export const listByName = query({
|
|
55
|
+
args: {
|
|
56
|
+
name: v.string(),
|
|
57
|
+
limit: v.optional(v.number()),
|
|
58
|
+
},
|
|
59
|
+
returns: v.array(checkpointValidator),
|
|
60
|
+
handler: async (ctx, args) => {
|
|
61
|
+
return await ctx.db
|
|
62
|
+
.query("checkpoints")
|
|
63
|
+
.withIndex("by_name_and_reachedAt", (q) => q.eq("name", args.name))
|
|
64
|
+
.order("desc")
|
|
65
|
+
.take(args.limit ?? 100);
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
export const listByUser = query({
|
|
69
|
+
args: {
|
|
70
|
+
userId: v.string(),
|
|
71
|
+
limit: v.optional(v.number()),
|
|
72
|
+
},
|
|
73
|
+
returns: v.array(checkpointValidator),
|
|
74
|
+
handler: async (ctx, args) => {
|
|
75
|
+
return await ctx.db
|
|
76
|
+
.query("checkpoints")
|
|
77
|
+
.withIndex("by_userId_and_reachedAt", (q) => q.eq("userId", args.userId))
|
|
78
|
+
.order("desc")
|
|
79
|
+
.take(args.limit ?? 100);
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
export const get = internalQuery({
|
|
83
|
+
args: {
|
|
84
|
+
checkpointId: v.id("checkpoints"),
|
|
85
|
+
},
|
|
86
|
+
returns: v.union(v.null(), checkpointValidator),
|
|
87
|
+
handler: async (ctx, args) => {
|
|
88
|
+
return await ctx.db.get("checkpoints", args.checkpointId);
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
//# sourceMappingURL=lib.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lib.js","sourceRoot":"","sources":["../../src/component/lib.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
declare const _default: import("convex/server").SchemaDefinition<{
|
|
2
|
+
checkpoints: import("convex/server").TableDefinition<import("convex/values").VObject<{
|
|
3
|
+
userId?: string | undefined;
|
|
4
|
+
idempotencyKey?: string | undefined;
|
|
5
|
+
payload?: any;
|
|
6
|
+
name: string;
|
|
7
|
+
reachedAt: number;
|
|
8
|
+
receivedAt: number;
|
|
9
|
+
}, {
|
|
10
|
+
name: import("convex/values").VString<string, "required">;
|
|
11
|
+
userId: import("convex/values").VString<string | undefined, "optional">;
|
|
12
|
+
payload: import("convex/values").VAny<any, "optional", string>;
|
|
13
|
+
idempotencyKey: import("convex/values").VString<string | undefined, "optional">;
|
|
14
|
+
reachedAt: import("convex/values").VFloat64<number, "required">;
|
|
15
|
+
receivedAt: import("convex/values").VFloat64<number, "required">;
|
|
16
|
+
}, "required", "name" | "userId" | "idempotencyKey" | "payload" | "reachedAt" | "receivedAt" | `payload.${string}`>, {
|
|
17
|
+
by_name_and_reachedAt: ["name", "reachedAt", "_creationTime"];
|
|
18
|
+
by_userId_and_reachedAt: ["userId", "reachedAt", "_creationTime"];
|
|
19
|
+
by_idempotencyKey: ["idempotencyKey", "_creationTime"];
|
|
20
|
+
}, {}, {}>;
|
|
21
|
+
}, true>;
|
|
22
|
+
export default _default;
|
|
23
|
+
//# sourceMappingURL=schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/component/schema.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;AAGA,wBAYG"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { defineSchema, defineTable } from "convex/server";
|
|
2
|
+
import { v } from "convex/values";
|
|
3
|
+
export default defineSchema({
|
|
4
|
+
checkpoints: defineTable({
|
|
5
|
+
name: v.string(),
|
|
6
|
+
userId: v.optional(v.string()),
|
|
7
|
+
payload: v.optional(v.any()),
|
|
8
|
+
idempotencyKey: v.optional(v.string()),
|
|
9
|
+
reachedAt: v.number(),
|
|
10
|
+
receivedAt: v.number(),
|
|
11
|
+
})
|
|
12
|
+
.index("by_name_and_reachedAt", ["name", "reachedAt"])
|
|
13
|
+
.index("by_userId_and_reachedAt", ["userId", "reachedAt"])
|
|
14
|
+
.index("by_idempotencyKey", ["idempotencyKey"]),
|
|
15
|
+
});
|
|
16
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/component/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC1D,OAAO,EAAE,CAAC,EAAE,MAAM,eAAe,CAAC;AAElC,eAAe,YAAY,CAAC;IAC1B,WAAW,EAAE,WAAW,CAAC;QACvB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;QAChB,MAAM,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;QAC9B,OAAO,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC;QAC5B,cAAc,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;QACtC,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;QACrB,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE;KACvB,CAAC;SACC,KAAK,CAAC,uBAAuB,EAAE,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;SACrD,KAAK,CAAC,yBAAyB,EAAE,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;SACzD,KAAK,CAAC,mBAAmB,EAAE,CAAC,gBAAgB,CAAC,CAAC;CAClD,CAAC,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@abdssamie/convex-checkpoints",
|
|
3
|
+
"description": "A Convex component for event-driven user checkpoints.",
|
|
4
|
+
"repository": "github:abdssamie/convex-checkpoints",
|
|
5
|
+
"homepage": "https://github.com/abdssamie/convex-checkpoints#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/abdssamie/convex-checkpoints/issues"
|
|
8
|
+
},
|
|
9
|
+
"version": "0.1.1",
|
|
10
|
+
"license": "Apache-2.0",
|
|
11
|
+
"keywords": [
|
|
12
|
+
"convex",
|
|
13
|
+
"component",
|
|
14
|
+
"events",
|
|
15
|
+
"checkpoints"
|
|
16
|
+
],
|
|
17
|
+
"type": "module",
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"src"
|
|
21
|
+
],
|
|
22
|
+
"exports": {
|
|
23
|
+
"./package.json": "./package.json",
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/client/index.d.ts",
|
|
26
|
+
"default": "./dist/client/index.js"
|
|
27
|
+
},
|
|
28
|
+
"./test": "./src/test.ts",
|
|
29
|
+
"./_generated/component.js": {
|
|
30
|
+
"types": "./dist/component/_generated/component.d.ts"
|
|
31
|
+
},
|
|
32
|
+
"./_generated/component": {
|
|
33
|
+
"types": "./dist/component/_generated/component.d.ts"
|
|
34
|
+
},
|
|
35
|
+
"./convex.config.js": {
|
|
36
|
+
"types": "./dist/component/convex.config.d.ts",
|
|
37
|
+
"default": "./dist/component/convex.config.js"
|
|
38
|
+
},
|
|
39
|
+
"./convex.config": {
|
|
40
|
+
"types": "./dist/component/convex.config.d.ts",
|
|
41
|
+
"default": "./dist/component/convex.config.js"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"convex": "^1.36.1"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@convex-dev/eslint-plugin": "^1.2.2",
|
|
49
|
+
"@edge-runtime/vm": "^5.0.0",
|
|
50
|
+
"@eslint/eslintrc": "^3.3.5",
|
|
51
|
+
"@eslint/js": "9.39.4",
|
|
52
|
+
"@types/node": "^24.12.2",
|
|
53
|
+
"@types/react": "^19.2.14",
|
|
54
|
+
"@types/react-dom": "^19.2.3",
|
|
55
|
+
"@vitejs/plugin-react": "^5.2.0",
|
|
56
|
+
"chokidar-cli": "3.0.0",
|
|
57
|
+
"convex": "1.36.1",
|
|
58
|
+
"convex-test": "0.0.49",
|
|
59
|
+
"eslint": "9.39.4",
|
|
60
|
+
"eslint-plugin-react": "^7.37.5",
|
|
61
|
+
"eslint-plugin-react-hooks": "^7.1.1",
|
|
62
|
+
"eslint-plugin-react-refresh": "^0.5.2",
|
|
63
|
+
"globals": "^17.5.0",
|
|
64
|
+
"pkg-pr-new": "^0.0.66",
|
|
65
|
+
"prettier": "3.8.3",
|
|
66
|
+
"react": "^19.2.5",
|
|
67
|
+
"react-dom": "^19.2.5",
|
|
68
|
+
"typescript": "6.0.3",
|
|
69
|
+
"typescript-eslint": "8.58.2",
|
|
70
|
+
"vite": "8.0.9",
|
|
71
|
+
"vitest": "4.1.4"
|
|
72
|
+
},
|
|
73
|
+
"types": "./dist/client/index.d.ts",
|
|
74
|
+
"module": "./dist/client/index.js",
|
|
75
|
+
"scripts": {
|
|
76
|
+
"dev": "convex dev --start 'npm run dev:build'",
|
|
77
|
+
"dev:frontend": "cd example && vite",
|
|
78
|
+
"dev:build": "chokidar 'tsconfig*.json' 'src/**/*.ts' -i '**/*.test.ts' -c 'npm run build:codegen' --initial",
|
|
79
|
+
"predev": "convex init && npm run build:codegen",
|
|
80
|
+
"build": "tsc --project ./tsconfig.build.json",
|
|
81
|
+
"build:codegen": "npx convex codegen --component-dir ./src/component && npm run build",
|
|
82
|
+
"build:clean": "rm -rf dist *.tsbuildinfo && npm run build:codegen",
|
|
83
|
+
"typecheck": "tsc --noEmit && tsc -p example && tsc -p example/convex",
|
|
84
|
+
"lint": "eslint .",
|
|
85
|
+
"test": "vitest run --typecheck",
|
|
86
|
+
"test:watch": "vitest --typecheck --clearScreen false",
|
|
87
|
+
"test:debug": "vitest --inspect-brk --no-file-parallelism",
|
|
88
|
+
"test:coverage": "vitest run --coverage --coverage.reporter=text",
|
|
89
|
+
"preversion": "npm ci && npm run build:clean && npm run test && npm run lint && npm run typecheck",
|
|
90
|
+
"alpha": "npm version prerelease --preid alpha && npm publish --tag alpha && git push --follow-tags",
|
|
91
|
+
"release": "npm version patch && npm publish && git push --follow-tags",
|
|
92
|
+
"version": "(npm whoami || npm login) && vim -c 'normal o' -c 'normal o## '$npm_package_version CHANGELOG.md && prettier -w CHANGELOG.md && git add CHANGELOG.md"
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// This is only here so convex-test can detect a _generated folder
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { describe, expect, test, vi } from "vitest";
|
|
2
|
+
import { anyApi, mutationGeneric, type ApiFromModules } from "convex/server";
|
|
3
|
+
import { v } from "convex/values";
|
|
4
|
+
import { ConvexCheckpoints } from "./index.js";
|
|
5
|
+
import { components, initConvexTest } from "./setup.test.js";
|
|
6
|
+
|
|
7
|
+
const checkpoints = new ConvexCheckpoints<{
|
|
8
|
+
"post.created": { postId: string };
|
|
9
|
+
}>(components.convexCheckpoints);
|
|
10
|
+
|
|
11
|
+
checkpoints.on("post.created", async () => {});
|
|
12
|
+
|
|
13
|
+
export const { listByUser } = checkpoints.api();
|
|
14
|
+
export const submitPostCreated = mutationGeneric({
|
|
15
|
+
args: {
|
|
16
|
+
userId: v.string(),
|
|
17
|
+
postId: v.string(),
|
|
18
|
+
},
|
|
19
|
+
handler: async (ctx, args) => {
|
|
20
|
+
return await checkpoints.submit(ctx, {
|
|
21
|
+
name: "post.created",
|
|
22
|
+
userId: args.userId,
|
|
23
|
+
payload: { postId: args.postId },
|
|
24
|
+
});
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const signupHandler = vi.fn();
|
|
29
|
+
const idempotentCheckpoints = new ConvexCheckpoints<{
|
|
30
|
+
"user.signup": { userId: string };
|
|
31
|
+
}>(components.convexCheckpoints);
|
|
32
|
+
idempotentCheckpoints.on("user.signup", signupHandler);
|
|
33
|
+
export const submitSignup = mutationGeneric({
|
|
34
|
+
args: {
|
|
35
|
+
userId: v.string(),
|
|
36
|
+
idempotencyKey: v.optional(v.string()),
|
|
37
|
+
},
|
|
38
|
+
handler: async (ctx, args) => {
|
|
39
|
+
return await idempotentCheckpoints.submit(ctx, {
|
|
40
|
+
name: "user.signup",
|
|
41
|
+
userId: args.userId,
|
|
42
|
+
payload: { userId: args.userId },
|
|
43
|
+
idempotencyKey: args.idempotencyKey,
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const testApi = (
|
|
49
|
+
anyApi as unknown as ApiFromModules<{
|
|
50
|
+
"index.test": {
|
|
51
|
+
submitPostCreated: typeof submitPostCreated;
|
|
52
|
+
submitSignup: typeof submitSignup;
|
|
53
|
+
listByUser: typeof listByUser;
|
|
54
|
+
};
|
|
55
|
+
}>
|
|
56
|
+
)["index.test"];
|
|
57
|
+
|
|
58
|
+
function assertSubmitTypes(
|
|
59
|
+
ctx: Parameters<
|
|
60
|
+
ConvexCheckpoints<{ "post.created": { postId: string } }>["submit"]
|
|
61
|
+
>[0],
|
|
62
|
+
) {
|
|
63
|
+
void checkpoints.submit(ctx, {
|
|
64
|
+
name: "post.created",
|
|
65
|
+
payload: { postId: "post1" },
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
void checkpoints.submit(ctx, {
|
|
69
|
+
// @ts-expect-error checkpoint names must exist in the checkpoint registry
|
|
70
|
+
name: "user.signup",
|
|
71
|
+
payload: { postId: "post1" },
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
void checkpoints.submit(ctx, {
|
|
75
|
+
name: "post.created",
|
|
76
|
+
// @ts-expect-error payload must match the checkpoint name
|
|
77
|
+
payload: { userId: "user1" },
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
void assertSubmitTypes;
|
|
82
|
+
|
|
83
|
+
describe("client tests", () => {
|
|
84
|
+
test("exports submit and list functions from checkpoint definitions", async () => {
|
|
85
|
+
const t = initConvexTest();
|
|
86
|
+
await t.mutation(testApi.submitPostCreated, {
|
|
87
|
+
userId: "user1",
|
|
88
|
+
postId: "post1",
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const checkpointsForUser = await t.query(testApi.listByUser, {
|
|
92
|
+
userId: "user1",
|
|
93
|
+
});
|
|
94
|
+
expect(checkpointsForUser).toHaveLength(1);
|
|
95
|
+
expect(checkpointsForUser[0].name).toBe("post.created");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("does not re-run handlers for duplicate idempotency keys", async () => {
|
|
99
|
+
signupHandler.mockClear();
|
|
100
|
+
const t = initConvexTest();
|
|
101
|
+
await t.mutation(testApi.submitSignup, {
|
|
102
|
+
userId: "user1",
|
|
103
|
+
idempotencyKey: "signup:user1",
|
|
104
|
+
});
|
|
105
|
+
await t.mutation(testApi.submitSignup, {
|
|
106
|
+
userId: "user1",
|
|
107
|
+
idempotencyKey: "signup:user1",
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
expect(signupHandler).toHaveBeenCalledOnce();
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { httpActionGeneric, httpRouter, queryGeneric } from "convex/server";
|
|
2
|
+
import { v } from "convex/values";
|
|
3
|
+
import type { ComponentApi } from "../component/_generated/component.js";
|
|
4
|
+
import {
|
|
5
|
+
ConvexCheckpoints as CheckpointDispatcher,
|
|
6
|
+
type CheckpointHandler,
|
|
7
|
+
} from "../component/checkpointDispatcher.js";
|
|
8
|
+
|
|
9
|
+
type CheckpointRegistry = Record<string, unknown>;
|
|
10
|
+
type CheckpointName<TCheckpointRegistry extends CheckpointRegistry> = Extract<
|
|
11
|
+
keyof TCheckpointRegistry,
|
|
12
|
+
string
|
|
13
|
+
>;
|
|
14
|
+
|
|
15
|
+
type BaseSubmitArgs = {
|
|
16
|
+
userId?: string;
|
|
17
|
+
idempotencyKey?: string;
|
|
18
|
+
reachedAt?: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type UntypedSubmitArgs = BaseSubmitArgs & {
|
|
22
|
+
name: string;
|
|
23
|
+
payload?: unknown;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type SubmitArgs<
|
|
27
|
+
TCheckpointRegistry extends CheckpointRegistry,
|
|
28
|
+
TCheckpoint extends CheckpointName<TCheckpointRegistry>,
|
|
29
|
+
> = BaseSubmitArgs & {
|
|
30
|
+
name: TCheckpoint;
|
|
31
|
+
payload: TCheckpointRegistry[TCheckpoint];
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export class ConvexCheckpoints<
|
|
35
|
+
TCheckpointRegistry extends CheckpointRegistry,
|
|
36
|
+
> extends CheckpointDispatcher<TCheckpointRegistry> {
|
|
37
|
+
constructor(private component: ComponentApi) {
|
|
38
|
+
super();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
public async submit<TCheckpoint extends CheckpointName<TCheckpointRegistry>>(
|
|
42
|
+
ctx: Parameters<ConvexCheckpoints<TCheckpointRegistry>["trigger"]>[0],
|
|
43
|
+
args: SubmitArgs<TCheckpointRegistry, TCheckpoint>,
|
|
44
|
+
) {
|
|
45
|
+
const result = await ctx.runMutation(this.component.lib.record, args);
|
|
46
|
+
if (result.created) {
|
|
47
|
+
await this.trigger(ctx, args.name, args.payload);
|
|
48
|
+
}
|
|
49
|
+
return result.checkpointId;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public api() {
|
|
53
|
+
return {
|
|
54
|
+
listRecent: queryGeneric({
|
|
55
|
+
args: { limit: v.optional(v.number()) },
|
|
56
|
+
handler: async (ctx, args) => {
|
|
57
|
+
return await ctx.runQuery(this.component.lib.listRecent, args);
|
|
58
|
+
},
|
|
59
|
+
}),
|
|
60
|
+
listByName: queryGeneric({
|
|
61
|
+
args: { name: v.string(), limit: v.optional(v.number()) },
|
|
62
|
+
handler: async (ctx, args) => {
|
|
63
|
+
return await ctx.runQuery(this.component.lib.listByName, args);
|
|
64
|
+
},
|
|
65
|
+
}),
|
|
66
|
+
listByUser: queryGeneric({
|
|
67
|
+
args: { userId: v.string(), limit: v.optional(v.number()) },
|
|
68
|
+
handler: async (ctx, args) => {
|
|
69
|
+
return await ctx.runQuery(this.component.lib.listByUser, args);
|
|
70
|
+
},
|
|
71
|
+
}),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
public http(path = "/checkpoints") {
|
|
76
|
+
const http = httpRouter();
|
|
77
|
+
http.route({
|
|
78
|
+
path,
|
|
79
|
+
method: "POST",
|
|
80
|
+
handler: httpActionGeneric(async (ctx, request) => {
|
|
81
|
+
const args = await readSubmitArgsFromBody(request);
|
|
82
|
+
if (args === null) {
|
|
83
|
+
return json({ error: "invalid_checkpoint" }, 400);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const result = await this.submitFromArgs(ctx, args);
|
|
87
|
+
|
|
88
|
+
return json(
|
|
89
|
+
{ checkpointId: result.checkpointId, created: result.created },
|
|
90
|
+
202,
|
|
91
|
+
);
|
|
92
|
+
}),
|
|
93
|
+
});
|
|
94
|
+
http.route({
|
|
95
|
+
path,
|
|
96
|
+
method: "OPTIONS",
|
|
97
|
+
handler: httpActionGeneric(async () => corsResponse()),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const pathPrefix = path.endsWith("/") ? path : `${path}/`;
|
|
101
|
+
http.route({
|
|
102
|
+
pathPrefix,
|
|
103
|
+
method: "POST",
|
|
104
|
+
handler: httpActionGeneric(async (ctx, request) => {
|
|
105
|
+
const checkpointName = readCheckpointNameFromPath(request, pathPrefix);
|
|
106
|
+
if (checkpointName === null) {
|
|
107
|
+
return json({ error: "invalid_checkpoint_path" }, 400);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const args = await readSubmitArgsFromPath(request, checkpointName);
|
|
111
|
+
if (args === null) {
|
|
112
|
+
return json({ error: "invalid_checkpoint" }, 400);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const result = await this.submitFromArgs(ctx, args);
|
|
116
|
+
|
|
117
|
+
return json(
|
|
118
|
+
{ checkpointId: result.checkpointId, created: result.created },
|
|
119
|
+
202,
|
|
120
|
+
);
|
|
121
|
+
}),
|
|
122
|
+
});
|
|
123
|
+
http.route({
|
|
124
|
+
pathPrefix,
|
|
125
|
+
method: "OPTIONS",
|
|
126
|
+
handler: httpActionGeneric(async () => corsResponse()),
|
|
127
|
+
});
|
|
128
|
+
return http;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private async submitFromArgs(
|
|
132
|
+
ctx: Parameters<ConvexCheckpoints<TCheckpointRegistry>["trigger"]>[0],
|
|
133
|
+
args: UntypedSubmitArgs,
|
|
134
|
+
) {
|
|
135
|
+
const result = await ctx.runMutation(this.component.lib.record, args);
|
|
136
|
+
if (result.created) {
|
|
137
|
+
await this.trigger(
|
|
138
|
+
ctx,
|
|
139
|
+
args.name as CheckpointName<TCheckpointRegistry>,
|
|
140
|
+
args.payload as TCheckpointRegistry[CheckpointName<TCheckpointRegistry>],
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export type { CheckpointHandler };
|
|
148
|
+
|
|
149
|
+
async function readSubmitArgsFromBody(
|
|
150
|
+
request: Request,
|
|
151
|
+
): Promise<UntypedSubmitArgs | null> {
|
|
152
|
+
const checkpoint = await readJsonObject(request);
|
|
153
|
+
if (checkpoint === null || typeof checkpoint.name !== "string") {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
return readSubmitArgs(checkpoint.name, checkpoint, checkpoint.payload);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function readSubmitArgsFromPath(
|
|
160
|
+
request: Request,
|
|
161
|
+
name: string,
|
|
162
|
+
): Promise<UntypedSubmitArgs | null> {
|
|
163
|
+
const checkpoint = await readJsonObject(request);
|
|
164
|
+
if (checkpoint === null) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return readSubmitArgs(name, checkpoint, checkpoint);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function readSubmitArgs(
|
|
172
|
+
name: string,
|
|
173
|
+
checkpoint: Record<string, unknown>,
|
|
174
|
+
payload: unknown,
|
|
175
|
+
): UntypedSubmitArgs | null {
|
|
176
|
+
if (
|
|
177
|
+
checkpoint.userId !== undefined &&
|
|
178
|
+
typeof checkpoint.userId !== "string"
|
|
179
|
+
) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
if (
|
|
183
|
+
checkpoint.idempotencyKey !== undefined &&
|
|
184
|
+
typeof checkpoint.idempotencyKey !== "string"
|
|
185
|
+
) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
if (
|
|
189
|
+
checkpoint.reachedAt !== undefined &&
|
|
190
|
+
typeof checkpoint.reachedAt !== "number"
|
|
191
|
+
) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
name,
|
|
197
|
+
userId: checkpoint.userId,
|
|
198
|
+
payload,
|
|
199
|
+
idempotencyKey: checkpoint.idempotencyKey,
|
|
200
|
+
reachedAt: checkpoint.reachedAt,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function readJsonObject(
|
|
205
|
+
request: Request,
|
|
206
|
+
): Promise<Record<string, unknown> | null> {
|
|
207
|
+
let body: unknown;
|
|
208
|
+
try {
|
|
209
|
+
body = await request.json();
|
|
210
|
+
} catch {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return body as Record<string, unknown>;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function readCheckpointNameFromPath(request: Request, pathPrefix: string) {
|
|
222
|
+
const path = new URL(request.url).pathname;
|
|
223
|
+
if (!path.startsWith(pathPrefix)) {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
const encodedCheckpointName = path.slice(pathPrefix.length);
|
|
227
|
+
if (
|
|
228
|
+
encodedCheckpointName.length === 0 ||
|
|
229
|
+
encodedCheckpointName.includes("/")
|
|
230
|
+
) {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
return decodeURIComponent(encodedCheckpointName);
|
|
236
|
+
} catch {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function json(body: unknown, status: number) {
|
|
242
|
+
return new Response(JSON.stringify(body), {
|
|
243
|
+
status,
|
|
244
|
+
headers: {
|
|
245
|
+
"Content-Type": "application/json",
|
|
246
|
+
...corsHeaders(),
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function corsResponse() {
|
|
252
|
+
return new Response(null, {
|
|
253
|
+
status: 204,
|
|
254
|
+
headers: corsHeaders(),
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function corsHeaders() {
|
|
259
|
+
return {
|
|
260
|
+
"Access-Control-Allow-Origin": "*",
|
|
261
|
+
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
|
262
|
+
"Access-Control-Allow-Headers": "Content-Type",
|
|
263
|
+
};
|
|
264
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
import { test } from "vitest";
|
|
3
|
+
import { convexTest } from "convex-test";
|
|
4
|
+
export const modules = import.meta.glob("./**/*.*s");
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
defineSchema,
|
|
8
|
+
type GenericSchema,
|
|
9
|
+
type SchemaDefinition,
|
|
10
|
+
} from "convex/server";
|
|
11
|
+
import { type ComponentApi } from "../component/_generated/component.js";
|
|
12
|
+
import { componentsGeneric } from "convex/server";
|
|
13
|
+
import { register } from "../test.js";
|
|
14
|
+
|
|
15
|
+
export function initConvexTest<
|
|
16
|
+
Schema extends SchemaDefinition<GenericSchema, boolean>,
|
|
17
|
+
>(schema?: Schema) {
|
|
18
|
+
const t = convexTest(schema ?? defineSchema({}), modules);
|
|
19
|
+
register(t);
|
|
20
|
+
return t;
|
|
21
|
+
}
|
|
22
|
+
export const components = componentsGeneric() as unknown as {
|
|
23
|
+
convexCheckpoints: ComponentApi;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
test("setup", () => {});
|