convex-devtools 0.1.0
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 +332 -0
- package/dist/cli/chunk-5AG6RQBI.js +102 -0
- package/dist/cli/chunk-5AG6RQBI.js.map +1 -0
- package/dist/cli/chunk-A7PTPOQI.js +137 -0
- package/dist/cli/chunk-A7PTPOQI.js.map +1 -0
- package/dist/cli/chunk-K2AL37MJ.js +216 -0
- package/dist/cli/chunk-K2AL37MJ.js.map +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +106 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/server/convex-client.d.ts +33 -0
- package/dist/cli/server/convex-client.js +7 -0
- package/dist/cli/server/convex-client.js.map +1 -0
- package/dist/cli/server/index.d.ts +14 -0
- package/dist/cli/server/index.js +8 -0
- package/dist/cli/server/index.js.map +1 -0
- package/dist/cli/server/schema-watcher.d.ts +52 -0
- package/dist/cli/server/schema-watcher.js +7 -0
- package/dist/cli/server/schema-watcher.js.map +1 -0
- package/dist/ui/assets/index-B7zbq--d.css +1 -0
- package/dist/ui/assets/index-CqKYd9Ws.js +56 -0
- package/dist/ui/convex-icon.svg +5 -0
- package/dist/ui/index.html +17 -0
- package/package.json +86 -0
package/README.md
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
# Convex DevTools
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="https://img.shields.io/npm/v/convex-devtools" alt="npm version">
|
|
5
|
+
<img src="https://img.shields.io/npm/l/convex-devtools" alt="license">
|
|
6
|
+
<img src="https://img.shields.io/npm/dt/convex-devtools" alt="downloads">
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
A standalone development tool for testing Convex queries, mutations, and actions with identity mocking, request saving, and auto-reloading schema discovery.
|
|
10
|
+
|
|
11
|
+
> ⚠️ **WARNING**: This tool is intended for **local development only**. It requires admin access to your Convex deployment.
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- 🔍 **Function Explorer** - Browse all your Convex queries, mutations, and actions in a tree view
|
|
16
|
+
- 🎭 **Identity Mocking** - Test functions as different users with custom roles and claims
|
|
17
|
+
- 💾 **Request Collections** - Save and organize requests like Postman
|
|
18
|
+
- 📜 **History** - View and replay previous function calls
|
|
19
|
+
- 🔄 **Auto-reload** - Schema updates automatically when your Convex files change
|
|
20
|
+
- 📤 **Import/Export** - Share collections with your team
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
### Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# Install globally
|
|
28
|
+
npm install -g convex-devtools
|
|
29
|
+
|
|
30
|
+
# Or with pnpm
|
|
31
|
+
pnpm add -g convex-devtools
|
|
32
|
+
|
|
33
|
+
# Or with yarn
|
|
34
|
+
yarn global add convex-devtools
|
|
35
|
+
|
|
36
|
+
# Or run directly with npx (no installation required)
|
|
37
|
+
npx convex-devtools
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Setup
|
|
41
|
+
|
|
42
|
+
1. Navigate to your Convex project directory
|
|
43
|
+
|
|
44
|
+
2. Add the following to your `.env.local` file:
|
|
45
|
+
|
|
46
|
+
```env
|
|
47
|
+
CONVEX_DEVTOOLS_ENABLED=true
|
|
48
|
+
CONVEX_URL=https://your-deployment.convex.cloud
|
|
49
|
+
CONVEX_DEPLOY_KEY=prod:your-deploy-key-here
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
3. Get your deploy key from the [Convex Dashboard](https://dashboard.convex.dev) under **Settings → Deploy Keys**.
|
|
53
|
+
|
|
54
|
+
### Running
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# Run in your Convex project directory
|
|
58
|
+
convex-devtools
|
|
59
|
+
|
|
60
|
+
# Or specify a different directory
|
|
61
|
+
convex-devtools --dir /path/to/your/project
|
|
62
|
+
|
|
63
|
+
# Run on a custom port
|
|
64
|
+
convex-devtools --port 3000
|
|
65
|
+
|
|
66
|
+
# Don't auto-open browser
|
|
67
|
+
convex-devtools --no-open
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The tool will automatically open in your browser at `http://localhost:5173`.
|
|
71
|
+
|
|
72
|
+
## CLI Options
|
|
73
|
+
|
|
74
|
+
| Option | Description | Default |
|
|
75
|
+
| --------------------- | -------------------------------- | ------- |
|
|
76
|
+
| `-p, --port <number>` | Port for the devtools server | `5173` |
|
|
77
|
+
| `-d, --dir <path>` | Path to Convex project directory | `.` |
|
|
78
|
+
| `--no-open` | Don't open browser automatically | - |
|
|
79
|
+
| `-V, --version` | Display version number | - |
|
|
80
|
+
| `-h, --help` | Display help information | - |
|
|
81
|
+
|
|
82
|
+
## Identity Mocking
|
|
83
|
+
|
|
84
|
+
The Identity Builder allows you to test functions as different users. You can set:
|
|
85
|
+
|
|
86
|
+
- **Subject** - The user's unique identifier (e.g., Clerk user ID)
|
|
87
|
+
- **Name/Email** - Display information
|
|
88
|
+
- **Roles** - Array of role strings (e.g., `["super_admin", "shop_admin"]`)
|
|
89
|
+
- **User Local ID** - Your app's internal user document ID
|
|
90
|
+
- **Custom Claims** - Any additional JWT claims your app uses
|
|
91
|
+
|
|
92
|
+
### Example Identity
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"subject": "clerk_user_123",
|
|
97
|
+
"name": "Test Admin",
|
|
98
|
+
"email": "admin@example.com",
|
|
99
|
+
"roles": ["super_admin"],
|
|
100
|
+
"user_local_id": "k975abc123def456"
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Collections
|
|
105
|
+
|
|
106
|
+
Save requests to collections for easy access:
|
|
107
|
+
|
|
108
|
+
1. Select a function and configure its arguments
|
|
109
|
+
2. Click the save icon in the request panel
|
|
110
|
+
3. Choose or create a collection
|
|
111
|
+
4. Give the request a descriptive name
|
|
112
|
+
|
|
113
|
+
### Export/Import
|
|
114
|
+
|
|
115
|
+
- Click the export icon to download your collections as JSON
|
|
116
|
+
- Click the import icon to load collections from a JSON file
|
|
117
|
+
- Share collections with your team via version control
|
|
118
|
+
|
|
119
|
+
### Export Format
|
|
120
|
+
|
|
121
|
+
The export format is a simple JSON structure:
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{
|
|
125
|
+
"version": "1.0",
|
|
126
|
+
"exportedAt": "2025-01-29T10:00:00.000Z",
|
|
127
|
+
"collections": [
|
|
128
|
+
{
|
|
129
|
+
"id": "abc123",
|
|
130
|
+
"name": "Product Tests",
|
|
131
|
+
"requests": [
|
|
132
|
+
{
|
|
133
|
+
"id": "def456",
|
|
134
|
+
"name": "List all products",
|
|
135
|
+
"functionPath": "products/products:list",
|
|
136
|
+
"functionType": "query",
|
|
137
|
+
"args": "{}",
|
|
138
|
+
"identity": null,
|
|
139
|
+
"savedAt": "2025-01-29T10:00:00.000Z"
|
|
140
|
+
}
|
|
141
|
+
],
|
|
142
|
+
"createdAt": "2025-01-29T10:00:00.000Z"
|
|
143
|
+
}
|
|
144
|
+
]
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Programmatic Usage
|
|
149
|
+
|
|
150
|
+
You can also use convex-devtools programmatically in your Node.js projects:
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
import { createServer, SchemaWatcher, ConvexClient } from 'convex-devtools';
|
|
154
|
+
|
|
155
|
+
// Start a schema watcher
|
|
156
|
+
const schemaWatcher = new SchemaWatcher('/path/to/project');
|
|
157
|
+
await schemaWatcher.start();
|
|
158
|
+
|
|
159
|
+
// Create and start the server
|
|
160
|
+
const server = await createServer({
|
|
161
|
+
port: 5173,
|
|
162
|
+
projectDir: '/path/to/project',
|
|
163
|
+
convexUrl: 'https://your-deployment.convex.cloud',
|
|
164
|
+
deployKey: 'prod:your-deploy-key',
|
|
165
|
+
schemaWatcher,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Clean up when done
|
|
169
|
+
schemaWatcher.stop();
|
|
170
|
+
server.close();
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Development
|
|
174
|
+
|
|
175
|
+
### Prerequisites
|
|
176
|
+
|
|
177
|
+
- Node.js 18+
|
|
178
|
+
- pnpm (recommended) or npm
|
|
179
|
+
|
|
180
|
+
### Setup
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
# Clone the repository
|
|
184
|
+
git clone https://github.com/your-username/convex-devtools.git
|
|
185
|
+
cd convex-devtools
|
|
186
|
+
|
|
187
|
+
# Install dependencies
|
|
188
|
+
pnpm install
|
|
189
|
+
|
|
190
|
+
# Run in development mode
|
|
191
|
+
pnpm dev
|
|
192
|
+
|
|
193
|
+
# Build for production
|
|
194
|
+
pnpm build:all
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Project Structure
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
convex-devtools/
|
|
201
|
+
├── src/
|
|
202
|
+
│ ├── cli/ # CLI entry point
|
|
203
|
+
│ ├── components/ # React components for the UI
|
|
204
|
+
│ ├── server/ # Express server & Convex client
|
|
205
|
+
│ └── stores/ # Zustand state management
|
|
206
|
+
├── public/ # Static assets
|
|
207
|
+
├── dist/ # Built output (generated)
|
|
208
|
+
└── package.json
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Scripts
|
|
212
|
+
|
|
213
|
+
| Script | Description |
|
|
214
|
+
| ---------------- | -------------------------------- |
|
|
215
|
+
| `pnpm dev` | Start development server |
|
|
216
|
+
| `pnpm build` | Build the frontend |
|
|
217
|
+
| `pnpm build:cli` | Build the CLI |
|
|
218
|
+
| `pnpm build:all` | Build everything for production |
|
|
219
|
+
| `pnpm typecheck` | Run TypeScript type checking |
|
|
220
|
+
| `pnpm lint` | Run ESLint |
|
|
221
|
+
| `pnpm preview` | Preview production build locally |
|
|
222
|
+
|
|
223
|
+
## Publishing to npm
|
|
224
|
+
|
|
225
|
+
### First-time Setup
|
|
226
|
+
|
|
227
|
+
1. Create an npm account at [npmjs.com](https://www.npmjs.com/signup)
|
|
228
|
+
|
|
229
|
+
2. Login to npm from your terminal:
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
npm login
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
3. Verify your login:
|
|
236
|
+
```bash
|
|
237
|
+
npm whoami
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Building and Publishing
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
# 1. Make sure all tests pass and there are no errors
|
|
244
|
+
pnpm typecheck
|
|
245
|
+
pnpm lint
|
|
246
|
+
|
|
247
|
+
# 2. Build the project
|
|
248
|
+
pnpm build:all
|
|
249
|
+
|
|
250
|
+
# 3. Update the version (choose one)
|
|
251
|
+
npm version patch # for bug fixes (0.1.0 -> 0.1.1)
|
|
252
|
+
npm version minor # for new features (0.1.0 -> 0.2.0)
|
|
253
|
+
npm version major # for breaking changes (0.1.0 -> 1.0.0)
|
|
254
|
+
|
|
255
|
+
# 4. Publish to npm
|
|
256
|
+
npm publish
|
|
257
|
+
|
|
258
|
+
# Or for a scoped package (if using @your-org/convex-devtools)
|
|
259
|
+
npm publish --access public
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Testing Before Publishing
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
# Create a tarball to see what will be published
|
|
266
|
+
npm pack
|
|
267
|
+
|
|
268
|
+
# This creates convex-devtools-x.x.x.tgz
|
|
269
|
+
# You can inspect it or install it locally:
|
|
270
|
+
npm install ./convex-devtools-0.1.0.tgz -g
|
|
271
|
+
|
|
272
|
+
# Test the CLI
|
|
273
|
+
convex-devtools --help
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Publishing a Pre-release
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
# For beta versions
|
|
280
|
+
npm version prerelease --preid=beta
|
|
281
|
+
npm publish --tag beta
|
|
282
|
+
|
|
283
|
+
# Users can install with: npm install convex-devtools@beta
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## Security Considerations
|
|
287
|
+
|
|
288
|
+
- **Never use in production** - This tool has full admin access to your Convex deployment
|
|
289
|
+
- **Keep your deploy key secret** - Don't commit it to version control
|
|
290
|
+
- **Local development only** - The `CONVEX_DEVTOOLS_ENABLED` flag should never be set in production
|
|
291
|
+
- **Use dev deploy keys when possible** - Prefer `dev:xxx` keys over `prod:xxx` keys during development
|
|
292
|
+
|
|
293
|
+
## Troubleshooting
|
|
294
|
+
|
|
295
|
+
### "CONVEX_DEVTOOLS_ENABLED is not set to true"
|
|
296
|
+
|
|
297
|
+
Add `CONVEX_DEVTOOLS_ENABLED=true` to your `.env.local` file. This safety check ensures you don't accidentally run the devtools against production.
|
|
298
|
+
|
|
299
|
+
### "Convex generated files not found"
|
|
300
|
+
|
|
301
|
+
Run `npx convex dev` in your project directory first to generate the required files.
|
|
302
|
+
|
|
303
|
+
### "Identity mocking is disabled"
|
|
304
|
+
|
|
305
|
+
You need to set `CONVEX_DEPLOY_KEY` in your `.env.local` file. Get your deploy key from the Convex Dashboard under Settings → Deploy Keys.
|
|
306
|
+
|
|
307
|
+
### Port already in use
|
|
308
|
+
|
|
309
|
+
Use the `--port` flag to specify a different port:
|
|
310
|
+
|
|
311
|
+
```bash
|
|
312
|
+
convex-devtools --port 3001
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
## Contributing
|
|
316
|
+
|
|
317
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
318
|
+
|
|
319
|
+
1. Fork the repository
|
|
320
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
321
|
+
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
|
322
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
323
|
+
5. Open a Pull Request
|
|
324
|
+
|
|
325
|
+
## License
|
|
326
|
+
|
|
327
|
+
MIT © [Šahzudin Mahmić]
|
|
328
|
+
|
|
329
|
+
## Related Projects
|
|
330
|
+
|
|
331
|
+
- [Convex](https://convex.dev) - The backend platform this tool is designed for
|
|
332
|
+
- [Convex Dashboard](https://dashboard.convex.dev) - Official Convex admin dashboard
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ConvexClient
|
|
3
|
+
} from "./chunk-A7PTPOQI.js";
|
|
4
|
+
|
|
5
|
+
// src/server/index.ts
|
|
6
|
+
import cors from "cors";
|
|
7
|
+
import express from "express";
|
|
8
|
+
import http from "http";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import { fileURLToPath } from "url";
|
|
11
|
+
import { WebSocket, WebSocketServer } from "ws";
|
|
12
|
+
var __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
async function createServer(config) {
|
|
14
|
+
const app = express();
|
|
15
|
+
app.use(cors());
|
|
16
|
+
app.use(express.json({ limit: "10mb" }));
|
|
17
|
+
const uiPath = path.join(__dirname, "..", "ui");
|
|
18
|
+
app.use(express.static(uiPath));
|
|
19
|
+
const convexClient = new ConvexClient(config.convexUrl, config.deployKey);
|
|
20
|
+
app.get("/api/schema", (_req, res) => {
|
|
21
|
+
const schema = config.schemaWatcher.getSchema();
|
|
22
|
+
if (!schema) {
|
|
23
|
+
res.status(503).json({ error: "Schema not yet loaded" });
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
res.json(schema);
|
|
27
|
+
});
|
|
28
|
+
app.get("/api/health", (_req, res) => {
|
|
29
|
+
res.json({
|
|
30
|
+
status: "ok",
|
|
31
|
+
convexUrl: config.convexUrl,
|
|
32
|
+
projectDir: config.projectDir
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
app.post("/api/invoke", async (req, res) => {
|
|
36
|
+
const { functionPath, functionType, args, jwtToken } = req.body;
|
|
37
|
+
if (!functionPath || !functionType) {
|
|
38
|
+
res.status(400).json({ error: "Missing functionPath or functionType" });
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const startTime = Date.now();
|
|
43
|
+
const result = await convexClient.invoke(
|
|
44
|
+
functionPath,
|
|
45
|
+
functionType,
|
|
46
|
+
args || {},
|
|
47
|
+
{ jwtToken }
|
|
48
|
+
);
|
|
49
|
+
const duration = Date.now() - startTime;
|
|
50
|
+
res.json({
|
|
51
|
+
success: true,
|
|
52
|
+
result,
|
|
53
|
+
duration,
|
|
54
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
55
|
+
});
|
|
56
|
+
} catch (error) {
|
|
57
|
+
res.json({
|
|
58
|
+
success: false,
|
|
59
|
+
error: {
|
|
60
|
+
message: error.message,
|
|
61
|
+
code: error.code,
|
|
62
|
+
data: error.data
|
|
63
|
+
},
|
|
64
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
const server = http.createServer(app);
|
|
69
|
+
const wss = new WebSocketServer({ server, path: "/ws" });
|
|
70
|
+
const clients = /* @__PURE__ */ new Set();
|
|
71
|
+
wss.on("connection", (ws) => {
|
|
72
|
+
clients.add(ws);
|
|
73
|
+
const schema = config.schemaWatcher.getSchema();
|
|
74
|
+
if (schema) {
|
|
75
|
+
ws.send(JSON.stringify({ type: "schema", data: schema }));
|
|
76
|
+
}
|
|
77
|
+
ws.on("close", () => {
|
|
78
|
+
clients.delete(ws);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
config.schemaWatcher.on("schema-updated", (schema) => {
|
|
82
|
+
const message = JSON.stringify({ type: "schema", data: schema });
|
|
83
|
+
for (const client of clients) {
|
|
84
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
85
|
+
client.send(message);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
app.get("*", (_req, res) => {
|
|
90
|
+
res.sendFile(path.join(uiPath, "index.html"));
|
|
91
|
+
});
|
|
92
|
+
return new Promise((resolve) => {
|
|
93
|
+
server.listen(config.port, () => {
|
|
94
|
+
resolve(server);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export {
|
|
100
|
+
createServer
|
|
101
|
+
};
|
|
102
|
+
//# sourceMappingURL=chunk-5AG6RQBI.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/server/index.ts"],"sourcesContent":["import cors from 'cors';\nimport express from 'express';\nimport http from 'http';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport { WebSocket, WebSocketServer } from 'ws';\nimport { ConvexClient } from './convex-client.js';\nimport { SchemaWatcher } from './schema-watcher.js';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\nexport interface ServerConfig {\n port: number;\n projectDir: string;\n convexUrl: string;\n deployKey: string;\n schemaWatcher: SchemaWatcher;\n}\n\nexport async function createServer(config: ServerConfig): Promise<http.Server> {\n const app = express();\n\n app.use(cors());\n app.use(express.json({ limit: '10mb' }));\n\n // Serve static UI files in production\n const uiPath = path.join(__dirname, '..', 'ui');\n app.use(express.static(uiPath));\n\n // Create Convex client\n const convexClient = new ConvexClient(config.convexUrl, config.deployKey);\n\n // API Routes\n app.get('/api/schema', (_req, res) => {\n const schema = config.schemaWatcher.getSchema();\n if (!schema) {\n res.status(503).json({ error: 'Schema not yet loaded' });\n return;\n }\n res.json(schema);\n });\n\n app.get('/api/health', (_req, res) => {\n res.json({\n status: 'ok',\n convexUrl: config.convexUrl,\n projectDir: config.projectDir,\n });\n });\n\n // Invoke a function\n app.post('/api/invoke', async (req, res) => {\n const { functionPath, functionType, args, jwtToken } = req.body;\n\n if (!functionPath || !functionType) {\n res.status(400).json({ error: 'Missing functionPath or functionType' });\n return;\n }\n\n try {\n const startTime = Date.now();\n const result = await convexClient.invoke(\n functionPath,\n functionType,\n args || {},\n { jwtToken }\n );\n const duration = Date.now() - startTime;\n\n res.json({\n success: true,\n result,\n duration,\n timestamp: new Date().toISOString(),\n });\n } catch (error: any) {\n res.json({\n success: false,\n error: {\n message: error.message,\n code: error.code,\n data: error.data,\n },\n timestamp: new Date().toISOString(),\n });\n }\n });\n\n // Create HTTP server\n const server = http.createServer(app);\n\n // WebSocket for real-time schema updates\n const wss = new WebSocketServer({ server, path: '/ws' });\n\n const clients = new Set<WebSocket>();\n\n wss.on('connection', (ws) => {\n clients.add(ws);\n\n // Send initial schema\n const schema = config.schemaWatcher.getSchema();\n if (schema) {\n ws.send(JSON.stringify({ type: 'schema', data: schema }));\n }\n\n ws.on('close', () => {\n clients.delete(ws);\n });\n });\n\n // Broadcast schema updates\n config.schemaWatcher.on('schema-updated', (schema) => {\n const message = JSON.stringify({ type: 'schema', data: schema });\n for (const client of clients) {\n if (client.readyState === WebSocket.OPEN) {\n client.send(message);\n }\n }\n });\n\n // SPA fallback - serve index.html for non-API routes\n app.get('*', (_req, res) => {\n res.sendFile(path.join(uiPath, 'index.html'));\n });\n\n return new Promise((resolve) => {\n server.listen(config.port, () => {\n resolve(server);\n });\n });\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// src/server/convex-client.ts
|
|
2
|
+
var ConvexClient = class {
|
|
3
|
+
baseUrl;
|
|
4
|
+
deployKey;
|
|
5
|
+
constructor(convexUrl, deployKey) {
|
|
6
|
+
this.baseUrl = convexUrl;
|
|
7
|
+
this.deployKey = deployKey;
|
|
8
|
+
}
|
|
9
|
+
async invoke(functionPath, functionType, args = {}, options) {
|
|
10
|
+
const normalizedPath = this.normalizeFunctionPath(functionPath);
|
|
11
|
+
const endpoint = this.getEndpoint(functionType);
|
|
12
|
+
const url = `${this.baseUrl}/${endpoint}`;
|
|
13
|
+
const headers = {
|
|
14
|
+
"Content-Type": "application/json",
|
|
15
|
+
"Convex-Client": "convex-devtools-0.1.0"
|
|
16
|
+
};
|
|
17
|
+
if (options?.jwtToken) {
|
|
18
|
+
headers["Authorization"] = `Bearer ${options.jwtToken}`;
|
|
19
|
+
console.log("[ConvexClient] Using JWT token authentication");
|
|
20
|
+
} else if (this.deployKey) {
|
|
21
|
+
headers["Authorization"] = `Convex ${this.deployKey}`;
|
|
22
|
+
console.log("[ConvexClient] Using deploy key (admin) authentication");
|
|
23
|
+
}
|
|
24
|
+
const body = {
|
|
25
|
+
path: normalizedPath,
|
|
26
|
+
args: this.encodeArgs(args),
|
|
27
|
+
format: "json"
|
|
28
|
+
};
|
|
29
|
+
const response = await fetch(url, {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers,
|
|
32
|
+
body: JSON.stringify(body)
|
|
33
|
+
});
|
|
34
|
+
if (!response.ok) {
|
|
35
|
+
const errorText = await response.text();
|
|
36
|
+
let errorData;
|
|
37
|
+
try {
|
|
38
|
+
errorData = JSON.parse(errorText);
|
|
39
|
+
} catch {
|
|
40
|
+
errorData = { message: errorText };
|
|
41
|
+
}
|
|
42
|
+
const error = new Error(errorData.message || `HTTP ${response.status}`);
|
|
43
|
+
error.code = errorData.code;
|
|
44
|
+
error.data = errorData;
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
const result = await response.json();
|
|
48
|
+
return this.decodeResult(result);
|
|
49
|
+
}
|
|
50
|
+
normalizeFunctionPath(path) {
|
|
51
|
+
if (path.includes(":")) {
|
|
52
|
+
return path;
|
|
53
|
+
}
|
|
54
|
+
if (path.includes(".")) {
|
|
55
|
+
const parts2 = path.split(".");
|
|
56
|
+
const funcName = parts2.pop();
|
|
57
|
+
return `${parts2.join("/")}:${funcName}`;
|
|
58
|
+
}
|
|
59
|
+
const parts = path.split("/");
|
|
60
|
+
if (parts.length > 1) {
|
|
61
|
+
const funcName = parts.pop();
|
|
62
|
+
return `${parts.join("/")}:${funcName}`;
|
|
63
|
+
}
|
|
64
|
+
return path;
|
|
65
|
+
}
|
|
66
|
+
getEndpoint(functionType) {
|
|
67
|
+
switch (functionType) {
|
|
68
|
+
case "query":
|
|
69
|
+
return "api/query";
|
|
70
|
+
case "mutation":
|
|
71
|
+
return "api/mutation";
|
|
72
|
+
case "action":
|
|
73
|
+
return "api/action";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
encodeArgs(args) {
|
|
77
|
+
return this.convertToConvexJson(args);
|
|
78
|
+
}
|
|
79
|
+
convertToConvexJson(value) {
|
|
80
|
+
if (value === null || value === void 0) {
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
if (Array.isArray(value)) {
|
|
84
|
+
return value.map((v) => this.convertToConvexJson(v));
|
|
85
|
+
}
|
|
86
|
+
if (typeof value === "object") {
|
|
87
|
+
const obj = value;
|
|
88
|
+
if ("$type" in obj) {
|
|
89
|
+
return obj;
|
|
90
|
+
}
|
|
91
|
+
const converted = {};
|
|
92
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
93
|
+
converted[k] = this.convertToConvexJson(v);
|
|
94
|
+
}
|
|
95
|
+
return converted;
|
|
96
|
+
}
|
|
97
|
+
if (typeof value === "bigint") {
|
|
98
|
+
return { $type: "bigint", value: value.toString() };
|
|
99
|
+
}
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
102
|
+
decodeResult(result) {
|
|
103
|
+
return this.convertFromConvexJson(result);
|
|
104
|
+
}
|
|
105
|
+
convertFromConvexJson(value) {
|
|
106
|
+
if (value === null || value === void 0) {
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
if (Array.isArray(value)) {
|
|
110
|
+
return value.map((v) => this.convertFromConvexJson(v));
|
|
111
|
+
}
|
|
112
|
+
if (typeof value === "object") {
|
|
113
|
+
const obj = value;
|
|
114
|
+
if ("$type" in obj) {
|
|
115
|
+
switch (obj.$type) {
|
|
116
|
+
case "bigint":
|
|
117
|
+
return BigInt(obj.value);
|
|
118
|
+
case "bytes":
|
|
119
|
+
return new Uint8Array(obj.value);
|
|
120
|
+
default:
|
|
121
|
+
return obj;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const result = {};
|
|
125
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
126
|
+
result[k] = this.convertFromConvexJson(v);
|
|
127
|
+
}
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
return value;
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export {
|
|
135
|
+
ConvexClient
|
|
136
|
+
};
|
|
137
|
+
//# sourceMappingURL=chunk-A7PTPOQI.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/server/convex-client.ts"],"sourcesContent":["/**\n * Convex HTTP Client wrapper for DevTools\n * Supports two authentication methods:\n * 1. Deploy key - runs as admin (for admin operations)\n * 2. JWT token - runs as the authenticated user (from your auth provider like Clerk)\n */\n\nexport interface UserIdentity {\n subject: string;\n issuer?: string;\n tokenIdentifier?: string;\n name?: string;\n email?: string;\n pictureUrl?: string;\n // Custom claims\n [key: string]: unknown;\n}\n\nexport interface InvokeOptions {\n identity?: UserIdentity;\n jwtToken?: string; // Real JWT token from auth provider\n}\n\nexport class ConvexClient {\n private baseUrl: string;\n private deployKey: string;\n\n constructor(convexUrl: string, deployKey: string) {\n // Convert deployment URL to HTTP endpoint\n // e.g., https://happy-otter-123.convex.cloud -> https://happy-otter-123.convex.cloud\n this.baseUrl = convexUrl;\n this.deployKey = deployKey;\n }\n\n async invoke(\n functionPath: string,\n functionType: 'query' | 'mutation' | 'action',\n args: Record<string, unknown> = {},\n options?: InvokeOptions\n ): Promise<unknown> {\n // Normalize function path: module/submodule:functionName\n const normalizedPath = this.normalizeFunctionPath(functionPath);\n\n // Build the request\n const endpoint = this.getEndpoint(functionType);\n const url = `${this.baseUrl}/${endpoint}`;\n\n // Build headers\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n 'Convex-Client': 'convex-devtools-0.1.0',\n };\n\n // Authentication priority:\n // 1. JWT token (authenticates as the user who owns the token)\n // 2. Deploy key (authenticates as admin)\n // 3. No auth (unauthenticated request)\n if (options?.jwtToken) {\n // Use Bearer token auth - this is what the Convex HTTP API expects for user auth\n headers['Authorization'] = `Bearer ${options.jwtToken}`;\n console.log('[ConvexClient] Using JWT token authentication');\n } else if (this.deployKey) {\n // Deploy key gives admin access but cannot impersonate users\n headers['Authorization'] = `Convex ${this.deployKey}`;\n console.log('[ConvexClient] Using deploy key (admin) authentication');\n }\n // Without any auth, calls will be unauthenticated\n\n const body = {\n path: normalizedPath,\n args: this.encodeArgs(args),\n format: 'json',\n };\n\n const response = await fetch(url, {\n method: 'POST',\n headers,\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n let errorData;\n try {\n errorData = JSON.parse(errorText);\n } catch {\n errorData = { message: errorText };\n }\n\n const error = new Error(errorData.message || `HTTP ${response.status}`);\n (error as any).code = errorData.code;\n (error as any).data = errorData;\n throw error;\n }\n\n const result = await response.json();\n return this.decodeResult(result);\n }\n\n private normalizeFunctionPath(path: string): string {\n // Convert various formats to Convex function path format\n // Input: \"products/products:list\" or \"products.products.list\" or \"products/products/list\"\n // Output: \"products/products:list\"\n\n // Already in correct format\n if (path.includes(':')) {\n return path;\n }\n\n // Dot notation: products.products.list -> products/products:list\n if (path.includes('.')) {\n const parts = path.split('.');\n const funcName = parts.pop()!;\n return `${parts.join('/')}:${funcName}`;\n }\n\n // Slash only: products/products/list -> products/products:list\n const parts = path.split('/');\n if (parts.length > 1) {\n const funcName = parts.pop()!;\n return `${parts.join('/')}:${funcName}`;\n }\n\n return path;\n }\n\n private getEndpoint(functionType: 'query' | 'mutation' | 'action'): string {\n switch (functionType) {\n case 'query':\n return 'api/query';\n case 'mutation':\n return 'api/mutation';\n case 'action':\n return 'api/action';\n }\n }\n\n private encodeArgs(args: Record<string, unknown>): Record<string, unknown> {\n // Convex uses a special encoding format for complex types\n // For now, we'll pass through JSON-serializable values\n // Special handling for Convex types like Id, etc.\n return this.convertToConvexJson(args) as Record<string, unknown>;\n }\n\n private convertToConvexJson(value: unknown): unknown {\n if (value === null || value === undefined) {\n return value;\n }\n\n if (Array.isArray(value)) {\n return value.map((v) => this.convertToConvexJson(v));\n }\n\n if (typeof value === 'object') {\n const obj = value as Record<string, unknown>;\n\n // Check for special $type markers (Convex encoded JSON)\n if ('$type' in obj) {\n return obj; // Already encoded\n }\n\n // Regular object\n const converted: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(obj)) {\n converted[k] = this.convertToConvexJson(v);\n }\n return converted;\n }\n\n // Handle BigInt\n if (typeof value === 'bigint') {\n return { $type: 'bigint', value: value.toString() };\n }\n\n return value;\n }\n\n private decodeResult(result: unknown): unknown {\n // Decode Convex-specific types back to JavaScript\n return this.convertFromConvexJson(result);\n }\n\n private convertFromConvexJson(value: unknown): unknown {\n if (value === null || value === undefined) {\n return value;\n }\n\n if (Array.isArray(value)) {\n return value.map((v) => this.convertFromConvexJson(v));\n }\n\n if (typeof value === 'object') {\n const obj = value as Record<string, unknown>;\n\n // Check for special $type markers\n if ('$type' in obj) {\n switch (obj.$type) {\n case 'bigint':\n return BigInt(obj.value as string);\n case 'bytes':\n return new Uint8Array(obj.value as number[]);\n default:\n return obj;\n }\n }\n\n // Regular object\n const result: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(obj)) {\n result[k] = this.convertFromConvexJson(v);\n }\n return result;\n }\n\n return value;\n }\n}\n"],"mappings":"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","names":["parts"]}
|