-
Notifications
You must be signed in to change notification settings - Fork 6
feat: add inline function support for t.query, t.mutation, and t.action #59
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
73a3db8
275fd5b
65f0794
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,143 @@ | ||
| import { expect, test } from "vitest"; | ||
| import { convexTest } from "../index"; | ||
| import schema from "./schema"; | ||
|
|
||
| test("inline query", async () => { | ||
| const t = convexTest(schema); | ||
| await t.run(async (ctx) => { | ||
| await ctx.db.insert("messages", { author: "sarah", body: "hello1" }); | ||
| await ctx.db.insert("messages", { author: "sarah", body: "hello2" }); | ||
| }); | ||
| const messages = await t.query(async (ctx) => { | ||
| return await ctx.db.query("messages").collect(); | ||
| }); | ||
| expect(messages).toMatchObject([ | ||
| { author: "sarah", body: "hello1" }, | ||
| { author: "sarah", body: "hello2" }, | ||
| ]); | ||
| }); | ||
|
|
||
| test("inline query with first", async () => { | ||
| const t = convexTest(schema); | ||
| await t.run(async (ctx) => { | ||
| await ctx.db.insert("messages", { author: "sarah", body: "hello" }); | ||
| }); | ||
| const message = await t.query(async (ctx) => { | ||
| return await ctx.db.query("messages").first(); | ||
| }); | ||
| expect(message).toMatchObject({ author: "sarah", body: "hello" }); | ||
| }); | ||
|
|
||
| test("inline mutation insert", async () => { | ||
| const t = convexTest(schema); | ||
| const id = await t.mutation(async (ctx) => { | ||
| return await ctx.db.insert("messages", { author: "sarah", body: "hello" }); | ||
| }); | ||
| expect(id).toBeDefined(); | ||
| const messages = await t.query(async (ctx) => { | ||
| return await ctx.db.query("messages").collect(); | ||
| }); | ||
| expect(messages).toMatchObject([{ author: "sarah", body: "hello" }]); | ||
| }); | ||
|
|
||
| test("inline mutation patch", async () => { | ||
| const t = convexTest(schema); | ||
| const id = await t.mutation(async (ctx) => { | ||
| return await ctx.db.insert("messages", { author: "sarah", body: "hello" }); | ||
| }); | ||
| await t.mutation(async (ctx) => { | ||
| await ctx.db.patch(id, { body: "updated" }); | ||
| }); | ||
| const message = await t.query(async (ctx) => { | ||
| return await ctx.db.get(id); | ||
| }); | ||
| expect(message).toMatchObject({ author: "sarah", body: "updated" }); | ||
| }); | ||
|
|
||
| test("inline mutation delete", async () => { | ||
| const t = convexTest(schema); | ||
| const id = await t.mutation(async (ctx) => { | ||
| return await ctx.db.insert("messages", { author: "sarah", body: "hello" }); | ||
| }); | ||
| await t.mutation(async (ctx) => { | ||
| await ctx.db.delete(id); | ||
| }); | ||
| const message = await t.query(async (ctx) => { | ||
| return await ctx.db.get(id); | ||
| }); | ||
| expect(message).toBeNull(); | ||
| }); | ||
|
|
||
| test("inline action calling inline query and mutation", async () => { | ||
| const t = convexTest(schema); | ||
| const result = await t.action(async (ctx) => { | ||
| await ctx.runMutation(async (mutCtx) => { | ||
| await mutCtx.db.insert("messages", { author: "action", body: "test" }); | ||
| }); | ||
|
||
| const messages = await ctx.runQuery(async (queryCtx) => { | ||
| return await queryCtx.db.query("messages").collect(); | ||
| }); | ||
| return messages; | ||
| }); | ||
| expect(result).toMatchObject([{ author: "action", body: "test" }]); | ||
| }); | ||
|
|
||
| test("inline query is read-only (no db.insert)", async () => { | ||
| const t = convexTest(schema); | ||
| await expect(async () => { | ||
| await t.query(async (ctx) => { | ||
| // @ts-expect-error - queries should not have insert | ||
| await ctx.db.insert("messages", { author: "sarah", body: "hello" }); | ||
| }); | ||
| }).rejects.toThrow(); | ||
| }); | ||
|
|
||
| test("inline mutation transaction rollback", async () => { | ||
| const t = convexTest(schema); | ||
| await expect(async () => { | ||
| await t.mutation(async (ctx) => { | ||
| await ctx.db.insert("messages", { author: "sarah", body: "hello" }); | ||
| throw new Error("rollback"); | ||
| }); | ||
| }).rejects.toThrowError("rollback"); | ||
|
|
||
| const messages = await t.query(async (ctx) => { | ||
| return await ctx.db.query("messages").collect(); | ||
| }); | ||
| expect(messages).toMatchObject([]); | ||
| }); | ||
|
|
||
| test("inline functions with identity", async () => { | ||
| const t = convexTest(schema); | ||
| const identity = await t | ||
| .withIdentity({ name: "Test User" }) | ||
| .query(async (ctx) => { | ||
| return await ctx.auth.getUserIdentity(); | ||
| }); | ||
| expect(identity).toMatchObject({ name: "Test User" }); | ||
| }); | ||
|
|
||
| test("inline query returns value", async () => { | ||
| const t = convexTest(schema); | ||
| await t.run(async (ctx) => { | ||
| await ctx.db.insert("messages", { author: "sarah", body: "hello" }); | ||
| }); | ||
| const count = await t.query(async (ctx) => { | ||
| const messages = await ctx.db.query("messages").collect(); | ||
| return messages.length + 41; | ||
| }); | ||
| expect(count).toBe(42); | ||
| }); | ||
|
|
||
| test("inline mutation returns inserted id", async () => { | ||
| const t = convexTest(schema); | ||
| const id = await t.mutation(async (ctx) => { | ||
| const insertedId = await ctx.db.insert("messages", { | ||
| author: "sarah", | ||
| body: "hello", | ||
| }); | ||
| return insertedId; | ||
| }); | ||
| expect(typeof id).toBe("string"); | ||
| expect(id).toContain("messages"); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,7 @@ import { | |
| GenericDataModel, | ||
| GenericDocument, | ||
| GenericMutationCtx, | ||
| GenericQueryCtx, | ||
| GenericSchema, | ||
| HttpRouter, | ||
| OptionalRestArgs, | ||
|
|
@@ -1158,7 +1159,10 @@ function asyncSyscallImpl() { | |
| const { name, args: queryArgs } = args; | ||
| return JSON.stringify( | ||
| convexToJson( | ||
| await withAuth().query(makeFunctionReference(name), queryArgs), | ||
| (await withAuth().query( | ||
| makeFunctionReference(name), | ||
| queryArgs, | ||
| )) as Value, | ||
| ), | ||
| ); | ||
| } | ||
|
|
@@ -1177,7 +1181,10 @@ function asyncSyscallImpl() { | |
| const { name, args: actionArgs } = args; | ||
| return JSON.stringify( | ||
| convexToJson( | ||
| await withAuth().action(makeFunctionReference(name), actionArgs), | ||
| (await withAuth().action( | ||
| makeFunctionReference(name), | ||
| actionArgs, | ||
| )) as Value, | ||
| ), | ||
| ); | ||
| } | ||
|
|
@@ -1478,43 +1485,61 @@ export type TestConvex<SchemaDef extends SchemaDefinition<any, boolean>> = | |
|
|
||
| export type TestConvexForDataModel<DataModel extends GenericDataModel> = { | ||
| /** | ||
| * Call a public or internal query. | ||
| * Call a public or internal query, or run an inline query function. | ||
| * | ||
| * @param query A {@link FunctionReference} for the query. | ||
| * @param query A {@link FunctionReference} for the query, or an inline | ||
| * async function that receives a query context. | ||
| * @param args An arguments object for the query. If this is omitted, | ||
| * the arguments will be `{}`. | ||
| * the arguments will be `{}`. Not used for inline functions. | ||
| * @returns A `Promise` of the query's result. | ||
| */ | ||
| query: <Query extends FunctionReference<"query", any>>( | ||
| query: Query, | ||
| ...args: OptionalRestArgs<Query> | ||
| ) => Promise<FunctionReturnType<Query>>; | ||
| query: { | ||
| <Query extends FunctionReference<"query", any>>( | ||
| query: Query, | ||
| ...args: OptionalRestArgs<Query> | ||
| ): Promise<FunctionReturnType<Query>>; | ||
| <Output>( | ||
| func: (ctx: GenericQueryCtx<DataModel>) => Promise<Output>, | ||
| ): Promise<Output>; | ||
| }; | ||
|
|
||
| /** | ||
| * Call a public or internal mutation. | ||
| * Call a public or internal mutation, or run an inline mutation function. | ||
| * | ||
| * @param mutation A {@link FunctionReference} for the mutation. | ||
| * @param mutation A {@link FunctionReference} for the mutation, or an inline | ||
| * async function that receives a mutation context. | ||
| * @param args An arguments object for the mutation. If this is omitted, | ||
| * the arguments will be `{}`. | ||
| * the arguments will be `{}`. Not used for inline functions. | ||
| * @returns A `Promise` of the mutation's result. | ||
| */ | ||
| mutation: <Mutation extends FunctionReference<"mutation", any>>( | ||
| mutation: Mutation, | ||
| ...args: OptionalRestArgs<Mutation> | ||
| ) => Promise<FunctionReturnType<Mutation>>; | ||
| mutation: { | ||
| <Mutation extends FunctionReference<"mutation", any>>( | ||
| mutation: Mutation, | ||
| ...args: OptionalRestArgs<Mutation> | ||
| ): Promise<FunctionReturnType<Mutation>>; | ||
| <Output>( | ||
| func: (ctx: GenericMutationCtx<DataModel>) => Promise<Output>, | ||
| ): Promise<Output>; | ||
| }; | ||
|
|
||
| /** | ||
| * Call a public or internal action. | ||
| * Call a public or internal action, or run an inline action function. | ||
| * | ||
| * @param action A {@link FunctionReference} for the action. | ||
| * @param action A {@link FunctionReference} for the action, or an inline | ||
| * async function that receives an action context. | ||
| * @param args An arguments object for the action. If this is omitted, | ||
| * the arguments will be `{}`. | ||
| * the arguments will be `{}`. Not used for inline functions. | ||
| * @returns A `Promise` of the action's result. | ||
| */ | ||
| action: <Action extends FunctionReference<"action", any>>( | ||
| action: Action, | ||
| ...args: OptionalRestArgs<Action> | ||
| ) => Promise<FunctionReturnType<Action>>; | ||
| action: { | ||
| <Action extends FunctionReference<"action", any>>( | ||
| action: Action, | ||
| ...args: OptionalRestArgs<Action> | ||
| ): Promise<FunctionReturnType<Action>>; | ||
| <Output>( | ||
| func: (ctx: GenericActionCtx<DataModel>) => Promise<Output>, | ||
| ): Promise<Output>; | ||
| }; | ||
|
|
||
| /** | ||
| * Read from and write to the mock backend. | ||
|
|
@@ -1947,30 +1972,118 @@ function withAuth(auth: AuthFake = new AuthFake()) { | |
| }, | ||
| }; | ||
|
|
||
| const runInlineQuery = async <T>( | ||
| componentPath: string, | ||
| handler: (ctx: any) => T, | ||
| ): Promise<T> => { | ||
| const q = queryGeneric({ | ||
| handler: (ctx: any) => { | ||
| const testCtx = { ...ctx, auth }; | ||
| return handler(testCtx); | ||
| }, | ||
| }); | ||
| const transactionManager = getTransactionManager(); | ||
| const functionPath = { componentPath, udfPath: "inline" }; | ||
| await transactionManager.begin(functionPath, false); | ||
| try { | ||
| const rawResult = await ( | ||
| q as unknown as { invokeQuery: (args: string) => Promise<string> } | ||
| ).invokeQuery(JSON.stringify(convexToJson([{}]))); | ||
| return jsonToConvex(JSON.parse(rawResult)) as T; | ||
| } finally { | ||
| transactionManager.rollback(false); | ||
| } | ||
| }; | ||
|
|
||
| const runInlineMutation = async <T>( | ||
| componentPath: string, | ||
| handler: (ctx: any) => T, | ||
| ): Promise<T> => { | ||
| return await runTransaction( | ||
| handler, | ||
| {}, | ||
| {}, | ||
| { componentPath, udfPath: "inline" }, | ||
| false, | ||
| ); | ||
| }; | ||
|
|
||
| const runInlineAction = async <T>( | ||
| componentPath: string, | ||
| handler: (ctx: any) => T, | ||
| ): Promise<T> => { | ||
| const a = actionGeneric({ | ||
| handler: (ctx: any) => { | ||
| const testCtx = { | ||
| ...ctx, | ||
| runQuery: byType.query, | ||
| runMutation: byType.mutation, | ||
| runAction: byType.action, | ||
| auth, | ||
| }; | ||
| return handler(testCtx); | ||
| }, | ||
| }); | ||
| const functionPath = { componentPath, udfPath: "inline" }; | ||
| getTransactionManager().beginAction(functionPath); | ||
| const requestId = "" + Math.random(); | ||
| const rawResult = await ( | ||
| a as unknown as { | ||
| invokeAction: (requestId: string, args: string) => Promise<string>; | ||
| } | ||
| ).invokeAction(requestId, JSON.stringify(convexToJson([{}]))); | ||
| getTransactionManager().finishAction(); | ||
| return jsonToConvex(JSON.parse(rawResult)) as T; | ||
| }; | ||
|
||
|
|
||
| const byType = { | ||
| query: async (functionReference: any, args: any) => { | ||
| const functionPath = | ||
| await getFunctionPathFromReference(functionReference); | ||
| query: async (functionReferenceOrHandler: any, args?: any) => { | ||
| if (typeof functionReferenceOrHandler === "function") { | ||
| return await runInlineQuery( | ||
| getCurrentComponentPath(), | ||
| functionReferenceOrHandler, | ||
| ); | ||
| } | ||
| const functionPath = await getFunctionPathFromReference( | ||
| functionReferenceOrHandler, | ||
| ); | ||
| return await byTypeWithPath.queryFromPath( | ||
| functionPath, | ||
| /* isNested */ false, | ||
| args, | ||
| ); | ||
| }, | ||
|
|
||
| mutation: async (functionReference: any, args: any): Promise<Value> => { | ||
| const functionPath = | ||
| await getFunctionPathFromReference(functionReference); | ||
| mutation: async ( | ||
| functionReferenceOrHandler: any, | ||
| args?: any, | ||
| ): Promise<Value> => { | ||
| if (typeof functionReferenceOrHandler === "function") { | ||
| return await runInlineMutation( | ||
| getCurrentComponentPath(), | ||
| functionReferenceOrHandler, | ||
| ); | ||
| } | ||
| const functionPath = await getFunctionPathFromReference( | ||
| functionReferenceOrHandler, | ||
| ); | ||
| return await byTypeWithPath.mutationFromPath( | ||
| functionPath, | ||
| /* isNested */ false, | ||
| args, | ||
| ); | ||
| }, | ||
|
|
||
| action: async (functionReference: any, args: any) => { | ||
| const functionPath = | ||
| await getFunctionPathFromReference(functionReference); | ||
| action: async (functionReferenceOrHandler: any, args?: any) => { | ||
| if (typeof functionReferenceOrHandler === "function") { | ||
| return await runInlineAction( | ||
| getCurrentComponentPath(), | ||
| functionReferenceOrHandler, | ||
| ); | ||
| } | ||
| const functionPath = await getFunctionPathFromReference( | ||
| functionReferenceOrHandler, | ||
| ); | ||
| return await byTypeWithPath.actionFromPath(functionPath, args); | ||
| }, | ||
| }; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this query adds much value
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed this test as suggested.