convex-helpers 0.1.22 → 0.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -136,6 +136,68 @@ export const myComplexQuery = zodQuery({
136
136
  })
137
137
  ```
138
138
 
139
+ ## Hono for advanced HTTP endpoint definitions
140
+
141
+ [Hono](https://hono.dev/) is an optimized web framework you can use to define
142
+ HTTP api endpoints easily
143
+ ([`httpAction` in Convex](https://docs.convex.dev/functions/http-actions)).
144
+
145
+ See the [guide on Stack](https://stack.convex.dev/hono-with-convex) for tips on using Hono for HTTP endpoints.
146
+
147
+ To use it, put this in your `convex/http.ts` file:
148
+ ```ts
149
+ import {
150
+ Hono,
151
+ HonoWithConvex,
152
+ HttpRouterWithHono,
153
+ } from "convex-helpers/server/hono";
154
+ import { ActionCtx } from "./_generated/server";
155
+
156
+ const app: HonoWithConvex<ActionCtx> = new Hono();
157
+
158
+ // See the [guide on Stack](https://stack.convex.dev/hono-with-convex)
159
+ // for tips on using Hono for HTTP endpoints.
160
+ app.get("/", async (c) => {
161
+ return c.json("Hello world!");
162
+ });
163
+
164
+ export default new HttpRouterWithHono(app);
165
+ ```
166
+
167
+ ## CRUD utilities
168
+
169
+ To generate a basic CRUD api for your tables, you can use this helper to define
170
+ these functions for a given table:
171
+
172
+ - `create`
173
+ - `read`
174
+ - `update`
175
+ - `delete`
176
+ - `paginate`
177
+
178
+ **Note: I recommend only doing this for prototyping or [internal functions](https://docs.convex.dev/functions/internal-functions)**
179
+
180
+ Example:
181
+ ```ts
182
+
183
+ // in convex/users.ts
184
+ import { crud } from "convex-helpers/server";
185
+ import { internalMutation, internalQuery } from "../convex/_generated/server";
186
+
187
+ const Users = Table("users", {...});
188
+
189
+ export const { read, update } = crud(Users, internalQuery, internalMutation);
190
+
191
+ // in convex/schema.ts
192
+ import { Users } from "./users";
193
+ export default defineSchema({users: Users.table});
194
+
195
+ // in some file, in an action:
196
+ const user = await ctx.runQuery(internal.users.read, { id: userId });
197
+
198
+ await ctx.runMutation(internal.users.update, { status: "inactive" });
199
+ ```
200
+
139
201
  ## Validator utilities
140
202
 
141
203
  When using validators for defining database schema or function arguments,
@@ -145,18 +207,17 @@ these validators help:
145
207
  to avoid re-defining validators. To learn more about sharing validators, read
146
208
  [this article](https://stack.convex.dev/argument-validation-without-repetition),
147
209
  an extension of [this article](https://stack.convex.dev/types-cookbook).
148
- 2. Make the validators look more like TypeScript types, even though they're
149
- runtime values.
150
- 3. Add utilties for partial, pick and omit to match the TypeScript type
210
+ 2. Add utilties for partial, pick and omit to match the TypeScript type
151
211
  utilities.
152
- 4. Add shorthand for a union of `literals`, a `nullable` field, a `deprecated`
212
+ 3. Add shorthand for a union of `literals`, a `nullable` field, a `deprecated`
153
213
  field, and `brandedString`. To learn more about branded strings see
154
214
  [this article](https://stack.convex.dev/using-branded-types-in-validators).
215
+ 4. Make the validators look more like TypeScript types, even though they're
216
+ runtime values. (This is controvercial and not required to use the above).
155
217
 
156
218
  Example:
157
219
  ```js
158
220
  import { Table } from "convex-helpers/server";
159
- // Note some redefinitions in the import for even more terse definitions.
160
221
  import {
161
222
  literals, partial, deprecated, brandedString,
162
223
  } from "convex-helpers/validators";
