Convex 0.13.0 is here and it’s kinda a big deal. The changes in this release generally fall among 2 themes:
- Function arguments: Convex functions now receive a single object as their argument and support validating this object.
- Actions: HTTP endpoints and actions are now unified and more powerful.
Along the way we’ve had to make a lot of breaking changes to the platform. Your existing app will continue to function, but the upgrade to 0.13.0 may be involved. Please feel free to reach out in the Convex Discord community if you have questions or need a hand updating your app.
We’re looking to stabilize the API of Convex for 1.0 and plan to make many less breaking changes in the future.
The full list of changes in this release is:
Function Arguments:
- Breaking: Convex functions receive a single arguments object
- Argument Validation
- Breaking:
sfromconvex/schemais nowvinconvex/values - Breaking:
usePaginatedQueryAPI changes - Breaking:
ConvexReactClientAPI changes - Breaking: Optimistic update API changes
- Breaking:
ConvexHttpClientAPI changes - Breaking:
InternalConvexClient→BaseConvexClientand API changes
Actions:
- Actions can run outside of Node.js
- Breaking: Node.js actions need
"use node"; - Breaking:
httpEndpoint→httpAction schedulerin HTTP actions- Node.js actions run in Node.js 18
fetchin HTTP actionsstoragein actions- Breaking: HTTP action
storageAPI changes
Other Changes:
- Breaking: Querying for documents missing a field
- Minor Improvements
You can read more about these changes below.
Function Arguments
Breaking: Convex functions receive a single arguments object
Convex queries, mutations, and actions now receive a single object as their argument.
As of version 0.13.0, you can no longer pass multiple arguments to Convex functions. All functions must receive a single object as their argument, or no argument at all.
Previously, you could define functions that took positional arguments like:
export default mutation(async ({ db }, body, author) => {
//...
});
Now, you define your functions to take an arguments object like:
// `{ body, author }` destructures the single arguments object
// to extract the "body" and "author" fields.
export default mutation(async ({ db }, { body, author }) => {
//...
});
Similarly, when calling your functions, you must switch from passing in positional arguments like:
const sendMessage = useMutation("sendMessage");
async function onClick() {
await sendMessage("Welcome to Convex v0.13.0", "The Convex Team");
}
To passing in an arguments object like:
const sendMessage = useMutation("sendMessage");
async function onClick() {
await sendMessage({
body: "Welcome to Convex v0.13.0",
author: "The Convex Team"
});
}
If your functions don’t need any arguments, no change is needed.
The rationale for this change is two-fold:
- This enabled us to build argument validation (see below). Using named arguments makes it straightforward to tell which validator corresponds to which argument.
- This enables more API consistency. Now all of the methods for calling Convex functions can have the same
(name, arguments, options)API because they don’t have to deal with a variable number of arguments.
The newest versions of the ConvexReactClient, ConvexHttpClient, BaseConvexClient, and Python Convex client all only support calling functions with a single arguments object.
We know that updating your apps to use arguments objects will take work. If you need a hand or want advice on the migration, please join our Discord community. We’re happy to help!
Argument validation
Convex now supports adding optional argument validators to queries, mutations, and actions. Argument validators ensure that your Convex functions are called with the correct types of arguments.
Without argument validation, a malicious user can call your public functions with unexpected arguments and cause surprising results. We recommend adding argument validation to all public functions in production apps.
Luckily, adding argument validation to your functions is a breeze!
To add argument validation, define your functions as objects with args and handler properties:
import { mutation } from "./_generated/server";
import { v } from "convex/values";
export default mutation({
args: {
body: v.string(),
author: v.string(),
},
handler: async ({ db }, { body, author }) => {
const message = { body, author };
await db.insert("messages", message);
},
});
The args property is an object mapping argument names to validators. Validators are written with the validator builder, v. This is the same v that is used to define schemas.
The handler is the implementation function.
If you’re using TypeScript, the types of your functions will be inferred from your argument validators! No need to manually annotate your functions with types anymore.
If you don’t want to use Convex’s argument validation, you can continue defining functions via inline functions as before, and validating arguments manually.
To learn more, read the full documentation.
Breaking: s → v and moved to convex/values
We’ve renamed the schema builder from s to v and moved it from "convex/schema" to "convex/values". If your project has a schema, it should be updated to look like:
import { defineSchema, defineTable } from "convex/schema";
import { v } from "convex/values";
export default defineSchema({
messages: defineTable({
body: v.string(),
author: v.string(),
}),
});
We made this change to support argument validation. Now that this builder is shared between schemas and argument validation, it’s more clear to export it from "convex/values".
Breaking: usePaginatedQuery API changes
The API of the usePaginatedQuery React hook has changed to no longer use positional arguments.
To use the new version, define a query function that expects a paginationOpts property in its arguments object like:
import { query } from "./_generated/server";
export default query(async ({ db }, { paginationOpts }) => {
return await db.query("messages").order("desc").paginate(paginationOpts);
});
You can also include additional properties in the arguments object.
Then you can load the paginated query like:
const { results, status, loadMore } = usePaginatedQuery(
"listMessages",
{},
{
initialNumItems: 5,
}
);
To learn more, read the Paginated Queries documentation.
Breaking: ConvexReactClient API changes
We’ve made some tweaks to the mutation and action methods on ConvexReactClient.
Previously they returned callbacks to execute the mutation or action. Now they directly invoke the function and return a Promise of the result.
Calling a mutation now looks like:
const result = await reactClient.mutation("mutationName", { arg: "value" });
Similarly, calling an action now looks like:
const result = await reactClient.action("actionName", { arg: "value" });
Additionally, the mutation method takes in an optional third options parameter that allows you to specify an optimistic update:
const result = await reactClient.mutation(
"mutationName",
{ arg: "value" },
{
optimisticUpdate: (localStore, { arg }) => {
// do the optimistic update here!
},
}
);
The useMutation and useAction hooks are unaffected.
We’ve made these changes for consistency with our other clients.
Breaking: Optimistic update API changes
The API of the methods on OptimisticLocalStore has changed:
- The second parameter to
getQueryis now an arguments object (not an array). - The second parameter to
setQueryis now an arguments object (not an array). - The return value from
getAllQueriesincludes an arguments object (not an array).
This is for consistency with our other APIs for interacting with function results.
Breaking: ConvexHttpClient API changes
We’ve changed the ConvexHttpClient to no longer return callbacks from query, mutation, or action. The new API looks like:
const result = await httpClient.query("queryName", { arg: "value" });
const result = await httpClient.mutation("mutationName", { arg: "value" });
const result = await httpClient.action("actionName", { arg: "value" });
If you haven’t gotten the picture yet, we’re serious about consistency!
Breaking: InternalConvexClient → BaseConvexClient and API changes
We’ve renamed InternaConvexClient to BaseConvexClient to make it more clear that this is a foundation for building framework-specific clients.
We’ve also made a couple of API changes for consistency with our other clients:
mutateis renamed tomutation.- The second parameter to
mutationis now an arguments object (not an array). - The third parameter to
mutationis now an optional options object with anoptimisticUpdatefield. - The second parameter to
actionis now an arguments object (not an array). - The second parameter to
subscribeis now an arguments object (not an array). - The third parameter to
subscribeis now an optional options object with ajournalfield.
Actions
Actions can run outside of Node.js
Convex 0.13.0 supports running actions within Convex’s custom JavaScript environment in addition to Node.js. Convex’s JavaScript environment is faster than Node.js (no cold starts!) and allows you to put actions in the same files as queries and mutations.
Convex’s JavaScript environment supports fetch and is a good fit for actions that simply want to call a third party API. If you need to use Node.js npm packages, you can still declare that the action should run in Node.js by putting "use node"; at the top of the file.
Here’s an example of colocating an action and an internal mutation in the same file:
import { action, internalMutation } from "./_generated/server";
export default action(async ({ runMutation }, { a, b }) => {
await runMutation("myAction:writeData", { a });
// Do other things like call `fetch`.
});
export const writeData = internalMutation(async ({ db }, { a }) => {
// Write to the `db` in here.
});
To learn more, read the documentation on actions.
Breaking: Node.js actions need "use node";
As mentioned above, if you’d like your action to continue to run in Node.js, you must add "use node;" to the top of the file. Here’s an example:
"use node";
import { action } from "./_generated/server";
import SomeNpmPackage from "some-npm-package";
export default action(async _ => {
// do something with SomeNpmPackage
});
To minimize errors during migrations, we require all files defined in /convex/actions folder to have “use node”;. The simplest way to migrate is to add this to all relevant files. If you want to try running actions outside of Node.js, move the file outside of the /convex/actions directory, which will default to Convex’s custom JavaScript environment.
Breaking: httpEndpoint → httpAction
Convex 0.13.0 is unifying the concepts of “actions” and “HTTP endpoints”. To recap,
- Actions are functions that can call third party services or use Node.js npm packages.
- HTTP endpoints are functions used to build an HTTP API.
We’ve decided to make these the same underlying abstraction. In Convex 0.13.0, actions and HTTP endpoints (now called “HTTP actions”) can be run in the same environments and support the same functionality. The only difference is that HTTP actions receive a Request and return a Response whereas action functions receive an arguments object and can return any Convex value.
To migrate your code, replace all usages of httpEndpoint with httpAction.
scheduler in HTTP actions
As part of unifying HTTP actions and actions, we’ve added the scheduler to HTTP actions. Now HTTP actions can directly schedule functions to run in the future:
import { httpAction } from "./_generated/server";
export default httpAction(async ({ scheduler }, request) => {
await scheduler.runAfter(5000, "fiveSecLaterFunc",);
return new Response(null, {
status: 200,
});
});
Node.js actions run in Node.js 18
We’ve upgraded all Node.js actions to use Node.js 18. This has fetch available automatically.
fetch in HTTP actions
As said above, we’ve added fetch to Convex’s custom JavaScript environment. This means that actions and HTTP actions can call fetch directly without adding "use node;":
import { httpAction } from "./_generated/server";
const postMessage = httpAction(async ({ runMutation }, request) => {
const data = await fetch("<https://convex.dev>");
// Do something with data here.
return new Response(null, {
status: 200,
});
});
storage in actions
As part of unifying HTTP actions and actions, we’ve added storage to actions! Now actions can directly upload and retrieve files:
import { action } from "./_generated/server";
export default action(({ storage }) => {
const blob = new Blob(["hello world"], {
type: "text/plain",
});
return await storage.store(blob);
});
Breaking: HTTP action storage API changes
We’ve made a few API changes to storage in HTTP actions:
storage.storenow receives aBlob(not aRequest).storage.getnow returns aPromiseof aBlobornull(not aResponseornull).
The rationale for this change is that we want to expose the same storage API in action functions and HTTP actions. Given that action functions don’t necessary have a Request already, Blob is a more natural type for these methods to use.
Other Changes
Breaking: Querying for documents missing a field
Previously, if you wanted to write a database query for documents without an author field, you could query like:
const messagesWithoutAuthors = await db
.query("messages")
.filter(q => q.eq(q.field("author"), null))
.collect();
This would match documents without an author like { message: "hello" }. as well as documents with a null author like { message: "hello", author: null }.
In version 0.13.0, you should instead compare the field to undefined to find documents that are missing a field:
const messagesWithoutAuthors = await db
.query("messages")
.filter(q => q.eq(q.field("author"), undefined))
.collect();
Currently, the behavior is the same (it will include documents missing author and documents with a null author).
On May 15, 2023, this behavior will change. At this point:
q.eq(q.field("fieldName"), null)will only match documents withfieldName: null.q.eq(q.field("fieldName"), undefined)will only match documents withoutfieldName.
This same change also applies to filters within index range expressions and search expressions.
If you never filter for documents without a field, no change is needed.
The rationale for this change is that we’d like to enable developers to differentiate documents that have a field set to null from documents that are missing the field. They are different in JS so we should allow you to query for them separately.
Minor Improvements
- Added support for imports like
require("node:*")(exrequire("node:fs")) in Node.js actions. - Convex’s JavaScript environment now supports constructing URLs with
new URL(href, base)likenew URL("/foo", "<https://convex.dev>"). - Fixed a number of bugs with
URLSearchParamsin Convex’s JavaScript environment. ConvexReactClientandBaseConvexClientnow support an additionalreportDebugInfoToConvexoption. If you’re having connection or performance problems, turn this option on and reach out in Discord!- Improved several confusing error messages.