nuxt-convex 0.0.1-alpha.0 → 0.0.2
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 +5 -488
- package/dist/module.d.mts +2 -2
- package/dist/module.json +1 -1
- package/dist/module.mjs +24 -6
- package/dist/runtime/composables/useConvexQuery.d.ts +30 -0
- package/dist/runtime/composables/useConvexQuery.js +119 -0
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
<h1 align="center">nuxt-convex</h1>
|
|
6
6
|
|
|
7
|
-
<p align="center">Nuxt module for <a href="https://convex.dev">Convex</a
|
|
7
|
+
<p align="center">Nuxt module for <a href="https://convex.dev">Convex</a></p>
|
|
8
8
|
|
|
9
9
|
<p align="center">
|
|
10
10
|
<a href="https://npmjs.com/package/nuxt-convex"><img src="https://img.shields.io/npm/v/nuxt-convex/latest.svg?style=flat&colorA=020420&colorB=00DC82" alt="npm version"></a>
|
|
@@ -13,495 +13,12 @@
|
|
|
13
13
|
<a href="https://nuxt.com"><img src="https://img.shields.io/badge/Nuxt-020420?logo=nuxt.js" alt="Nuxt"></a>
|
|
14
14
|
</p>
|
|
15
15
|
|
|
16
|
-
>
|
|
16
|
+
> [!WARNING]
|
|
17
|
+
> This library is a work in progress and not ready for production use.
|
|
17
18
|
|
|
18
|
-
##
|
|
19
|
+
## Documentation
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
- **Virtual modules** - Import from `#convex`, `#convex/api`, and `#convex/storage`
|
|
22
|
-
- **Auto-imports** - Composables available without manual imports
|
|
23
|
-
- **File storage** - Upload API with auto-scaffolded Convex functions
|
|
24
|
-
- **DevTools** - Convex dashboard tab in Nuxt DevTools
|
|
25
|
-
|
|
26
|
-
## Requirements
|
|
27
|
-
|
|
28
|
-
- Nuxt 3.0.0 or higher
|
|
29
|
-
- A Convex project (run `npx convex dev` to create one)
|
|
30
|
-
|
|
31
|
-
## Setup
|
|
32
|
-
|
|
33
|
-
Install the module and its peer dependencies:
|
|
34
|
-
|
|
35
|
-
```bash
|
|
36
|
-
pnpm add nuxt-convex convex @convex-vue/core
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
Add the module to your Nuxt configuration:
|
|
40
|
-
|
|
41
|
-
```ts [nuxt.config.ts]
|
|
42
|
-
export default defineNuxtConfig({
|
|
43
|
-
modules: ['nuxt-convex'],
|
|
44
|
-
convex: {
|
|
45
|
-
storage: true, // Enable file storage (optional)
|
|
46
|
-
},
|
|
47
|
-
})
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
The module reads `CONVEX_URL` or `NUXT_PUBLIC_CONVEX_URL` environment variables automatically. Run `npx convex dev` to start the Convex development server.
|
|
51
|
-
|
|
52
|
-
## Creating Your Backend
|
|
53
|
-
|
|
54
|
-
Convex functions live in the `convex/` directory at your project root. See [Convex docs](https://docs.convex.dev/functions) for full reference.
|
|
55
|
-
|
|
56
|
-
### Schema
|
|
57
|
-
|
|
58
|
-
Define your database tables with [schemas](https://docs.convex.dev/database/schemas). Tables are created automatically when you push to Convex.
|
|
59
|
-
|
|
60
|
-
```ts [convex/schema.ts]
|
|
61
|
-
import { defineSchema, defineTable } from 'convex/server'
|
|
62
|
-
import { v } from 'convex/values'
|
|
63
|
-
|
|
64
|
-
export default defineSchema({
|
|
65
|
-
tasks: defineTable({
|
|
66
|
-
title: v.string(),
|
|
67
|
-
userId: v.optional(v.string()),
|
|
68
|
-
isCompleted: v.boolean(),
|
|
69
|
-
createdAt: v.number(),
|
|
70
|
-
}).index('by_user', ['userId']),
|
|
71
|
-
})
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
The `v` object provides [validators](https://docs.convex.dev/database/schemas#validators) for all Convex types: `v.string()`, `v.number()`, `v.boolean()`, `v.id('tableName')`, `v.array()`, `v.object()`, `v.optional()`, `v.union()`, etc.
|
|
75
|
-
|
|
76
|
-
### Queries
|
|
77
|
-
|
|
78
|
-
[Queries](https://docs.convex.dev/functions/query-functions) read data from the database. They are reactive - the UI updates automatically when underlying data changes.
|
|
79
|
-
|
|
80
|
-
```ts [convex/tasks.ts]
|
|
81
|
-
import { v } from 'convex/values'
|
|
82
|
-
import { query } from './_generated/server'
|
|
83
|
-
|
|
84
|
-
export const list = query({
|
|
85
|
-
args: { userId: v.string() },
|
|
86
|
-
handler: async (ctx, { userId }) => {
|
|
87
|
-
return await ctx.db
|
|
88
|
-
.query('tasks')
|
|
89
|
-
.withIndex('by_user', q => q.eq('userId', userId))
|
|
90
|
-
.order('desc')
|
|
91
|
-
.collect()
|
|
92
|
-
},
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
export const get = query({
|
|
96
|
-
args: { id: v.id('tasks') },
|
|
97
|
-
handler: async (ctx, { id }) => {
|
|
98
|
-
return await ctx.db.get(id)
|
|
99
|
-
},
|
|
100
|
-
})
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
### Mutations
|
|
104
|
-
|
|
105
|
-
[Mutations](https://docs.convex.dev/functions/mutation-functions) write data to the database. They are transactional and run on the server.
|
|
106
|
-
|
|
107
|
-
```ts [convex/tasks.ts]
|
|
108
|
-
import { v } from 'convex/values'
|
|
109
|
-
import { mutation } from './_generated/server'
|
|
110
|
-
|
|
111
|
-
export const add = mutation({
|
|
112
|
-
args: { title: v.string(), userId: v.string() },
|
|
113
|
-
handler: async (ctx, { title, userId }) => {
|
|
114
|
-
return await ctx.db.insert('tasks', {
|
|
115
|
-
title,
|
|
116
|
-
userId,
|
|
117
|
-
isCompleted: false,
|
|
118
|
-
createdAt: Date.now(),
|
|
119
|
-
})
|
|
120
|
-
},
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
export const toggle = mutation({
|
|
124
|
-
args: { id: v.id('tasks') },
|
|
125
|
-
handler: async (ctx, { id }) => {
|
|
126
|
-
const task = await ctx.db.get(id)
|
|
127
|
-
if (task) {
|
|
128
|
-
await ctx.db.patch(id, { isCompleted: !task.isCompleted })
|
|
129
|
-
}
|
|
130
|
-
},
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
export const remove = mutation({
|
|
134
|
-
args: { id: v.id('tasks') },
|
|
135
|
-
handler: async (ctx, { id }) => {
|
|
136
|
-
await ctx.db.delete(id)
|
|
137
|
-
},
|
|
138
|
-
})
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
### Actions
|
|
142
|
-
|
|
143
|
-
[Actions](https://docs.convex.dev/functions/actions) run arbitrary code including external API calls, but cannot directly access the database. Use them for third-party integrations.
|
|
144
|
-
|
|
145
|
-
```ts [convex/ai.ts]
|
|
146
|
-
import { v } from 'convex/values'
|
|
147
|
-
import { action } from './_generated/server'
|
|
148
|
-
|
|
149
|
-
export const summarize = action({
|
|
150
|
-
args: { text: v.string() },
|
|
151
|
-
handler: async (ctx, { text }) => {
|
|
152
|
-
const response = await fetch('https://api.openai.com/v1/...', {
|
|
153
|
-
method: 'POST',
|
|
154
|
-
headers: { Authorization: `Bearer ${process.env.OPENAI_API_KEY}` },
|
|
155
|
-
body: JSON.stringify({ prompt: text }),
|
|
156
|
-
})
|
|
157
|
-
return await response.json()
|
|
158
|
-
},
|
|
159
|
-
})
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
Actions can call mutations/queries via `ctx.runMutation()` and `ctx.runQuery()`.
|
|
163
|
-
|
|
164
|
-
## Usage
|
|
165
|
-
|
|
166
|
-
Composables are auto-imported. They wrap [@convex-vue/core](https://github.com/niconiahi/convex-vue).
|
|
167
|
-
|
|
168
|
-
### useConvexQuery
|
|
169
|
-
|
|
170
|
-
Subscribe to a [query](https://docs.convex.dev/functions/query-functions). Returns reactive data that auto-updates when the database changes.
|
|
171
|
-
|
|
172
|
-
```vue [app/pages/tasks.vue]
|
|
173
|
-
<script setup lang="ts">
|
|
174
|
-
import { api } from '#convex/api'
|
|
175
|
-
|
|
176
|
-
const { data: tasks, isLoading, error } = useConvexQuery(api.tasks.list, { userId: 'user_123' })
|
|
177
|
-
</script>
|
|
178
|
-
|
|
179
|
-
<template>
|
|
180
|
-
<div v-if="isLoading">
|
|
181
|
-
Loading...
|
|
182
|
-
</div>
|
|
183
|
-
<div v-else-if="error">
|
|
184
|
-
Error: {{ error.message }}
|
|
185
|
-
</div>
|
|
186
|
-
<ul v-else>
|
|
187
|
-
<li v-for="task in tasks" :key="task._id">
|
|
188
|
-
{{ task.title }}
|
|
189
|
-
</li>
|
|
190
|
-
</ul>
|
|
191
|
-
</template>
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
**Return values:**
|
|
195
|
-
|
|
196
|
-
| Property | Type | Description |
|
|
197
|
-
| ----------- | --------------------- | ----------------------- |
|
|
198
|
-
| `data` | `Ref<T \| undefined>` | Query result, reactive |
|
|
199
|
-
| `isLoading` | `Ref<boolean>` | True while initial load |
|
|
200
|
-
| `error` | `Ref<Error \| null>` | Error if query failed |
|
|
201
|
-
|
|
202
|
-
### useConvexMutation
|
|
203
|
-
|
|
204
|
-
Call a [mutation](https://docs.convex.dev/functions/mutation-functions) to write data.
|
|
205
|
-
|
|
206
|
-
```vue [app/components/AddTask.vue]
|
|
207
|
-
<script setup lang="ts">
|
|
208
|
-
import { api } from '#convex/api'
|
|
209
|
-
|
|
210
|
-
const { mutate: addTask, isLoading, error } = useConvexMutation(api.tasks.add)
|
|
211
|
-
|
|
212
|
-
const title = ref('')
|
|
213
|
-
|
|
214
|
-
async function handleSubmit() {
|
|
215
|
-
await addTask({ title: title.value, userId: 'user_123' })
|
|
216
|
-
title.value = ''
|
|
217
|
-
}
|
|
218
|
-
</script>
|
|
219
|
-
|
|
220
|
-
<template>
|
|
221
|
-
<form @submit.prevent="handleSubmit">
|
|
222
|
-
<input v-model="title" :disabled="isLoading" placeholder="New task...">
|
|
223
|
-
<button :disabled="isLoading">
|
|
224
|
-
Add
|
|
225
|
-
</button>
|
|
226
|
-
<p v-if="error">
|
|
227
|
-
{{ error.message }}
|
|
228
|
-
</p>
|
|
229
|
-
</form>
|
|
230
|
-
</template>
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
**Return values:**
|
|
234
|
-
|
|
235
|
-
| Property | Type | Description |
|
|
236
|
-
| ----------- | ---------------------- | ------------------------ |
|
|
237
|
-
| `mutate` | `(args) => Promise<T>` | Call the mutation |
|
|
238
|
-
| `isLoading` | `Ref<boolean>` | True while running |
|
|
239
|
-
| `error` | `Ref<Error \| null>` | Error if mutation failed |
|
|
240
|
-
|
|
241
|
-
### useConvexAction
|
|
242
|
-
|
|
243
|
-
Call an [action](https://docs.convex.dev/functions/actions) for external API calls or long-running tasks.
|
|
244
|
-
|
|
245
|
-
```vue [app/components/Summarize.vue]
|
|
246
|
-
<script setup lang="ts">
|
|
247
|
-
import { api } from '#convex/api'
|
|
248
|
-
|
|
249
|
-
const { execute: summarize, isLoading, error } = useConvexAction(api.ai.summarize)
|
|
250
|
-
|
|
251
|
-
const result = ref('')
|
|
252
|
-
|
|
253
|
-
async function handleSummarize(text: string) {
|
|
254
|
-
result.value = await summarize({ text })
|
|
255
|
-
}
|
|
256
|
-
</script>
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
**Return values:**
|
|
260
|
-
|
|
261
|
-
| Property | Type | Description |
|
|
262
|
-
| ----------- | ---------------------- | ---------------------- |
|
|
263
|
-
| `execute` | `(args) => Promise<T>` | Call the action |
|
|
264
|
-
| `isLoading` | `Ref<boolean>` | True while running |
|
|
265
|
-
| `error` | `Ref<Error \| null>` | Error if action failed |
|
|
266
|
-
|
|
267
|
-
### useConvex
|
|
268
|
-
|
|
269
|
-
Get the raw [ConvexClient](https://docs.convex.dev/api/classes/browser.ConvexClient) for advanced usage.
|
|
270
|
-
|
|
271
|
-
```vue [app/components/Advanced.vue]
|
|
272
|
-
<script setup lang="ts">
|
|
273
|
-
import { api } from '#convex/api'
|
|
274
|
-
|
|
275
|
-
const client = useConvex()
|
|
276
|
-
|
|
277
|
-
// Direct client access for advanced patterns
|
|
278
|
-
const result = await client.query(api.tasks.get, { id: 'abc123' })
|
|
279
|
-
</script>
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
## File Storage
|
|
283
|
-
|
|
284
|
-
Convex provides built-in [file storage](https://docs.convex.dev/file-storage). Enable it in your config to auto-scaffold the required functions.
|
|
285
|
-
|
|
286
|
-
### Setup
|
|
287
|
-
|
|
288
|
-
```ts [nuxt.config.ts]
|
|
289
|
-
export default defineNuxtConfig({
|
|
290
|
-
modules: ['nuxt-convex'],
|
|
291
|
-
convex: {
|
|
292
|
-
storage: true,
|
|
293
|
-
},
|
|
294
|
-
})
|
|
295
|
-
```
|
|
296
|
-
|
|
297
|
-
This creates `convex/_hub/storage.ts` with `generateUploadUrl`, `getUrl`, and `remove` functions. Customize as needed.
|
|
298
|
-
|
|
299
|
-
Add the `uploads` table to your schema:
|
|
300
|
-
|
|
301
|
-
```ts [convex/schema.ts]
|
|
302
|
-
import { defineSchema, defineTable } from 'convex/server'
|
|
303
|
-
import { v } from 'convex/values'
|
|
304
|
-
|
|
305
|
-
export default defineSchema({
|
|
306
|
-
uploads: defineTable({
|
|
307
|
-
storageId: v.id('_storage'),
|
|
308
|
-
name: v.string(),
|
|
309
|
-
type: v.string(),
|
|
310
|
-
url: v.optional(v.string()),
|
|
311
|
-
userId: v.string(),
|
|
312
|
-
createdAt: v.number(),
|
|
313
|
-
}).index('by_user', ['userId']),
|
|
314
|
-
})
|
|
315
|
-
```
|
|
316
|
-
|
|
317
|
-
### useConvexStorage
|
|
318
|
-
|
|
319
|
-
Low-level composable for storage operations. Auto-imported when storage is enabled.
|
|
320
|
-
|
|
321
|
-
```vue [app/components/Storage.vue]
|
|
322
|
-
<script setup lang="ts">
|
|
323
|
-
import { api } from '#convex/api'
|
|
324
|
-
|
|
325
|
-
const { generateUploadUrl, getUrl, remove } = useConvexStorage(api)
|
|
326
|
-
|
|
327
|
-
// Get a reactive URL for a stored file
|
|
328
|
-
const fileUrl = getUrl('storage_id_here')
|
|
329
|
-
|
|
330
|
-
// Generate upload URL for direct upload
|
|
331
|
-
const uploadUrl = await generateUploadUrl.mutate()
|
|
332
|
-
|
|
333
|
-
// Delete a file
|
|
334
|
-
await remove.mutate({ storageId: 'storage_id_here' })
|
|
335
|
-
</script>
|
|
336
|
-
```
|
|
337
|
-
|
|
338
|
-
**Return values:**
|
|
339
|
-
|
|
340
|
-
| Property | Type | Description |
|
|
341
|
-
| ------------------- | -------------------------------------------- | --------------------- |
|
|
342
|
-
| `generateUploadUrl` | `{ mutate: () => Promise<string> }` | Get URL for uploading |
|
|
343
|
-
| `getUrl` | `(storageId: string) => Ref<string \| null>` | Reactive file URL |
|
|
344
|
-
| `remove` | `{ mutate: (args) => Promise<void> }` | Delete a file |
|
|
345
|
-
|
|
346
|
-
### useConvexUpload
|
|
347
|
-
|
|
348
|
-
High-level composable for file uploads with progress tracking. Auto-imported.
|
|
349
|
-
|
|
350
|
-
```vue [app/pages/upload.vue]
|
|
351
|
-
<script setup lang="ts">
|
|
352
|
-
import { api } from '#convex/api'
|
|
353
|
-
|
|
354
|
-
const { generateUploadUrl } = useConvexStorage(api)
|
|
355
|
-
const saveFile = useConvexMutation(api._hub.storage.saveFile)
|
|
356
|
-
|
|
357
|
-
const { upload, isUploading, progress, error } = useConvexUpload({
|
|
358
|
-
generateUploadUrl,
|
|
359
|
-
onSuccess: async (storageId, file) => {
|
|
360
|
-
await saveFile.mutate({
|
|
361
|
-
storageId,
|
|
362
|
-
name: file.name,
|
|
363
|
-
type: file.type,
|
|
364
|
-
userId: 'user_123',
|
|
365
|
-
})
|
|
366
|
-
},
|
|
367
|
-
onError: err => console.error('Upload failed:', err),
|
|
368
|
-
})
|
|
369
|
-
|
|
370
|
-
async function handleFileChange(event: Event) {
|
|
371
|
-
const file = (event.target as HTMLInputElement).files?.[0]
|
|
372
|
-
if (file) {
|
|
373
|
-
const storageId = await upload(file)
|
|
374
|
-
console.log('Uploaded:', storageId)
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
</script>
|
|
378
|
-
|
|
379
|
-
<template>
|
|
380
|
-
<div>
|
|
381
|
-
<input type="file" :disabled="isUploading" @change="handleFileChange">
|
|
382
|
-
<p v-if="isUploading">
|
|
383
|
-
Uploading... {{ progress }}%
|
|
384
|
-
</p>
|
|
385
|
-
<p v-if="error">
|
|
386
|
-
{{ error.message }}
|
|
387
|
-
</p>
|
|
388
|
-
</div>
|
|
389
|
-
</template>
|
|
390
|
-
```
|
|
391
|
-
|
|
392
|
-
**Options:**
|
|
393
|
-
|
|
394
|
-
| Option | Type | Description |
|
|
395
|
-
| ------------------- | ----------------------------------------- | --------------------------------- |
|
|
396
|
-
| `generateUploadUrl` | `{ mutate: () => Promise<string> }` | Required. From `useConvexStorage` |
|
|
397
|
-
| `onSuccess` | `(storageId: string, file: File) => void` | Called after successful upload |
|
|
398
|
-
| `onError` | `(error: Error) => void` | Called on upload error |
|
|
399
|
-
|
|
400
|
-
**Return values:**
|
|
401
|
-
|
|
402
|
-
| Property | Type | Description |
|
|
403
|
-
| ------------- | ----------------------------------------- | -------------------------------- |
|
|
404
|
-
| `upload` | `(file: File) => Promise<string \| null>` | Upload a file, returns storageId |
|
|
405
|
-
| `isUploading` | `Ref<boolean>` | True while uploading |
|
|
406
|
-
| `progress` | `Ref<number>` | Upload progress 0-100 |
|
|
407
|
-
| `error` | `Ref<Error \| null>` | Error if upload failed |
|
|
408
|
-
|
|
409
|
-
## Configuration
|
|
410
|
-
|
|
411
|
-
| Option | Type | Default | Description |
|
|
412
|
-
| --------- | --------- | ------------------------ | ------------------------------- |
|
|
413
|
-
| `url` | `string` | `process.env.CONVEX_URL` | Convex deployment URL |
|
|
414
|
-
| `storage` | `boolean` | `false` | Enable file storage integration |
|
|
415
|
-
|
|
416
|
-
**Environment variables:**
|
|
417
|
-
|
|
418
|
-
| Variable | Description |
|
|
419
|
-
| ------------------------ | ----------------------------------------------- |
|
|
420
|
-
| `CONVEX_URL` | Convex deployment URL (set by `npx convex dev`) |
|
|
421
|
-
| `NUXT_PUBLIC_CONVEX_URL` | Alternative, follows Nuxt convention |
|
|
422
|
-
|
|
423
|
-
## DevTools
|
|
424
|
-
|
|
425
|
-
When running in development, the module adds a **Convex** tab to Nuxt DevTools with an embedded Convex dashboard. Access your data, run functions, and view logs directly from DevTools.
|
|
426
|
-
|
|
427
|
-
## Better Auth Integration
|
|
428
|
-
|
|
429
|
-
Use [nuxt-better-auth](https://nuxt-better-auth.onmax.me/) for authentication alongside Convex for your app data.
|
|
430
|
-
|
|
431
|
-
### 1. Install dependencies
|
|
432
|
-
|
|
433
|
-
```bash
|
|
434
|
-
pnpm add @onmax/nuxt-better-auth
|
|
435
|
-
```
|
|
436
|
-
|
|
437
|
-
### 2. Configure nuxt-better-auth
|
|
438
|
-
|
|
439
|
-
```ts [nuxt.config.ts]
|
|
440
|
-
export default defineNuxtConfig({
|
|
441
|
-
modules: ['nuxt-convex', '@onmax/nuxt-better-auth'],
|
|
442
|
-
convex: { storage: true },
|
|
443
|
-
})
|
|
444
|
-
```
|
|
445
|
-
|
|
446
|
-
### 3. Create server auth config
|
|
447
|
-
|
|
448
|
-
```ts [server/auth.config.ts]
|
|
449
|
-
import { defineServerAuth } from '@onmax/nuxt-better-auth/config'
|
|
450
|
-
|
|
451
|
-
export default defineServerAuth(() => ({
|
|
452
|
-
emailAndPassword: { enabled: true },
|
|
453
|
-
}))
|
|
454
|
-
```
|
|
455
|
-
|
|
456
|
-
See [nuxt-better-auth docs](https://nuxt-better-auth.onmax.me/) for database adapters, social providers, and plugins.
|
|
457
|
-
|
|
458
|
-
### Using Convex as Auth Database (Advanced)
|
|
459
|
-
|
|
460
|
-
To store auth data directly in Convex instead of a SQL database:
|
|
461
|
-
|
|
462
|
-
```bash
|
|
463
|
-
pnpm add @convex-dev/better-auth better-auth --save-exact
|
|
464
|
-
```
|
|
465
|
-
|
|
466
|
-
```ts [convex/convex.config.ts]
|
|
467
|
-
import betterAuth from '@convex-dev/better-auth/convex.config'
|
|
468
|
-
import { defineApp } from 'convex/server'
|
|
469
|
-
|
|
470
|
-
const app = defineApp()
|
|
471
|
-
app.use(betterAuth)
|
|
472
|
-
export default app
|
|
473
|
-
```
|
|
474
|
-
|
|
475
|
-
```ts [server/auth.config.ts]
|
|
476
|
-
import { components } from '#convex/api'
|
|
477
|
-
import { createClient } from '@convex-dev/better-auth'
|
|
478
|
-
import { convex } from '@convex-dev/better-auth/server/plugins'
|
|
479
|
-
import { defineServerAuth } from '@onmax/nuxt-better-auth/config'
|
|
480
|
-
|
|
481
|
-
const authComponent = createClient(components.betterAuth)
|
|
482
|
-
|
|
483
|
-
export default defineServerAuth(() => ({
|
|
484
|
-
database: authComponent.adapter(),
|
|
485
|
-
plugins: [convex()],
|
|
486
|
-
emailAndPassword: { enabled: true },
|
|
487
|
-
}))
|
|
488
|
-
```
|
|
489
|
-
|
|
490
|
-
This stores users, sessions, and accounts in Convex. See [@convex-dev/better-auth](https://github.com/get-convex/convex-better-auth) for details.
|
|
491
|
-
|
|
492
|
-
## Development
|
|
493
|
-
|
|
494
|
-
```bash
|
|
495
|
-
pnpm install
|
|
496
|
-
pnpm dev:prepare
|
|
497
|
-
pnpm dev
|
|
498
|
-
```
|
|
499
|
-
|
|
500
|
-
Run tests:
|
|
501
|
-
|
|
502
|
-
```bash
|
|
503
|
-
pnpm test
|
|
504
|
-
```
|
|
21
|
+
**[nuxt-convex.onmax.me](https://nuxt-convex.onmax.me/)**
|
|
505
22
|
|
|
506
23
|
## License
|
|
507
24
|
|
package/dist/module.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as nuxt_schema from 'nuxt/schema';
|
|
2
2
|
|
|
3
3
|
interface ConvexConfig {
|
|
4
4
|
/** Convex deployment URL. Auto-detected from CONVEX_URL or NUXT_PUBLIC_CONVEX_URL env vars */
|
|
@@ -11,7 +11,7 @@ interface ResolvedConvexConfig {
|
|
|
11
11
|
storage: boolean;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
declare const _default:
|
|
14
|
+
declare const _default: nuxt_schema.NuxtModule<ConvexConfig, ConvexConfig, false>;
|
|
15
15
|
|
|
16
16
|
export { _default as default };
|
|
17
17
|
export type { ConvexConfig, ResolvedConvexConfig };
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { consola } from 'consola';
|
|
|
6
6
|
import { defu } from 'defu';
|
|
7
7
|
import { join } from 'pathe';
|
|
8
8
|
|
|
9
|
-
const version = "0.0.
|
|
9
|
+
const version = "0.0.2";
|
|
10
10
|
|
|
11
11
|
const log = consola.withTag("nuxt:convex");
|
|
12
12
|
const module$1 = defineNuxtModule({
|
|
@@ -33,9 +33,11 @@ const module$1 = defineNuxtModule({
|
|
|
33
33
|
{ url: config.url }
|
|
34
34
|
);
|
|
35
35
|
addPlugin({ src: resolve("./runtime/plugin.client"), mode: "client" });
|
|
36
|
-
addImports(
|
|
36
|
+
addImports([
|
|
37
|
+
{ name: "useConvexUpload", from: resolve("./runtime/composables/useConvexUpload") }
|
|
38
|
+
]);
|
|
37
39
|
setupConvexApiAlias(nuxt);
|
|
38
|
-
setupConvexClient(nuxt);
|
|
40
|
+
setupConvexClient(nuxt, resolve);
|
|
39
41
|
if (config.storage) {
|
|
40
42
|
await setupConvexStorage(nuxt, resolve);
|
|
41
43
|
}
|
|
@@ -55,13 +57,14 @@ function resolveConfig(options) {
|
|
|
55
57
|
const url = options.url || process.env.CONVEX_URL || process.env.NUXT_PUBLIC_CONVEX_URL || "";
|
|
56
58
|
return defu(options, { url, storage: false });
|
|
57
59
|
}
|
|
58
|
-
function setupConvexClient(nuxt,
|
|
60
|
+
function setupConvexClient(nuxt, resolve) {
|
|
59
61
|
const template = addTemplate({
|
|
60
62
|
filename: "convex/client.mjs",
|
|
61
63
|
getContents: () => `// Auto-generated by nuxt-convex
|
|
62
64
|
import { inject } from 'vue'
|
|
63
65
|
import { CONVEX_INJECTION_KEY } from '@convex-vue/core'
|
|
64
|
-
export {
|
|
66
|
+
export { useConvexMutation, useConvexAction } from '@convex-vue/core'
|
|
67
|
+
export { useConvexQuery } from '${resolve("./runtime/composables/useConvexQuery")}'
|
|
65
68
|
|
|
66
69
|
// Get the Convex client - MUST be called during component setup
|
|
67
70
|
export function useConvex() {
|
|
@@ -76,8 +79,23 @@ export function useConvex() {
|
|
|
76
79
|
filename: "types/convex.d.ts",
|
|
77
80
|
getContents: () => `// Auto-generated by nuxt-convex
|
|
78
81
|
import type { ConvexClient } from 'convex/browser'
|
|
82
|
+
import type { FunctionReference, FunctionArgs, FunctionReturnType } from 'convex/server'
|
|
83
|
+
import type { AsyncData, AsyncDataOptions } from 'nuxt/app'
|
|
84
|
+
import type { MaybeRefOrGetter } from 'vue'
|
|
85
|
+
|
|
79
86
|
declare module '#convex' {
|
|
80
|
-
export {
|
|
87
|
+
export { useConvexMutation, useConvexAction } from '@convex-vue/core'
|
|
88
|
+
|
|
89
|
+
export interface UseConvexQueryOptions<T> extends Omit<AsyncDataOptions<T>, 'transform' | 'default'> {
|
|
90
|
+
ssr?: boolean
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function useConvexQuery<Query extends FunctionReference<'query'>>(
|
|
94
|
+
query: Query,
|
|
95
|
+
args: MaybeRefOrGetter<FunctionArgs<Query>>,
|
|
96
|
+
options?: UseConvexQueryOptions<FunctionReturnType<Query>>
|
|
97
|
+
): Promise<AsyncData<FunctionReturnType<Query> | null, Error | null>>
|
|
98
|
+
|
|
81
99
|
export function useConvex(): ConvexClient
|
|
82
100
|
}
|
|
83
101
|
`
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { FunctionArgs, FunctionReference, FunctionReturnType } from 'convex/server';
|
|
2
|
+
import type { AsyncData, AsyncDataOptions } from 'nuxt/app';
|
|
3
|
+
import type { MaybeRefOrGetter } from 'vue';
|
|
4
|
+
type QueryReference = FunctionReference<'query'>;
|
|
5
|
+
export interface UseConvexQueryOptions<T> extends Omit<AsyncDataOptions<T>, 'transform' | 'default'> {
|
|
6
|
+
/**
|
|
7
|
+
* Enable server-side data fetching.
|
|
8
|
+
* @default true
|
|
9
|
+
*/
|
|
10
|
+
ssr?: boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Convex query composable with SSR support.
|
|
14
|
+
*
|
|
15
|
+
* By default, fetches data on the server (SSR) and subscribes to real-time updates on the client.
|
|
16
|
+
* Set `ssr: false` to disable server-side fetching and use client-only mode.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```vue
|
|
20
|
+
* <script setup>
|
|
21
|
+
* // SSR + real-time (default)
|
|
22
|
+
* const { data, pending, error } = await useConvexQuery(api.tasks.list, {})
|
|
23
|
+
*
|
|
24
|
+
* // Client-only mode
|
|
25
|
+
* const { data } = await useConvexQuery(api.tasks.list, {}, { ssr: false })
|
|
26
|
+
* </script>
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export declare function useConvexQuery<Query extends QueryReference>(query: Query, args: MaybeRefOrGetter<FunctionArgs<Query>>, options?: UseConvexQueryOptions<FunctionReturnType<Query>>): Promise<AsyncData<FunctionReturnType<Query> | null, Error | null>>;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { useAsyncData, useNuxtApp, useRuntimeConfig } from "#imports";
|
|
2
|
+
import { CONVEX_INJECTION_KEY } from "@convex-vue/core";
|
|
3
|
+
import { inject, onScopeDispose, ref, toValue, watch } from "vue";
|
|
4
|
+
export async function useConvexQuery(query, args, options = {}) {
|
|
5
|
+
const nuxtApp = useNuxtApp();
|
|
6
|
+
const config = useRuntimeConfig();
|
|
7
|
+
const convexUrl = config.public.convex?.url;
|
|
8
|
+
const client = import.meta.client ? inject(CONVEX_INJECTION_KEY) : null;
|
|
9
|
+
if (!convexUrl) {
|
|
10
|
+
console.warn("[nuxt-convex] No Convex URL configured");
|
|
11
|
+
return createErrorAsyncData(new Error("Convex URL not configured"));
|
|
12
|
+
}
|
|
13
|
+
const ssrEnabled = options.ssr ?? true;
|
|
14
|
+
const shouldUseSSR = ssrEnabled && (import.meta.server || nuxtApp.isHydrating);
|
|
15
|
+
if (shouldUseSSR) {
|
|
16
|
+
return useConvexQuerySSR(query, args, options, convexUrl, client);
|
|
17
|
+
}
|
|
18
|
+
return useConvexQueryClient(query, args, client);
|
|
19
|
+
}
|
|
20
|
+
async function useConvexQuerySSR(query, args, options, convexUrl, client) {
|
|
21
|
+
const queryName = query._name ?? "unknown";
|
|
22
|
+
const sortedArgs = sortObjectKeys(toValue(args));
|
|
23
|
+
const key = `convex:${queryName}:${JSON.stringify(sortedArgs)}`;
|
|
24
|
+
const asyncData = await useAsyncData(
|
|
25
|
+
key,
|
|
26
|
+
async () => {
|
|
27
|
+
const { fetchQuery } = await import("convex/nextjs");
|
|
28
|
+
return fetchQuery(query, toValue(args), { url: convexUrl });
|
|
29
|
+
},
|
|
30
|
+
{ ...options, server: options.ssr ?? true }
|
|
31
|
+
);
|
|
32
|
+
if (import.meta.client) {
|
|
33
|
+
setupClientSubscription(query, args, {
|
|
34
|
+
data: asyncData.data,
|
|
35
|
+
error: asyncData.error,
|
|
36
|
+
pending: asyncData.pending,
|
|
37
|
+
status: asyncData.status
|
|
38
|
+
}, client);
|
|
39
|
+
}
|
|
40
|
+
return asyncData;
|
|
41
|
+
}
|
|
42
|
+
function useConvexQueryClient(query, args, client) {
|
|
43
|
+
const data = ref(null);
|
|
44
|
+
const error = ref(null);
|
|
45
|
+
const pending = ref(true);
|
|
46
|
+
const status = ref("pending");
|
|
47
|
+
const state = { data, error, pending, status };
|
|
48
|
+
const { refresh } = setupClientSubscription(query, args, state, client);
|
|
49
|
+
return {
|
|
50
|
+
data,
|
|
51
|
+
pending,
|
|
52
|
+
error,
|
|
53
|
+
status,
|
|
54
|
+
refresh,
|
|
55
|
+
execute: refresh,
|
|
56
|
+
clear: () => {
|
|
57
|
+
data.value = null;
|
|
58
|
+
error.value = null;
|
|
59
|
+
pending.value = false;
|
|
60
|
+
status.value = "idle";
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function setupClientSubscription(query, args, state, client) {
|
|
65
|
+
if (!client) {
|
|
66
|
+
console.warn("[nuxt-convex] Convex client not found. Real-time updates disabled.");
|
|
67
|
+
state.pending.value = false;
|
|
68
|
+
state.status.value = "error";
|
|
69
|
+
state.error.value = new Error("Convex client not found");
|
|
70
|
+
return { refresh: async () => {
|
|
71
|
+
} };
|
|
72
|
+
}
|
|
73
|
+
let unsubscribe = null;
|
|
74
|
+
const subscribe = () => {
|
|
75
|
+
unsubscribe?.();
|
|
76
|
+
const currentArgs = toValue(args);
|
|
77
|
+
unsubscribe = client.onUpdate(query, currentArgs, (result) => {
|
|
78
|
+
if (result !== void 0) {
|
|
79
|
+
state.data.value = result;
|
|
80
|
+
state.pending.value = false;
|
|
81
|
+
state.status.value = "success";
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
subscribe();
|
|
86
|
+
watch(() => toValue(args), subscribe, { deep: true });
|
|
87
|
+
onScopeDispose(() => unsubscribe?.());
|
|
88
|
+
return {
|
|
89
|
+
refresh: async () => {
|
|
90
|
+
state.pending.value = true;
|
|
91
|
+
subscribe();
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function createErrorAsyncData(err) {
|
|
96
|
+
return {
|
|
97
|
+
data: ref(null),
|
|
98
|
+
pending: ref(false),
|
|
99
|
+
error: ref(err),
|
|
100
|
+
status: ref("error"),
|
|
101
|
+
refresh: async () => {
|
|
102
|
+
},
|
|
103
|
+
execute: async () => {
|
|
104
|
+
},
|
|
105
|
+
clear: () => {
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function sortObjectKeys(obj) {
|
|
110
|
+
if (obj === null || typeof obj !== "object")
|
|
111
|
+
return obj;
|
|
112
|
+
if (Array.isArray(obj))
|
|
113
|
+
return obj.map(sortObjectKeys);
|
|
114
|
+
const sorted = {};
|
|
115
|
+
for (const key of Object.keys(obj).sort()) {
|
|
116
|
+
sorted[key] = sortObjectKeys(obj[key]);
|
|
117
|
+
}
|
|
118
|
+
return sorted;
|
|
119
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxt-convex",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.2",
|
|
5
5
|
"packageManager": "pnpm@10.15.1",
|
|
6
6
|
"description": "Nuxt module for Convex - reactive backend with real-time sync, file storage, and auto-imports",
|
|
7
7
|
"author": "onmax",
|
|
@@ -72,11 +72,12 @@
|
|
|
72
72
|
"@nuxt/module-builder": "catalog:build",
|
|
73
73
|
"@nuxt/test-utils": "catalog:test",
|
|
74
74
|
"@types/node": "catalog:types",
|
|
75
|
-
"
|
|
75
|
+
"@vue/test-utils": "catalog:test",
|
|
76
|
+
"bumpp": "catalog:",
|
|
76
77
|
"convex": "catalog:convex",
|
|
77
78
|
"eslint": "catalog:lint",
|
|
78
79
|
"eslint-plugin-format": "catalog:lint",
|
|
79
|
-
"nuxt": "catalog:
|
|
80
|
+
"nuxt": "catalog:",
|
|
80
81
|
"typescript": "catalog:build",
|
|
81
82
|
"vitest": "catalog:test",
|
|
82
83
|
"vitest-package-exports": "catalog:test",
|