@@ -0,0 +1,105 @@
1
+ /**
2
+ * This file contains a helper class for integrating Convex with Hono.
3
+ *
4
+ * See the [guide on Stack](https://stack.convex.dev/hono-with-convex)
5
+ * for tips on using Hono for HTTP endpoints.
6
+ *
7
+ * To use this helper, create a new Hono app in convex/http.ts like so:
8
+ * ```ts
9
+ * import {
10
+ * Hono,
11
+ * HonoWithConvex,
12
+ * HttpRouterWithHono,
13
+ * } from "convex-helpers/server/hono";
14
+ * import { ActionCtx } from "./_generated/server";
15
+ *
16
+ * const app: HonoWithConvex<ActionCtx> = new Hono();
17
+ *
18
+ * app.get("/", async (c) => {
19
+ * return c.json("Hello world!");
20
+ * });
21
+ *
22
+ * export default new HttpRouterWithHono(app);
23
+ * ```
24
+ */
25
+ import { HttpRouter, PublicHttpAction, RoutableMethod, GenericActionCtx } from "convex/server";
26
+ import { Hono } from "hono";
27
+ export { Hono };
28
+ /**
29
+ * Hono uses the `FetchEvent` type internally, which has to do with service workers
30
+ * and isn't included in the Convex tsconfig.
31
+ *
32
+ * As a workaround, define this type here so Hono + Convex compiles.
33
+ */
34
+ declare global {
35
+ type FetchEvent = any;
36
+ }
37
+ /**
38
+ * A type representing a Hono app with `c.env` containing Convex's
39
+ * `HttpEndpointCtx` (e.g. `c.env.runQuery` is valid).
40
+ */
41
+ export type HonoWithConvex<ActionCtx extends GenericActionCtx<any>> = Hono<{
42
+ Bindings: {
43
+ [Name in keyof ActionCtx]: ActionCtx[Name];
44
+ };
45
+ }>;
46
+ /**
47
+ * An implementation of the Convex `HttpRouter` that integrates with Hono by
48
+ * overridding `getRoutes` and `lookup`.
49
+ *
50
+ * This defers all routing and request handling to the provided Hono app, and
51
+ * passes along the Convex `HttpEndpointCtx` to the Hono handlers as part of
52
+ * `env`.
53
+ *
54
+ * It will attempt to log each request with the most specific Hono route it can
55
+ * find. For example,
56
+ *
57
+ * ```
58
+ * app.on("GET", "*", ...)
59
+ * app.on("GET", "/profile/:userId", ...)
60
+ *
61
+ * const http = new HttpRouterWithHono(app);
62
+ * http.lookup("/profile/abc", "GET") // [handler, "GET", "/profile/:userId"]
63
+ * ```
64
+ *
65
+ * An example `convex/http.ts` file would look like this:
66
+ * ```
67
+ * const app: HonoWithConvex = new Hono();
68
+ *
69
+ * // add Hono routes on `app`
70
+ *
71
+ * export default new HttpRouterWithHono(app);
72
+ * ```
73
+ */
74
+ export declare class HttpRouterWithHono<ActionCtx extends GenericActionCtx<any>> extends HttpRouter {
75
+ private _app;
76
+ private _handler;
77
+ private _handlerInfoCache;
78
+ constructor(app: HonoWithConvex<ActionCtx>);
79
+ /**
80
+ * Returns a list of routed HTTP endpoints.
81
+ *
82
+ * These are used to populate the list of routes shown in the Functions page of the Convex dashboard.
83
+ *
84
+ * @returns - an array of [path, method, endpoint] tuples.
85
+ */
86
+ getRoutes: () => [string, "GET" | "POST" | "PUT" | "DELETE" | "OPTIONS" | "PATCH", (...args: any) => any][];
87
+ /**
88
+ * Returns the appropriate HTTP endpoint and its routed request path and method.
89
+ *
90
+ * The path and method returned are used for logging and metrics, and should
91
+ * match up with one of the routes returned by `getRoutes`.
92
+ *
93
+ * For example,
94
+ *
95
+ * ```js
96
+ * http.route({ pathPrefix: "/profile/", method: "GET", handler: getProfile});
97
+ *
98
+ * http.lookup("/profile/abc", "GET") // returns [getProfile, "GET", "/profile/*"]
99
+ *```
100
+ *
101
+ * @returns - a tuple [PublicHttpEndpoint, method, path] or null.
102
+ */
103
+ lookup: (path: string, method: RoutableMethod | "HEAD") => readonly [PublicHttpAction, "GET" | "POST" | "PUT" | "DELETE" | "OPTIONS" | "PATCH", string];
104
+ }
105
+ export declare function normalizeMethod(method: RoutableMethod | "HEAD"): RoutableMethod;
@@ -0,0 +1,154 @@
1
+ /**
2
+ * This file contains a helper class for integrating Convex with Hono.
3
+ *
4
+ * See the [guide on Stack](https://stack.convex.dev/hono-with-convex)
5
+ * for tips on using Hono for HTTP endpoints.
6
+ *
7
+ * To use this helper, create a new Hono app in convex/http.ts like so:
8
+ * ```ts
9
+ * import {
10
+ * Hono,
11
+ * HonoWithConvex,
12
+ * HttpRouterWithHono,
13
+ * } from "convex-helpers/server/hono";
14
+ * import { ActionCtx } from "./_generated/server";
15
+ *
16
+ * const app: HonoWithConvex<ActionCtx> = new Hono();
17
+ *
18
+ * app.get("/", async (c) => {
19
+ * return c.json("Hello world!");
20
+ * });
21
+ *
22
+ * export default new HttpRouterWithHono(app);
23
+ * ```
24
+ */
25
+ import { httpActionGeneric, HttpRouter, ROUTABLE_HTTP_METHODS, } from "convex/server";
26
+ import { Hono } from "hono";
27
+ export { Hono };
28
+ /**
29
+ * An implementation of the Convex `HttpRouter` that integrates with Hono by
30
+ * overridding `getRoutes` and `lookup`.
31
+ *
32
+ * This defers all routing and request handling to the provided Hono app, and
33
+ * passes along the Convex `HttpEndpointCtx` to the Hono handlers as part of
34
+ * `env`.
35
+ *
36
+ * It will attempt to log each request with the most specific Hono route it can
37
+ * find. For example,
38
+ *
39
+ * ```
40
+ * app.on("GET", "*", ...)
41
+ * app.on("GET", "/profile/:userId", ...)
42
+ *
43
+ * const http = new HttpRouterWithHono(app);
44
+ * http.lookup("/profile/abc", "GET") // [handler, "GET", "/profile/:userId"]
45
+ * ```
46
+ *
47
+ * An example `convex/http.ts` file would look like this:
48
+ * ```
49
+ * const app: HonoWithConvex = new Hono();
50
+ *
51
+ * // add Hono routes on `app`
52
+ *
53
+ * export default new HttpRouterWithHono(app);
54
+ * ```
55
+ */
56
+ export class HttpRouterWithHono extends HttpRouter {
57
+ _app;
58
+ _handler;
59
+ _handlerInfoCache;
60
+ constructor(app) {
61
+ super();
62
+ this._app = app;
63
+ // Single Convex httpEndpoint handler that just forwards the request to the
64
+ // Hono framework
65
+ this._handler = httpActionGeneric(async (ctx, request) => {
66
+ return await app.fetch(request, ctx);
67
+ });
68
+ this._handlerInfoCache = new Map();
69
+ }
70
+ /**
71
+ * Returns a list of routed HTTP endpoints.
72
+ *
73
+ * These are used to populate the list of routes shown in the Functions page of the Convex dashboard.
74
+ *
75
+ * @returns - an array of [path, method, endpoint] tuples.
76
+ */
77
+ getRoutes = () => {
78
+ const convexRoutes = [];
79
+ // Likely a better way to do this, but hono will have multiple handlers with the same
80
+ // name (i.e. for middleware), so de-duplicate so we don't show multiple routes in the dashboard.
81
+ const seen = new Set();
82
+ this._app.routes.forEach((route) => {
83
+ // Hono uses "ALL" in its router, which is not supported by the Convex router.
84
+ // Expand this into a route for every routable method supported by Convex.
85
+ if (route.method === "ALL") {
86
+ for (const method of ROUTABLE_HTTP_METHODS) {
87
+ const name = `${method} ${route.path}`;
88
+ if (!seen.has(name)) {
89
+ seen.add(name);
90
+ convexRoutes.push([route.path, method, route.handler]);
91
+ }
92
+ }
93
+ }
94
+ else {
95
+ const name = `${route.method} ${route.path}`;
96
+ if (!seen.has(name)) {
97
+ seen.add(name);
98
+ convexRoutes.push([
99
+ route.path,
100
+ route.method,
101
+ route.handler,
102
+ ]);
103
+ }
104
+ }
105
+ });
106
+ return convexRoutes;
107
+ };
108
+ /**
109
+ * Returns the appropriate HTTP endpoint and its routed request path and method.
110
+ *
111
+ * The path and method returned are used for logging and metrics, and should
112
+ * match up with one of the routes returned by `getRoutes`.
113
+ *
114
+ * For example,
115
+ *
116
+ * ```js
117
+ * http.route({ pathPrefix: "/profile/", method: "GET", handler: getProfile});
118
+ *
119
+ * http.lookup("/profile/abc", "GET") // returns [getProfile, "GET", "/profile/*"]
120
+ *```
121
+ *
122
+ * @returns - a tuple [PublicHttpEndpoint, method, path] or null.
123
+ */
124
+ lookup = (path, method) => {
125
+ const match = this._app.router.match(method, path);
126
+ if (match === null) {
127
+ return [this._handler, normalizeMethod(method), path];
128
+ }
129
+ // There might be multiple handlers for a route (in the case of middleware),
130
+ // so choose the most specific one for the purposes of logging
131
+ const handlersAndRoutes = match[0];
132
+ const mostSpecificHandler = handlersAndRoutes[handlersAndRoutes.length - 1][0][0];
133
+ // On the first request let's populate a lookup from handler to info
134
+ if (this._handlerInfoCache.size === 0) {
135
+ for (const r of this._app.routes) {
136
+ this._handlerInfoCache.set(r.handler, {
137
+ method: normalizeMethod(method),
138
+ path: r.path,
139
+ });
140
+ }
141
+ }
142
+ const info = this._handlerInfoCache.get(mostSpecificHandler);
143
+ if (info) {
144
+ return [this._handler, info.method, info.path];
145
+ }
146
+ return [this._handler, normalizeMethod(method), path];
147
+ };
148
+ }
149
+ export function normalizeMethod(method) {
150
+ // HEAD is handled by Convex by running GET and stripping the body.
151
+ if (method === "HEAD")
152
+ return "GET";
153
+ return method;
154
+ }
@@ -1,4 +1,5 @@
1
- import { Validator } from "convex/values";
1
+ import { QueryBuilder, MutationBuilder, GenericDataModel, WithoutSystemFields, DocumentByName, RegisteredMutation, RegisteredQuery, FunctionVisibility, paginationOptsValidator, PaginationResult } from "convex/server";
2
+ import { GenericId, Infer, ObjectType, Validator } from "convex/values";
2
3
  import { Expand } from "..";
