@brainfish-ai/devdoc 0.1.48 → 0.1.50
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/dist/cli/commands/deploy.js +83 -10
- package/dist/cli/commands/domain.js +134 -59
- package/package.json +1 -1
- package/renderer/app/[...slug]/client.js +17 -0
- package/renderer/app/[...slug]/page.js +125 -0
- package/renderer/app/api/assets/[...path]/route.js +23 -4
- package/renderer/app/api/chat/route.js +188 -25
- package/renderer/app/api/collections/route.js +105 -3
- package/renderer/app/api/deploy/route.js +7 -3
- package/renderer/app/api/domains/add/route.js +118 -40
- package/renderer/app/api/domains/remove/route.js +16 -3
- package/renderer/app/api/domains/verify/route.js +82 -17
- package/renderer/app/api/schema/route.js +12 -11
- package/renderer/app/api/suggestions/route.js +98 -10
- package/renderer/app/globals.css +33 -0
- package/renderer/app/layout.js +83 -8
- package/renderer/components/docs/mdx/cards.js +16 -45
- package/renderer/components/docs/mdx/file-tree.js +102 -0
- package/renderer/components/docs/mdx/index.js +7 -0
- package/renderer/components/docs-header.js +11 -11
- package/renderer/components/docs-viewer/agent/agent-chat.js +75 -11
- package/renderer/components/docs-viewer/agent/messages/assistant-message.js +67 -3
- package/renderer/components/docs-viewer/agent/messages/tool-call-display.js +49 -4
- package/renderer/components/docs-viewer/content/content-router.js +1 -1
- package/renderer/components/docs-viewer/content/doc-page.js +36 -28
- package/renderer/components/docs-viewer/index.js +223 -58
- package/renderer/components/docs-viewer/playground/graphql-playground.js +131 -33
- package/renderer/components/docs-viewer/shared/method-badge.js +11 -2
- package/renderer/components/docs-viewer/sidebar/collection-tree.js +44 -6
- package/renderer/components/docs-viewer/sidebar/index.js +2 -1
- package/renderer/components/docs-viewer/sidebar/right-sidebar.js +3 -1
- package/renderer/components/docs-viewer/sidebar/sidebar-item.js +5 -7
- package/renderer/hooks/use-route-state.js +44 -56
- package/renderer/lib/api-docs/agent/indexer.js +73 -12
- package/renderer/lib/api-docs/agent/use-suggestions.js +26 -16
- package/renderer/lib/api-docs/code-editor/mode-context.js +16 -18
- package/renderer/lib/api-docs/parsers/openapi/transformer.js +8 -1
- package/renderer/lib/cache/purge.js +98 -0
- package/renderer/lib/docs/config/domain-schema.js +23 -1
- package/renderer/lib/docs/config/index.js +1 -1
- package/renderer/lib/docs-link-utils.js +146 -0
- package/renderer/lib/docs-navigation-context.js +3 -2
- package/renderer/lib/docs-navigation.js +50 -41
- package/renderer/lib/rate-limit.js +203 -0
- package/renderer/lib/storage/blob.js +4 -2
- package/renderer/lib/vercel/domains.js +275 -0
|
@@ -4,7 +4,25 @@ import { z } from 'zod';
|
|
|
4
4
|
// Use Node.js runtime for better compatibility
|
|
5
5
|
export const runtime = 'nodejs';
|
|
6
6
|
function buildSystemPrompt(endpoints, docs, changelog, currentEndpointId, apiSummary) {
|
|
7
|
-
|
|
7
|
+
// Separate GraphQL and REST endpoints for better organization
|
|
8
|
+
const graphqlEndpoints = endpoints.filter((ep)=>ep.type === 'graphql');
|
|
9
|
+
const restEndpoints = endpoints.filter((ep)=>ep.type === 'rest');
|
|
10
|
+
// Format REST endpoints
|
|
11
|
+
const restEndpointsList = restEndpoints.map((ep, i)=>{
|
|
12
|
+
const params = ep.parameters.length > 0 ? ` (params: ${ep.parameters.join(', ')})` : '';
|
|
13
|
+
return `${i + 1}. [${ep.method}] ${ep.name} - ${ep.path}${params}${ep.description ? ` - ${ep.description.slice(0, 100)}` : ''} (id: ${ep.id})`;
|
|
14
|
+
}).join('\n');
|
|
15
|
+
// Format GraphQL endpoints with operation type
|
|
16
|
+
const graphqlEndpointsList = graphqlEndpoints.map((ep, i)=>{
|
|
17
|
+
const opType = ep.operationType?.toUpperCase() || 'GRAPHQL';
|
|
18
|
+
const variables = ep.parameters.length > 0 ? ` (variables: ${ep.parameters.join(', ')})` : '';
|
|
19
|
+
return `${i + 1}. [${opType}] ${ep.name}${variables}${ep.description ? ` - ${ep.description.slice(0, 100)}` : ''} (id: ${ep.id})`;
|
|
20
|
+
}).join('\n');
|
|
21
|
+
// Combine endpoint lists
|
|
22
|
+
const endpointsList = [
|
|
23
|
+
restEndpointsList && `### REST Endpoints\n${restEndpointsList}`,
|
|
24
|
+
graphqlEndpointsList && `### GraphQL Operations\n${graphqlEndpointsList}`
|
|
25
|
+
].filter(Boolean).join('\n\n') || restEndpoints.concat(graphqlEndpoints).map((ep, i)=>{
|
|
8
26
|
const params = ep.parameters.length > 0 ? ` (params: ${ep.parameters.join(', ')})` : '';
|
|
9
27
|
return `${i + 1}. [${ep.method}] ${ep.name} - ${ep.path}${params}${ep.description ? ` - ${ep.description.slice(0, 100)}` : ''} (id: ${ep.id})`;
|
|
10
28
|
}).join('\n');
|
|
@@ -166,16 +184,24 @@ Before answering ANY question about this documentation, you MUST use search_docs
|
|
|
166
184
|
- Use when user asks about an endpoint, wants documentation, or asks "what is", "how to use"
|
|
167
185
|
- Call this AFTER navigate_to_endpoint to show the documentation view
|
|
168
186
|
|
|
169
|
-
**prefill_parameters**: Fill in values
|
|
187
|
+
**prefill_parameters**: Fill in values for REST endpoints
|
|
170
188
|
- Use when user gives specific values like "user_id is 123", "set the name to John"
|
|
171
189
|
- Can fill params (query), headers, body, or pathVariables (URL path params)
|
|
172
190
|
- **IMPORTANT**: For URL path parameters like {id}, {document_id}, <<id>>, use pathVariables NOT params
|
|
173
191
|
- Example: If URL is /documents/{id}, use pathVariables: [{key: "id", value: "123"}]
|
|
174
192
|
- **IMPORTANT**: Call switch_to_api_client BEFORE prefilling so user can see the API Client
|
|
193
|
+
- **DO NOT use for GraphQL** - use prefill_graphql_variables instead
|
|
194
|
+
|
|
195
|
+
**prefill_graphql_variables**: Fill in GraphQL variables
|
|
196
|
+
- Use when user provides specific values for GraphQL query variables
|
|
197
|
+
- Pass a JSON object with the variable values, e.g. { "code": "EU" }
|
|
198
|
+
- **IMPORTANT**: Navigate to the GraphQL operation FIRST, then prefill variables
|
|
199
|
+
- Works in Docs mode (GraphQL Playground)
|
|
175
200
|
|
|
176
201
|
**switch_to_api_client**: Switch to API Client tab
|
|
177
|
-
- Use when helping users test endpoints, send requests, or prefill data
|
|
202
|
+
- Use when helping users test REST endpoints, send requests, or prefill data
|
|
178
203
|
- Call this FIRST before prefill_parameters so user can see the changes
|
|
204
|
+
- **DO NOT use for GraphQL endpoints** - GraphQL uses the GraphQL Playground in Docs mode
|
|
179
205
|
|
|
180
206
|
**check_request_validity**: Validate if request is ready to send
|
|
181
207
|
- Use BEFORE sending to check what's missing
|
|
@@ -187,11 +213,17 @@ Before answering ANY question about this documentation, you MUST use search_docs
|
|
|
187
213
|
- ALWAYS check_request_validity first if you haven't already
|
|
188
214
|
- Returns full response with status, body, headers, timing
|
|
189
215
|
|
|
190
|
-
**get_current_request**: Get the user's current request configuration from the API Client
|
|
216
|
+
**get_current_request**: Get the user's current REST request configuration from the API Client
|
|
191
217
|
- Use when user says "use my request", "use current payload", "based on my config", "copy from playground"
|
|
192
218
|
- Returns: method, URL, query params, headers, body - exactly as configured in the playground
|
|
193
219
|
- ALWAYS use this when generating code examples if user has configured a request
|
|
194
220
|
|
|
221
|
+
**get_current_graphql_state**: Get the current GraphQL Playground state
|
|
222
|
+
- Returns: current query, variables, response (if executed), loading state, errors
|
|
223
|
+
- **ALWAYS call this** before answering questions about the GraphQL response or current state
|
|
224
|
+
- Use when user asks "what did the query return", "what's in the response", "show me the result"
|
|
225
|
+
- Use to check if a query has been executed and what data it returned
|
|
226
|
+
|
|
195
227
|
**list_notes**: List existing files in the Notes workspace
|
|
196
228
|
- **ALWAYS call this BEFORE creating a new file** to check for duplicates
|
|
197
229
|
- If a similar file exists (same topic, endpoint, or type), UPDATE it instead of creating new
|
|
@@ -231,7 +263,18 @@ Before answering ANY question about this documentation, you MUST use search_docs
|
|
|
231
263
|
- **NEVER assume navigation**: If user asks "fill the params" or "prefill", do it on the CURRENT endpoint - don't navigate elsewhere.
|
|
232
264
|
- **Use tools to verify**: When answering questions, USE search_docs or search_endpoints to get accurate information - don't make up answers.
|
|
233
265
|
|
|
234
|
-
##
|
|
266
|
+
## GraphQL Endpoints (IMPORTANT)
|
|
267
|
+
For GraphQL endpoints (type: "graphql", method: "QUERY", "MUTATION", "SUBSCRIPTION"):
|
|
268
|
+
- The GraphQL Playground is shown in **Docs mode** (NOT API Client mode)
|
|
269
|
+
- When user wants to test a GraphQL query, use **navigate** to go to the operation
|
|
270
|
+
- **DO NOT call switch_to_api_client** for GraphQL endpoints
|
|
271
|
+
- The GraphQL Playground will automatically show the query when you navigate to the endpoint
|
|
272
|
+
- **To prefill variables**: Use **prefill_graphql_variables** with the variable values
|
|
273
|
+
- Example: User says "get continent EU" → navigate to continent query, then prefill_graphql_variables({ "code": "EU" })
|
|
274
|
+
- **To check current state/response**: Use **get_current_graphql_state** to see what query is displayed, what variables are set, and what response was returned (if any)
|
|
275
|
+
- **ALWAYS call get_current_graphql_state** before answering questions about the GraphQL response or helping with query modifications
|
|
276
|
+
|
|
277
|
+
## API Client Assistance (For REST endpoints)
|
|
235
278
|
|
|
236
279
|
When helping users build and send requests, follow this flow:
|
|
237
280
|
|
|
@@ -308,6 +351,53 @@ User: "send it"
|
|
|
308
351
|
4. **Testable**: Include example test cases or curl commands to verify
|
|
309
352
|
5. **Secure**: Never hardcode secrets, use environment variables
|
|
310
353
|
|
|
354
|
+
### IMPORTANT: Keep Examples Focused (NOT Full Applications)
|
|
355
|
+
The sandbox is for **learning and quick integration** - NOT building complete applications.
|
|
356
|
+
|
|
357
|
+
**DO create:**
|
|
358
|
+
- Single-file examples demonstrating ONE API concept (e.g., authentication, creating a resource)
|
|
359
|
+
- Minimal working code that users can copy and adapt
|
|
360
|
+
- Helper functions/classes they can import into their own projects
|
|
361
|
+
- Quick scripts to test and understand API behavior
|
|
362
|
+
|
|
363
|
+
**DON'T create:**
|
|
364
|
+
- Full application structures (no app.py with Flask/FastAPI, no full React apps)
|
|
365
|
+
- Multi-service architectures or microservices
|
|
366
|
+
- Complete CRUD applications with all endpoints
|
|
367
|
+
- Database integrations, frontend UIs, or deployment configs
|
|
368
|
+
- Anything beyond what's needed to demonstrate the API
|
|
369
|
+
|
|
370
|
+
**Example - GOOD (focused):**
|
|
371
|
+
\`\`\`python
|
|
372
|
+
# create-post.py - Create a Facebook Page post
|
|
373
|
+
import requests
|
|
374
|
+
import os
|
|
375
|
+
|
|
376
|
+
def create_page_post(page_id: str, message: str, access_token: str) -> dict:
|
|
377
|
+
"""Create a post on a Facebook Page."""
|
|
378
|
+
response = requests.post(
|
|
379
|
+
f"https://graph.facebook.com/v21.0/{page_id}/feed",
|
|
380
|
+
data={"message": message, "access_token": access_token}
|
|
381
|
+
)
|
|
382
|
+
response.raise_for_status()
|
|
383
|
+
return response.json()
|
|
384
|
+
|
|
385
|
+
# Usage
|
|
386
|
+
if __name__ == "__main__":
|
|
387
|
+
result = create_page_post("your-page-id", "Hello World!", os.getenv("PAGE_TOKEN"))
|
|
388
|
+
print(f"Post ID: {result['id']}")
|
|
389
|
+
\`\`\`
|
|
390
|
+
|
|
391
|
+
**Example - BAD (too much):**
|
|
392
|
+
\`\`\`python
|
|
393
|
+
# DON'T: Full Flask app with routes, templates, database, etc.
|
|
394
|
+
from flask import Flask, render_template
|
|
395
|
+
from sqlalchemy import create_engine
|
|
396
|
+
# ... 200 lines of app code
|
|
397
|
+
\`\`\`
|
|
398
|
+
|
|
399
|
+
Keep it simple: **One concept, one file, under 100 lines.**
|
|
400
|
+
|
|
311
401
|
### Use Mermaid Diagrams (.mmd) for:
|
|
312
402
|
- Authentication/authorization flows → sequenceDiagram
|
|
313
403
|
- Multi-endpoint workflows (e.g., "create order → add items → checkout")
|
|
@@ -381,35 +471,98 @@ class APIClient:
|
|
|
381
471
|
**For workflows, create:**
|
|
382
472
|
1. A Mermaid diagram showing the flow (.mmd)
|
|
383
473
|
2. Implementation code files for each step
|
|
384
|
-
3.
|
|
474
|
+
3. A README.md tying them together
|
|
475
|
+
|
|
476
|
+
## IMPORTANT: Always Create README.md with Code Examples
|
|
477
|
+
|
|
478
|
+
**When creating ANY code file, ALWAYS create a README.md alongside it** to help users understand how to use the code.
|
|
479
|
+
|
|
480
|
+
**README.md template:**
|
|
481
|
+
\`\`\`markdown
|
|
482
|
+
# {Title - What This Does}
|
|
483
|
+
|
|
484
|
+
{One-line description of what this code does}
|
|
485
|
+
|
|
486
|
+
## Quick Start
|
|
487
|
+
|
|
488
|
+
\`\`\`bash
|
|
489
|
+
# Install dependencies
|
|
490
|
+
{pip install X / npm install X / etc.}
|
|
491
|
+
|
|
492
|
+
# Set your credentials
|
|
493
|
+
export API_KEY="your-api-key"
|
|
494
|
+
export OTHER_VAR="value"
|
|
495
|
+
|
|
496
|
+
# Run it
|
|
497
|
+
{python file.py / node file.js / etc.}
|
|
498
|
+
\`\`\`
|
|
499
|
+
|
|
500
|
+
## What's Included
|
|
501
|
+
|
|
502
|
+
- **{filename}**: {Brief description of what the file does}
|
|
503
|
+
|
|
504
|
+
## Configuration
|
|
505
|
+
|
|
506
|
+
| Environment Variable | Description | Required |
|
|
507
|
+
|---------------------|-------------|----------|
|
|
508
|
+
| \`API_KEY\` | Your API key | Yes |
|
|
509
|
+
| \`OTHER_VAR\` | Description | No |
|
|
510
|
+
|
|
511
|
+
## Usage Examples
|
|
512
|
+
|
|
513
|
+
{Show 2-3 common usage patterns with code snippets}
|
|
514
|
+
|
|
515
|
+
## Error Handling
|
|
516
|
+
|
|
517
|
+
{List common errors and how to resolve them}
|
|
518
|
+
\`\`\`
|
|
519
|
+
|
|
520
|
+
**README requirements:**
|
|
521
|
+
- **Always include Quick Start** with copy-paste ready commands
|
|
522
|
+
- **List ALL dependencies** with exact install commands
|
|
523
|
+
- **Show environment variables** in a table format
|
|
524
|
+
- **Provide usage examples** for the most common scenarios
|
|
525
|
+
- Keep it concise - users should be able to start in under 1 minute
|
|
385
526
|
|
|
386
527
|
## Steps (MUST follow this order):
|
|
387
528
|
1. If user mentions "my request" → use get_current_request FIRST
|
|
388
529
|
2. Use get_endpoint_details to get the full schema
|
|
389
530
|
3. **ALWAYS call list_notes** to check for existing similar files
|
|
390
|
-
4. **If similar file exists**: UPDATE
|
|
391
|
-
5. **If NO similar file**:
|
|
392
|
-
|
|
531
|
+
4. **If similar file exists**: UPDATE files in existing folder
|
|
532
|
+
5. **If NO similar file**:
|
|
533
|
+
- **ALWAYS create a folder first** using create_folder (e.g., \`create-post\`)
|
|
534
|
+
- Then create files inside the folder (e.g., \`create-post/create-post.py\`, \`create-post/README.md\`)
|
|
535
|
+
6. **ALWAYS create/update README.md** alongside code files (see README template above)
|
|
393
536
|
|
|
394
537
|
**IMPORTANT**: Use EXACT values from get_current_request in generated code.
|
|
395
538
|
|
|
396
|
-
**
|
|
397
|
-
|
|
398
|
-
|
|
539
|
+
**Folder structure (ALWAYS organize files in folders):**
|
|
540
|
+
\`\`\`
|
|
541
|
+
{action-name}/
|
|
542
|
+
├── {action-name}.{lang} # Main implementation file
|
|
543
|
+
├── README.md # Quick start guide (REQUIRED)
|
|
544
|
+
└── {flow-diagram}.mmd # Optional: visual flow diagram
|
|
545
|
+
\`\`\`
|
|
546
|
+
|
|
547
|
+
**Examples:**
|
|
548
|
+
- \`create-post/create-post.py\` + \`create-post/README.md\`
|
|
549
|
+
- \`auth-flow/auth-implementation.ts\` + \`auth-flow/auth-flow.mmd\` + \`auth-flow/README.md\`
|
|
550
|
+
- \`webhook-handler/webhook-handler.js\` + \`webhook-handler/README.md\`
|
|
399
551
|
|
|
400
552
|
## Concept Explanations → Implementation Guides
|
|
401
553
|
When users ask to "explain" something, create ACTIONABLE content:
|
|
402
|
-
1. Create a
|
|
403
|
-
2. Create
|
|
404
|
-
3.
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
- "
|
|
409
|
-
- "
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
554
|
+
1. Create a folder for the concept (e.g., \`auth-flow/\`)
|
|
555
|
+
2. Create a .mmd diagram showing the flow visually
|
|
556
|
+
3. Create implementation code showing how to do it
|
|
557
|
+
4. Create README.md with quick start instructions
|
|
558
|
+
|
|
559
|
+
**Always pair explanations with working code + README in a folder:**
|
|
560
|
+
- "Explain authentication" → \`auth-flow/auth-flow.mmd\` + \`auth-flow/auth-implementation.py\` + \`auth-flow/README.md\`
|
|
561
|
+
- "How do webhooks work" → \`webhook-handler/webhook-flow.mmd\` + \`webhook-handler/webhook-handler.ts\` + \`webhook-handler/README.md\`
|
|
562
|
+
- "Show the order process" → \`order-flow/order-flow.mmd\` + \`order-flow/create-order.py\` + \`order-flow/README.md\`
|
|
563
|
+
|
|
564
|
+
PREFER: Folder + Diagram + Working Code + README
|
|
565
|
+
AVOID: Long text explanations without code, loose files at root level
|
|
413
566
|
`;
|
|
414
567
|
}
|
|
415
568
|
// Convert raw UIMessages to model messages for the AI SDK
|
|
@@ -629,7 +782,7 @@ export async function POST(req) {
|
|
|
629
782
|
})
|
|
630
783
|
}),
|
|
631
784
|
prefill_parameters: tool({
|
|
632
|
-
description: 'Prefill request parameters, headers, body, or path variables for the current endpoint. Use this when the user provides values they want to use. For URL path parameters like {id} or <<id>>, use pathVariables.',
|
|
785
|
+
description: 'Prefill request parameters, headers, body, or path variables for the current REST endpoint. Use this when the user provides values they want to use. For URL path parameters like {id} or <<id>>, use pathVariables. DO NOT use for GraphQL - use prefill_graphql_variables instead.',
|
|
633
786
|
inputSchema: z.object({
|
|
634
787
|
params: z.array(z.object({
|
|
635
788
|
key: z.string(),
|
|
@@ -646,6 +799,12 @@ export async function POST(req) {
|
|
|
646
799
|
})).optional().describe('URL path variables to prefill (e.g., {id}, <<document_id>>). These replace placeholders in the URL path.')
|
|
647
800
|
})
|
|
648
801
|
}),
|
|
802
|
+
prefill_graphql_variables: tool({
|
|
803
|
+
description: 'Prefill GraphQL query variables in the GraphQL Playground. Use this for GraphQL endpoints when user provides specific values for query variables. Navigate to the GraphQL operation first, then call this to fill in the variables panel.',
|
|
804
|
+
inputSchema: z.object({
|
|
805
|
+
variables: z.record(z.string(), z.unknown()).describe('GraphQL variables as an object, e.g. { "code": "EU", "filter": { "name": { "eq": "Europe" } } }')
|
|
806
|
+
})
|
|
807
|
+
}),
|
|
649
808
|
get_endpoint_details: tool({
|
|
650
809
|
description: 'Get FULL details about a specific endpoint. Returns: parameters (name, description, required), headers, authentication requirements, request body schema (contentType, required fields, example payload), and response schemas with status codes. Use this when users ask about payload structure, required fields, body format, or how to call an endpoint.',
|
|
651
810
|
inputSchema: z.object({
|
|
@@ -656,13 +815,17 @@ export async function POST(req) {
|
|
|
656
815
|
description: 'Get the current request payload from the API Client playground. Returns the actual values the user has configured: HTTP method, URL, query parameters, headers, and request body. Use this when user asks to "use my request", "use current payload", "copy from playground", "based on my configuration", or wants code examples using their exact request setup.',
|
|
657
816
|
inputSchema: z.object({})
|
|
658
817
|
}),
|
|
818
|
+
get_current_graphql_state: tool({
|
|
819
|
+
description: 'Get the current state of the GraphQL Playground. Returns the current query, variables, and response (if any). Use this to understand what the user is currently working with, check if a query has been executed, and see what data was returned. ALWAYS call this before answering questions about the current GraphQL state or response.',
|
|
820
|
+
inputSchema: z.object({})
|
|
821
|
+
}),
|
|
659
822
|
// === MODE SWITCHING TOOLS ===
|
|
660
823
|
switch_to_docs: tool({
|
|
661
824
|
description: 'Switch to the Docs tab to show endpoint documentation. Use this when user asks about an endpoint, wants to understand how it works, asks "what is", "how to use", "what does this do", or needs to see documentation. This shows the detailed documentation view.',
|
|
662
825
|
inputSchema: z.object({})
|
|
663
826
|
}),
|
|
664
827
|
switch_to_api_client: tool({
|
|
665
|
-
description: 'Switch to the API Client tab to test/send requests. Use this when user wants to test
|
|
828
|
+
description: 'Switch to the API Client tab to test/send REST requests. Use this when user wants to test a REST endpoint, send a request, try it out, or you need to prefill parameters. Call this BEFORE prefilling data so user can see the API Client. DO NOT use for GraphQL endpoints - GraphQL uses the GraphQL Playground in Docs mode instead.',
|
|
666
829
|
inputSchema: z.object({})
|
|
667
830
|
}),
|
|
668
831
|
// === NOTES TOOLS ===
|
|
@@ -4,7 +4,9 @@ import { join, basename, isAbsolute } from 'path';
|
|
|
4
4
|
import matter from 'gray-matter';
|
|
5
5
|
import { CacheUtils } from '@/lib/cache';
|
|
6
6
|
import { importOpenAPISpec } from '@/lib/api-docs/parsers/openapi';
|
|
7
|
+
import { importGraphQLSchema } from '@/lib/api-docs/parsers/graphql';
|
|
7
8
|
import { generateAPISummary, formatAPISummaryForPrompt } from '@/lib/api-docs/agent/spec-summary';
|
|
9
|
+
import { buildEndpointIndex } from '@/lib/api-docs/agent/indexer';
|
|
8
10
|
import { getProjectContent } from '@/lib/storage/blob';
|
|
9
11
|
import { isDevMode, shouldShowItem } from '@/lib/docs/config/environment';
|
|
10
12
|
// Configuration
|
|
@@ -74,6 +76,80 @@ function loadGraphQLSchema(schemaPath) {
|
|
|
74
76
|
return null;
|
|
75
77
|
}
|
|
76
78
|
}
|
|
79
|
+
// Build combined endpoint index from REST collection and GraphQL schemas (local/dev mode)
|
|
80
|
+
async function buildCombinedEndpointIndex(collection, navigationTabs) {
|
|
81
|
+
const allEndpoints = [];
|
|
82
|
+
// Add REST endpoints from collection
|
|
83
|
+
if (collection && (collection.folders.length > 0 || collection.requests.length > 0)) {
|
|
84
|
+
const restEndpoints = buildEndpointIndex(collection);
|
|
85
|
+
allEndpoints.push(...restEndpoints);
|
|
86
|
+
}
|
|
87
|
+
// Add GraphQL endpoints from all GraphQL tabs
|
|
88
|
+
for (const tab of navigationTabs){
|
|
89
|
+
if (tab.type === 'graphql' && tab.graphqlSchemas) {
|
|
90
|
+
for (const schemaConfig of tab.graphqlSchemas){
|
|
91
|
+
try {
|
|
92
|
+
const schemaContent = loadGraphQLSchema(schemaConfig.schema);
|
|
93
|
+
if (!schemaContent) continue;
|
|
94
|
+
const graphqlCollection = await importGraphQLSchema(schemaContent, {
|
|
95
|
+
name: schemaConfig.name || 'GraphQL API',
|
|
96
|
+
endpoint: schemaConfig.endpoint || '/graphql'
|
|
97
|
+
});
|
|
98
|
+
const graphqlEndpoints = buildEndpointIndex(graphqlCollection);
|
|
99
|
+
// Add endpoints, avoiding duplicates by ID
|
|
100
|
+
for (const ep of graphqlEndpoints){
|
|
101
|
+
if (!allEndpoints.find((e)=>e.id === ep.id)) {
|
|
102
|
+
allEndpoints.push(ep);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
// Skip schemas that fail to parse
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return allEndpoints;
|
|
112
|
+
}
|
|
113
|
+
// Build combined endpoint index from REST collection and GraphQL schemas (blob/multi-tenant mode)
|
|
114
|
+
async function buildCombinedEndpointIndexFromBlob(collection, navigationTabs, projectContent) {
|
|
115
|
+
const allEndpoints = [];
|
|
116
|
+
// Add REST endpoints from collection
|
|
117
|
+
if (collection && (collection.folders.length > 0 || collection.requests.length > 0)) {
|
|
118
|
+
const restEndpoints = buildEndpointIndex(collection);
|
|
119
|
+
allEndpoints.push(...restEndpoints);
|
|
120
|
+
}
|
|
121
|
+
// Add GraphQL endpoints from all GraphQL tabs
|
|
122
|
+
for (const tab of navigationTabs){
|
|
123
|
+
if (tab.type === 'graphql' && tab.graphqlSchemas) {
|
|
124
|
+
for (const schemaConfig of tab.graphqlSchemas){
|
|
125
|
+
try {
|
|
126
|
+
// Load GraphQL schema from dedicated graphqlSchemas map (like OpenAPI specs)
|
|
127
|
+
const schemaPath = schemaConfig.schema;
|
|
128
|
+
const schemaContent = projectContent.graphqlSchemas?.[schemaPath] || projectContent.graphqlSchemas?.[schemaPath.replace(/^\//, '')] // Try without leading slash
|
|
129
|
+
;
|
|
130
|
+
if (!schemaContent) {
|
|
131
|
+
console.log('[Collections] GraphQL schema not found in blob:', schemaPath, 'Available:', Object.keys(projectContent.graphqlSchemas || {}));
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const graphqlCollection = await importGraphQLSchema(schemaContent, {
|
|
135
|
+
name: schemaConfig.name || 'GraphQL API',
|
|
136
|
+
endpoint: schemaConfig.endpoint || '/graphql'
|
|
137
|
+
});
|
|
138
|
+
const graphqlEndpoints = buildEndpointIndex(graphqlCollection);
|
|
139
|
+
// Add endpoints, avoiding duplicates by ID
|
|
140
|
+
for (const ep of graphqlEndpoints){
|
|
141
|
+
if (!allEndpoints.find((e)=>e.id === ep.id)) {
|
|
142
|
+
allEndpoints.push(ep);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
} catch (err) {
|
|
146
|
+
console.error('[Collections] Failed to parse GraphQL schema from blob:', err);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return allEndpoints;
|
|
152
|
+
}
|
|
77
153
|
// Load OpenAPI spec from local file
|
|
78
154
|
function loadLocalSpec(specPath) {
|
|
79
155
|
try {
|
|
@@ -400,6 +476,14 @@ function getOpenApiSpec(specPath) {
|
|
|
400
476
|
console.error('[Collections] Failed to parse theme.json:', e);
|
|
401
477
|
}
|
|
402
478
|
}
|
|
479
|
+
// Load custom CSS from project files if specified in theme.json
|
|
480
|
+
let customCssContent = null;
|
|
481
|
+
if (themeConfig?.customCss) {
|
|
482
|
+
const cssFile = projectContent.files.find((f)=>f.path === themeConfig.customCss);
|
|
483
|
+
if (cssFile) {
|
|
484
|
+
customCssContent = cssFile.content;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
403
487
|
// Build navigation tabs and doc groups from config
|
|
404
488
|
// Multi-tenant projects are always in production mode (deployed), so devMode=false
|
|
405
489
|
const navigationTabs = buildNavigationTabs(docsConfig, false);
|
|
@@ -438,6 +522,8 @@ function getOpenApiSpec(specPath) {
|
|
|
438
522
|
console.error('[Collections] Failed to parse OpenAPI spec:', e);
|
|
439
523
|
}
|
|
440
524
|
}
|
|
525
|
+
// Build combined endpoint index (REST + GraphQL) for multi-tenant
|
|
526
|
+
const endpointIndex = await buildCombinedEndpointIndexFromBlob(collection, navigationTabs, projectContent);
|
|
441
527
|
// Build the response
|
|
442
528
|
const response = {
|
|
443
529
|
id: projectSlug,
|
|
@@ -452,6 +538,7 @@ function getOpenApiSpec(specPath) {
|
|
|
452
538
|
headers: collection?.headers || [],
|
|
453
539
|
variables: collection?.variables || [],
|
|
454
540
|
apiSummary: null,
|
|
541
|
+
endpointIndex,
|
|
455
542
|
docGroups,
|
|
456
543
|
navigationTabs,
|
|
457
544
|
changelogReleases: [],
|
|
@@ -462,7 +549,7 @@ function getOpenApiSpec(specPath) {
|
|
|
462
549
|
docsNavbar: themeConfig?.navbar || null,
|
|
463
550
|
docsColors: themeConfig?.colors || null,
|
|
464
551
|
defaultTheme: themeConfig?.defaultTheme || null,
|
|
465
|
-
customCss:
|
|
552
|
+
customCss: customCssContent,
|
|
466
553
|
apiVersions,
|
|
467
554
|
selectedApiVersion: selectedVersion,
|
|
468
555
|
notice: docsConfig.notice || null,
|
|
@@ -472,7 +559,12 @@ function getOpenApiSpec(specPath) {
|
|
|
472
559
|
return NextResponse.json(response, {
|
|
473
560
|
headers: {
|
|
474
561
|
'Content-Type': 'application/json',
|
|
475
|
-
|
|
562
|
+
// CDN caching: cache for 5 min at edge, 1 min in browser, revalidate in background for 1 hour
|
|
563
|
+
'Cache-Control': 'public, max-age=60, s-maxage=300, stale-while-revalidate=3600',
|
|
564
|
+
// Vercel-specific: cache at edge for 5 minutes
|
|
565
|
+
'CDN-Cache-Control': 'public, s-maxage=300, stale-while-revalidate=3600',
|
|
566
|
+
// Vercel Edge: cache tag for invalidation
|
|
567
|
+
'x-vercel-cache-tags': `project-${projectSlug}`
|
|
476
568
|
}
|
|
477
569
|
});
|
|
478
570
|
} catch (error) {
|
|
@@ -649,6 +741,8 @@ export async function GET(request) {
|
|
|
649
741
|
const spec = await getOpenApiSpec(specPath);
|
|
650
742
|
// Handle no spec available
|
|
651
743
|
if (!spec) {
|
|
744
|
+
// Still build endpoint index from GraphQL schemas (if any)
|
|
745
|
+
const endpointIndex = await buildCombinedEndpointIndex(null, navigationTabs);
|
|
652
746
|
return NextResponse.json({
|
|
653
747
|
id: 'empty',
|
|
654
748
|
name: docsConfig?.name || 'Documentation',
|
|
@@ -662,6 +756,7 @@ export async function GET(request) {
|
|
|
662
756
|
headers: [],
|
|
663
757
|
variables: [],
|
|
664
758
|
apiSummary: null,
|
|
759
|
+
endpointIndex,
|
|
665
760
|
docGroups,
|
|
666
761
|
navigationTabs,
|
|
667
762
|
changelogReleases,
|
|
@@ -685,6 +780,8 @@ export async function GET(request) {
|
|
|
685
780
|
}
|
|
686
781
|
// Check if spec has paths
|
|
687
782
|
if (!spec.paths || Object.keys(spec.paths).length === 0) {
|
|
783
|
+
// Still build endpoint index from GraphQL schemas (if any)
|
|
784
|
+
const endpointIndex = await buildCombinedEndpointIndex(null, navigationTabs);
|
|
688
785
|
return NextResponse.json({
|
|
689
786
|
id: 'empty',
|
|
690
787
|
name: spec.info?.title || docsConfig?.name || 'Documentation',
|
|
@@ -698,6 +795,7 @@ export async function GET(request) {
|
|
|
698
795
|
headers: [],
|
|
699
796
|
variables: [],
|
|
700
797
|
apiSummary: null,
|
|
798
|
+
endpointIndex,
|
|
701
799
|
docGroups,
|
|
702
800
|
navigationTabs,
|
|
703
801
|
changelogReleases,
|
|
@@ -733,10 +831,13 @@ export async function GET(request) {
|
|
|
733
831
|
// Generate or get cached API summary
|
|
734
832
|
const specVersion = spec.info?.version || 'unknown';
|
|
735
833
|
const apiSummary = await getOrGenerateSummary(collection, specVersion);
|
|
736
|
-
//
|
|
834
|
+
// Build combined endpoint index (REST + GraphQL)
|
|
835
|
+
const endpointIndex = await buildCombinedEndpointIndex(collection, navigationTabs);
|
|
836
|
+
// Return collection with summary, endpoint index, and doc groups
|
|
737
837
|
return NextResponse.json({
|
|
738
838
|
...collection,
|
|
739
839
|
apiSummary,
|
|
840
|
+
endpointIndex,
|
|
740
841
|
specVersion,
|
|
741
842
|
docGroups,
|
|
742
843
|
navigationTabs,
|
|
@@ -772,6 +873,7 @@ export async function GET(request) {
|
|
|
772
873
|
},
|
|
773
874
|
headers: [],
|
|
774
875
|
variables: [],
|
|
876
|
+
endpointIndex: [],
|
|
775
877
|
docGroups: []
|
|
776
878
|
}, {
|
|
777
879
|
headers: {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server';
|
|
2
2
|
import { storeProjectContent, updateProjectContent, projectExists, generateProjectSlug, generateApiKey, storeProjectApiKey, validateApiKey } from '@/lib/storage/blob';
|
|
3
3
|
import { getProjectUrl, isValidSlug } from '@/lib/multi-tenant/context';
|
|
4
|
+
import { purgeProjectCache } from '@/lib/cache/purge';
|
|
4
5
|
/**
|
|
5
6
|
* Deploy API - receives content from CLI and stores in Vercel Blob
|
|
6
7
|
*
|
|
@@ -20,7 +21,7 @@ import { getProjectUrl, isValidSlug } from '@/lib/multi-tenant/context';
|
|
|
20
21
|
try {
|
|
21
22
|
const body = await request.json();
|
|
22
23
|
// Validate request body
|
|
23
|
-
const { name, slug: existingSlug, docsJson, themeJson, openApiSpecs, files } = body;
|
|
24
|
+
const { name, slug: existingSlug, docsJson, themeJson, openApiSpecs, graphqlSchemas, files } = body;
|
|
24
25
|
if (!name || typeof name !== 'string') {
|
|
25
26
|
return NextResponse.json({
|
|
26
27
|
error: 'Missing or invalid project name'
|
|
@@ -126,14 +127,17 @@ import { getProjectUrl, isValidSlug } from '@/lib/multi-tenant/context';
|
|
|
126
127
|
// Store or update content
|
|
127
128
|
let result;
|
|
128
129
|
if (isUpdate) {
|
|
129
|
-
result = await updateProjectContent(slug, docsJson, validFiles, themeJson, openApiSpecs);
|
|
130
|
+
result = await updateProjectContent(slug, docsJson, validFiles, themeJson, openApiSpecs, graphqlSchemas);
|
|
130
131
|
} else {
|
|
131
|
-
result = await storeProjectContent(slug, name, docsJson, validFiles, themeJson, openApiSpecs);
|
|
132
|
+
result = await storeProjectContent(slug, name, docsJson, validFiles, themeJson, openApiSpecs, graphqlSchemas);
|
|
132
133
|
// Store the API key for new projects
|
|
133
134
|
if (apiKey) {
|
|
134
135
|
await storeProjectApiKey(slug, apiKey);
|
|
135
136
|
}
|
|
136
137
|
}
|
|
138
|
+
// Invalidate CDN cache for this project
|
|
139
|
+
// This ensures users see fresh content after deploy
|
|
140
|
+
await purgeProjectCache(slug);
|
|
137
141
|
// Build response
|
|
138
142
|
const projectUrl = getProjectUrl(slug);
|
|
139
143
|
// For new projects, include the API key in response
|