convex-devtools 1.0.1 → 1.1.1
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 +20 -4
- package/dist/cli/{chunk-6H4WSVWY.js → chunk-7LFMAA6L.js} +93 -8
- package/dist/cli/chunk-7LFMAA6L.js.map +1 -0
- package/dist/cli/chunk-V3TQXD75.js +556 -0
- package/dist/cli/chunk-V3TQXD75.js.map +1 -0
- package/dist/cli/index.js +4 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/server/index.js +1 -1
- package/dist/cli/server/schema-watcher.d.ts +4 -1
- package/dist/cli/server/schema-watcher.js +1 -1
- package/dist/ui/assets/index-CfJtWlh3.css +1 -0
- package/dist/ui/assets/index-CmJMX4m3.js +62 -0
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/dist/cli/chunk-6H4WSVWY.js.map +0 -1
- package/dist/cli/chunk-7RO6B2AJ.js +0 -205
- package/dist/cli/chunk-7RO6B2AJ.js.map +0 -1
- package/dist/ui/assets/index-CoKLkVGV.css +0 -1
- package/dist/ui/assets/index-DyHgwxFr.js +0 -56
package/README.md
CHANGED
|
@@ -6,18 +6,21 @@
|
|
|
6
6
|
<img src="https://img.shields.io/npm/dt/convex-devtools" alt="downloads">
|
|
7
7
|
</p>
|
|
8
8
|
|
|
9
|
-
A standalone development tool for testing Convex
|
|
9
|
+
A standalone development tool for testing Convex functions and exploring data with identity mocking, request saving, schema discovery, and a schema‑driven Data Explorer.
|
|
10
10
|
|
|
11
|
-
> 💡 **Note**: This tool is designed for **development and testing**. A deploy key is
|
|
11
|
+
> 💡 **Note**: This tool is designed for **development and testing**. A deploy key is required for Data Explorer queries.
|
|
12
12
|
|
|
13
13
|
## Features
|
|
14
14
|
|
|
15
15
|
- 🔍 **Function Explorer** - Browse all your Convex queries, mutations, and actions in a tree view
|
|
16
16
|
- 🎭 **Identity Mocking** - Test functions as different users with custom roles and claims
|
|
17
17
|
- 💾 **Request Collections** - Save and organize requests like Postman
|
|
18
|
-
- 📜 **History** - View and replay previous function calls
|
|
18
|
+
- 📜 **History** - View and replay previous function calls and schema queries
|
|
19
19
|
- 🔄 **Auto-reload** - Schema updates automatically when your Convex files change
|
|
20
20
|
- 📤 **Import/Export** - Share collections with your team
|
|
21
|
+
- 🧭 **Data Explorer** - Schema‑driven table view, column picker, pagination, and JSON view
|
|
22
|
+
- 🧱 **Query Builder** - Build filters/order via UI or switch to raw JSON
|
|
23
|
+
- 🗂️ **Query Tabs** - Recent tabs for functions and schema queries
|
|
21
24
|
|
|
22
25
|
## Quick Start
|
|
23
26
|
|
|
@@ -75,6 +78,19 @@ convex-devtools --no-open
|
|
|
75
78
|
|
|
76
79
|
The tool will automatically open in your browser at `http://localhost:5173`.
|
|
77
80
|
|
|
81
|
+
## Data Explorer (Schema Queries)
|
|
82
|
+
|
|
83
|
+
The Data Explorer lets you run read‑only schema queries against `ctx.db.query()` in your Convex project:
|
|
84
|
+
|
|
85
|
+
- **Builder**: pick table, order, and filters with a UI
|
|
86
|
+
- **JSON**: edit the query payload directly
|
|
87
|
+
- **Table view**: column picker, pagination, and search
|
|
88
|
+
- **JSON view**: raw response with search
|
|
89
|
+
- **Resizable panes**: adjust the Data Explorer/Response split
|
|
90
|
+
- **Recent tabs**: quickly jump between recent schema queries
|
|
91
|
+
|
|
92
|
+
The helper query uses Convex pagination (cursor + page size). The deploy key is required to run these queries.
|
|
93
|
+
|
|
78
94
|
## CLI Options
|
|
79
95
|
|
|
80
96
|
| Option | Description | Default |
|
|
@@ -138,7 +154,7 @@ The export format is a simple JSON structure:
|
|
|
138
154
|
|
|
139
155
|
```json
|
|
140
156
|
{
|
|
141
|
-
"version": "1.
|
|
157
|
+
"version": "1.1.1",
|
|
142
158
|
"exportedAt": "2025-01-29T10:00:00.000Z",
|
|
143
159
|
"collections": [
|
|
144
160
|
{
|
|
@@ -3,6 +3,11 @@ import chokidar from "chokidar";
|
|
|
3
3
|
import { EventEmitter } from "events";
|
|
4
4
|
import fs from "fs";
|
|
5
5
|
import path from "path";
|
|
6
|
+
import {
|
|
7
|
+
Node,
|
|
8
|
+
Project,
|
|
9
|
+
SyntaxKind
|
|
10
|
+
} from "ts-morph";
|
|
6
11
|
var SchemaWatcher = class extends EventEmitter {
|
|
7
12
|
projectDir;
|
|
8
13
|
watcher = null;
|
|
@@ -56,7 +61,7 @@ var SchemaWatcher = class extends EventEmitter {
|
|
|
56
61
|
this.schemaInfo = {
|
|
57
62
|
modules,
|
|
58
63
|
tables,
|
|
59
|
-
lastUpdated: /* @__PURE__ */ new Date()
|
|
64
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
60
65
|
};
|
|
61
66
|
const funcCount = this.countFunctions(modules);
|
|
62
67
|
console.log(
|
|
@@ -333,13 +338,48 @@ var SchemaWatcher = class extends EventEmitter {
|
|
|
333
338
|
}
|
|
334
339
|
try {
|
|
335
340
|
const content = fs.readFileSync(schemaPath, "utf-8");
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
341
|
+
const project = new Project({ useInMemoryFileSystem: true });
|
|
342
|
+
const sourceFile = project.createSourceFile(
|
|
343
|
+
"schema.ts",
|
|
344
|
+
content,
|
|
345
|
+
{ overwrite: true }
|
|
346
|
+
);
|
|
347
|
+
const defineSchemaCall = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).find(
|
|
348
|
+
(call) => call.getExpression().getText() === "defineSchema"
|
|
349
|
+
);
|
|
350
|
+
if (!defineSchemaCall) {
|
|
351
|
+
return tables;
|
|
352
|
+
}
|
|
353
|
+
const schemaArg = defineSchemaCall.getArguments()[0];
|
|
354
|
+
if (!schemaArg || !Node.isObjectLiteralExpression(schemaArg)) {
|
|
355
|
+
return tables;
|
|
356
|
+
}
|
|
357
|
+
for (const prop of schemaArg.getProperties()) {
|
|
358
|
+
if (!Node.isPropertyAssignment(prop)) continue;
|
|
359
|
+
const tableName = this.normalizePropertyName(prop.getName());
|
|
360
|
+
const initializer = prop.getInitializer();
|
|
361
|
+
if (!initializer) continue;
|
|
362
|
+
const defineTableCall = this.findDefineTableCall(initializer);
|
|
363
|
+
if (!defineTableCall) continue;
|
|
364
|
+
const fieldsArg = defineTableCall.getArguments()[0];
|
|
365
|
+
const fields = [];
|
|
366
|
+
if (fieldsArg && Node.isObjectLiteralExpression(fieldsArg)) {
|
|
367
|
+
for (const fieldProp of fieldsArg.getProperties()) {
|
|
368
|
+
if (!Node.isPropertyAssignment(fieldProp)) continue;
|
|
369
|
+
const fieldName = this.normalizePropertyName(fieldProp.getName());
|
|
370
|
+
const fieldInit = fieldProp.getInitializer();
|
|
371
|
+
if (!fieldInit) continue;
|
|
372
|
+
const parsedField = this.parseFieldInfo(fieldInit);
|
|
373
|
+
fields.push({
|
|
374
|
+
name: fieldName,
|
|
375
|
+
type: parsedField.type,
|
|
376
|
+
optional: parsedField.optional
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
}
|
|
339
380
|
tables.push({
|
|
340
|
-
name:
|
|
341
|
-
fields
|
|
342
|
-
// Could parse fields but keeping simple for now
|
|
381
|
+
name: tableName,
|
|
382
|
+
fields
|
|
343
383
|
});
|
|
344
384
|
}
|
|
345
385
|
} catch (error) {
|
|
@@ -347,9 +387,54 @@ var SchemaWatcher = class extends EventEmitter {
|
|
|
347
387
|
}
|
|
348
388
|
return tables;
|
|
349
389
|
}
|
|
390
|
+
findDefineTableCall(expr) {
|
|
391
|
+
if (Node.isCallExpression(expr)) {
|
|
392
|
+
const callee = expr.getExpression();
|
|
393
|
+
if (Node.isIdentifier(callee) && callee.getText() === "defineTable") {
|
|
394
|
+
return expr;
|
|
395
|
+
}
|
|
396
|
+
if (Node.isPropertyAccessExpression(callee)) {
|
|
397
|
+
const inner = callee.getExpression();
|
|
398
|
+
if (Node.isCallExpression(inner)) {
|
|
399
|
+
return this.findDefineTableCall(inner);
|
|
400
|
+
}
|
|
401
|
+
if (Node.isPropertyAccessExpression(inner)) {
|
|
402
|
+
return this.findDefineTableCall(inner.getExpression());
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (Node.isPropertyAccessExpression(expr)) {
|
|
407
|
+
return this.findDefineTableCall(expr.getExpression());
|
|
408
|
+
}
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
parseFieldInfo(expr) {
|
|
412
|
+
let optional = false;
|
|
413
|
+
let typeExpr = expr;
|
|
414
|
+
if (Node.isCallExpression(expr)) {
|
|
415
|
+
const calleeText = expr.getExpression().getText();
|
|
416
|
+
if (calleeText === "v.optional") {
|
|
417
|
+
optional = true;
|
|
418
|
+
const inner = expr.getArguments()[0];
|
|
419
|
+
if (inner && Node.isExpression(inner)) {
|
|
420
|
+
typeExpr = inner;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return {
|
|
425
|
+
type: typeExpr?.getText() ?? "unknown",
|
|
426
|
+
optional
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
normalizePropertyName(name) {
|
|
430
|
+
if (name.startsWith("'") && name.endsWith("'") || name.startsWith('"') && name.endsWith('"')) {
|
|
431
|
+
return name.slice(1, -1);
|
|
432
|
+
}
|
|
433
|
+
return name;
|
|
434
|
+
}
|
|
350
435
|
};
|
|
351
436
|
|
|
352
437
|
export {
|
|
353
438
|
SchemaWatcher
|
|
354
439
|
};
|
|
355
|
-
//# sourceMappingURL=chunk-
|
|
440
|
+
//# sourceMappingURL=chunk-7LFMAA6L.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/server/schema-watcher.ts"],"sourcesContent":["import chokidar from 'chokidar';\nimport { EventEmitter } from 'events';\nimport fs from 'fs';\nimport path from 'path';\nimport {\n CallExpression,\n Expression,\n Node,\n Project,\n SyntaxKind,\n} from 'ts-morph';\n\nexport interface FunctionInfo {\n name: string;\n path: string; // Full path like \"products/products:list\"\n type: 'query' | 'mutation' | 'action';\n args: ArgInfo[];\n returns?: string;\n}\n\nexport interface ArgInfo {\n name: string;\n type: string;\n optional: boolean;\n description?: string;\n enumValues?: string[];\n}\n\nexport interface ModuleInfo {\n name: string;\n path: string;\n functions: FunctionInfo[];\n children: ModuleInfo[];\n}\n\nexport interface SchemaInfo {\n modules: ModuleInfo[];\n tables: TableInfo[];\n lastUpdated: string;\n}\n\nexport interface TableInfo {\n name: string;\n fields: FieldInfo[];\n}\n\nexport interface FieldInfo {\n name: string;\n type: string;\n optional: boolean;\n}\n\nexport class SchemaWatcher extends EventEmitter {\n private projectDir: string;\n private watcher: chokidar.FSWatcher | null = null;\n private schemaInfo: SchemaInfo | null = null;\n\n constructor(projectDir: string) {\n super();\n this.projectDir = projectDir;\n }\n\n async start(): Promise<void> {\n // Initial parse\n await this.parseSchema();\n\n // Watch for changes\n const convexDir = path.join(this.projectDir, 'convex');\n\n this.watcher = chokidar.watch(convexDir, {\n persistent: true,\n ignoreInitial: true,\n ignored: [\n '**/node_modules/**',\n '**/_generated/**',\n '**/test.setup.ts',\n '**/*.test.ts',\n ],\n });\n\n this.watcher.on('change', async (filePath) => {\n if (filePath.endsWith('.ts') && !filePath.includes('_generated')) {\n console.log(`[SchemaWatcher] File changed: ${filePath}`);\n await this.parseSchema();\n this.emit('schema-updated', this.schemaInfo);\n }\n });\n\n this.watcher.on('add', async (filePath) => {\n if (filePath.endsWith('.ts') && !filePath.includes('_generated')) {\n console.log(`[SchemaWatcher] File added: ${filePath}`);\n await this.parseSchema();\n this.emit('schema-updated', this.schemaInfo);\n }\n });\n }\n\n stop(): void {\n if (this.watcher) {\n this.watcher.close();\n this.watcher = null;\n }\n }\n\n getSchema(): SchemaInfo | null {\n return this.schemaInfo;\n }\n\n private async parseSchema(): Promise<void> {\n try {\n const convexDir = path.join(this.projectDir, 'convex');\n const modules = await this.parseConvexDirectory(convexDir);\n const tables = await this.parseSchemaFile(convexDir);\n\n this.schemaInfo = {\n modules,\n tables,\n lastUpdated: new Date().toISOString(),\n };\n\n const funcCount = this.countFunctions(modules);\n console.log(\n `[SchemaWatcher] Parsed ${funcCount} functions from ${modules.length} modules`\n );\n } catch (error) {\n console.error('[SchemaWatcher] Error parsing schema:', error);\n }\n }\n\n private countFunctions(modules: ModuleInfo[]): number {\n let count = 0;\n for (const mod of modules) {\n count += mod.functions.length;\n count += this.countFunctions(mod.children);\n }\n return count;\n }\n\n private async parseConvexDirectory(convexDir: string): Promise<ModuleInfo[]> {\n const modules: ModuleInfo[] = [];\n\n // Read convex directory structure\n const entries = fs.readdirSync(convexDir, { withFileTypes: true });\n\n for (const entry of entries) {\n // Skip hidden, generated, and test files\n if (\n entry.name.startsWith('.') ||\n entry.name.startsWith('_') ||\n entry.name === 'node_modules' ||\n entry.name.endsWith('.test.ts') ||\n entry.name === 'test.setup.ts'\n ) {\n continue;\n }\n\n if (entry.isDirectory()) {\n // Parse subdirectory as module\n const subModule = await this.parseSubdirectory(\n path.join(convexDir, entry.name),\n entry.name\n );\n if (subModule.functions.length > 0 || subModule.children.length > 0) {\n modules.push(subModule);\n }\n } else if (entry.isFile() && entry.name.endsWith('.ts')) {\n // Parse root-level file\n const filePath = path.join(convexDir, entry.name);\n const moduleName = entry.name.replace('.ts', '');\n const functions = await this.parseFile(filePath, moduleName);\n\n if (functions.length > 0) {\n modules.push({\n name: moduleName,\n path: moduleName,\n functions,\n children: [],\n });\n }\n }\n }\n\n return modules;\n }\n\n private async parseSubdirectory(\n dirPath: string,\n parentPath: string\n ): Promise<ModuleInfo> {\n const module: ModuleInfo = {\n name: path.basename(dirPath),\n path: parentPath,\n functions: [],\n children: [],\n };\n\n const entries = fs.readdirSync(dirPath, { withFileTypes: true });\n\n for (const entry of entries) {\n // Skip tests directory and test files\n if (\n entry.name === 'tests' ||\n entry.name.endsWith('.test.ts') ||\n entry.name.startsWith('.')\n ) {\n continue;\n }\n\n if (entry.isDirectory()) {\n const subModule = await this.parseSubdirectory(\n path.join(dirPath, entry.name),\n `${parentPath}/${entry.name}`\n );\n if (subModule.functions.length > 0 || subModule.children.length > 0) {\n module.children.push(subModule);\n }\n } else if (entry.isFile() && entry.name.endsWith('.ts')) {\n const filePath = path.join(dirPath, entry.name);\n const moduleName = entry.name.replace('.ts', '');\n const modulePath = `${parentPath}/${moduleName}`;\n const functions = await this.parseFile(filePath, modulePath);\n\n // Create a child module for each file (group by file)\n if (functions.length > 0) {\n module.children.push({\n name: moduleName,\n path: modulePath,\n functions,\n children: [],\n });\n }\n }\n }\n\n return module;\n }\n\n private async parseFile(\n filePath: string,\n modulePath: string\n ): Promise<FunctionInfo[]> {\n const functions: FunctionInfo[] = [];\n\n try {\n const content = fs.readFileSync(filePath, 'utf-8');\n\n // Parse exported functions using regex\n // Match patterns like: export const functionName = query({ or mutation({ or action({\n const exportPattern =\n /export\\s+const\\s+(\\w+)\\s*=\\s*(query|mutation|action|internalQuery|internalMutation|internalAction)\\s*\\(\\s*\\{/g;\n\n let match;\n while ((match = exportPattern.exec(content)) !== null) {\n const [, funcName, funcType] = match;\n\n // Normalize internal functions to their base type\n let normalizedType = funcType as 'query' | 'mutation' | 'action';\n if (funcType.startsWith('internal')) {\n normalizedType = funcType.replace('internal', '').toLowerCase() as\n | 'query'\n | 'mutation'\n | 'action';\n }\n\n // Extract JSDoc comment above the function\n const jsdocComment = this.extractJSDocAbove(content, match.index);\n\n // Extract args from the function definition\n const args = this.extractArgsFromPosition(\n content,\n match.index,\n jsdocComment\n );\n\n functions.push({\n name: funcName,\n path: `${modulePath}:${funcName}`,\n type: normalizedType,\n args,\n });\n }\n } catch (error) {\n console.error(`[SchemaWatcher] Error parsing file ${filePath}:`, error);\n }\n\n return functions;\n }\n\n private extractJSDocAbove(content: string, position: number): string {\n // Look backwards from position to find JSDoc comment\n // Allow some whitespace and newlines between the JSDoc and the export\n const beforePosition = content.slice(0, position);\n // Match JSDoc that ends with */ followed by optional whitespace before the export\n const jsdocMatch = beforePosition.match(/\\/\\*\\*([\\s\\S]*?)\\*\\/\\s*$/);\n if (jsdocMatch) {\n return jsdocMatch[1];\n }\n\n // Also try to find JSDoc within the last 500 chars (in case there's space between)\n const last500 = beforePosition.slice(-500);\n const jsdocMatch2 = last500.match(/\\/\\*\\*([\\s\\S]*?)\\*\\//);\n return jsdocMatch2 ? jsdocMatch2[1] : '';\n }\n\n private parseJSDocParams(\n jsdoc: string\n ): Map<string, { description: string; enumValues?: string[] }> {\n const params = new Map<\n string,\n { description: string; enumValues?: string[] }\n >();\n\n // Match @param patterns like: @param sortBy - Sort order: 'newest', 'oldest'\n const paramPattern = /@param\\s+(\\w+)\\s*-?\\s*([^@]*)/g;\n let match;\n while ((match = paramPattern.exec(jsdoc)) !== null) {\n const [, paramName, description] = match;\n const trimmedDesc = description.trim();\n\n // Extract enum values from description (quoted strings like 'value1', 'value2')\n const enumMatches = trimmedDesc.match(/'([^']+)'/g);\n const enumValues = enumMatches\n ? enumMatches.map((e) => e.replace(/'/g, ''))\n : undefined;\n\n params.set(paramName, {\n description: trimmedDesc,\n enumValues:\n enumValues && enumValues.length > 0 ? enumValues : undefined,\n });\n }\n\n return params;\n }\n\n private extractArgsFromPosition(\n content: string,\n startIndex: number,\n jsdocComment: string = ''\n ): ArgInfo[] {\n const args: ArgInfo[] = [];\n const jsdocParams = this.parseJSDocParams(jsdocComment);\n\n // Limit parsing to the current function block to avoid false positives\n const functionBlock = this.extractFunctionBlock(content, startIndex);\n\n // Find the args: { ... } section\n const argsMatch = functionBlock.match(/args:\\s*\\{([^}]*)\\}/s);\n\n if (argsMatch) {\n const argsContent = argsMatch[1];\n\n // Parse individual args\n // Matches patterns like: argName: v.string(), argName: v.optional(v.id('users'))\n const argPattern = /(\\w+):\\s*(v\\.optional\\()?v\\.(\\w+)/g;\n\n let argMatch;\n while ((argMatch = argPattern.exec(argsContent)) !== null) {\n const [, argName, isOptional, argType] = argMatch;\n const jsdocInfo = jsdocParams.get(argName);\n\n args.push({\n name: argName,\n type: argType,\n optional: !!isOptional,\n description: jsdocInfo?.description,\n enumValues: jsdocInfo?.enumValues,\n });\n }\n }\n\n // Check for paginationOpts (built-in Convex pagination)\n const hasPaginationOpts = functionBlock.match(/paginationOptsValidator/s);\n if (hasPaginationOpts) {\n // Add paginationOpts as a synthetic argument\n args.push({\n name: 'paginationOpts',\n type: 'PaginationOptions',\n optional: false,\n description: 'Pagination options with cursor and numItems',\n });\n }\n\n return args;\n }\n\n private extractFunctionBlock(content: string, startIndex: number): string {\n const afterStart = content.slice(startIndex);\n const openIndex = afterStart.indexOf('{');\n if (openIndex === -1) {\n return afterStart;\n }\n\n let depth = 0;\n let inSingle = false;\n let inDouble = false;\n let inTemplate = false;\n let inLineComment = false;\n let inBlockComment = false;\n let escape = false;\n\n for (let i = openIndex; i < afterStart.length; i += 1) {\n const char = afterStart[i];\n const next = afterStart[i + 1];\n\n if (inLineComment) {\n if (char === '\\n') {\n inLineComment = false;\n }\n continue;\n }\n\n if (inBlockComment) {\n if (char === '*' && next === '/') {\n inBlockComment = false;\n i += 1;\n }\n continue;\n }\n\n if (inSingle) {\n if (escape) {\n escape = false;\n } else if (char === '\\\\') {\n escape = true;\n } else if (char === \"'\") {\n inSingle = false;\n }\n continue;\n }\n\n if (inDouble) {\n if (escape) {\n escape = false;\n } else if (char === '\\\\') {\n escape = true;\n } else if (char === '\"') {\n inDouble = false;\n }\n continue;\n }\n\n if (inTemplate) {\n if (escape) {\n escape = false;\n } else if (char === '\\\\') {\n escape = true;\n } else if (char === '`') {\n inTemplate = false;\n }\n continue;\n }\n\n if (char === '/' && next === '/') {\n inLineComment = true;\n i += 1;\n continue;\n }\n\n if (char === '/' && next === '*') {\n inBlockComment = true;\n i += 1;\n continue;\n }\n\n if (char === \"'\") {\n inSingle = true;\n escape = false;\n continue;\n }\n\n if (char === '\"') {\n inDouble = true;\n escape = false;\n continue;\n }\n\n if (char === '`') {\n inTemplate = true;\n escape = false;\n continue;\n }\n\n if (char === '{') {\n depth += 1;\n } else if (char === '}') {\n depth -= 1;\n if (depth === 0) {\n return afterStart.slice(openIndex, i + 1);\n }\n }\n }\n\n return afterStart;\n }\n\n private async parseSchemaFile(convexDir: string): Promise<TableInfo[]> {\n const tables: TableInfo[] = [];\n const schemaPath = path.join(convexDir, 'schema.ts');\n\n if (!fs.existsSync(schemaPath)) {\n return tables;\n }\n\n try {\n const content = fs.readFileSync(schemaPath, 'utf-8');\n const project = new Project({ useInMemoryFileSystem: true });\n const sourceFile = project.createSourceFile(\n 'schema.ts',\n content,\n { overwrite: true }\n );\n\n const defineSchemaCall = sourceFile\n .getDescendantsOfKind(SyntaxKind.CallExpression)\n .find(\n (call) => call.getExpression().getText() === 'defineSchema'\n );\n\n if (!defineSchemaCall) {\n return tables;\n }\n\n const schemaArg = defineSchemaCall.getArguments()[0];\n if (!schemaArg || !Node.isObjectLiteralExpression(schemaArg)) {\n return tables;\n }\n\n for (const prop of schemaArg.getProperties()) {\n if (!Node.isPropertyAssignment(prop)) continue;\n\n const tableName = this.normalizePropertyName(prop.getName());\n const initializer = prop.getInitializer();\n if (!initializer) continue;\n\n const defineTableCall = this.findDefineTableCall(initializer);\n if (!defineTableCall) continue;\n\n const fieldsArg = defineTableCall.getArguments()[0];\n const fields: FieldInfo[] = [];\n\n if (fieldsArg && Node.isObjectLiteralExpression(fieldsArg)) {\n for (const fieldProp of fieldsArg.getProperties()) {\n if (!Node.isPropertyAssignment(fieldProp)) continue;\n const fieldName = this.normalizePropertyName(fieldProp.getName());\n const fieldInit = fieldProp.getInitializer();\n if (!fieldInit) continue;\n\n const parsedField = this.parseFieldInfo(fieldInit);\n fields.push({\n name: fieldName,\n type: parsedField.type,\n optional: parsedField.optional,\n });\n }\n }\n\n tables.push({\n name: tableName,\n fields,\n });\n }\n } catch (error) {\n console.error('[SchemaWatcher] Error parsing schema file:', error);\n }\n\n return tables;\n }\n\n private findDefineTableCall(\n expr: Expression\n ): CallExpression | null {\n if (Node.isCallExpression(expr)) {\n const callee = expr.getExpression();\n if (Node.isIdentifier(callee) && callee.getText() === 'defineTable') {\n return expr;\n }\n if (Node.isPropertyAccessExpression(callee)) {\n const inner = callee.getExpression();\n if (Node.isCallExpression(inner)) {\n return this.findDefineTableCall(inner);\n }\n if (Node.isPropertyAccessExpression(inner)) {\n return this.findDefineTableCall(inner.getExpression());\n }\n }\n }\n\n if (Node.isPropertyAccessExpression(expr)) {\n return this.findDefineTableCall(expr.getExpression());\n }\n\n return null;\n }\n\n private parseFieldInfo(\n expr: Expression\n ): { type: string; optional: boolean } {\n let optional = false;\n let typeExpr: Expression | undefined = expr;\n\n if (Node.isCallExpression(expr)) {\n const calleeText = expr.getExpression().getText();\n if (calleeText === 'v.optional') {\n optional = true;\n const inner = expr.getArguments()[0];\n if (inner && Node.isExpression(inner)) {\n typeExpr = inner;\n }\n }\n }\n\n return {\n type: typeExpr?.getText() ?? 'unknown',\n optional,\n };\n }\n\n private normalizePropertyName(name: string): string {\n if (\n (name.startsWith(\"'\") && name.endsWith(\"'\")) ||\n (name.startsWith('\"') && name.endsWith('\"'))\n ) {\n return name.slice(1, -1);\n }\n return name;\n }\n}\n"],"mappings":"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","names":[]}
|