convex-helpers 0.1.22 → 0.1.24
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 +111 -5
- package/dist/server/filter.d.ts +53 -0
- package/dist/server/filter.js +136 -0
- package/dist/server/hono.d.ts +105 -0
- package/dist/server/hono.js +154 -0
- package/dist/server/index.d.ts +55 -5
- package/dist/server/index.js +75 -1
- package/dist/server/rowLevelSecurity.js +2 -116
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -1
- package/server/filter.ts +191 -0
- package/server/hono.ts +191 -0
- package/server/index.ts +137 -2
- package/server/rowLevelSecurity.ts +2 -163
package/README.md
CHANGED
|
@@ -83,6 +83,51 @@ server-side helpers in [server/sessions](./server/sessions.ts).
|
|
|
83
83
|
|
|
84
84
|
See the associated [Stack post](https://stack.convex.dev/track-sessions-without-cookies) for more information.
|
|
85
85
|
|
|
86
|
+
Example for a query (action & mutation are similar):
|
|
87
|
+
|
|
88
|
+
In your React's root, add the `SessionProvider`:
|
|
89
|
+
```js
|
|
90
|
+
import { SessionProvider } from "convex-helpers/react/sessions";
|
|
91
|
+
//...
|
|
92
|
+
<ConvexProvider client={convex}>
|
|
93
|
+
<SessionProvider>
|
|
94
|
+
<App />
|
|
95
|
+
</SessionProvider>
|
|
96
|
+
</ConvexProvider>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Pass the session ID from the client automatically to a server query:
|
|
100
|
+
```js
|
|
101
|
+
import { useSessionQuery } from "convex-helpers/react/sessions";
|
|
102
|
+
|
|
103
|
+
const results = useSessionQuery(api.myModule.mySessionQuery, { arg1: 1 });
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Define a server query function in `convex/myModule.ts`:
|
|
107
|
+
```js
|
|
108
|
+
export const mySessionQuery = queryWithSession({
|
|
109
|
+
args: { arg1: v.number() },
|
|
110
|
+
handler: async (ctx, args) => {
|
|
111
|
+
// ctx.anonymousUser
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Using `customQuery` to make `queryWithSession`:
|
|
117
|
+
```js
|
|
118
|
+
import { customQuery } from "convex-helpers/server/customFunctions";
|
|
119
|
+
import { SessionIdArg } from "convex-helpers/server/sessions";
|
|
120
|
+
|
|
121
|
+
export const queryWithSession = customQuery(query, {
|
|
122
|
+
args: SessionIdArg,
|
|
123
|
+
input: async (ctx, { sessionId }) => {
|
|
124
|
+
const anonymousUser = await getAnonUser(ctx, sessionId);
|
|
125
|
+
return { ctx: { ...ctx, anonymousUser }, args: {} };
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
**Note:** `getAnonUser` is some function you write to look up a user by session.
|
|
130
|
+
|
|
86
131
|
## Row-level security
|
|
87
132
|
|
|
88
133
|
See the [Stack post on row-level security](https://stack.convex.dev/row-level-security)
|
|
@@ -136,6 +181,68 @@ export const myComplexQuery = zodQuery({
|
|
|
136
181
|
})
|
|
137
182
|
```
|
|
138
183
|
|
|
184
|
+
## Hono for advanced HTTP endpoint definitions
|
|
185
|
+
|
|
186
|
+
[Hono](https://hono.dev/) is an optimized web framework you can use to define
|
|
187
|
+
HTTP api endpoints easily
|
|
188
|
+
([`httpAction` in Convex](https://docs.convex.dev/functions/http-actions)).
|
|
189
|
+
|
|
190
|
+
See the [guide on Stack](https://stack.convex.dev/hono-with-convex) for tips on using Hono for HTTP endpoints.
|
|
191
|
+
|
|
192
|
+
To use it, put this in your `convex/http.ts` file:
|
|
193
|
+
```ts
|
|
194
|
+
import {
|
|
195
|
+
Hono,
|
|
196
|
+
HonoWithConvex,
|
|
197
|
+
HttpRouterWithHono,
|
|
198
|
+
} from "convex-helpers/server/hono";
|
|
199
|
+
import { ActionCtx } from "./_generated/server";
|
|
200
|
+
|
|
201
|
+
const app: HonoWithConvex<ActionCtx> = new Hono();
|
|
202
|
+
|
|
203
|
+
// See the [guide on Stack](https://stack.convex.dev/hono-with-convex)
|
|
204
|
+
// for tips on using Hono for HTTP endpoints.
|
|
205
|
+
app.get("/", async (c) => {
|
|
206
|
+
return c.json("Hello world!");
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
export default new HttpRouterWithHono(app);
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## CRUD utilities
|
|
213
|
+
|
|
214
|
+
To generate a basic CRUD api for your tables, you can use this helper to define
|
|
215
|
+
these functions for a given table:
|
|
216
|
+
|
|
217
|
+
- `create`
|
|
218
|
+
- `read`
|
|
219
|
+
- `update`
|
|
220
|
+
- `delete`
|
|
221
|
+
- `paginate`
|
|
222
|
+
|
|
223
|
+
**Note: I recommend only doing this for prototyping or [internal functions](https://docs.convex.dev/functions/internal-functions)**
|
|
224
|
+
|
|
225
|
+
Example:
|
|
226
|
+
```ts
|
|
227
|
+
|
|
228
|
+
// in convex/users.ts
|
|
229
|
+
import { crud } from "convex-helpers/server";
|
|
230
|
+
import { internalMutation, internalQuery } from "../convex/_generated/server";
|
|
231
|
+
|
|
232
|
+
const Users = Table("users", {...});
|
|
233
|
+
|
|
234
|
+
export const { read, update } = crud(Users, internalQuery, internalMutation);
|
|
235
|
+
|
|
236
|
+
// in convex/schema.ts
|
|
237
|
+
import { Users } from "./users";
|
|
238
|
+
export default defineSchema({users: Users.table});
|
|
239
|
+
|
|
240
|
+
// in some file, in an action:
|
|
241
|
+
const user = await ctx.runQuery(internal.users.read, { id: userId });
|
|
242
|
+
|
|
243
|
+
await ctx.runMutation(internal.users.update, { status: "inactive" });
|
|
244
|
+
```
|
|
245
|
+
|
|
139
246
|
## Validator utilities
|
|
140
247
|
|
|
141
248
|
When using validators for defining database schema or function arguments,
|
|
@@ -145,18 +252,17 @@ these validators help:
|
|
|
145
252
|
to avoid re-defining validators. To learn more about sharing validators, read
|
|
146
253
|
[this article](https://stack.convex.dev/argument-validation-without-repetition),
|
|
147
254
|
an extension of [this article](https://stack.convex.dev/types-cookbook).
|
|
148
|
-
2.
|
|
149
|
-
runtime values.
|
|
150
|
-
3. Add utilties for partial, pick and omit to match the TypeScript type
|
|
255
|
+
2. Add utilties for partial, pick and omit to match the TypeScript type
|
|
151
256
|
utilities.
|
|
152
|
-
|
|
257
|
+
3. Add shorthand for a union of `literals`, a `nullable` field, a `deprecated`
|
|
153
258
|
field, and `brandedString`. To learn more about branded strings see
|
|
154
259
|
[this article](https://stack.convex.dev/using-branded-types-in-validators).
|
|
260
|
+
4. Make the validators look more like TypeScript types, even though they're
|
|
261
|
+
runtime values. (This is controvercial and not required to use the above).
|
|
155
262
|
|
|
156
263
|
Example:
|
|
157
264
|
```js
|
|
158
265
|
import { Table } from "convex-helpers/server";
|
|
159
|
-
// Note some redefinitions in the import for even more terse definitions.
|
|
160
266
|
import {
|
|
161
267
|
literals, partial, deprecated, brandedString,
|
|
162
268
|
} from "convex-helpers/validators";
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Defines a function `filter` that wraps a query, attaching a
|
|
3
|
+
* JavaScript/TypeScript function that filters results just like
|
|
4
|
+
* `db.query(...).filter(...)` but with more generality.
|
|
5
|
+
*
|
|
6
|
+
*/
|
|
7
|
+
import { DocumentByInfo, GenericTableInfo, Query } from "convex/server";
|
|
8
|
+
export type Predicate<T extends GenericTableInfo> = (doc: DocumentByInfo<T>) => Promise<boolean> | boolean;
|
|
9
|
+
type QueryTableInfo<Q> = Q extends Query<infer T> ? T : never;
|
|
10
|
+
/**
|
|
11
|
+
* Applies a filter to a database query, just like `.filter((q) => ...)` but
|
|
12
|
+
* supporting arbitrary JavaScript/TypeScript.
|
|
13
|
+
* Performance is roughly the same as `.filter((q) => ...)`. If you want better
|
|
14
|
+
* performance, use an index to narrow down the results before filtering.
|
|
15
|
+
*
|
|
16
|
+
* Examples:
|
|
17
|
+
*
|
|
18
|
+
* // Full table scan, filtered to short messages.
|
|
19
|
+
* return await filter(
|
|
20
|
+
* ctx.db.query("messages"),
|
|
21
|
+
* async (message) => message.body.length < 10,
|
|
22
|
+
* ).collect();
|
|
23
|
+
*
|
|
24
|
+
* // Short messages by author, paginated.
|
|
25
|
+
* return await filter(
|
|
26
|
+
* ctx.db.query("messages").withIndex("by_author, q=>q.eq("author", args.author)),
|
|
27
|
+
* async (message) => message.body.length < 10,
|
|
28
|
+
* ).paginate(args.paginationOpts);
|
|
29
|
+
*
|
|
30
|
+
* // Same behavior as above: Short messages by author, paginated.
|
|
31
|
+
* // Note the filter can wrap any part of the query pipeline, and it is applied
|
|
32
|
+
* // at the end. This is how RowLevelSecurity works.
|
|
33
|
+
* const shortMessages = await filter(
|
|
34
|
+
* ctx.db.query("messages"),
|
|
35
|
+
* async (message) => message.body.length < 10,
|
|
36
|
+
* );
|
|
37
|
+
* return await shortMessages
|
|
38
|
+
* .withIndex("by_author, q=>q.eq("author", args.author))
|
|
39
|
+
* .paginate(args.paginationOpts);
|
|
40
|
+
*
|
|
41
|
+
* // Also works with `order()`, `take()`, `unique()`, and `first()`.
|
|
42
|
+
* return await filter(
|
|
43
|
+
* ctx.db.query("messages").order("desc"),
|
|
44
|
+
* async (message) => message.body.length < 10,
|
|
45
|
+
* ).first();
|
|
46
|
+
*
|
|
47
|
+
* @param query The query to filter.
|
|
48
|
+
* @param predicate Async function to run on each document before it is yielded
|
|
49
|
+
* from the query pipeline.
|
|
50
|
+
* @returns A new query with the filter applied.
|
|
51
|
+
*/
|
|
52
|
+
export declare function filter<Q extends Query<GenericTableInfo>>(query: Q, predicate: Predicate<QueryTableInfo<Q>>): Q;
|
|
53
|
+
export {};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Defines a function `filter` that wraps a query, attaching a
|
|
3
|
+
* JavaScript/TypeScript function that filters results just like
|
|
4
|
+
* `db.query(...).filter(...)` but with more generality.
|
|
5
|
+
*
|
|
6
|
+
*/
|
|
7
|
+
async function asyncFilter(arr, predicate) {
|
|
8
|
+
const results = await Promise.all(arr.map(predicate));
|
|
9
|
+
return arr.filter((_v, index) => results[index]);
|
|
10
|
+
}
|
|
11
|
+
class QueryWithFilter {
|
|
12
|
+
// q actually is only guaranteed to implement OrderedQuery<T>,
|
|
13
|
+
// but we forward all QueryInitializer methods to it and if they fail they fail.
|
|
14
|
+
q;
|
|
15
|
+
p;
|
|
16
|
+
iterator;
|
|
17
|
+
constructor(q, p) {
|
|
18
|
+
this.q = q;
|
|
19
|
+
this.p = p;
|
|
20
|
+
}
|
|
21
|
+
filter(predicate) {
|
|
22
|
+
return new QueryWithFilter(this.q.filter(predicate), this.p);
|
|
23
|
+
}
|
|
24
|
+
order(order) {
|
|
25
|
+
return new QueryWithFilter(this.q.order(order), this.p);
|
|
26
|
+
}
|
|
27
|
+
async paginate(paginationOpts) {
|
|
28
|
+
const result = await this.q.paginate(paginationOpts);
|
|
29
|
+
return { ...result, page: await asyncFilter(result.page, this.p) };
|
|
30
|
+
}
|
|
31
|
+
async collect() {
|
|
32
|
+
const results = await this.q.collect();
|
|
33
|
+
return await asyncFilter(results, this.p);
|
|
34
|
+
}
|
|
35
|
+
async take(n) {
|
|
36
|
+
const results = [];
|
|
37
|
+
for await (const result of this) {
|
|
38
|
+
results.push(result);
|
|
39
|
+
if (results.length >= n) {
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return results;
|
|
44
|
+
}
|
|
45
|
+
async first() {
|
|
46
|
+
for await (const result of this) {
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
async unique() {
|
|
52
|
+
let uniqueResult = null;
|
|
53
|
+
for await (const result of this) {
|
|
54
|
+
if (uniqueResult === null) {
|
|
55
|
+
uniqueResult = result;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
throw new Error("not unique");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return uniqueResult;
|
|
62
|
+
}
|
|
63
|
+
[Symbol.asyncIterator]() {
|
|
64
|
+
this.iterator = this.q[Symbol.asyncIterator]();
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
async next() {
|
|
68
|
+
for (;;) {
|
|
69
|
+
const { value, done } = await this.iterator.next();
|
|
70
|
+
if (value && (await this.p(value))) {
|
|
71
|
+
return { value, done };
|
|
72
|
+
}
|
|
73
|
+
if (done) {
|
|
74
|
+
return { value: null, done: true };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return() {
|
|
79
|
+
return this.iterator.return();
|
|
80
|
+
}
|
|
81
|
+
// Implement the remainder of QueryInitializer.
|
|
82
|
+
fullTableScan() {
|
|
83
|
+
return new QueryWithFilter(this.q.fullTableScan(), this.p);
|
|
84
|
+
}
|
|
85
|
+
withIndex(indexName, indexRange) {
|
|
86
|
+
return new QueryWithFilter(this.q.withIndex(indexName, indexRange), this.p);
|
|
87
|
+
}
|
|
88
|
+
withSearchIndex(indexName, searchFilter) {
|
|
89
|
+
return new QueryWithFilter(this.q.withSearchIndex(indexName, searchFilter), this.p);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Applies a filter to a database query, just like `.filter((q) => ...)` but
|
|
94
|
+
* supporting arbitrary JavaScript/TypeScript.
|
|
95
|
+
* Performance is roughly the same as `.filter((q) => ...)`. If you want better
|
|
96
|
+
* performance, use an index to narrow down the results before filtering.
|
|
97
|
+
*
|
|
98
|
+
* Examples:
|
|
99
|
+
*
|
|
100
|
+
* // Full table scan, filtered to short messages.
|
|
101
|
+
* return await filter(
|
|
102
|
+
* ctx.db.query("messages"),
|
|
103
|
+
* async (message) => message.body.length < 10,
|
|
104
|
+
* ).collect();
|
|
105
|
+
*
|
|
106
|
+
* // Short messages by author, paginated.
|
|
107
|
+
* return await filter(
|
|
108
|
+
* ctx.db.query("messages").withIndex("by_author, q=>q.eq("author", args.author)),
|
|
109
|
+
* async (message) => message.body.length < 10,
|
|
110
|
+
* ).paginate(args.paginationOpts);
|
|
111
|
+
*
|
|
112
|
+
* // Same behavior as above: Short messages by author, paginated.
|
|
113
|
+
* // Note the filter can wrap any part of the query pipeline, and it is applied
|
|
114
|
+
* // at the end. This is how RowLevelSecurity works.
|
|
115
|
+
* const shortMessages = await filter(
|
|
116
|
+
* ctx.db.query("messages"),
|
|
117
|
+
* async (message) => message.body.length < 10,
|
|
118
|
+
* );
|
|
119
|
+
* return await shortMessages
|
|
120
|
+
* .withIndex("by_author, q=>q.eq("author", args.author))
|
|
121
|
+
* .paginate(args.paginationOpts);
|
|
122
|
+
*
|
|
123
|
+
* // Also works with `order()`, `take()`, `unique()`, and `first()`.
|
|
124
|
+
* return await filter(
|
|
125
|
+
* ctx.db.query("messages").order("desc"),
|
|
126
|
+
* async (message) => message.body.length < 10,
|
|
127
|
+
* ).first();
|
|
128
|
+
*
|
|
129
|
+
* @param query The query to filter.
|
|
130
|
+
* @param predicate Async function to run on each document before it is yielded
|
|
131
|
+
* from the query pipeline.
|
|
132
|
+
* @returns A new query with the filter applied.
|
|
133
|
+
*/
|
|
134
|
+
export function filter(query, predicate) {
|
|
135
|
+
return new QueryWithFilter(query, predicate);
|
|
136
|
+
}
|
|
@@ -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
|
+
}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
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<
|
|
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<
|
|
28
|
+
_id: Validator<GenericId<TableName>, false, never>;
|
|
27
29
|
_creationTime: Validator<number, false, never>;
|
|
28
30
|
}>;
|
|
29
31
|
systemFields: {
|
|
30
|
-
_id: Validator<
|
|
32
|
+
_id: Validator<GenericId<TableName>, false, never>;
|
|
31
33
|
_creationTime: Validator<number, false, never>;
|
|
32
34
|
};
|
|
33
|
-
_id: Validator<
|
|
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
|
+
};
|