@theihtisham/devtools-with-cloud 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/.env.example +15 -0
  2. package/LICENSE +21 -0
  3. package/README.md +73 -0
  4. package/docker-compose.yml +23 -0
  5. package/jest.config.js +7 -0
  6. package/next-env.d.ts +5 -0
  7. package/next.config.mjs +22 -0
  8. package/package.json +82 -0
  9. package/postcss.config.js +6 -0
  10. package/prisma/schema.prisma +105 -0
  11. package/prisma/seed.ts +211 -0
  12. package/src/app/(app)/ai/page.tsx +122 -0
  13. package/src/app/(app)/collections/page.tsx +155 -0
  14. package/src/app/(app)/environments/page.tsx +96 -0
  15. package/src/app/(app)/history/page.tsx +107 -0
  16. package/src/app/(app)/import/page.tsx +102 -0
  17. package/src/app/(app)/layout.tsx +60 -0
  18. package/src/app/(app)/settings/page.tsx +79 -0
  19. package/src/app/(app)/workspace/page.tsx +284 -0
  20. package/src/app/api/ai/discover/route.ts +17 -0
  21. package/src/app/api/ai/explain/route.ts +29 -0
  22. package/src/app/api/ai/generate-tests/route.ts +37 -0
  23. package/src/app/api/ai/suggest/route.ts +29 -0
  24. package/src/app/api/collections/[id]/route.ts +66 -0
  25. package/src/app/api/collections/route.ts +48 -0
  26. package/src/app/api/environments/route.ts +40 -0
  27. package/src/app/api/export/openapi/route.ts +17 -0
  28. package/src/app/api/export/postman/route.ts +18 -0
  29. package/src/app/api/import/curl/route.ts +18 -0
  30. package/src/app/api/import/har/route.ts +20 -0
  31. package/src/app/api/import/openapi/route.ts +21 -0
  32. package/src/app/api/import/postman/route.ts +21 -0
  33. package/src/app/api/proxy/route.ts +35 -0
  34. package/src/app/api/requests/[id]/execute/route.ts +85 -0
  35. package/src/app/api/requests/[id]/history/route.ts +23 -0
  36. package/src/app/api/requests/[id]/route.ts +66 -0
  37. package/src/app/api/requests/route.ts +49 -0
  38. package/src/app/api/workspaces/route.ts +38 -0
  39. package/src/app/globals.css +99 -0
  40. package/src/app/layout.tsx +24 -0
  41. package/src/app/page.tsx +182 -0
  42. package/src/components/ai/ai-panel.tsx +65 -0
  43. package/src/components/ai/code-explainer.tsx +51 -0
  44. package/src/components/ai/endpoint-discovery.tsx +62 -0
  45. package/src/components/ai/test-generator.tsx +49 -0
  46. package/src/components/collections/collection-actions.tsx +36 -0
  47. package/src/components/collections/collection-tree.tsx +55 -0
  48. package/src/components/collections/folder-creator.tsx +54 -0
  49. package/src/components/landing/comparison.tsx +43 -0
  50. package/src/components/landing/cta.tsx +16 -0
  51. package/src/components/landing/features.tsx +24 -0
  52. package/src/components/landing/hero.tsx +23 -0
  53. package/src/components/response/body-viewer.tsx +33 -0
  54. package/src/components/response/headers-viewer.tsx +23 -0
  55. package/src/components/response/status-badge.tsx +25 -0
  56. package/src/components/response/test-results.tsx +50 -0
  57. package/src/components/response/timing-chart.tsx +39 -0
  58. package/src/components/ui/badge.tsx +24 -0
  59. package/src/components/ui/button.tsx +32 -0
  60. package/src/components/ui/code-editor.tsx +51 -0
  61. package/src/components/ui/dialog.tsx +56 -0
  62. package/src/components/ui/dropdown.tsx +63 -0
  63. package/src/components/ui/input.tsx +22 -0
  64. package/src/components/ui/key-value-editor.tsx +75 -0
  65. package/src/components/ui/select.tsx +24 -0
  66. package/src/components/ui/tabs.tsx +85 -0
  67. package/src/components/ui/textarea.tsx +22 -0
  68. package/src/components/ui/toast.tsx +54 -0
  69. package/src/components/workspace/request-panel.tsx +38 -0
  70. package/src/components/workspace/response-panel.tsx +81 -0
  71. package/src/components/workspace/sidebar.tsx +52 -0
  72. package/src/components/workspace/split-pane.tsx +49 -0
  73. package/src/components/workspace/tabs/auth-tab.tsx +94 -0
  74. package/src/components/workspace/tabs/body-tab.tsx +41 -0
  75. package/src/components/workspace/tabs/headers-tab.tsx +23 -0
  76. package/src/components/workspace/tabs/params-tab.tsx +23 -0
  77. package/src/components/workspace/tabs/pre-request-tab.tsx +26 -0
  78. package/src/components/workspace/url-bar.tsx +53 -0
  79. package/src/hooks/use-ai.ts +115 -0
  80. package/src/hooks/use-collection.ts +71 -0
  81. package/src/hooks/use-environment.ts +73 -0
  82. package/src/hooks/use-request.ts +111 -0
  83. package/src/lib/ai/endpoint-discovery.ts +158 -0
  84. package/src/lib/ai/explainer.ts +127 -0
  85. package/src/lib/ai/suggester.ts +164 -0
  86. package/src/lib/ai/test-generator.ts +161 -0
  87. package/src/lib/auth/api-key.ts +28 -0
  88. package/src/lib/auth/aws-sig.ts +131 -0
  89. package/src/lib/auth/basic.ts +17 -0
  90. package/src/lib/auth/bearer.ts +15 -0
  91. package/src/lib/auth/oauth2.ts +155 -0
  92. package/src/lib/auth/types.ts +16 -0
  93. package/src/lib/db/client.ts +15 -0
  94. package/src/lib/env/manager.ts +32 -0
  95. package/src/lib/env/resolver.ts +30 -0
  96. package/src/lib/exporters/openapi.ts +193 -0
  97. package/src/lib/exporters/postman.ts +140 -0
  98. package/src/lib/graphql/builder.ts +249 -0
  99. package/src/lib/graphql/formatter.ts +147 -0
  100. package/src/lib/graphql/index.ts +43 -0
  101. package/src/lib/graphql/introspection.ts +175 -0
  102. package/src/lib/graphql/types.ts +99 -0
  103. package/src/lib/graphql/validator.ts +216 -0
  104. package/src/lib/http/client.ts +112 -0
  105. package/src/lib/http/proxy.ts +83 -0
  106. package/src/lib/http/request-builder.ts +214 -0
  107. package/src/lib/http/response-parser.ts +106 -0
  108. package/src/lib/http/timing.ts +63 -0
  109. package/src/lib/importers/curl-parser.ts +346 -0
  110. package/src/lib/importers/har-parser.ts +128 -0
  111. package/src/lib/importers/openapi.ts +324 -0
  112. package/src/lib/importers/postman.ts +312 -0
  113. package/src/lib/test-runner/assertions.ts +163 -0
  114. package/src/lib/test-runner/reporter.ts +90 -0
  115. package/src/lib/test-runner/runner.ts +69 -0
  116. package/src/lib/utils/api-response.ts +85 -0
  117. package/src/lib/utils/cn.ts +6 -0
  118. package/src/lib/utils/content-type.ts +123 -0
  119. package/src/lib/utils/download.ts +53 -0
  120. package/src/lib/utils/errors.ts +92 -0
  121. package/src/lib/utils/format.ts +142 -0
  122. package/src/lib/utils/syntax-highlight.ts +108 -0
  123. package/src/lib/utils/validation.ts +231 -0
  124. package/src/lib/websocket/client.ts +182 -0
  125. package/src/lib/websocket/frames.ts +96 -0
  126. package/src/lib/websocket/history.ts +121 -0
  127. package/src/lib/websocket/index.ts +25 -0
  128. package/src/lib/websocket/types.ts +57 -0
  129. package/src/types/ai.ts +28 -0
  130. package/src/types/collection.ts +24 -0
  131. package/src/types/environment.ts +16 -0
  132. package/src/types/request.ts +54 -0
  133. package/src/types/response.ts +37 -0
  134. package/tailwind.config.ts +82 -0
  135. package/tests/lib/env/resolver.test.ts +108 -0
  136. package/tests/lib/graphql/builder.test.ts +349 -0
  137. package/tests/lib/graphql/formatter.test.ts +99 -0
  138. package/tests/lib/http/request-builder.test.ts +160 -0
  139. package/tests/lib/http/response-parser.test.ts +150 -0
  140. package/tests/lib/http/timing.test.ts +188 -0
  141. package/tests/lib/importers/curl-parser.test.ts +245 -0
  142. package/tests/lib/test-runner/assertions.test.ts +342 -0
  143. package/tests/lib/utils/cn.test.ts +46 -0
  144. package/tests/lib/utils/content-type.test.ts +175 -0
  145. package/tests/lib/utils/format.test.ts +188 -0
  146. package/tests/lib/utils/validation.test.ts +237 -0
  147. package/tests/lib/websocket/history.test.ts +186 -0
  148. package/tsconfig.json +29 -0
  149. package/tsconfig.tsbuildinfo +1 -0
  150. package/vitest.config.ts +21 -0
