@brainfish-ai/devdoc 0.1.48 → 0.1.49

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 (36) hide show
  1. package/dist/cli/commands/deploy.js +16 -11
  2. package/package.json +1 -1
  3. package/renderer/app/[...slug]/client.js +17 -0
  4. package/renderer/app/[...slug]/page.js +125 -0
  5. package/renderer/app/api/assets/[...path]/route.js +23 -4
  6. package/renderer/app/api/chat/route.js +188 -25
  7. package/renderer/app/api/collections/route.js +95 -2
  8. package/renderer/app/api/deploy/route.js +4 -0
  9. package/renderer/app/api/suggestions/route.js +98 -10
  10. package/renderer/app/globals.css +33 -0
  11. package/renderer/app/layout.js +83 -8
  12. package/renderer/components/docs/mdx/cards.js +16 -45
  13. package/renderer/components/docs/mdx/file-tree.js +102 -0
  14. package/renderer/components/docs/mdx/index.js +7 -0
  15. package/renderer/components/docs-viewer/agent/agent-chat.js +75 -11
  16. package/renderer/components/docs-viewer/agent/messages/assistant-message.js +67 -3
  17. package/renderer/components/docs-viewer/agent/messages/tool-call-display.js +49 -4
  18. package/renderer/components/docs-viewer/content/content-router.js +1 -1
  19. package/renderer/components/docs-viewer/content/doc-page.js +36 -28
  20. package/renderer/components/docs-viewer/index.js +223 -58
  21. package/renderer/components/docs-viewer/playground/graphql-playground.js +131 -33
  22. package/renderer/components/docs-viewer/shared/method-badge.js +11 -2
  23. package/renderer/components/docs-viewer/sidebar/collection-tree.js +44 -6
  24. package/renderer/components/docs-viewer/sidebar/index.js +2 -1
  25. package/renderer/components/docs-viewer/sidebar/right-sidebar.js +3 -1
  26. package/renderer/components/docs-viewer/sidebar/sidebar-item.js +5 -7
  27. package/renderer/hooks/use-route-state.js +44 -56
  28. package/renderer/lib/api-docs/agent/indexer.js +73 -12
  29. package/renderer/lib/api-docs/agent/use-suggestions.js +26 -16
  30. package/renderer/lib/api-docs/code-editor/mode-context.js +16 -18
  31. package/renderer/lib/api-docs/parsers/openapi/transformer.js +8 -1
  32. package/renderer/lib/cache/purge.js +98 -0
  33. package/renderer/lib/docs-link-utils.js +146 -0
  34. package/renderer/lib/docs-navigation-context.js +3 -2
  35. package/renderer/lib/docs-navigation.js +50 -41
  36. package/renderer/lib/rate-limit.js +203 -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,79 @@ 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 blob storage (check files array)
127
+ const schemaFile = projectContent.files.find((f)=>f.path === schemaConfig.schema || f.path === schemaConfig.schema.replace(/^\//, '') // Remove leading slash
128
+ );
129
+ if (!schemaFile) {
130
+ console.log('[Collections] GraphQL schema not found in blob:', schemaConfig.schema);
131
+ continue;
132
+ }
133
+ const graphqlCollection = await importGraphQLSchema(schemaFile.content, {
134
+ name: schemaConfig.name || 'GraphQL API',
135
+ endpoint: schemaConfig.endpoint || '/graphql'
136
+ });
137
+ const graphqlEndpoints = buildEndpointIndex(graphqlCollection);
138
+ // Add endpoints, avoiding duplicates by ID
139
+ for (const ep of graphqlEndpoints){
140
+ if (!allEndpoints.find((e)=>e.id === ep.id)) {
141
+ allEndpoints.push(ep);
142
+ }
143
+ }
144
+ } catch (err) {
145
+ console.error('[Collections] Failed to parse GraphQL schema from blob:', err);
146
+ }
147
+ }
148
+ }
149
+ }
150
+ return allEndpoints;
151
+ }
77
152
  // Load OpenAPI spec from local file
