CRUD Operations for Projects
CRUD stands for Create, Read, Update, and Delete. Those are the four basic operations we perform on a database. We saw this in the Weather App chapter, where we looked at the HTTP verbs that go with each one.
Here is what each operation does:
- Create: inserts new records, such as creating a new project or adding a task.
- Read: retrieves data, such as listing all projects or viewing the tasks in a specific project.
- Update: modifies existing data, such as changing a task’s status or updating a project’s description.
- Delete: removes data, such as deleting a task or an entire project.
In Convex, the “Read” operation is called a query, and the other three are mutations.
Let’s implement these operations for our projects. We will create a new file, convex/projects.ts, and define the queries and mutations for projects there. Start with the import statement at the top of the file:
import { query, mutation } from "./_generated/server";
import { v } from "convex/values";
Project Queries
Add the following code to convex/projects.ts to define two queries:
export const list = query({
args: {},
handler: async (ctx) => {
return await ctx.db.query("projects").collect();
},
});
export const get = query({
args: {
id: v.id("projects")
},
handler: async (ctx, args) => {
return await ctx.db.get(args.id);
},
});
Two queries:
list— returns all projects. This is the same pattern as thetasks.listquery from the previous chapter.get— returns a single project by its_id. This usesctx.db.get(id), which is the fastest way to fetch a document when you know its ID. It returns the document, ornullif the document does not exist.
Recall that the export names become the API paths: api.projects.list and api.projects.get.
Project Mutations
Now let’s implement the mutations for creating, updating, and deleting projects. Add the following code to convex/projects.ts:
create
export const create = mutation({
args: {
name: v.string(),
description: v.optional(v.string()),
},
handler: async (ctx, args) => {
const projectId = await ctx.db.insert("projects", {
name: args.name,
description: args.description,
});
return projectId;
},
});
This is similar to the tasks.create mutation from the previous chapter. One difference: we return the new project’s ID. ctx.db.insert() always returns the ID of the document it created. We return that ID so the client can navigate to the new project after creating it (you will see this shortly when we update the UI).
update
export const update = mutation({
args: {
id: v.id("projects"),
name: v.optional(v.string()),
description: v.optional(v.string()),
},
handler: async (ctx, args) => {
const patch: { name?: string; description?: string } = {};
if (args.name !== undefined) {
patch.name = args.name;
}
if (args.description !== undefined) {
patch.description = args.description;
}
if (Object.keys(patch).length === 0) {
throw new Error("No fields provided to update");
}
await ctx.db.patch("projects", args.id, patch);
},
});
This uses ctx.db.patch(), the same operation we used in tasks.updateStatus. The difference is that here we build the patch object first, and we add each field to it only if the caller provided that field.
We do that because patch does a shallow merge: it updates only the fields you include and leaves everything else unchanged. If we passed name: args.name or description: args.description directly, an omitted field would be undefined, and that removes the field instead of leaving it alone.
Because we add a property to patch only when it was actually provided, this mutation handles partial updates safely. The user can update just the name, just the description, or both.
delete
export const remove = mutation({
args: {
id: v.id("projects"),
},
handler: async (ctx, args) => {
await ctx.db.delete(args.id);
},
});
This deletes the project document. It does not delete the project’s tasks. If you delete a project, its tasks are still in the tasks table, with a projectId that points to a document that no longer exists. Those are called orphaned documents. For now that is fine. We will handle cascade deletion later in the chapter.
Checkpoint: Commit your progress.
git add .
git commit -m "planner-03: Add CRUD operations for projects"
git push