3
4
  /**
4
5
  * Define a table with system fields _id and _creationTime. This also returns
@@ -16,21 +17,22 @@ import { Expand } from "..";
16
17
  * }
17
18
  */
18
19
  export declare function Table<T extends Record<string, Validator<any, any, any>>, TableName extends string>(name: TableName, fields: T): {
20
+ name: TableName;
19
21
  table: import("convex/server").TableDefinition<import("convex/dist/cjs-types/type_utils").Expand<import("convex/dist/cjs-types/server/system_fields").SystemFields & import("convex/dist/cjs-types/type_utils").Expand<{ [Property_1 in { [Property in keyof T]: T[Property]["isOptional"] extends true ? Property : never; }[keyof T]]?: T[Property_1]["type"] | undefined; } & { [Property_2 in Exclude<keyof T, { [Property in keyof T]: T[Property]["isOptional"] extends true ? Property : never; }[keyof T]>]: T[Property_2]["type"]; }>>, "_creationTime" | ({ [Property_3 in keyof T]: Property_3 | `${Property_3 & string}.${T[Property_3]["fieldPaths"]}`; }[keyof T] & string), {}, {}, {}>;
20
22
  doc: import("convex/dist/cjs-types/values/validator").ObjectValidator<Expand<T & {
21
- _id: Validator<import("convex/values").GenericId<TableName>, false, never>;
23
+ _id: Validator<GenericId<TableName>, false, never>;
22
24
  _creationTime: Validator<number, false, never>;
23
25
  }>>;
24
26
  withoutSystemFields: T;
25
27
  withSystemFields: Expand<T & {
26
- _id: Validator<import("convex/values").GenericId<TableName>, false, never>;
28
+ _id: Validator<GenericId<TableName>, false, never>;
27
29
  _creationTime: Validator<number, false, never>;
28
30
  }>;
29
31
  systemFields: {
30
- _id: Validator<import("convex/values").GenericId<TableName>, false, never>;
32
+ _id: Validator<GenericId<TableName>, false, never>;
31
33
  _creationTime: Validator<number, false, never>;
32
34
  };
33
- _id: Validator<import("convex/values").GenericId<TableName>, false, never>;
35
+ _id: Validator<GenericId<TableName>, false, never>;
34
36
  };
