Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 143 additions & 0 deletions convex/inlineFunctions.test.ts
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" });
});

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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" });
});

I don't think this query adds much value

Copy link
Contributor Author

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.

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" });
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ctx.runMutation does not support an inline function in regular convex, so it doesn't need to work here and we shouldn't document / encourage it. only t.mutation / t.action / t.query please.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point - removed this test and updated the implementation so that ctx.runMutation, ctx.runQuery, and ctx.runAction inside actions only accept function references (not inline functions). The inline function support is now limited to the top-level t.query, t.mutation, and t.action methods only.

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");
});
177 changes: 145 additions & 32 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
GenericDataModel,
GenericDocument,
GenericMutationCtx,
GenericQueryCtx,
GenericSchema,
HttpRouter,
OptionalRestArgs,
Expand Down Expand Up @@ -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,
),
);
}
Expand All @@ -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,
),
);
}
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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;
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks duplicative with other code in this file. how might we share more code here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactored to reduce duplication by extracting shared helpers:

  • runQueryWithHandler - shared query execution logic used by both byTypeWithPath.queryFromPath and inline queries
  • runActionWithHandler - shared action execution logic used by both byTypeWithPath.actionFromPath and inline actions

Also created refOnlyQuery, refOnlyMutation, and refOnlyAction helpers that only accept function references (no inline support) for use inside action contexts.


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);
},
};
Expand Down