mwarger
mwarger5mo ago

Convex CRUD "framework" example - anyone doing this?

Managed to get a basic CRUD helper built using @convex_dev (and lots of ChatGPT assistance) using some conventions. Really simple entities, no relations (yet), but I think this could end up being something useful with some typescript helpers.
Convex already provides something similar with their "Table" utility, in convex-helpers, but it's really just a wrapper for table operations and convenience. The one I'm playing with has some extra params for auth and zod validation (when you need more specific than what the built-in convex values get you).
So far the UI is pretty bad, but "make it pretty" comes after "make it work", right? Here's an example, using basic lists. There are convex server entity helpers that provide the apiEndpoints, and I tried to get everything typed based on the generated convex types. The name basically just puts a title at the top, and the fields allow for type-safe names based on the fields of the entity (select dropdowns with options are supported as well). My TypeScript is not the best, and I'm guessing there might be room to infer more things rather than passing generics like I'm doing, but that's for another day. I also assume some things about my current app, like route parameters, but I haven't extracted them yet. (see image) Is anyone doing anything like this? Any "framework"-ish examples? I feel like the Convex "code-first/code-only" approach to things would make generating a somewhat turn-key CRUD workflow possible, especially one that can generate backend/frontend code based on the entities and strongly-type schemas that Convex provides, a la react-admin or http://refine.dev. I'll keep playing with it. taken from: https://x.com/mwarger/status/1825273759197966362
Refine | Open-source Retool for Enterprise
Build React-based internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.
mat (@mwarger) on X
Managed to get a basic CRUD helper built using @convex_dev (and lots of ChatGPT assistance) using some conventions. Really simple entities, no relations (yet), but I think this could end up being something useful with some typescript helpers.
Convex already provides something
From An unknown user
Twitter
No description
7 Replies
ian
ian5mo ago
Have you seen the crud helper? Also, with the new introspect-able schema, you can do things like this:
import schema from "./schema";
import {
internalMutation,
internalQuery,
} from "./_generated/server";

const teamFields = schema.tables.teams.validator.fields;

export const create = internalMutation({
args: teamFields,
handler: (ctx, args) => ctx.db.insert("teams", args),
});

export const read = internalQuery({
args: { id: v.id("teams") },
handler: (ctx, args) => ctx.db.get(args.id),
});

export const update = internalMutation({
args: {
id: v.id("teams"),
patch: v.object(partial(teamFields)),
},
handler: (ctx, args) => ctx.db.patch(args.id, args.patch),
});

export const delete_ = internalMutation({
args: { id: v.id("teams") },
handler: (ctx, args) => ctx.db.delete(args.id),
});
import schema from "./schema";
import {
internalMutation,
internalQuery,
} from "./_generated/server";

const teamFields = schema.tables.teams.validator.fields;

export const create = internalMutation({
args: teamFields,
handler: (ctx, args) => ctx.db.insert("teams", args),
});

export const read = internalQuery({
args: { id: v.id("teams") },
handler: (ctx, args) => ctx.db.get(args.id),
});

export const update = internalMutation({
args: {
id: v.id("teams"),
patch: v.object(partial(teamFields)),
},
handler: (ctx, args) => ctx.db.patch(args.id, args.patch),
});