@@ -0,0 +1,79 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+
5
+ export default function SettingsPage() {
6
+ const [theme, setTheme] = useState<'dark' | 'light'>('dark');
7
+ const [defaultTimeout, setDefaultTimeout] = useState('30000');
8
+ const [followRedirects, setFollowRedirects] = useState(true);
9
+ const [verifySSL, setVerifySSL] = useState(true);
10
+
11
+ return (
12
+ <div className="p-6 max-w-2xl">
13
+ <h1 className="text-2xl font-bold text-foreground mb-6">Settings</h1>
14
+
15
+ <div className="space-y-6">
16
+ {/* Appearance */}
17
+ <section>
18
+ <h2 className="text-lg font-semibold text-foreground mb-3">Appearance</h2>
19
+ <div className="space-y-3">
20
+ <div className="flex items-center justify-between">
21
+ <label className="text-sm text-foreground">Theme</label>
22
+ <select
23
+ value={theme}
24
+ onChange={(e) => setTheme(e.target.value as 'dark' | 'light')}
25
+ className="px-3 py-1.5 rounded-md border border-border bg-background text-foreground text-sm"
26
+ >
27
+ <option value="dark">Dark</option>
28
+ <option value="light">Light</option>
29
+ </select>
30
+ </div>
31
+ </div>
32
+ </section>
33
+
34
+ {/* Request Defaults */}
35
+ <section>
36
+ <h2 className="text-lg font-semibold text-foreground mb-3">Request Defaults</h2>
37
+ <div className="space-y-3">
38
+ <div className="flex items-center justify-between">
39
+ <label className="text-sm text-foreground">Default Timeout (ms)</label>
40
+ <input
41
+ type="number"
42
+ value={defaultTimeout}
43
+ onChange={(e) => setDefaultTimeout(e.target.value)}
44
+ className="w-32 px-3 py-1.5 rounded-md border border-border bg-background text-foreground text-sm text-right"
45
+ />
46
+ </div>
47
+ <div className="flex items-center justify-between">
48
+ <label className="text-sm text-foreground">Follow Redirects</label>
49
+ <button
50
+ onClick={() => setFollowRedirects(!followRedirects)}
51
+ className={`w-10 h-6 rounded-full transition-colors ${followRedirects ? 'bg-primary' : 'bg-border'}`}
52
+ >
53
+ <span className={`block w-4 h-4 rounded-full bg-white transition-transform ${followRedirects ? 'translate-x-5' : 'translate-x-1'}`} />
54
+ </button>
55
+ </div>
56
+ <div className="flex items-center justify-between">
57
+ <label className="text-sm text-foreground">Verify SSL</label>
58
+ <button
59
+ onClick={() => setVerifySSL(!verifySSL)}
60
+ className={`w-10 h-6 rounded-full transition-colors ${verifySSL ? 'bg-primary' : 'bg-border'}`}
61
+ >
62
+ <span className={`block w-4 h-4 rounded-full bg-white transition-transform ${verifySSL ? 'translate-x-5' : 'translate-x-1'}`} />
63
+ </button>
64
+ </div>
65
+ </div>
66
+ </section>
67
+
68
+ {/* About */}
69
+ <section>
70
+ <h2 className="text-lg font-semibold text-foreground mb-3">About</h2>
71
+ <div className="text-sm text-muted-foreground space-y-1">
72
+ <p>APITester v1.0.0</p>
73
+ <p>Open-source API testing tool with AI-powered features</p>
74
+ </div>
75
+ </section>
76
+ </div>
77
+ </div>
78
+ );
79
+ }
@@ -0,0 +1,284 @@
1
+ 'use client';
2
+
3
+ import { useState, useCallback } from 'react';
4
+ import type { HttpMethod, KeyValue } from '@/lib/utils/validation';
5
+ import { cn } from '@/lib/utils/cn';
6
+
7
+ const METHODS: HttpMethod[] = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'];
8
+
9
+ interface RequestState {
10
+ method: HttpMethod;
11
+ url: string;
12
+ headers: KeyValue[];
13
+ params: KeyValue[];
14
+ body: string;
15
+ bodyType: string;
16
+ activeTab: string;
17
+ }
18
+
19
+ type ResponseState = {
20
+ status: number;
21
+ statusText: string;
22
+ headers: Record<string, string>;
23
+ body: string;
24
+ duration: number;
25
+ size: number;
26
+ } | null;
27
+
28
+ export default function WorkspacePage() {
29
+ const [request, setRequest] = useState<RequestState>({
30
+ method: 'GET',
31
+ url: '',
32
+ headers: [],
33
+ params: [],
34
+ body: '',
35
+ bodyType: 'none',
36
+ activeTab: 'params',
37
+ });
38
+ const [response, setResponse] = useState<ResponseState>(null);
39
+ const [loading, setLoading] = useState(false);
40
+ const [error, setError] = useState<string | null>(null);
41
+
42
+ const sendRequest = useCallback(async () => {
43
+ if (!request.url.trim()) return;
44
+ setLoading(true);
45
+ setError(null);
46
+
47
+ try {
48
+ const start = performance.now();
49
+ const headerMap: Record<string, string> = {};
50
+ for (const h of request.headers) {
51
+ if (h.enabled && h.key) {
52
+ headerMap[h.key] = h.value;
53
+ }
54
+ }
55
+
56
+ let body: string | undefined;
57
+ if (request.bodyType !== 'none' && request.method !== 'GET' && request.method !== 'HEAD') {
58
+ body = request.body;
59
+ if (request.bodyType === 'json' && !headerMap['Content-Type']) {
60
+ headerMap['Content-Type'] = 'application/json';
61
+ }
62
+ }
63
+
64
+ const res = await fetch('/api/proxy', {
65
+ method: 'POST',
66
+ headers: { 'Content-Type': 'application/json' },
67
+ body: JSON.stringify({
68
+ url: request.url,
69
+ method: request.method,
70
+ headers: headerMap,
71
+ body,
72
+ }),
73
+ });
74
+
75
+ const data = await res.json();
76
+ const elapsed = Math.round(performance.now() - start);
77
+
78
+ const responseHeaders: Record<string, string> = {};
79
+ if (data.headers) {
80
+ for (const [key, value] of Object.entries(data.headers as Record<string, string>)) {
81
+ responseHeaders[key] = value;
82
+ }
83
+ }
84
+
85
+ setResponse({
86
+ status: data.status ?? res.status,
87
+ statusText: data.statusText ?? res.statusText,
88
+ headers: responseHeaders,
89
+ body: typeof data.body === 'string' ? data.body : JSON.stringify(data.body, null, 2),
90
+ duration: elapsed,
91
+ size: typeof data.body === 'string' ? new TextEncoder().encode(data.body).length : 0,
92
+ });
93
+ } catch (err) {
94
+ setError(err instanceof Error ? err.message : 'Request failed');
95
+ } finally {
96
+ setLoading(false);
97
+ }
98
+ }, [request]);
99
+
100
+ const updateMethod = (method: HttpMethod) => {
101
+ setRequest((prev) => ({ ...prev, method }));
102
+ };
103
+
104
+ const updateUrl = (url: string) => {
105
+ setRequest((prev) => ({ ...prev, url }));
106
+ };
107
+
108
+ return (
109
+ <div className="flex flex-col h-full">
110
+ {/* URL Bar */}
111
+ <div className="flex items-center gap-2 p-3 border-b border-border">
112
+ <select
113
+ value={request.method}
114
+ onChange={(e) => updateMethod(e.target.value as HttpMethod)}
115
+ className={cn(
116
+ 'px-3 py-2 rounded-md border border-border bg-background text-sm font-medium font-mono',
117
+ request.method === 'GET' && 'text-method-get',
118
+ request.method === 'POST' && 'text-method-post',
119
+ request.method === 'PUT' && 'text-method-put',
120
+ request.method === 'DELETE' && 'text-method-delete',
121
+ request.method === 'PATCH' && 'text-method-patch',
122
+ )}
123
+ >
124
+ {METHODS.map((m) => (
125
+ <option key={m} value={m}>{m}</option>
126
+ ))}
127
+ </select>
128
+ <input
129
+ type="text"
130
+ value={request.url}
131
+ onChange={(e) => updateUrl(e.target.value)}
132
+ onKeyDown={(e) => e.key === 'Enter' && sendRequest()}
133
+ placeholder="Enter request URL (e.g., https://api.example.com/users)"
134
+ className="flex-1 px-3 py-2 rounded-md border border-border bg-background text-foreground text-sm font-mono placeholder:text-muted-foreground"
135
+ />
136
+ <button
137
+ onClick={sendRequest}
138
+ disabled={loading || !request.url.trim()}
139
+ className="px-6 py-2 rounded-md bg-primary text-primary-foreground font-medium text-sm hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
140
+ >
141
+ {loading ? 'Sending...' : 'Send'}
142
+ </button>
143
+ </div>
144
+
145
+ {/* Request/Response Split */}
146
+ <div className="flex flex-1 overflow-hidden">
147
+ {/* Request Panel */}
148
+ <div className="w-1/2 border-r border-border flex flex-col overflow-hidden">
149
+ {/* Tabs */}
150
+ <div className="flex border-b border-border">
151
+ {['params', 'headers', 'body', 'auth'].map((tab) => (
152
+ <button
153
+ key={tab}
154
+ onClick={() => setRequest((prev) => ({ ...prev, activeTab: tab }))}
155
+ className={cn(
156
+ 'px-4 py-2 text-sm capitalize transition-colors',
157
+ request.activeTab === tab
158
+ ? 'text-primary border-b-2 border-primary'
159
+ : 'text-muted-foreground hover:text-foreground',
160
+ )}
161
+ >
162
+ {tab}
163
+ </button>
164
+ ))}
165
+ </div>
166
+
167
+ {/* Tab Content */}
168
+ <div className="flex-1 overflow-auto p-4">
169
+ {request.activeTab === 'params' && (
170
+ <div>
171
+ <div className="text-sm text-muted-foreground mb-2">Query Parameters</div>
172
+ <textarea
173
+ value={request.headers.map((h) => `${h.key}: ${h.value}`).join('\n')}
174
+ readOnly
175
+ className="w-full h-32 px-3 py-2 rounded-md border border-border bg-background text-foreground font-mono text-sm"
176
+ placeholder="No parameters"
177
+ />
178
+ </div>
179
+ )}
180
+ {request.activeTab === 'headers' && (
181
+ <div>
182
+ <div className="text-sm text-muted-foreground mb-2">Request Headers</div>
183
+ <textarea
184
+ value={request.headers.map((h) => `${h.key}: ${h.value}`).join('\n')}
185
+ readOnly
186
+ className="w-full h-32 px-3 py-2 rounded-md border border-border bg-background text-foreground font-mono text-sm"
187
+ placeholder="No headers"
188
+ />
189
+ </div>
190
+ )}
191
+ {request.activeTab === 'body' && (
192
+ <div>
193
+ <div className="flex items-center gap-2 mb-2">
194
+ <select
195
+ value={request.bodyType}
196
+ onChange={(e) => setRequest((prev) => ({ ...prev, bodyType: e.target.value }))}
197
+ className="px-2 py-1 rounded border border-border bg-background text-sm text-foreground"
198
+ >
199
+ <option value="none">none</option>
200
+ <option value="json">JSON</option>
201
+ <option value="text">Text</option>
202
+ <option value="xml">XML</option>
203
+ <option value="urlencoded">Form URL Encoded</option>
204
+ </select>
205
+ </div>
206
+ {request.bodyType !== 'none' && (
207
+ <textarea
208
+ value={request.body}
209
+ onChange={(e) => setRequest((prev) => ({ ...prev, body: e.target.value }))}
210
+ className="w-full h-64 px-3 py-2 rounded-md border border-border bg-background text-foreground font-mono text-sm resize-y"
211
+ placeholder={`Enter ${request.bodyType} body...`}
212
+ />
213
+ )}
214
+ </div>
215
+ )}
216
+ {request.activeTab === 'auth' && (
217
+ <div className="text-sm text-muted-foreground">
218
+ <p>Configure authentication in the request settings.</p>
219
+ </div>
220
+ )}
221
+ </div>
222
+ </div>
223
+
224
+ {/* Response Panel */}
225
+ <div className="w-1/2 flex flex-col overflow-hidden">
226
+ {error && (
227
+ <div className="p-3 bg-destructive/10 text-destructive text-sm border-b border-border">
228
+ {error}
229
+ </div>
230
+ )}
231
+ {response ? (
232
+ <>
233
+ <div className="flex items-center gap-4 p-3 border-b border-border">
234
+ <span className={cn(
235
+ 'px-2 py-1 rounded text-sm font-mono font-medium',
236
+ response.status < 300 && 'bg-green-500/10 text-green-400',
237
+ response.status >= 300 && response.status < 400 && 'bg-blue-500/10 text-blue-400',
238
+ response.status >= 400 && response.status < 500 && 'bg-orange-500/10 text-orange-400',
239
+ response.status >= 500 && 'bg-red-500/10 text-red-400',
240
+ )}>
241
+ {response.status} {response.statusText}
242
+ </span>
243
+ <span className="text-sm text-muted-foreground">
244
+ {response.duration}ms
245
+ </span>
246
+ <span className="text-sm text-muted-foreground">
247
+ {formatBytes(response.size)}
248
+ </span>
249
+ </div>
250
+ <div className="flex-1 overflow-auto p-4">
251
+ <pre className="text-sm font-mono text-foreground whitespace-pre-wrap break-all">
252
+ {tryFormatJson(response.body)}
253
+ </pre>
254
+ </div>
255
+ </>
256
+ ) : (
257
+ <div className="flex-1 flex items-center justify-center text-muted-foreground">
258
+ <div className="text-center">
259
+ <p className="text-lg mb-2">No response yet</p>
260
+ <p className="text-sm">Enter a URL and click Send to make a request</p>
261
+ </div>
262
+ </div>
263
+ )}
264
+ </div>
265
+ </div>
266
+ </div>
267
+ );
268
+ }
269
+
270
+ function formatBytes(bytes: number): string {
271
+ if (bytes === 0) return '0 B';
272
+ const k = 1024;
273
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
274
+ const units = ['B', 'KB', 'MB', 'GB'];
275
+ return `${(bytes / Math.pow(k, i)).toFixed(1)} ${units[i] ?? 'B'}`;
276
+ }
277
+
278
+ function tryFormatJson(body: string): string {
279
+ try {
280
+ return JSON.stringify(JSON.parse(body), null, 2);
281
+ } catch {
282
+ return body;
283
+ }
284
+ }
@@ -0,0 +1,17 @@
1
+ import { NextRequest } from 'next/server';
2
+ import { success, error } from '@/lib/utils/api-response';
3
+ import { AiDiscoverSchema } from '@/lib/utils/validation';
4
+ import { discoverEndpoints } from '@/lib/ai/endpoint-discovery';
5
+
6
+ export async function POST(req: NextRequest): Promise<Response> {
7
+ try {
8
+ const body = await req.json();
9
+ const parsed = AiDiscoverSchema.parse(body);
10
+
11
+ const endpoints = discoverEndpoints(parsed.code, parsed.framework);
12
+
13
+ return success({ endpoints, count: endpoints.length }) as Response;
14
+ } catch (err) {
15
+ return error(err) as Response;
16
+ }
17
+ }
@@ -0,0 +1,29 @@
1
+ import { NextRequest } from 'next/server';
2
+ import { success, error } from '@/lib/utils/api-response';
3
+ import { AiExplainSchema } from '@/lib/utils/validation';
4
+ import { explainResponse } from '@/lib/ai/explainer';
5
+
6
+ export async function POST(req: NextRequest): Promise<Response> {
7
+ try {
8
+ const body = await req.json();
9
+ const parsed = AiExplainSchema.parse(body);
10
+
11
+ const responseData = typeof parsed.response === 'object' && parsed.response !== null && 'status' in parsed.response
12
+ ? parsed.response as { status: number; statusText: string; headers: Record<string, string>; body: string }
13
+ : { status: 200, statusText: 'OK', headers: {}, body: JSON.stringify(parsed.response) };
14
+
15
+ const explanation = await explainResponse(
16
+ {
17
+ response: responseData,
18
+ question: parsed.question,
19
+ },
20
+ process.env['OPENAI_API_KEY'],
21
+ process.env['OPENAI_BASE_URL'],
22
+ process.env['OPENAI_MODEL'],
23
+ );
24
+
25
+ return success({ explanation }) as Response;
26
+ } catch (err) {
27
+ return error(err) as Response;
28
+ }
29
+ }
@@ -0,0 +1,37 @@
1
+ import { NextRequest } from 'next/server';
2
+ import { success, error } from '@/lib/utils/api-response';
3
+ import { AiGenerateTestsSchema } from '@/lib/utils/validation';
4
+ import { generateTests } from '@/lib/ai/test-generator';
5
+
6
+ export async function POST(req: NextRequest): Promise<Response> {
7
+ try {
8
+ const body = await req.json();
9
+ const parsed = AiGenerateTestsSchema.parse(body);
10
+
11
+ const tests = await generateTests(
12
+ {
13
+ method: 'GET',
14
+ url: '',
15
+ headers: {},
16
+ },
17
+ {
18
+ status: typeof parsed.response === 'object' && parsed.response !== null && 'status' in parsed.response
19
+ ? (parsed.response as { status: number }).status
20
+ : 200,
21
+ statusText: 'OK',
22
+ headers: {},
23
+ body: typeof parsed.response === 'string'
24
+ ? parsed.response
25
+ : JSON.stringify(parsed.response, null, 2),
26
+ duration: 0,
27
+ },
28
+ process.env['OPENAI_API_KEY'],
29
+ process.env['OPENAI_BASE_URL'],
30
+ process.env['OPENAI_MODEL'],
31
+ );
32
+
33
+ return success({ tests }) as Response;
34
+ } catch (err) {
35
+ return error(err) as Response;
36
+ }
37
+ }
@@ -0,0 +1,29 @@
1
+ import { NextRequest } from 'next/server';
2
+ import { success, error } from '@/lib/utils/api-response';
3
+ import { AiSuggestSchema } from '@/lib/utils/validation';
4
+ import { suggestImprovements } from '@/lib/ai/suggester';
5
+
6
+ export async function POST(req: NextRequest): Promise<Response> {
7
+ try {
8
+ const body = await req.json();
9
+ const parsed = AiSuggestSchema.parse(body);
10
+
11
+ const requestData = typeof parsed.request === 'object' && parsed.request !== null
12
+ ? parsed.request as { method: string; url: string; headers: Record<string, string>; body?: string; bodyType?: string; auth?: unknown }
13
+ : { method: 'GET', url: String(parsed.request), headers: {} };
14
+
15
+ const suggestions = await suggestImprovements(
16
+ {
17
+ request: requestData,
18
+ response: parsed.response as { status: number; statusText: string; headers: Record<string, string>; body: string; duration: number } | undefined,
19
+ },
20
+ process.env['OPENAI_API_KEY'],
21
+ process.env['OPENAI_BASE_URL'],
22
+ process.env['OPENAI_MODEL'],
23
+ );
24
+
25
+ return success({ suggestions }) as Response;
26
+ } catch (err) {
27
+ return error(err) as Response;
28
+ }
29
+ }
@@ -0,0 +1,66 @@
1
+ import { NextRequest } from 'next/server';
2
+ import { prisma } from '@/lib/db/client';
3
+ import { success, noContent, error } from '@/lib/utils/api-response';
4
+ import { NotFoundError } from '@/lib/utils/errors';
5
+ import { UpdateCollectionSchema } from '@/lib/utils/validation';
6
+
7
+ interface RouteContext {
8
+ params: { id: string };
9
+ }
10
+
11
+ export async function GET(_req: NextRequest, context: RouteContext): Promise<Response> {
12
+ try {
13
+ const { id } = context.params;
14
+ const collection = await prisma.collection.findUnique({
15
+ where: { id },
16
+ include: {
17
+ requests: { orderBy: { sortOrder: 'asc' } },
18
+ children: { include: { requests: { orderBy: { sortOrder: 'asc' } } } },
19
+ },
20
+ });
21
+
22
+ if (!collection) {
23
+ throw new NotFoundError('Collection', id);
24
+ }
25
+
26
+ return success(collection) as Response;
27
+ } catch (err) {
28
+ return error(err) as Response;
29
+ }
30
+ }
31
+
32
+ export async function PUT(req: NextRequest, context: RouteContext): Promise<Response> {
33
+ try {
34
+ const { id } = context.params;
35
+ const body = await req.json();
36
+ const parsed = UpdateCollectionSchema.parse({ ...body, id });
37
+
38
+ const collection = await prisma.collection.update({
39
+ where: { id },
40
+ data: {
41
+ name: parsed.name,
42
+ description: parsed.description,
43
+ parentId: parsed.parentId,
44
+ variables: parsed.variables,
45
+ preScript: parsed.preScript,
46
+ postScript: parsed.postScript,
47
+ sortOrder: parsed.sortOrder,
48
+ },
49
+ });
50
+
51
+ return success(collection) as Response;
52
+ } catch (err) {
53
+ return error(err) as Response;
54
+ }
55
+ }
56
+
57
+ export async function DELETE(_req: NextRequest, context: RouteContext): Promise<Response> {
58
+ try {
59
+ const { id } = context.params;
60
+
61
+ await prisma.collection.delete({ where: { id } });
62
+ return noContent() as Response;
63
+ } catch (err) {
64
+ return error(err) as Response;
65
+ }
66
+ }
@@ -0,0 +1,48 @@
1
+ import { NextRequest } from 'next/server';
2
+ import { prisma } from '@/lib/db/client';
3
+ import { success, created, error } from '@/lib/utils/api-response';
4
+ import { CreateCollectionSchema } from '@/lib/utils/validation';
5
+
6
+ export async function GET(req: NextRequest): Promise<Response> {
7
+ try {
8
+ const { searchParams } = new URL(req.url);
9
+ const workspaceId = searchParams.get('workspaceId');
10
+
11
+ const collections = await prisma.collection.findMany({
12
+ where: workspaceId ? { workspaceId, parentId: null } : { parentId: null },
13
+ include: {
14
+ requests: { orderBy: { sortOrder: 'asc' } },
15
+ children: { include: { requests: { orderBy: { sortOrder: 'asc' } } } },
16
+ },
17
+ orderBy: { sortOrder: 'asc' },
18
+ });
19
+
20
+ return success(collections) as Response;
21
+ } catch (err) {
22
+ return error(err) as Response;
23
+ }
24
+ }
25
+
26
+ export async function POST(req: NextRequest): Promise<Response> {
27
+ try {
28
+ const body = await req.json();
29
+ const parsed = CreateCollectionSchema.parse(body);
30
+
31
+ const collection = await prisma.collection.create({
32
+ data: {
33
+ name: parsed.name,
34
+ description: parsed.description,
35
+ workspaceId: parsed.workspaceId,
36
+ parentId: parsed.parentId,
37
+ variables: parsed.variables,
38
+ preScript: parsed.preScript,
39
+ postScript: parsed.postScript,
40
+ sortOrder: parsed.sortOrder,
41
+ },
42
+ });
43
+
44
+ return created(collection) as Response;
45
+ } catch (err) {
46
+ return error(err) as Response;
47
+ }
48
+ }
@@ -0,0 +1,40 @@
1
+ import { NextRequest } from 'next/server';
2
+ import { prisma } from '@/lib/db/client';
3
+ import { success, created, error } from '@/lib/utils/api-response';
4
+ import { CreateEnvironmentSchema } from '@/lib/utils/validation';
5
+
6
+ export async function GET(req: NextRequest): Promise<Response> {
7
+ try {
8
+ const { searchParams } = new URL(req.url);
9
+ const workspaceId = searchParams.get('workspaceId');
10
+
11
+ const environments = await prisma.environment.findMany({
12
+ where: workspaceId ? { workspaceId } : undefined,
13
+ orderBy: { createdAt: 'asc' },
14
+ });
15
+
16
+ return success(environments) as Response;
17
+ } catch (err) {
18
+ return error(err) as Response;
19
+ }
20
+ }
21
+
22
+ export async function POST(req: NextRequest): Promise<Response> {
23
+ try {
24
+ const body = await req.json();
25
+ const parsed = CreateEnvironmentSchema.parse(body);
26
+
27
+ const environment = await prisma.environment.create({
28
+ data: {
29
+ name: parsed.name,
30
+ workspaceId: parsed.workspaceId,
31
+ variables: parsed.variables,
32
+ isDefault: parsed.isDefault,
33
+ },
34
+ });
35
+
36
+ return created(environment) as Response;
37
+ } catch (err) {
38
+ return error(err) as Response;
39
+ }
40
+ }
@@ -0,0 +1,17 @@
1
+ import { NextRequest } from 'next/server';
2
+ import { success, error } from '@/lib/utils/api-response';
3
+ import { exportToOpenApi, type ExportCollection } from '@/lib/exporters/openapi';
4
+
5
+ export async function POST(req: NextRequest): Promise<Response> {
6
+ try {
7
+ const body = await req.json() as {
8
+ collection: ExportCollection;
9
+ };
10
+
11
+ const spec = exportToOpenApi(body.collection);
12
+
13
+ return success({ spec, format: 'openapi-3.1' }) as Response;
14
+ } catch (err) {
15
+ return error(err) as Response;
16
+ }
17
+ }
@@ -0,0 +1,18 @@
1
+ import { NextRequest } from 'next/server';
2
+ import { success, error } from '@/lib/utils/api-response';
3
+ import { exportToPostman } from '@/lib/exporters/postman';
4
+ import type { ExportCollection } from '@/lib/exporters/openapi';
5
+
6
+ export async function POST(req: NextRequest): Promise<Response> {
7
+ try {
8
+ const body = await req.json() as {
9
+ collection: ExportCollection;
10
+ };
11
+
12
+ const spec = exportToPostman(body.collection);
13
+
14
+ return success({ spec, format: 'postman-v2.1' }) as Response;
15
+ } catch (err) {
16
+ return error(err) as Response;
17
+ }
18
+ }