convex-helpers
Version:
A collection of useful code to complement the official convex package.
180 lines (177 loc) • 5.6 kB
text/typescript
import type {
QueryBuilder,
MutationBuilder,
WithoutSystemFields,
DocumentByName,
RegisteredMutation,
RegisteredQuery,
FunctionVisibility,
PaginationResult,
SchemaDefinition,
GenericSchema,
TableNamesInDataModel,
DataModelFromSchemaDefinition,
} from "convex/server";
import {
paginationOptsValidator,
internalQueryGeneric,
internalMutationGeneric,
} from "convex/server";
import type {
GenericId,
Infer,
Validator,
VObject,
VUnion,
} from "convex/values";
import { v } from "convex/values";
import { doc, partial, systemFields } from "../validators.js";
/**
* Create CRUD operations for a table.
* You can expose these operations in your API. For example, in convex/users.ts:
*
* ```ts
* // in convex/users.ts
* import { crud } from "convex-helpers/server/crud";
* import schema from "./schema";
*
* export const { create, read, update, destroy } = crud(schema, "users");
* ```
*
* Then you can access the functions like `internal.users.create` from actions.
*
* To expose these functions publicly, you can pass in custom query and
* mutation arguments. Be careful what you expose publicly: you wouldn't want
* any client to be able to delete users, for example.
*
* @param schema Your project's schema.
* @param table The table name to create CRUD operations for.
* @param query The query to use - use internalQuery or query from
* "./convex/_generated/server" or a customQuery.
* @param mutation The mutation to use - use internalMutation or mutation from
* "./convex/_generated/server" or a customMutation.
* @returns An object with create, read, update, and delete functions.
* You must export these functions at the top level of your file to use them.
*/
export function crud<
Schema extends GenericSchema,
TableName extends TableNamesInDataModel<
DataModelFromSchemaDefinition<SchemaDefinition<Schema, any>>
>,
QueryVisibility extends FunctionVisibility = "internal",
MutationVisibility extends FunctionVisibility = "internal",
>(
schema: SchemaDefinition<Schema, any>,
table: TableName,
query: QueryBuilder<
DataModelFromSchemaDefinition<SchemaDefinition<Schema, any>>,
QueryVisibility
> = internalQueryGeneric as any,
mutation: MutationBuilder<
DataModelFromSchemaDefinition<SchemaDefinition<Schema, any>>,
MutationVisibility
> = internalMutationGeneric as any,
) {
type DataModel = DataModelFromSchemaDefinition<SchemaDefinition<Schema, any>>;
const validator = schema.tables[table]?.validator;
if (!validator) {
throw new Error(
`Table ${table} not found in schema. Did you define it in defineSchema?`,
);
}
if (validator.kind !== "object" && validator.kind !== "union") {
throw new Error("Validator must be an object or union");
}
const makeSystemFieldsOptional = <V extends Validator<any, any, any>>(
validator: V,
): V => {
if (validator.kind === "object") {
return v.object({
...validator.fields,
...partial(systemFields(table)),
}) as any;
} else if (validator.kind === "union") {
return v.union(
...validator.members.map((value) => makeSystemFieldsOptional(value)),
) as any;
} else {
throw new Error("Validator must be an object or union");
}
};
return {
create: mutation({
args: makeSystemFieldsOptional(validator),
handler: async (ctx, args) => {
if ("_id" in args) delete args._id;
if ("_creationTime" in args) delete args._creationTime;
const id = await ctx.db.insert(table, args);
return (await ctx.db.get(id))!;
},
}) as RegisteredMutation<
MutationVisibility,
WithoutSystemFields<DocumentByName<DataModel, TableName>>,
Promise<DocumentByName<DataModel, TableName>>
>,
read: query({
args: { id: v.id(table) },
handler: async (ctx, args) => {
return await ctx.db.get(args.id);
},
}) as RegisteredQuery<
QueryVisibility,
{ id: GenericId<TableName> },
Promise<DocumentByName<DataModel, TableName> | null>
>,
paginate: query({
args: {
paginationOpts: paginationOptsValidator,
},
handler: async (ctx, args) => {
return ctx.db.query(table).paginate(args.paginationOpts);
},
}) as RegisteredQuery<
QueryVisibility,
{ paginationOpts: Infer<typeof paginationOptsValidator> },
Promise<PaginationResult<DocumentByName<DataModel, TableName>>>
>,
update: mutation({
args: {
id: v.id(table),
// this could be partial(table.withSystemFields) but keeping
// the api less coupled to Table
patch: partial(
doc(schema, table) as VObject<any, any, any> | VUnion<any, any, any>,
),
},
handler: async (ctx, args) => {
await ctx.db.patch(
args.id,
args.patch as Partial<DocumentByName<DataModel, TableName>>,
);
},
}) as RegisteredMutation<
MutationVisibility,
{
id: GenericId<TableName>;
patch: Partial<
WithoutSystemFields<DocumentByName<DataModel, TableName>>
>;
},
Promise<void>
>,
destroy: mutation({
args: { id: v.id(table) },
handler: async (ctx, args) => {
const old = await ctx.db.get(args.id);
if (old) {
await ctx.db.delete(args.id);
}
return old;
},
}) as RegisteredMutation<
MutationVisibility,
{ id: GenericId<TableName> },
Promise<null | DocumentByName<DataModel, TableName>>
>,
};
}