@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,193 @@
1
+ export interface ExportRequest {
2
+ name: string;
3
+ method: string;
4
+ url: string;
5
+ description?: string;
6
+ headers: { key: string; value: string }[];
7
+ params: { key: string; value: string }[];
8
+ body?: string;
9
+ bodyType?: string;
10
+ }
11
+
12
+ export interface ExportCollection {
13
+ name: string;
14
+ description?: string;
15
+ requests: ExportRequest[];
16
+ baseUrl?: string;
17
+ }
18
+
19
+ /**
20
+ * Export a collection to OpenAPI 3.1 specification.
21
+ */
22
+ export function exportToOpenApi(collection: ExportCollection): string {
23
+ const spec: Record<string, unknown> = {
24
+ openapi: '3.1.0',
25
+ info: {
26
+ title: collection.name,
27
+ description: collection.description,
28
+ version: '1.0.0',
29
+ },
30
+ servers: collection.baseUrl
31
+ ? [{ url: collection.baseUrl }]
32
+ : [],
33
+ paths: {},
34
+ components: {
35
+ schemas: {},
36
+ securitySchemes: {},
37
+ },
38
+ };
39
+
40
+ const paths: Record<string, Record<string, unknown>> = {};
41
+ const schemas: Record<string, Record<string, unknown>> = {};
42
+
43
+ for (const req of collection.requests) {
44
+ const path = extractPath(req.url);
45
+ const method = req.method.toLowerCase();
46
+
47
+ if (!paths[path]) {
48
+ paths[path] = {} as Record<string, unknown>;
49
+ }
50
+
51
+ const operation: Record<string, unknown> = {
52
+ operationId: toOperationId(method, path),
53
+ summary: req.name,
54
+ description: req.description,
55
+ tags: [collection.name],
56
+ parameters: buildParameters(req),
57
+ responses: {},
58
+ };
59
+
60
+ // Build request body
61
+ if (req.body && req.bodyType !== 'none') {
62
+ const contentType = req.bodyType === 'json' ? 'application/json'
63
+ : req.bodyType === 'urlencoded' ? 'application/x-www-form-urlencoded'
64
+ : req.bodyType === 'form' ? 'multipart/form-data'
65
+ : 'text/plain';
66
+
67
+ let schema: Record<string, unknown>;
68
+ if (req.bodyType === 'json') {
69
+ try {
70
+ const parsed = JSON.parse(req.body);
71
+ schema = inferSchema(parsed, req.name);
72
+ } catch {
73
+ schema = { type: 'string' };
74
+ }
75
+ } else {
76
+ schema = { type: 'string' };
77
+ }
78
+
79
+ const schemaName = `${toPascalCase(req.name)}Body`;
80
+ schemas[schemaName] = schema;
81
+
82
+ operation['requestBody'] = {
83
+ required: true,
84
+ content: {
85
+ [contentType]: {
86
+ schema: { $ref: `#/components/schemas/${schemaName}` },
87
+ },
88
+ },
89
+ };
90
+ }
91
+
92
+ // Add default response
93
+ operation['responses'] = {
94
+ '200': {
95
+ description: 'Successful response',
96
+ },
97
+ };
98
+
99
+ paths[path]![method] = operation;
100
+ }
101
+
102
+ spec['paths'] = paths;
103
+ (spec['components'] as Record<string, unknown>)['schemas'] = schemas;
104
+
105
+ return JSON.stringify(spec, null, 2);
106
+ }
107
+
108
+ function extractPath(url: string): string {
109
+ try {
110
+ const urlObj = new URL(url);
111
+ return urlObj.pathname;
112
+ } catch {
113
+ // Might be a path without host
114
+ if (url.startsWith('/')) return url;
115
+ return `/${url}`;
116
+ }
117
+ }
118
+
119
+ function toOperationId(method: string, path: string): string {
120
+ const segments = path
121
+ .split('/')
122
+ .filter(Boolean)
123
+ .map((s) => s.replace(/[{}]/g, '').replace(/[^a-zA-Z0-9]/g, '_'));
124
+
125
+ return `${method}${segments.map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join('')}`;
126
+ }
127
+
128
+ function toPascalCase(str: string): string {
129
+ return str
130
+ .replace(/[^a-zA-Z0-9]+(.)/g, (_, c: string) => c.toUpperCase())
131
+ .replace(/^[a-z]/, (c) => c.toUpperCase())
132
+ .replace(/[^a-zA-Z0-9]/g, '');
133
+ }
134
+
135
+ function buildParameters(req: ExportRequest): Array<Record<string, unknown>> {
136
+ const parameters: Array<Record<string, unknown>> = [];
137
+
138
+ for (const param of req.params) {
139
+ parameters.push({
140
+ name: param.key,
141
+ in: 'query',
142
+ required: false,
143
+ schema: { type: 'string' },
144
+ });
145
+ }
146
+
147
+ for (const header of req.headers) {
148
+ const name = header.key.toLowerCase();
149
+ if (name === 'authorization' || name === 'content-type' || name === 'accept') continue;
150
+
151
+ parameters.push({
152
+ name: header.key,
153
+ in: 'header',
154
+ required: false,
155
+ schema: { type: 'string' },
156
+ });
157
+ }
158
+
159
+ return parameters;
160
+ }
161
+
162
+ function inferSchema(data: unknown, prefix: string): Record<string, unknown> {
163
+ if (data === null) return { type: 'string', nullable: true };
164
+ if (typeof data === 'string') return { type: 'string', example: data };
165
+ if (typeof data === 'number') return Number.isInteger(data) ? { type: 'integer', example: data } : { type: 'number', example: data };
166
+ if (typeof data === 'boolean') return { type: 'boolean', example: data };
167
+
168
+ if (Array.isArray(data)) {
169
+ if (data.length === 0) return { type: 'array', items: {} };
170
+ return {
171
+ type: 'array',
172
+ items: inferSchema(data[0], prefix),
173
+ example: data,
174
+ };
175
+ }
176
+
177
+ if (typeof data === 'object') {
178
+ const properties: Record<string, unknown> = {};
179
+ const obj = data as Record<string, unknown>;
180
+
181
+ for (const [key, value] of Object.entries(obj)) {
182
+ properties[key] = inferSchema(value, `${prefix}_${key}`);
183
+ }
184
+
185
+ return {
186
+ type: 'object',
187
+ properties,
188
+ example: data,
189
+ };
190
+ }
191
+
192
+ return { type: 'string' };
193
+ }
@@ -0,0 +1,140 @@
1
+ import type { ExportRequest, ExportCollection } from './openapi';
2
+
3
+ export type { ExportCollection };
4
+
5
+ /**
6
+ * Export a collection to Postman Collection v2.1 format.
7
+ */
8
+ export function exportToPostman(collection: ExportCollection): string {
9
+ const postmanCollection = {
10
+ info: {
11
+ _postman_id: generateId(),
12
+ name: collection.name,
13
+ description: collection.description,
14
+ schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json',
15
+ },
16
+ item: collection.requests.map((req) => buildPostmanItem(req)),
17
+ variable: [],
18
+ };
19
+
20
+ return JSON.stringify(postmanCollection, null, 2);
21
+ }
22
+
23
+ interface PostmanItem {
24
+ name: string;
25
+ request: {
26
+ method: string;
27
+ header: Array<{ key: string; value: string; type: string }>;
28
+ body?: {
29
+ mode: string;
30
+ raw?: string;
31
+ urlencoded?: Array<{ key: string; value: string; type: string }>;
32
+ formdata?: Array<{ key: string; value: string; type: string }>;
33
+ };
34
+ url: {
35
+ raw: string;
36
+ protocol: string;
37
+ host: string[];
38
+ path: string[];
39
+ query: Array<{ key: string; value: string }>;
40
+ };
41
+ auth?: {
42
+ type: string;
43
+ bearer?: Array<{ key: string; value: string; type: string }>;
44
+ };
45
+ description?: string;
46
+ };
47
+ response: unknown[];
48
+ }
49
+
50
+ function buildPostmanItem(req: ExportRequest): PostmanItem {
51
+ const urlParts = parseUrl(req.url);
52
+
53
+ const item: PostmanItem = {
54
+ name: req.name,
55
+ request: {
56
+ method: req.method.toUpperCase(),
57
+ header: req.headers.map((h) => ({
58
+ key: h.key,
59
+ value: h.value,
60
+ type: 'text',
61
+ })),
62
+ url: {
63
+ raw: req.url,
64
+ protocol: urlParts.protocol,
65
+ host: urlParts.host,
66
+ path: urlParts.path,
67
+ query: req.params.map((p) => ({
68
+ key: p.key,
69
+ value: p.value,
70
+ })),
71
+ },
72
+ description: req.description,
73
+ },
74
+ response: [],
75
+ };
76
+
77
+ // Build body
78
+ if (req.body && req.bodyType !== 'none') {
79
+ switch (req.bodyType) {
80
+ case 'json':
81
+ case 'raw':
82
+ item.request.body = {
83
+ mode: 'raw',
84
+ raw: req.body,
85
+ };
86
+ break;
87
+ case 'urlencoded':
88
+ try {
89
+ const params = new URLSearchParams(req.body);
90
+ item.request.body = {
91
+ mode: 'urlencoded',
92
+ urlencoded: Array.from(params.entries()).map(([key, value]) => ({
93
+ key,
94
+ value,
95
+ type: 'text',
96
+ })),
97
+ };
98
+ } catch {
99
+ item.request.body = { mode: 'raw', raw: req.body };
100
+ }
101
+ break;
102
+ case 'form':
103
+ item.request.body = {
104
+ mode: 'formdata',
105
+ formdata: req.body.split('&').map((pair) => {
106
+ const [key, value] = pair.split('=');
107
+ return { key: key ?? '', value: value ?? '', type: 'text' };
108
+ }),
109
+ };
110
+ break;
111
+ }
112
+ }
113
+
114
+ return item;
115
+ }
116
+
117
+ function parseUrl(url: string): { protocol: string; host: string[]; path: string[] } {
118
+ try {
119
+ const urlObj = new URL(url);
120
+ return {
121
+ protocol: urlObj.protocol.replace(':', ''),
122
+ host: urlObj.hostname.split('.'),
123
+ path: urlObj.pathname.split('/').filter(Boolean),
124
+ };
125
+ } catch {
126
+ return {
127
+ protocol: 'https',
128
+ host: ['example', 'com'],
129
+ path: url.split('/').filter(Boolean),
130
+ };
131
+ }
132
+ }
133
+
134
+ function generateId(): string {
135
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
136
+ const r = Math.random() * 16 | 0;
137
+ const v = c === 'x' ? r : (r & 0x3 | 0x8);
138
+ return v.toString(16);
139
+ });
140
+ }
@@ -0,0 +1,249 @@
1
+ /**
2
+ * GraphQL Query Builder — fluent API for constructing GraphQL queries and mutations.
3
+ */
4
+
5
+ import type {
6
+ GraphQLOperationType,
7
+ GraphQLVariable,
8
+ GraphQLField,
9
+ GraphQLFragment,
10
+ GraphQLQuery,
11
+ } from './types';
12
+
13
+ export class QueryBuilder {
14
+ private operationType: GraphQLOperationType = 'query';
15
+ private operationName?: string;
16
+ private variables: GraphQLVariable[] = [];
17
+ private fields: GraphQLField[] = [];
18
+ private fragments: GraphQLFragment[] = [];
19
+
20
+ /**
21
+ * Start a query operation.
22
+ */
23
+ query(name?: string): this {
24
+ this.operationType = 'query';
25
+ this.operationName = name;
26
+ return this;
27
+ }
28
+
29
+ /**
30
+ * Start a mutation operation.
31
+ */
32
+ mutation(name?: string): this {
33
+ this.operationType = 'mutation';
34
+ this.operationName = name;
35
+ return this;
36
+ }
37
+
38
+ /**
39
+ * Start a subscription operation.
40
+ */
41
+ subscription(name?: string): this {
42
+ this.operationType = 'subscription';
43
+ this.operationName = name;
44
+ return this;
45
+ }
46
+
47
+ /**
48
+ * Add a field to the selection set.
49
+ * Supports nested subfields and arguments.
50
+ */
51
+ field(name: string, args?: Record<string, string>, subfields?: GraphQLField[] | string[]): this {
52
+ const fieldEntry: GraphQLField = { name };
53
+
54
+ if (args && Object.keys(args).length > 0) {
55
+ fieldEntry.arguments = Object.entries(args).map(([key, value]) => ({
56
+ name: key,
57
+ value,
58
+ }));
59
+ }
60
+
61
+ if (subfields && subfields.length > 0) {
62
+ fieldEntry.subfields = subfields.map((sf) =>
63
+ typeof sf === 'string' ? { name: sf } : sf,
64
+ );
65
+ }
66
+
67
+ this.fields.push(fieldEntry);
68
+ return this;
69
+ }
70
+
71
+ /**
72
+ * Add a field with an alias.
73
+ */
74
+ aliasField(alias: string, name: string, args?: Record<string, string>, subfields?: GraphQLField[] | string[]): this {
75
+ const fieldEntry: GraphQLField = { name, alias };
76
+
77
+ if (args && Object.keys(args).length > 0) {
78
+ fieldEntry.arguments = Object.entries(args).map(([key, value]) => ({
79
+ name: key,
80
+ value,
81
+ }));
82
+ }
83
+
84
+ if (subfields && subfields.length > 0) {
85
+ fieldEntry.subfields = subfields.map((sf) =>
86
+ typeof sf === 'string' ? { name: sf } : sf,
87
+ );
88
+ }
89
+
90
+ this.fields.push(fieldEntry);
91
+ return this;
92
+ }
93
+
94
+ /**
95
+ * Add a variable definition.
96
+ */
97
+ variable(name: string, type: string, defaultValue?: string): this {
98
+ this.variables.push({ name, type, defaultValue });
99
+ return this;
100
+ }
101
+
102
+ /**
103
+ * Define a fragment that can be spread into fields.
104
+ */
105
+ fragment(name: string, onType: string, fields: GraphQLField[] | string[]): this {
106
+ this.fragments.push({
107
+ name,
108
+ onType,
109
+ fields: fields.map((f) => (typeof f === 'string' ? { name: f } : f)),
110
+ });
111
+ return this;
112
+ }
113
+
114
+ /**
115
+ * Spread a fragment into the top-level selection set.
116
+ */
117
+ spreadFragment(fragmentName: string): this {
118
+ this.fields.push({ name: `...${fragmentName}`, fragments: [fragmentName] });
119
+ return this;
120
+ }
121
+
122
+ /**
123
+ * Build the GraphQL query string.
124
+ */
125
+ build(): string {
126
+ const parts: string[] = [];
127
+
128
+ // Build fragments first
129
+ for (const fragment of this.fragments) {
130
+ parts.push(buildFragmentString(fragment));
131
+ }
132
+
133
+ // Build the main operation
134
+ parts.push(buildOperationString(
135
+ this.operationType,
136
+ this.operationName,
137
+ this.variables,
138
+ this.fields,
139
+ ));
140
+
141
+ return parts.join('\n\n');
142
+ }
143
+
144
+ /**
145
+ * Build and return the structured query object.
146
+ */
147
+ toQuery(): GraphQLQuery {
148
+ return {
149
+ operationType: this.operationType,
150
+ name: this.operationName,
151
+ variables: [...this.variables],
152
+ fields: [...this.fields],
153
+ fragments: [...this.fragments],
154
+ };
155
+ }
156
+
157
+ /**
158
+ * Reset the builder for reuse.
159
+ */
160
+ reset(): this {
161
+ this.operationType = 'query';
162
+ this.operationName = undefined;
163
+ this.variables = [];
164
+ this.fields = [];
165
+ this.fragments = [];
166
+ return this;
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Build the operation string: query/mutation/subscription { ... }
172
+ */
173
+ function buildOperationString(
174
+ type: GraphQLOperationType,
175
+ name: string | undefined,
176
+ variables: GraphQLVariable[],
177
+ fields: GraphQLField[],
178
+ ): string {
179
+ let operation = type;
180
+
181
+ if (name) {
182
+ operation += ` ${name}`;
183
+ }
184
+
185
+ if (variables.length > 0) {
186
+ const varDefs = variables.map((v) => {
187
+ let def = `$${v.name}: ${v.type}`;
188
+ if (v.defaultValue !== undefined) {
189
+ def += ` = ${v.defaultValue}`;
190
+ }
191
+ return def;
192
+ });
193
+ operation += `(${varDefs.join(', ')})`;
194
+ }
195
+
196
+ const selectionSet = buildSelectionSet(fields);
197
+ return `${operation} ${selectionSet}`;
198
+ }
199
+
200
+ /**
201
+ * Build a selection set: { field1 field2(arg: val) { sub } }
202
+ */
203
+ function buildSelectionSet(fields: GraphQLField[]): string {
204
+ if (fields.length === 0) return '{}';
205
+
206
+ const parts = fields.map((field) => buildFieldString(field));
207
+ return `{ ${parts.join(' ')} }`;
208
+ }
209
+
210
+ /**
211
+ * Build a single field string, recursively handling subfields.
212
+ */
213
+ function buildFieldString(field: GraphQLField): string {
214
+ let str = '';
215
+
216
+ // Handle fragment spreads
217
+ if (field.name.startsWith('...')) {
218
+ return field.name;
219
+ }
220
+
221
+ // Alias
222
+ if (field.alias) {
223
+ str += `${field.alias}: `;
224
+ }
225
+
226
+ // Field name
227
+ str += field.name;
228
+
229
+ // Arguments
230
+ if (field.arguments && field.arguments.length > 0) {
231
+ const args = field.arguments.map((arg) => `${arg.name}: ${arg.value}`);
232
+ str += `(${args.join(', ')})`;
233
+ }
234
+
235
+ // Subfields
236
+ if (field.subfields && field.subfields.length > 0) {
237
+ str += ` ${buildSelectionSet(field.subfields)}`;
238
+ }
239
+
240
+ return str;
241
+ }
242
+
243
+ /**
244
+ * Build a fragment definition string.
245
+ */
246
+ function buildFragmentString(fragment: GraphQLFragment): string {
247
+ const selectionSet = buildSelectionSet(fragment.fields);
248
+ return `fragment ${fragment.name} on ${fragment.onType} ${selectionSet}`;
249
+ }