export const delete_ = internalMutation({
args: { id: v.id("teams") },
handler: (ctx, args) => ctx.db.delete(args.id),
});
I have a branch with a helper that just looks like export const { create, read, update, delete_ } = crud(schema, "teams") but honestly in my apps I so quickly need just one more thing from the callsite that I end up with only one or two cases where I actually use the helper And I typically only recommend doing it for internal functions - exposing this to a client is asking for waterfalls / non-transactional client-side code. And exposing your table needs auth - so using the existing crud helper passing in a custom query that does auth would be the way to go
ian
ian5mo ago
npm
convex-helpers
A collection of useful code to complement the official convex package.. Latest version: 0.1.52, last published: 2 hours ago. Start using convex-helpers in your project by running npm i convex-helpers. There are no other projects in the npm registry using convex-helpers.
ian
ian5mo ago
But I think I broke passing custom functions to the crud helper at some point, so you'd need to cast your query when you pass it in I think. like crud("users", customQuery as typeof query, customMutation as typeof mutation)?
mwarger
mwargerOP5mo ago
I have seen that, yeah. I mentioned the Table utility above, but I really should have said the crud helper. I was playing around with making different backend utilities, and the ones I used to replace the "table" ones have some built-in basic auth checks and whatnot.
I'm more curious if anyone (or convex) has considered the above, which is taking that style of convention (whether it's the built-in crud helper from convex or not) and building basic CRUD UI around it? I have not thought this through very deeply, except for using very basic examples for simple entities in my project - I'm sure I would hit some sort of complexity wall at some point. However, it seems like the type-safe schema (and possible reference traversal with Id<> types) would make it possible for basic use-cases (either during run-time or some type of codegen)
ian
ian5mo ago
We're playing with what a CMS built on convex would look like: https://github.com/get-convex/cms-template (not yet released so don't share broadly) - but it doesn't do much magic around CRUD. I feel like the bigger pain point for me is setting up a nice form builder based on a validator.
GitHub
GitHub - get-convex/cms-template
Contribute to get-convex/cms-template development by creating an account on GitHub.
ian
ian5mo ago
Others have made bespoke per-table interfaces where they hide away details of what create/delete/etc. do, and don't expose ctx.db at all - I thought that was pretty clever. That's within the server function, not making the query/mutation API
mwarger
mwargerOP5mo ago
For reference, here's what I'm playing with:
// Utility function to create a new record with optional Zod validation
export function createEntityMutation<T extends TableNames>({
tableName,
schema,
authCallback = checkEmployeeAuth,
validationSchema,
}: {
tableName: T;
schema: any;
authCallback?: AuthCallback;
validationSchema?: ZodSchema<any>;
}) {
// Optional Zod schema for validation
return authMutation({
args: schema,
handler: async (ctx, args) => {
await authCallback(ctx, args.facilityId);

// Perform Zod validation if a schema is provided
if (validationSchema) {
validationSchema.parse(args);
}

return await ctx.db.insert(tableName, {
...args,
created_by: ctx.user._id,
created_at: new Date().toISOString(),
updated_by: ctx.user._id,
updated_at: new Date().toISOString(),
});
},
});
}

export const createSport = createEntityMutation({
tableName: SPORTS_TABLE_NAME,
schema: {
name: v.string(),
facilityId: v.id('facilities'),
description: v.optional(v.string()),
},
});
// Utility function to create a new record with optional Zod validation
export function createEntityMutation<T extends TableNames>({
tableName,
schema,
authCallback = checkEmployeeAuth,
validationSchema,
}: {
tableName: T;
schema: any;
authCallback?: AuthCallback;
validationSchema?: ZodSchema<any>;
}) {
// Optional Zod schema for validation
return authMutation({
args: schema,
handler: async (ctx, args) => {
await authCallback(ctx, args.facilityId);

// Perform Zod validation if a schema is provided
if (validationSchema) {
validationSchema.parse(args);
}

return await ctx.db.insert(tableName, {
...args,
created_by: ctx.user._id,
created_at: new Date().toISOString(),
updated_by: ctx.user._id,
updated_at: new Date().toISOString(),
});
},
});
}

export const createSport = createEntityMutation({
tableName: SPORTS_TABLE_NAME,
schema: {
name: v.string(),
facilityId: v.id('facilities'),
description: v.optional(v.string()),
},
});
You can see I don't have this fully abstracted, and I effectively have a multi-tenant setup using the facilityId - and yeah, this same type of thing could be done using the crud helper or similar utility.

Did you find this page helpful?