35
37
  /**
36
38
  *
@@ -44,3 +46,51 @@ export declare function missingEnvVariableUrl(envVarName: string, whereToGet: st
44
46
  * @returns The deployment name, like "screaming-lemur-123"
45
47
  */
46
48
  export declare function deploymentName(): string | undefined;
49
+ /**
50
+ * Create CRUD operations for a table.
51
+ * You can expose these operations in your API. For example, in convex/users.ts:
52
+ *
53
+ * ```ts
54
+ * // in convex/users.ts
55
+ * import { crud } from "convex-helpers/server";
56
+ * import { query, mutation } from "./convex/_generated/server";
57
+ *
58
+ * const Users = Table("users", {
59
+ * name: v.string(),
60
+ * ///...
61
+ * });
62
+ *
63
+ * export const { create, read, paginate, update, destroy } =
64
+ * crud(Users, query, mutation);
65
+ * ```
66
+ *
67
+ * Then from a client, you can access `api.users.create`.
68
+ *
69
+ * @param table The table to create CRUD operations for.
70
+ * Of type returned from Table() in "convex-helpers/server".
71
+ * @param query The query to use - use internalQuery or query from
72
+ * "./convex/_generated/server" or a customQuery.
73
+ * @param mutation The mutation to use - use internalMutation or mutation from
74
+ * "./convex/_generated/server" or a customMutation.
75
+ * @returns An object with create, read, update, and delete functions.
76
+ */
77
+ 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: {
78
+ name: TableName;
79
+ _id: Validator<GenericId<TableName>>;
80
+ withoutSystemFields: Fields;
81
+ }, query: QueryBuilder<DataModel, QueryVisibility>, mutation: MutationBuilder<DataModel, MutationVisibility>): {
82
+ create: RegisteredMutation<MutationVisibility, ObjectType<Fields>, Promise<DocumentByName<DataModel, TableName>>>;
83
+ read: RegisteredQuery<QueryVisibility, {
84
+ id: GenericId<TableName>;
85
+ }, Promise<DocumentByName<DataModel, TableName> | null>>;
86
+ paginate: RegisteredQuery<QueryVisibility, {
87
+ paginationOpts: Infer<typeof paginationOptsValidator>;
88
+ }, Promise<PaginationResult<DocumentByName<DataModel, TableName>>>>;
89
+ update: RegisteredMutation<MutationVisibility, {
90
+ id: GenericId<TableName>;
91
+ patch: Partial<WithoutSystemFields<DocumentByName<DataModel, TableName>>>;
92
+ }, Promise<void>>;
93
+ destroy: RegisteredMutation<MutationVisibility, {
94
+ id: GenericId<TableName>;
95
+ }, Promise<DocumentByName<DataModel, TableName> | null>>;
96
+ };
@@ -1,4 +1,4 @@
1
- import { defineTable } from "convex/server";
1
+ import { defineTable, paginationOptsValidator, } from "convex/server";
2
2
  import { v } from "convex/values";