78
153
  function loadLocalSpec(specPath) {
79
154
  try {
@@ -438,6 +513,8 @@ function getOpenApiSpec(specPath) {
438
513
  console.error('[Collections] Failed to parse OpenAPI spec:', e);
439
514
  }
440
515
  }
516
+ // Build combined endpoint index (REST + GraphQL) for multi-tenant
517
+ const endpointIndex = await buildCombinedEndpointIndexFromBlob(collection, navigationTabs, projectContent);
441
518
  // Build the response
442
519
  const response = {
443
520
  id: projectSlug,
@@ -452,6 +529,7 @@ function getOpenApiSpec(specPath) {
452
529
  headers: collection?.headers || [],
453
530
  variables: collection?.variables || [],
454
531
  apiSummary: null,
532
+ endpointIndex,
455
533
  docGroups,
456
534
  navigationTabs,
457
535
  changelogReleases: [],
@@ -472,7 +550,12 @@ function getOpenApiSpec(specPath) {
472
550
  return NextResponse.json(response, {
473
551
  headers: {
474
552
  'Content-Type': 'application/json',
475
- 'Cache-Control': 'public, max-age=60'
553
+ // CDN caching: cache for 5 min at edge, 1 min in browser, revalidate in background for 1 hour
554
+ 'Cache-Control': 'public, max-age=60, s-maxage=300, stale-while-revalidate=3600',
555
+ // Vercel-specific: cache at edge for 5 minutes
556
+ 'CDN-Cache-Control': 'public, s-maxage=300, stale-while-revalidate=3600',
557
+ // Vercel Edge: cache tag for invalidation
558
+ 'x-vercel-cache-tags': `project-${projectSlug}`
476
559
  }
477
560
  });
478
561
  } catch (error) {
@@ -649,6 +732,8 @@ export async function GET(request) {
649
732
  const spec = await getOpenApiSpec(specPath);
650
733
  // Handle no spec available
651
734
  if (!spec) {
735
+ // Still build endpoint index from GraphQL schemas (if any)
736
+ const endpointIndex = await buildCombinedEndpointIndex(null, navigationTabs);
652
737
  return NextResponse.json({
653
738
  id: 'empty',
654
739
  name: docsConfig?.name || 'Documentation',
@@ -662,6 +747,7 @@ export async function GET(request) {
662
747
  headers: [],
663
748
  variables: [],
664
749
  apiSummary: null,
750
+ endpointIndex,
665
751
  docGroups,
666
752
  navigationTabs,
667
753
  changelogReleases,
@@ -685,6 +771,8 @@ export async function GET(request) {
685
771
  }
686
772
  // Check if spec has paths
687
773
  if (!spec.paths || Object.keys(spec.paths).length === 0) {
774
+ // Still build endpoint index from GraphQL schemas (if any)
775
+ const endpointIndex = await buildCombinedEndpointIndex(null, navigationTabs);
688
776
  return NextResponse.json({
689
777
  id: 'empty',
690
778
  name: spec.info?.title || docsConfig?.name || 'Documentation',
@@ -698,6 +786,7 @@ export async function GET(request) {
698
786
  headers: [],
699
787
  variables: [],
700
788
  apiSummary: null,
789
+ endpointIndex,
701
790
  docGroups,
702
791
  navigationTabs,
703
792
  changelogReleases,
@@ -733,10 +822,13 @@ export async function GET(request) {
733
822
  // Generate or get cached API summary
734
823
  const specVersion = spec.info?.version || 'unknown';
735
824
  const apiSummary = await getOrGenerateSummary(collection, specVersion);
736
- // Return collection with summary and doc groups
825
+ // Build combined endpoint index (REST + GraphQL)
826
+ const endpointIndex = await buildCombinedEndpointIndex(collection, navigationTabs);
827
+ // Return collection with summary, endpoint index, and doc groups
737
828
  return NextResponse.json({
738
829
  ...collection,
739
830
  apiSummary,
831
+ endpointIndex,
740
832
  specVersion,
741
833
  docGroups,
742
834
  navigationTabs,
@@ -772,6 +864,7 @@ export async function GET(request) {
772
864
  },
773
865
  headers: [],
774
866
  variables: [],
867
+ endpointIndex: [],
775
868
  docGroups: []
776
869
  }, {
777
870
  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
  *
@@ -134,6 +135,9 @@ import { getProjectUrl, isValidSlug } from '@/lib/multi-tenant/context';
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
@@ -3,10 +3,22 @@ import { anthropic } from '@ai-sdk/anthropic';
3
3
  import { z } from 'zod';
4
4
  import crypto from 'crypto';
5
5
  import { CacheUtils } from '@/lib/cache';
6
+ import { RateLimiter } from '@/lib/rate-limit';
6
7
  export const runtime = 'nodejs';
7
8
  // Cache TTL: 1 week in seconds
8
9
  const CACHE_TTL_SECONDS = 60 * 60 * 24 * 7 // 7 days
9
10
  ;
11
+ // Rate limit configuration (can be overridden via env vars)
12
+ // Set SUGGESTIONS_RATE_LIMIT_ENABLED=false to disable rate limiting
13
+ const RATE_LIMIT_ENABLED = process.env.SUGGESTIONS_RATE_LIMIT_ENABLED !== 'false';
14
+ const RATE_LIMIT_MAX = parseInt(process.env.SUGGESTIONS_RATE_LIMIT_MAX || '30', 10);
15
+ const RATE_LIMIT_WINDOW = parseInt(process.env.SUGGESTIONS_RATE_LIMIT_WINDOW || '60', 10);
16
+ // Create rate limiter with configurable values
17
+ const suggestionsRateLimiter = new RateLimiter({
18
+ prefix: 'suggestions',
19
+ limit: RATE_LIMIT_MAX,
20
+ windowSeconds: RATE_LIMIT_WINDOW
21
+ });
10
22
  const SuggestionsSchema = z.object({
11
23
  suggestions: z.array(z.object({
12
24
  title: z.string().describe('Short action phrase (2-4 words) like "Find endpoints" or "How do I"'),
@@ -22,10 +34,20 @@ const SuggestionsSchema = z.object({
22
34
  if (currentEndpointId) {
23
35
  // For specific endpoint: hash the endpoint details
24
36
  const endpoint = endpointIndex.find((e)=>e.id === currentEndpointId);
25
- content = endpoint ? `endpoint:${endpoint.id}:${endpoint.name}:${endpoint.method}:${endpoint.path}` : `endpoint:${currentEndpointId}`;
37
+ if (endpoint) {
38
+ // Include GraphQL operation type in the cache key for better differentiation
39
+ const opType = endpoint.operationType || endpoint.method;
40
+ content = `endpoint:${endpoint.id}:${endpoint.name}:${opType}:${endpoint.path}`;
41
+ } else {
42
+ content = `endpoint:${currentEndpointId}`;
43
+ }
26
44
  } else {
27
45
  // For general API: hash based on endpoint count and first few endpoint signatures
28
- const signature = endpointIndex.slice(0, 10).map((e)=>`${e.method}:${e.path}`).join('|');
46
+ const signature = endpointIndex.slice(0, 10).map((e)=>{
47
+ // Use operation type for GraphQL, method for REST
48
+ const opType = e.operationType || e.method;
49
+ return `${opType}:${e.name}`;
50
+ }).join('|');
29
51
  content = `general:${endpointIndex.length}:${signature}`;
30
52
  }
31
53
  const hash = crypto.createHash('sha256').update(content).digest('hex').slice(0, 16);
@@ -34,6 +56,30 @@ const SuggestionsSchema = z.object({
34
56
  function buildSuggestionPrompt(endpoints, currentEndpointId) {
35
57
  const currentEndpoint = currentEndpointId ? endpoints.find((e)=>e.id === currentEndpointId) : null;
36
58
  if (currentEndpoint) {
59
+ // Check if this is a GraphQL operation
60
+ const isGraphQL = currentEndpoint.type === 'graphql' || [
61
+ 'query',
62
+ 'mutation',
63
+ 'subscription'
64
+ ].includes(currentEndpoint.operationType || '');
65
+ if (isGraphQL) {
66
+ const opType = currentEndpoint.operationType || 'operation';
67
+ return `You are helping users explore a GraphQL API. Generate 4 helpful question suggestions for the "${currentEndpoint.name}" ${opType}.
68
+
69
+ GraphQL Operation details:
70
+ - Name: ${currentEndpoint.name}
71
+ - Type: ${opType}
72
+ - Description: ${currentEndpoint.description || 'No description'}
73
+ - Variables: ${currentEndpoint.parameters.join(', ') || 'None'}
74
+
75
+ Generate questions that help users:
76
+ 1. Understand what this ${opType} does and when to use it
77
+ 2. Know what variables are required and their types
78
+ 3. See example GraphQL queries and responses
79
+ 4. Understand how to handle errors or edge cases
80
+
81
+ Make questions specific to this GraphQL ${opType}, not generic.`;
82
+ }
37
83
  return `You are helping users explore an API. Generate 4 helpful question suggestions for the "${currentEndpoint.name}" endpoint.
38
84
 
39
85
  Endpoint details:
@@ -53,33 +99,75 @@ Generate questions that help users:
53
99
  Make questions specific to this endpoint, not generic.`;
54
100
  }
55
101
  // General suggestions based on the API
56
- const methodCounts = endpoints.reduce((acc, e)=>{
102
+ // Separate GraphQL and REST endpoints
103
+ const graphqlEndpoints = endpoints.filter((e)=>e.type === 'graphql' || [
104
+ 'query',
105
+ 'mutation',
106
+ 'subscription'
107
+ ].includes(e.operationType || ''));
108
+ const restEndpoints = endpoints.filter((e)=>e.type === 'rest' && ![
109
+ 'query',
110
+ 'mutation',
111
+ 'subscription'
112
+ ].includes(e.operationType || ''));
113
+ const methodCounts = restEndpoints.reduce((acc, e)=>{
57
114
  acc[e.method] = (acc[e.method] || 0) + 1;
58
115
  return acc;
59
116
  }, {});
117
+ const graphqlCounts = graphqlEndpoints.reduce((acc, e)=>{
118
+ const opType = e.operationType || 'operation';
119
+ acc[opType] = (acc[opType] || 0) + 1;
120
+ return acc;
121
+ }, {});
60
122
  const categories = [
61
123
  ...new Set(endpoints.flatMap((e)=>e.tags))
62
124
  ].slice(0, 5);
63
- return `You are helping users explore an API documentation. Generate 4 helpful starter question suggestions.
125
+ // Build API type description
126
+ const apiTypes = [];
127
+ if (restEndpoints.length > 0) apiTypes.push('REST');
128
+ if (graphqlEndpoints.length > 0) apiTypes.push('GraphQL');
129
+ return `You are helping users explore ${apiTypes.join(' and ')} API documentation. Generate 4 helpful starter question suggestions.
64
130
 
65
131
  API Overview:
66
- - Total endpoints: ${endpoints.length}
67
- - Methods: ${Object.entries(methodCounts).map(([m, c])=>`${m}: ${c}`).join(', ')}
132
+ - Total operations: ${endpoints.length}
133
+ ${restEndpoints.length > 0 ? `- REST Methods: ${Object.entries(methodCounts).map(([m, c])=>`${m}: ${c}`).join(', ')}` : ''}
134
+ ${graphqlEndpoints.length > 0 ? `- GraphQL Operations: ${Object.entries(graphqlCounts).map(([m, c])=>`${m}: ${c}`).join(', ')}` : ''}
68
135
  - Categories: ${categories.join(', ') || 'General'}
69
136
 
70
- Sample endpoints:
71
- ${endpoints.slice(0, 8).map((e)=>`- ${e.method} ${e.name}: ${e.description || 'No description'}`).join('\n')}
137
+ Sample operations:
138
+ ${endpoints.slice(0, 8).map((e)=>{
139
+ if (e.type === 'graphql' || [
140
+ 'query',
141
+ 'mutation',
142
+ 'subscription'
143
+ ].includes(e.operationType || '')) {
144
+ return `- [${e.operationType?.toUpperCase() || 'GRAPHQL'}] ${e.name}: ${e.description || 'No description'}`;
145
+ }
146
+ return `- [${e.method}] ${e.name}: ${e.description || 'No description'}`;
147
+ }).join('\n')}
72
148
 
73
149
  Generate questions that help users:
74
- 1. Find the right endpoint for their use case
150
+ 1. Find the right operation for their use case
75
151
  2. Understand authentication/authorization
76
- 3. Explore common operations (create, read, update, delete)
152
+ 3. Explore common operations (${graphqlEndpoints.length > 0 ? 'queries, mutations' : 'create, read, update, delete'})
77
153
  4. Get started quickly with the API
78
154
 
79
155
  Make questions specific to this API's capabilities.`;
80
156
  }
81
157
  export async function POST(req) {
82
158
  try {
159
+ // Rate limiting check (configurable via env vars)
160
+ if (RATE_LIMIT_ENABLED) {
161
+ const rateLimitResult = await suggestionsRateLimiter.check(req);
162
+ if (!rateLimitResult.success) {
163
+ console.log(`[Suggestions API] Rate limit exceeded for IP, count: ${rateLimitResult.count}/${rateLimitResult.limit}`);
164
+ return RateLimiter.tooManyRequestsResponse(rateLimitResult, 'Too many suggestion requests. Please wait before trying again.');
165
+ }
166
+ // Log rate limit status for monitoring (only on cache miss to reduce noise)
167
+ if (rateLimitResult.remaining < 5) {
168
+ console.log(`[Suggestions API] Rate limit warning: ${rateLimitResult.remaining} requests remaining`);
169
+ }
170
+ }
83
171
  const body = await req.json();
84
172
  const { endpointIndex, currentEndpointId } = body;
85
173
  // Generate hash-based cache key