@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.
Files changed (46) hide show
  1. package/dist/cli/commands/deploy.js +83 -10
  2. package/dist/cli/commands/domain.js +134 -59
  3. package/package.json +1 -1
  4. package/renderer/app/[...slug]/client.js +17 -0
  5. package/renderer/app/[...slug]/page.js +125 -0
  6. package/renderer/app/api/assets/[...path]/route.js +23 -4
  7. package/renderer/app/api/chat/route.js +188 -25
  8. package/renderer/app/api/collections/route.js +105 -3
  9. package/renderer/app/api/deploy/route.js +7 -3
  10. package/renderer/app/api/domains/add/route.js +118 -40
  11. package/renderer/app/api/domains/remove/route.js +16 -3
  12. package/renderer/app/api/domains/verify/route.js +82 -17
  13. package/renderer/app/api/schema/route.js +12 -11
  14. package/renderer/app/api/suggestions/route.js +98 -10
  15. package/renderer/app/globals.css +33 -0
  16. package/renderer/app/layout.js +83 -8
  17. package/renderer/components/docs/mdx/cards.js +16 -45
  18. package/renderer/components/docs/mdx/file-tree.js +102 -0
  19. package/renderer/components/docs/mdx/index.js +7 -0
  20. package/renderer/components/docs-header.js +11 -11
  21. package/renderer/components/docs-viewer/agent/agent-chat.js +75 -11
  22. package/renderer/components/docs-viewer/agent/messages/assistant-message.js +67 -3
  23. package/renderer/components/docs-viewer/agent/messages/tool-call-display.js +49 -4
  24. package/renderer/components/docs-viewer/content/content-router.js +1 -1
  25. package/renderer/components/docs-viewer/content/doc-page.js +36 -28
  26. package/renderer/components/docs-viewer/index.js +223 -58
  27. package/renderer/components/docs-viewer/playground/graphql-playground.js +131 -33
  28. package/renderer/components/docs-viewer/shared/method-badge.js +11 -2
  29. package/renderer/components/docs-viewer/sidebar/collection-tree.js +44 -6
  30. package/renderer/components/docs-viewer/sidebar/index.js +2 -1
  31. package/renderer/components/docs-viewer/sidebar/right-sidebar.js +3 -1
  32. package/renderer/components/docs-viewer/sidebar/sidebar-item.js +5 -7
  33. package/renderer/hooks/use-route-state.js +44 -56
  34. package/renderer/lib/api-docs/agent/indexer.js +73 -12
  35. package/renderer/lib/api-docs/agent/use-suggestions.js +26 -16
  36. package/renderer/lib/api-docs/code-editor/mode-context.js +16 -18
  37. package/renderer/lib/api-docs/parsers/openapi/transformer.js +8 -1
  38. package/renderer/lib/cache/purge.js +98 -0
  39. package/renderer/lib/docs/config/domain-schema.js +23 -1
  40. package/renderer/lib/docs/config/index.js +1 -1
  41. package/renderer/lib/docs-link-utils.js +146 -0
  42. package/renderer/lib/docs-navigation-context.js +3 -2
  43. package/renderer/lib/docs-navigation.js +50 -41
  44. package/renderer/lib/rate-limit.js +203 -0
  45. package/renderer/lib/storage/blob.js +4 -2
  46. 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
- const endpointsList = endpoints.map((ep, i)=>{
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 user provides
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
- ## API Client Assistance (IMPORTANT)
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. Optionally a README.md tying them together
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 it with open_file + write_file
391
- 5. **If NO similar file**: create_file + write_file
392
- 6. For multiple files: create_folder first, then files inside
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
- **File naming convention:**
397
- - \`{endpoint-action}.{lang}\` → e.g., \`create-user.py\`, \`list-orders.ts\`
398
- - \`{workflow-name}/\` folder for multi-step flows
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 .mmd diagram showing the flow visually
403
- 2. Create implementation code showing how to do it
404
- 3. Optionally add .md only if complex steps need documentation
405
-
406
- **Always pair explanations with working code:**
407
- - "Explain authentication" auth-flow.mmd + auth-implementation.py
408
- - "How do webhooks work" → webhook-flow.mmd + webhook-handler.ts
409
- - "Show the order process" → order-flow.mmd + create-order.py + process-payment.py
410
-
411
- PREFER: Diagram + Working Code
412
- AVOID: Long text explanations without code
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 an 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.',
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: null,
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
- 'Cache-Control': 'public, max-age=60'
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
- // Return collection with summary and doc groups
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