3
3
  /**
4
4
  * Define a table with system fields _id and _creationTime. This also returns
@@ -27,6 +27,7 @@ export function Table(name, fields) {
27
27
  ...systemFields,
28
28
  };
29
29
  return {
30
+ name,
30
31
  table,
31
32
  doc: v.object(withSystemFields),
32
33
  withoutSystemFields: fields,
@@ -60,3 +61,76 @@ export function deploymentName() {
60
61
  const regex = new RegExp("https://(.+).convex.cloud");
61
62
  return regex.exec(url)?.[1];
62
63
  }
64
+ import { partial } from "../validators";
65
+ /**
66
+ * Create CRUD operations for a table.
67
+ * You can expose these operations in your API. For example, in convex/users.ts:
68
+ *
69
+ * ```ts
70
+ * // in convex/users.ts
71
+ * import { crud } from "convex-helpers/server";
72
+ * import { query, mutation } from "./convex/_generated/server";
73
+ *
74
+ * const Users = Table("users", {
75
+ * name: v.string(),
76
+ * ///...
77
+ * });
78
+ *
79
+ * export const { create, read, paginate, update, destroy } =
80
+ * crud(Users, query, mutation);
81
+ * ```
82
+ *
83
+ * Then from a client, you can access `api.users.create`.
84
+ *
85
+ * @param table The table to create CRUD operations for.
86
+ * Of type returned from Table() in "convex-helpers/server".
87
+ * @param query The query to use - use internalQuery or query from
88
+ * "./convex/_generated/server" or a customQuery.
89
+ * @param mutation The mutation to use - use internalMutation or mutation from
90
+ * "./convex/_generated/server" or a customMutation.
91
+ * @returns An object with create, read, update, and delete functions.
92
+ */
93
+ export function crud(table, query, mutation) {
94
+ return {
95
+ create: mutation({
96
+ args: table.withoutSystemFields,
97
+ handler: async (ctx, args) => {
98
+ const id = await ctx.db.insert(table.name, args);
99
+ return (await ctx.db.get(id));
100
+ },
101
+ }),
102
+ read: query({
103
+ args: { id: table._id },
104
+ handler: async (ctx, args) => {
105
+ return await ctx.db.get(args.id);
106
+ },
107
+ }),
108
+ paginate: query({
109
+ args: {
110
+ paginationOpts: paginationOptsValidator,
111
+ },
112
+ handler: async (ctx, args) => {
113
+ return ctx.db.query(table.name).paginate(args.paginationOpts);
114
+ },
115
+ }),
116
+ update: mutation({
117
+ args: {
118
+ id: v.id(table.name),
119
+ patch: v.object(partial(table.withoutSystemFields)),
120
+ },
121
+ handler: async (ctx, args) => {
122
+ await ctx.db.patch(args.id, args.patch);
123
+ },
124
+ }),
125
+ destroy: mutation({
126
+ args: { id: table._id },
127
+ handler: async (ctx, args) => {
128
+ const old = await ctx.db.get(args.id);
129
+ if (old) {
130
+ await ctx.db.delete(args.id);
131
+ }
132
+ return old;
133
+ },
134
+ }),
135
+ };
136
+ }