convex-helpers
Advanced tools
Comparing version
{ | ||
"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
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
721497
-2.52%17234
-1.28%811
-5.92%