convex-helpers
Advanced tools
Comparing version 0.1.57-alpha.2 to 0.1.57-alpha.3
{ | ||
"name": "convex-helpers", | ||
"version": "0.1.57-alpha.2", | ||
"version": "0.1.57-alpha.3", | ||
"description": "A collection of useful code to complement the official convex package.", | ||
@@ -125,3 +125,3 @@ "type": "module", | ||
"peerDependencies": { | ||
"convex": "^1.13.0", | ||
"convex": "^1.13.0 || >=1.16.0-alpha.1", | ||
"hono": "^4.0.5", | ||
@@ -128,0 +128,0 @@ "react": "^17.0.2 || ^18.0.0", |
@@ -424,57 +424,6 @@ # convex-helpers | ||
Use the [RowLevelSecurity](./server/rowLevelSecurity.ts) helper to define | ||
database wrappers to add row-level checks for a server-side function. | ||
Any access to `db` inside functions wrapped with these | ||
`withQueryRLS` and `withMutationRLS` wrappers to add row-level checks for a | ||
server-side function. Any access to `db` inside functions wrapped with these | ||
will check your access rules on read/insert/modify per-document. | ||
```ts | ||
import { | ||
customCtx, | ||
customMutation, | ||
customQuery, | ||
} from "convex-helpers/server/customFunctions"; | ||
import { | ||
Rules, | ||
wrapDatabaseReader, | ||
wrapDatabaseWriter, | ||
} from "convex-helpers/server/rowLevelSecurity"; | ||
import { DataModel } from "./_generated/dataModel"; | ||
import { mutation, query, QueryCtx } from "./_generated/server"; | ||
async function rlsRules(ctx: QueryCtx) { | ||
const identity = await ctx.auth.getUserIdentity(); | ||
return { | ||
users: { | ||
read: async (_, user) => { | ||
// Unauthenticated users can only read users over 18 | ||
if (!identity && user.age < 18) return false; | ||
return true; | ||
}, | ||
insert: async (_, user) => { | ||
return true; | ||
}, | ||
modify: async (_, user) => { | ||
if (!identity) | ||
throw new Error("Must be authenticated to modify a user"); | ||
// Users can only modify their own user | ||
return user.tokenIdentifier === identity.tokenIdentifier; | ||
}, | ||
}, | ||
} satisfies Rules<QueryCtx, DataModel>; | ||
} | ||
const queryWithRLS = customQuery( | ||
query, | ||
customCtx(async (ctx) => ({ | ||
db: wrapDatabaseReader(ctx, ctx.db, await rlsRules(ctx)), | ||
})), | ||
); | ||
const mutationWithRLS = customMutation( | ||
mutation, | ||
customCtx(async (ctx) => ({ | ||
db: wrapDatabaseWriter(ctx, ctx.db, await rlsRules(ctx)), | ||
})), | ||
); | ||
``` | ||
## Zod Validation | ||
@@ -481,0 +430,0 @@ |
113
server.d.ts
@@ -1,3 +0,3 @@ | ||
import { QueryBuilder, MutationBuilder, WithoutSystemFields, DocumentByName, RegisteredMutation, RegisteredQuery, FunctionVisibility, paginationOptsValidator, PaginationResult, SchemaDefinition, GenericSchema, TableNamesInDataModel, DataModelFromSchemaDefinition } from "convex/server"; | ||
import { GenericId, Infer, Validator } from "convex/values"; | ||
import { QueryBuilder, MutationBuilder, GenericDataModel, WithoutSystemFields, DocumentByName, RegisteredMutation, RegisteredQuery, FunctionVisibility, paginationOptsValidator, PaginationResult } from "convex/server"; | ||
import { GenericId, Infer, ObjectType, Validator } from "convex/values"; | ||
import { Expand } from "./index.js"; | ||
@@ -95,15 +95,17 @@ /** | ||
* import { crud } from "convex-helpers/server"; | ||
* import schema from "./schema"; | ||
* import { query, mutation } from "./convex/_generated/server"; | ||
* | ||
* export const { create, read, update, destroy } = crud(schema, "users"); | ||
* const Users = Table("users", { | ||
* name: v.string(), | ||
* ///... | ||
* }); | ||
* | ||
* export const { create, read, paginate, update, destroy } = | ||
* crud(Users, query, mutation); | ||
* ``` | ||
* | ||
* Then you can access the functions like `internal.users.create` from actions. | ||
* Then from a client, you can access `api.users.create`. | ||
* | ||
* 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 table The table to create CRUD operations for. | ||
* Of type returned from Table() in "convex-helpers/server". | ||
* @param query The query to use - use internalQuery or query from | ||
@@ -114,92 +116,23 @@ * "./convex/_generated/server" or a customQuery. | ||
* @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 declare 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>, mutation?: MutationBuilder<DataModelFromSchemaDefinition<SchemaDefinition<Schema, any>>, MutationVisibility>): { | ||
create: RegisteredMutation<MutationVisibility, WithoutSystemFields<DocumentByName<{ [TableName_1 in keyof Schema & string]: Schema[TableName_1] extends import("convex/server").TableDefinition<infer DocumentType extends Validator<any, any, any>, infer Indexes extends import("convex/server").GenericTableIndexes, infer SearchIndexes extends import("convex/server").GenericTableSearchIndexes, infer VectorIndexes extends import("convex/server").GenericTableVectorIndexes> ? { | ||
document: import("convex/server").Expand<import("convex/server").IdField<TableName_1> & import("convex/server").Expand<import("convex/server").SystemFields & DocumentType["type"]>>; | ||
fieldPaths: keyof import("convex/server").IdField<TableName_2> | ("_creationTime" | DocumentType["fieldPaths"]); | ||
indexes: import("convex/server").Expand<Indexes & import("convex/server").SystemIndexes>; | ||
searchIndexes: SearchIndexes; | ||
vectorIndexes: VectorIndexes; | ||
} : never; } | import("convex/server").Expand<{ [TableName_1 in keyof Schema & string]: Schema[TableName_1] extends import("convex/server").TableDefinition<infer DocumentType extends Validator<any, any, any>, infer Indexes extends import("convex/server").GenericTableIndexes, infer SearchIndexes extends import("convex/server").GenericTableSearchIndexes, infer VectorIndexes extends import("convex/server").GenericTableVectorIndexes> ? { | ||
document: import("convex/server").Expand<import("convex/server").IdField<TableName_1> & import("convex/server").Expand<import("convex/server").SystemFields & DocumentType["type"]>>; | ||
fieldPaths: keyof import("convex/server").IdField<TableName_2> | ("_creationTime" | DocumentType["fieldPaths"]); | ||
indexes: import("convex/server").Expand<Indexes & import("convex/server").SystemIndexes>; | ||
searchIndexes: SearchIndexes; | ||
vectorIndexes: VectorIndexes; | ||
} : never; } & import("convex/server").AnyDataModel>, TableName>>, Promise<DocumentByName<{ [TableName_1 in keyof Schema & string]: Schema[TableName_1] extends import("convex/server").TableDefinition<infer DocumentType extends Validator<any, any, any>, infer Indexes extends import("convex/server").GenericTableIndexes, infer SearchIndexes extends import("convex/server").GenericTableSearchIndexes, infer VectorIndexes extends import("convex/server").GenericTableVectorIndexes> ? { | ||
document: import("convex/server").Expand<import("convex/server").IdField<TableName_1> & import("convex/server").Expand<import("convex/server").SystemFields & DocumentType["type"]>>; | ||
fieldPaths: keyof import("convex/server").IdField<TableName_2> | ("_creationTime" | DocumentType["fieldPaths"]); | ||
indexes: import("convex/server").Expand<Indexes & import("convex/server").SystemIndexes>; | ||
searchIndexes: SearchIndexes; | ||
vectorIndexes: VectorIndexes; | ||
} : never; } | import("convex/server").Expand<{ [TableName_1 in keyof Schema & string]: Schema[TableName_1] extends import("convex/server").TableDefinition<infer DocumentType extends Validator<any, any, any>, infer Indexes extends import("convex/server").GenericTableIndexes, infer SearchIndexes extends import("convex/server").GenericTableSearchIndexes, infer VectorIndexes extends import("convex/server").GenericTableVectorIndexes> ? { | ||
document: import("convex/server").Expand<import("convex/server").IdField<TableName_1> & import("convex/server").Expand<import("convex/server").SystemFields & DocumentType["type"]>>; | ||
fieldPaths: keyof import("convex/server").IdField<TableName_2> | ("_creationTime" | DocumentType["fieldPaths"]); | ||
indexes: import("convex/server").Expand<Indexes & import("convex/server").SystemIndexes>; | ||
searchIndexes: SearchIndexes; | ||
vectorIndexes: VectorIndexes; | ||
} : never; } & import("convex/server").AnyDataModel>, TableName>>>; | ||
export declare function crud<Fields extends Record<string, Validator<any, any, any>>, TableName extends string, DataModel extends GenericDataModel, QueryVisibility extends FunctionVisibility, MutationVisibility extends FunctionVisibility>(table: { | ||
name: TableName; | ||
_id: Validator<GenericId<TableName>>; | ||
withoutSystemFields: Fields; | ||
}, query: QueryBuilder<DataModel, QueryVisibility>, mutation: MutationBuilder<DataModel, MutationVisibility>): { | ||
create: RegisteredMutation<MutationVisibility, ObjectType<Fields>, Promise<DocumentByName<DataModel, TableName>>>; | ||
read: RegisteredQuery<QueryVisibility, { | ||
id: GenericId<TableName>; | ||
}, Promise<DocumentByName<{ [TableName_1 in keyof Schema & string]: Schema[TableName_1] extends import("convex/server").TableDefinition<infer DocumentType extends Validator<any, any, any>, infer Indexes extends import("convex/server").GenericTableIndexes, infer SearchIndexes extends import("convex/server").GenericTableSearchIndexes, infer VectorIndexes extends import("convex/server").GenericTableVectorIndexes> ? { | ||
document: import("convex/server").Expand<import("convex/server").IdField<TableName_1> & import("convex/server").Expand<import("convex/server").SystemFields & DocumentType["type"]>>; | ||
fieldPaths: keyof import("convex/server").IdField<TableName_2> | ("_creationTime" | DocumentType["fieldPaths"]); | ||
indexes: import("convex/server").Expand<Indexes & import("convex/server").SystemIndexes>; | ||
searchIndexes: SearchIndexes; | ||
vectorIndexes: VectorIndexes; | ||
} : never; } | import("convex/server").Expand<{ [TableName_1 in keyof Schema & string]: Schema[TableName_1] extends import("convex/server").TableDefinition<infer DocumentType extends Validator<any, any, any>, infer Indexes extends import("convex/server").GenericTableIndexes, infer SearchIndexes extends import("convex/server").GenericTableSearchIndexes, infer VectorIndexes extends import("convex/server").GenericTableVectorIndexes> ? { | ||
document: import("convex/server").Expand<import("convex/server").IdField<TableName_1> & import("convex/server").Expand<import("convex/server").SystemFields & DocumentType["type"]>>; | ||
fieldPaths: keyof import("convex/server").IdField<TableName_2> | ("_creationTime" | DocumentType["fieldPaths"]); | ||
indexes: import("convex/server").Expand<Indexes & import("convex/server").SystemIndexes>; | ||
searchIndexes: SearchIndexes; | ||
vectorIndexes: VectorIndexes; | ||
} : never; } & import("convex/server").AnyDataModel>, TableName> | null>>; | ||
}, Promise<DocumentByName<DataModel, TableName> | null>>; | ||
paginate: RegisteredQuery<QueryVisibility, { | ||
paginationOpts: Infer<typeof paginationOptsValidator>; | ||
}, Promise<PaginationResult<DocumentByName<{ [TableName_1 in keyof Schema & string]: Schema[TableName_1] extends import("convex/server").TableDefinition<infer DocumentType extends Validator<any, any, any>, infer Indexes extends import("convex/server").GenericTableIndexes, infer SearchIndexes extends import("convex/server").GenericTableSearchIndexes, infer VectorIndexes extends import("convex/server").GenericTableVectorIndexes> ? { | ||
document: import("convex/server").Expand<import("convex/server").IdField<TableName_1> & import("convex/server").Expand<import("convex/server").SystemFields & DocumentType["type"]>>; | ||
fieldPaths: keyof import("convex/server").IdField<TableName_2> | ("_creationTime" | DocumentType["fieldPaths"]); | ||
indexes: import("convex/server").Expand<Indexes & import("convex/server").SystemIndexes>; | ||
searchIndexes: SearchIndexes; | ||
vectorIndexes: VectorIndexes; | ||
} : never; } | import("convex/server").Expand<{ [TableName_1 in keyof Schema & string]: Schema[TableName_1] extends import("convex/server").TableDefinition<infer DocumentType extends Validator<any, any, any>, infer Indexes extends import("convex/server").GenericTableIndexes, infer SearchIndexes extends import("convex/server").GenericTableSearchIndexes, infer VectorIndexes extends import("convex/server").GenericTableVectorIndexes> ? { | ||
document: import("convex/server").Expand<import("convex/server").IdField<TableName_1> & import("convex/server").Expand<import("convex/server").SystemFields & DocumentType["type"]>>; | ||
fieldPaths: keyof import("convex/server").IdField<TableName_2> | ("_creationTime" | DocumentType["fieldPaths"]); | ||
indexes: import("convex/server").Expand<Indexes & import("convex/server").SystemIndexes>; | ||
searchIndexes: SearchIndexes; | ||
vectorIndexes: VectorIndexes; | ||
} : never; } & import("convex/server").AnyDataModel>, TableName>>>>; | ||
}, Promise<PaginationResult<DocumentByName<DataModel, TableName>>>>; | ||
update: RegisteredMutation<MutationVisibility, { | ||
id: GenericId<TableName>; | ||
patch: Partial<WithoutSystemFields<DocumentByName<{ [TableName_1 in keyof Schema & string]: Schema[TableName_1] extends import("convex/server").TableDefinition<infer DocumentType extends Validator<any, any, any>, infer Indexes extends import("convex/server").GenericTableIndexes, infer SearchIndexes extends import("convex/server").GenericTableSearchIndexes, infer VectorIndexes extends import("convex/server").GenericTableVectorIndexes> ? { | ||
document: import("convex/server").Expand<import("convex/server").IdField<TableName_1> & import("convex/server").Expand<import("convex/server").SystemFields & DocumentType["type"]>>; | ||
fieldPaths: keyof import("convex/server").IdField<TableName_2> | ("_creationTime" | DocumentType["fieldPaths"]); | ||
indexes: import("convex/server").Expand<Indexes & import("convex/server").SystemIndexes>; | ||
searchIndexes: SearchIndexes; | ||
vectorIndexes: VectorIndexes; | ||
} : never; } | import("convex/server").Expand<{ [TableName_1 in keyof Schema & string]: Schema[TableName_1] extends import("convex/server").TableDefinition<infer DocumentType extends Validator<any, any, any>, infer Indexes extends import("convex/server").GenericTableIndexes, infer SearchIndexes extends import("convex/server").GenericTableSearchIndexes, infer VectorIndexes extends import("convex/server").GenericTableVectorIndexes> ? { | ||
document: import("convex/server").Expand<import("convex/server").IdField<TableName_1> & import("convex/server").Expand<import("convex/server").SystemFields & DocumentType["type"]>>; | ||
fieldPaths: keyof import("convex/server").IdField<TableName_2> | ("_creationTime" | DocumentType["fieldPaths"]); | ||
indexes: import("convex/server").Expand<Indexes & import("convex/server").SystemIndexes>; | ||
searchIndexes: SearchIndexes; | ||
vectorIndexes: VectorIndexes; | ||
} : never; } & import("convex/server").AnyDataModel>, TableName>>>; | ||
patch: Partial<WithoutSystemFields<DocumentByName<DataModel, TableName>>>; | ||
}, Promise<void>>; | ||
destroy: RegisteredMutation<MutationVisibility, { | ||
id: GenericId<TableName>; | ||
}, Promise<null | DocumentByName<{ [TableName_1 in keyof Schema & string]: Schema[TableName_1] extends import("convex/server").TableDefinition<infer DocumentType extends Validator<any, any, any>, infer Indexes extends import("convex/server").GenericTableIndexes, infer SearchIndexes extends import("convex/server").GenericTableSearchIndexes, infer VectorIndexes extends import("convex/server").GenericTableVectorIndexes> ? { | ||
document: import("convex/server").Expand<import("convex/server").IdField<TableName_1> & import("convex/server").Expand<import("convex/server").SystemFields & DocumentType["type"]>>; | ||
fieldPaths: keyof import("convex/server").IdField<TableName_2> | ("_creationTime" | DocumentType["fieldPaths"]); | ||
indexes: import("convex/server").Expand<Indexes & import("convex/server").SystemIndexes>; | ||
searchIndexes: SearchIndexes; | ||
vectorIndexes: VectorIndexes; | ||
} : never; } | import("convex/server").Expand<{ [TableName_1 in keyof Schema & string]: Schema[TableName_1] extends import("convex/server").TableDefinition<infer DocumentType extends Validator<any, any, any>, infer Indexes extends import("convex/server").GenericTableIndexes, infer SearchIndexes extends import("convex/server").GenericTableSearchIndexes, infer VectorIndexes extends import("convex/server").GenericTableVectorIndexes> ? { | ||
document: import("convex/server").Expand<import("convex/server").IdField<TableName_1> & import("convex/server").Expand<import("convex/server").SystemFields & DocumentType["type"]>>; | ||
fieldPaths: keyof import("convex/server").IdField<TableName_2> | ("_creationTime" | DocumentType["fieldPaths"]); | ||
indexes: import("convex/server").Expand<Indexes & import("convex/server").SystemIndexes>; | ||
searchIndexes: SearchIndexes; | ||
vectorIndexes: VectorIndexes; | ||
} : never; } & import("convex/server").AnyDataModel>, TableName>>>; | ||
}, Promise<null | DocumentByName<DataModel, TableName>>>; | ||
}; | ||
//# sourceMappingURL=server.d.ts.map |
@@ -1,2 +0,2 @@ | ||
import { defineTable, paginationOptsValidator, internalQueryGeneric, internalMutationGeneric, } from "convex/server"; | ||
import { defineTable, paginationOptsValidator, } from "convex/server"; | ||
import { v } from "convex/values"; | ||
@@ -74,15 +74,17 @@ import { partial } from "./validators.js"; | ||
* import { crud } from "convex-helpers/server"; | ||
* import schema from "./schema"; | ||
* import { query, mutation } from "./convex/_generated/server"; | ||
* | ||
* export const { create, read, update, destroy } = crud(schema, "users"); | ||
* const Users = Table("users", { | ||
* name: v.string(), | ||
* ///... | ||
* }); | ||
* | ||
* export const { create, read, paginate, update, destroy } = | ||
* crud(Users, query, mutation); | ||
* ``` | ||
* | ||
* Then you can access the functions like `internal.users.create` from actions. | ||
* Then from a client, you can access `api.users.create`. | ||
* | ||
* 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 table The table to create CRUD operations for. | ||
* Of type returned from Table() in "convex-helpers/server". | ||
* @param query The query to use - use internalQuery or query from | ||
@@ -93,20 +95,12 @@ * "./convex/_generated/server" or a customQuery. | ||
* @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, table, query = internalQueryGeneric, mutation = internalMutationGeneric) { | ||
export function crud(table, query, mutation) { | ||
const systemFields = { | ||
_id: v.id(table), | ||
_id: v.id(table.name), | ||
_creationTime: v.number(), | ||
}; | ||
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") { | ||
throw new Error(`CRUD only supports simple tables ${table} is a ${validator.type}`); | ||
} | ||
return { | ||
create: mutation({ | ||
args: { | ||
...validator.fields, | ||
...table.withoutSystemFields, | ||
...partial(systemFields), | ||
@@ -119,3 +113,3 @@ }, | ||
delete args._creationTime; | ||
const id = await ctx.db.insert(table, args); | ||
const id = await ctx.db.insert(table.name, args); | ||
return (await ctx.db.get(id)); | ||
@@ -125,3 +119,3 @@ }, | ||
read: query({ | ||
args: { id: v.id(table) }, | ||
args: { id: table._id }, | ||
handler: async (ctx, args) => { | ||
@@ -136,3 +130,3 @@ return await ctx.db.get(args.id); | ||
handler: async (ctx, args) => { | ||
return ctx.db.query(table).paginate(args.paginationOpts); | ||
return ctx.db.query(table.name).paginate(args.paginationOpts); | ||
}, | ||
@@ -142,7 +136,7 @@ }), | ||
args: { | ||
id: v.id(table), | ||
id: v.id(table.name), | ||
// this could be partial(table.withSystemFields) but keeping | ||
// the api less coupled to Table | ||
patch: v.object({ | ||
...partial(validator.fields), | ||
...partial(table.withoutSystemFields), | ||
...partial(systemFields), | ||
@@ -156,3 +150,3 @@ }), | ||
destroy: mutation({ | ||
args: { id: v.id(table) }, | ||
args: { id: table._id }, | ||
handler: async (ctx, args) => { | ||
@@ -159,0 +153,0 @@ const old = await ctx.db.get(args.id); |
@@ -5,2 +5,3 @@ import { | ||
MutationBuilder, | ||
GenericDataModel, | ||
WithoutSystemFields, | ||
@@ -13,10 +14,4 @@ DocumentByName, | ||
PaginationResult, | ||
SchemaDefinition, | ||
GenericSchema, | ||
TableNamesInDataModel, | ||
DataModelFromSchemaDefinition, | ||
internalQueryGeneric, | ||
internalMutationGeneric, | ||
} from "convex/server"; | ||
import { GenericId, Infer, Validator, v } from "convex/values"; | ||
import { GenericId, Infer, ObjectType, Validator, v } from "convex/values"; | ||
import { Expand } from "./index.js"; | ||
@@ -107,15 +102,17 @@ import { partial } from "./validators.js"; | ||
* import { crud } from "convex-helpers/server"; | ||
* import schema from "./schema"; | ||
* import { query, mutation } from "./convex/_generated/server"; | ||
* | ||
* export const { create, read, update, destroy } = crud(schema, "users"); | ||
* const Users = Table("users", { | ||
* name: v.string(), | ||
* ///... | ||
* }); | ||
* | ||
* export const { create, read, paginate, update, destroy } = | ||
* crud(Users, query, mutation); | ||
* ``` | ||
* | ||
* Then you can access the functions like `internal.users.create` from actions. | ||
* Then from a client, you can access `api.users.create`. | ||
* | ||
* 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 table The table to create CRUD operations for. | ||
* Of type returned from Table() in "convex-helpers/server". | ||
* @param query The query to use - use internalQuery or query from | ||
@@ -126,44 +123,26 @@ * "./convex/_generated/server" or a customQuery. | ||
* @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", | ||
Fields extends Record<string, Validator<any, any, any>>, | ||
TableName extends string, | ||
DataModel extends GenericDataModel, | ||
QueryVisibility extends FunctionVisibility, | ||
MutationVisibility extends FunctionVisibility, | ||
>( | ||
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, | ||
table: { | ||
name: TableName; | ||
_id: Validator<GenericId<TableName>>; | ||
withoutSystemFields: Fields; | ||
}, | ||
query: QueryBuilder<DataModel, QueryVisibility>, | ||
mutation: MutationBuilder<DataModel, MutationVisibility>, | ||
) { | ||
type DataModel = DataModelFromSchemaDefinition<SchemaDefinition<Schema, any>>; | ||
const systemFields = { | ||
_id: v.id(table), | ||
_id: v.id(table.name), | ||
_creationTime: v.number(), | ||
}; | ||
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") { | ||
throw new Error( | ||
`CRUD only supports simple tables ${table} is a ${validator.type}`, | ||
); | ||
} | ||
return { | ||
create: mutation({ | ||
args: { | ||
...validator.fields, | ||
...table.withoutSystemFields, | ||
...partial(systemFields), | ||
@@ -175,3 +154,3 @@ }, | ||
const id = await ctx.db.insert( | ||
table, | ||
table.name, | ||
args as unknown as WithoutSystemFields< | ||
@@ -185,7 +164,7 @@ DocumentByName<DataModel, TableName> | ||
MutationVisibility, | ||
WithoutSystemFields<DocumentByName<DataModel, TableName>>, | ||
ObjectType<Fields>, | ||
Promise<DocumentByName<DataModel, TableName>> | ||
>, | ||
read: query({ | ||
args: { id: v.id(table) }, | ||
args: { id: table._id }, | ||
handler: async (ctx, args) => { | ||
@@ -204,3 +183,3 @@ return await ctx.db.get(args.id); | ||
handler: async (ctx, args) => { | ||
return ctx.db.query(table).paginate(args.paginationOpts); | ||
return ctx.db.query(table.name).paginate(args.paginationOpts); | ||
}, | ||
@@ -214,7 +193,7 @@ }) as RegisteredQuery< | ||
args: { | ||
id: v.id(table), | ||
id: v.id(table.name), | ||
// this could be partial(table.withSystemFields) but keeping | ||
// the api less coupled to Table | ||
patch: v.object({ | ||
...partial(validator.fields), | ||
...partial(table.withoutSystemFields), | ||
...partial(systemFields), | ||
@@ -240,3 +219,3 @@ }), | ||
destroy: mutation({ | ||
args: { id: v.id(table) }, | ||
args: { id: table._id }, | ||
handler: async (ctx, args) => { | ||
@@ -243,0 +222,0 @@ const old = await ctx.db.get(args.id); |
import { convexTest } from "convex-test"; | ||
import { expect, test } from "vitest"; | ||
import { crud } from "../server.js"; | ||
import { crud } from "."; | ||
import { | ||
@@ -16,3 +16,2 @@ anyApi, | ||
import { modules } from "./setup.test"; | ||
import { customCtx, customMutation, customQuery } from "./customFunctions.js"; | ||
@@ -40,4 +39,11 @@ const ExampleFields = { | ||
export const { create, read, paginate, update, destroy } = crud( | ||
schema, | ||
CrudTable, | ||
// We could use the Table helper instead, but showing it explicitly here. | ||
// E.g. Table("crud_example", ExampleFields) | ||
{ | ||
name: CrudTable, | ||
_id: v.id(CrudTable), | ||
withoutSystemFields: ExampleFields, | ||
}, | ||
internalQuery, | ||
internalMutation, | ||
); | ||
@@ -73,45 +79,1 @@ | ||
}); | ||
/** | ||
* Custom function tests | ||
*/ | ||
const customQ = customQuery( | ||
internalQuery, | ||
customCtx((ctx) => ({ foo: "bar" })), | ||
); | ||
const customM = customMutation( | ||
internalMutation, | ||
customCtx((ctx) => ({})), | ||
); | ||
const customCrud = crud(schema, CrudTable, customQ, customM); | ||
const customTestApi: ApiFromModules<{ | ||
fns: { | ||
create: typeof customCrud.create; | ||
read: typeof customCrud.read; | ||
update: typeof customCrud.update; | ||
paginate: typeof customCrud.paginate; | ||
destroy: typeof customCrud.destroy; | ||
}; | ||
}>["fns"] = anyApi["crud.test"] as any; | ||
test("custom crud for table", async () => { | ||
const t = convexTest(schema, modules); | ||
const doc = await t.mutation(customTestApi.create, { foo: "", bar: null }); | ||
expect(doc).toMatchObject({ foo: "", bar: null }); | ||
const read = await t.query(customTestApi.read, { id: doc._id }); | ||
expect(read).toMatchObject(doc); | ||
await t.mutation(customTestApi.update, { | ||
id: doc._id, | ||
patch: { foo: "new", bar: { n: 42 }, baz: true }, | ||
}); | ||
expect(await t.query(customTestApi.read, { id: doc._id })).toMatchObject({ | ||
foo: "new", | ||
bar: { n: 42 }, | ||
baz: true, | ||
}); | ||
await t.mutation(customTestApi.destroy, { id: doc._id }); | ||
expect(await t.query(customTestApi.read, { id: doc._id })).toBe(null); | ||
}); |
@@ -13,3 +13,3 @@ /** | ||
*/ | ||
import { GenericValidator, ObjectType, PropertyValidators, Validator } from "convex/values"; | ||
import { GenericValidator, ObjectType, PropertyValidators } from "convex/values"; | ||
import { ActionBuilder, ArgsArrayForOptionalValidator, ArgsArrayToObject, DefaultArgsForOptionalValidator, DefaultFunctionArgs, FunctionVisibility, GenericActionCtx, GenericDataModel, GenericMutationCtx, GenericQueryCtx, MutationBuilder, QueryBuilder, RegisteredAction, RegisteredMutation, RegisteredQuery, ReturnValueForOptionalValidator } from "convex/server"; | ||
@@ -232,3 +232,3 @@ /** | ||
export type CustomBuilder<FuncType extends "query" | "mutation" | "action", ModArgsValidator extends PropertyValidators, ModCtx extends Record<string, any>, ModMadeArgs extends Record<string, any>, InputCtx, Visibility extends FunctionVisibility> = { | ||
<ArgsValidator extends PropertyValidators | void | Validator<any, any, any>, ReturnsValidator extends PropertyValidators | GenericValidator | void, ReturnValue extends ReturnValueForOptionalValidator<ReturnsValidator> = any, OneOrZeroArgs extends ArgsArrayForOptionalValidator<ArgsValidator> = DefaultArgsForOptionalValidator<ArgsValidator>>(func: { | ||
<ArgsValidator extends PropertyValidators | void, ReturnsValidator extends PropertyValidators | GenericValidator | void, ReturnValue extends ReturnValueForOptionalValidator<ReturnsValidator> = any, OneOrZeroArgs extends ArgsArrayForOptionalValidator<ArgsValidator> = DefaultArgsForOptionalValidator<ArgsValidator>>(func: { | ||
args?: ArgsValidator; | ||
@@ -239,3 +239,3 @@ returns?: ReturnsValidator; | ||
(ctx: Overwrite<InputCtx, ModCtx>, ...args: ArgsForHandlerType<OneOrZeroArgs, ModMadeArgs>): ReturnValue; | ||
}): Registration<FuncType, Visibility, ArgsArrayToObject<ModArgsValidator extends Record<string, never> ? OneOrZeroArgs : OneOrZeroArgs extends [infer A] ? [Expand<A & ObjectType<ModArgsValidator>>] : [ObjectType<ModArgsValidator>]>, ReturnValue>; | ||
}): Registration<FuncType, Visibility, ArgsArrayToObject<OneOrZeroArgs extends [infer A] ? [Expand<A & ObjectType<ModArgsValidator>>] : [ObjectType<ModArgsValidator>]>, ReturnValue>; | ||
}; | ||
@@ -242,0 +242,0 @@ export type CustomCtx<Builder> = Builder extends CustomBuilder<any, any, infer ModCtx, any, infer InputCtx, any> ? Overwrite<InputCtx, ModCtx> : never; |
@@ -1,14 +0,1 @@ | ||
/** | ||
* This file contains helpers for defining custom functions that modify the | ||
* context and arguments of a Convex function. Allows you to: | ||
* | ||
* - Run authentication logic before the request starts. | ||
* - Look up commonly used data and add it to the ctx argument. | ||
* - Replace a ctx or argument field with a different value, such as a version | ||
* of `db` that runs custom functions on data access. | ||
* - Consume arguments from the client that are not passed to the query, such | ||
* as taking in an authentication parameter like an API key or session ID. | ||
* These arguments must be sent up by the client along with each request. | ||
*/ | ||
import { asObjectValidator, v, } from "convex/values"; | ||
import { pick } from "../index.js"; | ||
@@ -211,3 +198,6 @@ /** | ||
return builder({ | ||
args: addArgs(fn.args, inputArgs), | ||
args: { | ||
...fn.args, | ||
...inputArgs, | ||
}, | ||
returns: fn.returns, | ||
@@ -234,17 +224,1 @@ handler: async (ctx, allArgs) => { | ||
} | ||
// Adds args to a property validator or validator | ||
// Needs to call recursively in the case of unions. | ||
function addArgs(validatorOrPropertyValidator, args) { | ||
if (Object.keys(args).length === 0) { | ||
return asObjectValidator(validatorOrPropertyValidator); | ||
} | ||
const validator = asObjectValidator(validatorOrPropertyValidator); | ||
switch (validator.kind) { | ||
case "object": | ||
return v.object({ ...validator.fields, ...args }); | ||
case "union": | ||
return v.union(...validator.members.map((m) => addArgs(m, args))); | ||
default: | ||
throw new Error("Cannot add arguments to a validator that is not an object or union."); | ||
} | ||
} |
@@ -1,4 +0,3 @@ | ||
import { Equals, assert } from "../index.js"; | ||
import { Equals, assert } from ".."; | ||
import { | ||
customAction, | ||
CustomCtx, | ||
@@ -8,9 +7,7 @@ customCtx, | ||
customQuery, | ||
} from "./customFunctions.js"; | ||
import { wrapDatabaseWriter } from "./rowLevelSecurity.js"; | ||
import { SessionId, vSessionId } from "./sessions.js"; | ||
} from "./customFunctions"; | ||
import { wrapDatabaseWriter } from "./rowLevelSecurity"; | ||
import { SessionId, vSessionId } from "./sessions"; | ||
import { convexTest } from "convex-test"; | ||
import { | ||
ActionBuilder, | ||
actionGeneric, | ||
anyApi, | ||
@@ -30,3 +27,3 @@ DataModelFromSchemaDefinition, | ||
import { afterEach, beforeEach, describe, expect, test } from "vitest"; | ||
import { modules } from "./setup.test.js"; | ||
import { modules } from "./setup.test"; | ||
@@ -42,3 +39,2 @@ const schema = defineSchema({ | ||
const mutation = mutationGeneric as MutationBuilder<DataModel, "public">; | ||
const action = actionGeneric as ActionBuilder<DataModel, "public">; | ||
@@ -125,20 +121,2 @@ const authenticatedQueryBuilder = customQuery( | ||
/** | ||
* Testing that it conforms to query, mutation, action types when no args | ||
* are added | ||
*/ | ||
customQuery( | ||
query, | ||
customCtx((ctx) => ({ foo: "bar" })), | ||
) satisfies typeof query; | ||
customMutation( | ||
mutation, | ||
customCtx((ctx) => ({})), | ||
) satisfies typeof mutation; | ||
customAction( | ||
action, | ||
customCtx((ctx) => ({})), | ||
) satisfies typeof action; | ||
/** | ||
* Testing custom function modifications. | ||
@@ -145,0 +123,0 @@ */ |
@@ -17,5 +17,2 @@ /** | ||
PropertyValidators, | ||
Validator, | ||
asObjectValidator, | ||
v, | ||
} from "convex/values"; | ||
@@ -335,3 +332,6 @@ import { | ||
return builder({ | ||
args: addArgs(fn.args, inputArgs), | ||
args: { | ||
...fn.args, | ||
...inputArgs, | ||
}, | ||
returns: fn.returns, | ||
@@ -364,24 +364,2 @@ handler: async (ctx: any, allArgs: any) => { | ||
// Adds args to a property validator or validator | ||
// Needs to call recursively in the case of unions. | ||
function addArgs( | ||
validatorOrPropertyValidator: PropertyValidators | Validator<any, any, any>, | ||
args: PropertyValidators, | ||
): Validator<any, any, any> { | ||
if (Object.keys(args).length === 0) { | ||
return asObjectValidator(validatorOrPropertyValidator); | ||
} | ||
const validator = asObjectValidator(validatorOrPropertyValidator); | ||
switch (validator.kind) { | ||
case "object": | ||
return v.object({ ...validator.fields, ...args }); | ||
case "union": | ||
return v.union(...validator.members.map((m) => addArgs(m, args))); | ||
default: | ||
throw new Error( | ||
"Cannot add arguments to a validator that is not an object or union.", | ||
); | ||
} | ||
} | ||
/** | ||
@@ -439,3 +417,3 @@ * A Convex function (query, mutation, or action) to be registered for the API. | ||
< | ||
ArgsValidator extends PropertyValidators | void | Validator<any, any, any>, | ||
ArgsValidator extends PropertyValidators | void, | ||
ReturnsValidator extends PropertyValidators | GenericValidator | void, | ||
@@ -465,7 +443,5 @@ ReturnValue extends ReturnValueForOptionalValidator<ReturnsValidator> = any, | ||
ArgsArrayToObject< | ||
ModArgsValidator extends Record<string, never> | ||
? OneOrZeroArgs | ||
: OneOrZeroArgs extends [infer A] | ||
? [Expand<A & ObjectType<ModArgsValidator>>] | ||
: [ObjectType<ModArgsValidator>] | ||
OneOrZeroArgs extends [infer A] | ||
? [Expand<A & ObjectType<ModArgsValidator>>] | ||
: [ObjectType<ModArgsValidator>] | ||
>, | ||
@@ -472,0 +448,0 @@ ReturnValue |
import { defineTable, defineSchema, GenericDocument } from "convex/server"; | ||
import { convexTest } from "convex-test"; | ||
import { expect, test } from "vitest"; | ||
import { expect, test, vi } from "vitest"; | ||
import { IndexKey, getPage } from "./pagination.js"; | ||
@@ -150,3 +150,3 @@ import { modules } from "./setup.test.js"; | ||
// document already. | ||
await ctx.db.delete(page0[0]!._id as GenericId<"foo">); | ||
await ctx.db.delete(page0[0]._id as GenericId<"foo">); | ||
const { page: page0Refreshed } = await getPage(ctx, { | ||
@@ -204,3 +204,3 @@ table: "foo", | ||
} | ||
const { page: pageAt } = await getPage(ctx, { | ||
const { page: pageAt, indexKeys: indexKeysAt } = await getPage(ctx, { | ||
table: "foo", | ||
@@ -207,0 +207,0 @@ index: "abc", |
@@ -10,10 +10,6 @@ import { convexTest } from "convex-test"; | ||
defineTable, | ||
GenericDatabaseReader, | ||
GenericDatabaseWriter, | ||
MutationBuilder, | ||
mutationGeneric, | ||
queryGeneric, | ||
} from "convex/server"; | ||
import { modules } from "./setup.test.js"; | ||
import { customCtx, customMutation } from "./customFunctions.js"; | ||
import { crud } from "../server.js"; | ||
@@ -31,21 +27,22 @@ const schema = defineSchema({ | ||
type DataModel = DataModelFromSchemaDefinition<typeof schema>; | ||
type DatabaseReader = GenericDatabaseReader<DataModel>; | ||
type DatabaseWriter = GenericDatabaseWriter<DataModel>; | ||
const withRLS = async (ctx: { db: DatabaseWriter; auth: Auth }) => { | ||
const tokenIdentifier = (await ctx.auth.getUserIdentity())?.tokenIdentifier; | ||
if (!tokenIdentifier) throw new Error("Unauthenticated"); | ||
return { | ||
...ctx, | ||
db: wrapDatabaseWriter({ tokenIdentifier }, ctx.db, { | ||
notes: { | ||
read: async ({ tokenIdentifier }, doc) => { | ||
const author = await ctx.db.get(doc.userId); | ||
return tokenIdentifier === author?.tokenIdentifier; | ||
describe("row level security", () => { | ||
const withRLS = async (ctx: { db: DatabaseWriter; auth: Auth }) => { | ||
const tokenIdentifier = (await ctx.auth.getUserIdentity())?.tokenIdentifier; | ||
if (!tokenIdentifier) throw new Error("Unauthenticated"); | ||
return { | ||
...ctx, | ||
db: wrapDatabaseWriter({ tokenIdentifier }, ctx.db, { | ||
notes: { | ||
read: async ({ tokenIdentifier }, doc) => { | ||
const author = await ctx.db.get(doc.userId); | ||
return tokenIdentifier === author?.tokenIdentifier; | ||
}, | ||
}, | ||
}, | ||
}), | ||
}), | ||
}; | ||
}; | ||
}; | ||
describe("row level security", () => { | ||
test("can only read own notes", async () => { | ||
@@ -84,3 +81,3 @@ const t = convexTest(schema, modules); | ||
const aId = await ctx.db.insert("users", { tokenIdentifier: "Person A" }); | ||
await ctx.db.insert("users", { tokenIdentifier: "Person B" }); | ||
const bId = await ctx.db.insert("users", { tokenIdentifier: "Person B" }); | ||
return ctx.db.insert("notes", { | ||
@@ -105,15 +102,1 @@ note: "Hello from Person A", | ||
}); | ||
const mutation = mutationGeneric as MutationBuilder<DataModel, "public">; | ||
const mutationWithRLS = customMutation( | ||
mutation, | ||
customCtx((ctx) => withRLS(ctx)), | ||
); | ||
customMutation( | ||
mutationWithRLS, | ||
customCtx((ctx) => ({ foo: "bar" })), | ||
) satisfies typeof mutation; | ||
crud(schema, "users", queryGeneric, mutationWithRLS); |
@@ -1,4 +0,4 @@ | ||
import { assert, omit, pick, pruneNull } from "../index.js"; | ||
import { Table } from "../server.js"; | ||
import { partial } from "../validators.js"; | ||
import { assert, omit, pick, pruneNull } from ".."; | ||
import { Table } from "."; | ||
import { partial } from "../validators"; | ||
import { convexTest } from "convex-test"; | ||
@@ -17,3 +17,3 @@ import { | ||
import { expect, test } from "vitest"; | ||
import { modules } from "./setup.test.js"; | ||
import { modules } from "./setup.test"; | ||
@@ -20,0 +20,0 @@ // Define a table with system fields _id and _creationTime. This also returns |
@@ -10,8 +10,8 @@ import { | ||
} from "convex/server"; | ||
import { Equals, assert, omit } from "../index.js"; | ||
import { Equals, assert, omit } from ".."; | ||
import { convexTest } from "convex-test"; | ||
import { describe, expect, test } from "vitest"; | ||
import { modules } from "./setup.test.js"; | ||
import { zCustomQuery, zid, zodToConvexFields } from "./zod.js"; | ||
import { customCtx } from "./customFunctions.js"; | ||
import { modules } from "./setup.test"; | ||
import { zCustomQuery, zid, zodToConvexFields } from "./zod"; | ||
import { customCtx } from "./customFunctions"; | ||
import { v } from "convex/values"; | ||
@@ -18,0 +18,0 @@ import { z } from "zod"; |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
721497
17234
811