@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,147 @@
1
+ /**
2
+ * GraphQL Query Formatter — pretty-print GraphQL queries with proper indentation.
3
+ */
4
+
5
+ const INDENT = ' ';
6
+
7
+ /**
8
+ * Pretty-print a raw GraphQL query string with proper indentation.
9
+ * Handles queries, mutations, subscriptions, fragments, and inline fragments.
10
+ */
11
+ export function formatQuery(query: string): string {
12
+ const cleaned = removeComments(query).trim();
13
+ if (!cleaned) return '';
14
+
15
+ const tokens = tokenize(cleaned);
16
+ return formatTokens(tokens);
17
+ }
18
+
19
+ /**
20
+ * Minify a GraphQL query (remove whitespace/comments).
21
+ */
22
+ export function minifyQuery(query: string): string {
23
+ return removeComments(query)
24
+ .replace(/\s+/g, ' ')
25
+ .replace(/\s*({|}|\(|\)|:|,)\s*/g, '$1')
26
+ .trim();
27
+ }
28
+
29
+ /**
30
+ * Remove GraphQL comments (# ...) from a query string.
31
+ */
32
+ function removeComments(query: string): string {
33
+ return query.replace(/#[^\n]*/g, '');
34
+ }
35
+
36
+ /**
37
+ * Simple tokenizer — splits query into meaningful chunks.
38
+ */
39
+ function tokenize(query: string): string[] {
40
+ const tokens: string[] = [];
41
+ let current = '';
42
+ let inString = false;
43
+ let stringChar = '';
44
+
45
+ for (let i = 0; i < query.length; i++) {
46
+ const char = query[i];
47
+
48
+ if (inString) {
49
+ current += char;
50
+ if (char === stringChar && query[i - 1] !== '\\') {
51
+ inString = false;
52
+ tokens.push(current);
53
+ current = '';
54
+ }
55
+ continue;
56
+ }
57
+
58
+ if (char === '"' || char === "'") {
59
+ if (current.trim()) tokens.push(current.trim());
60
+ current = char;
61
+ inString = true;
62
+ stringChar = char;
63
+ continue;
64
+ }
65
+
66
+ if (char === '{' || char === '}' || char === '(' || char === ')') {
67
+ if (current.trim()) tokens.push(current.trim());
68
+ tokens.push(char);
69
+ current = '';
70
+ } else if (char === ':' || char === ',') {
71
+ if (current.trim()) tokens.push(current.trim());
72
+ tokens.push(char);
73
+ current = '';
74
+ } else {
75
+ current += char;
76
+ }
77
+ }
78
+
79
+ if (current.trim()) tokens.push(current.trim());
80
+ return tokens;
81
+ }
82
+
83
+ /**
84
+ * Format tokens into a properly indented string.
85
+ */
86
+ function formatTokens(tokens: string[]): string {
87
+ let result = '';
88
+ let indentLevel = 0;
89
+ let firstToken = true;
90
+ let afterColon = false;
91
+ let afterOpenParen = false;
92
+ let parenDepth = 0;
93
+
94
+ for (const token of tokens) {
95
+ switch (token) {
96
+ case '{':
97
+ result += ` {\n`;
98
+ indentLevel++;
99
+ afterColon = false;
100
+ break;
101
+
102
+ case '}':
103
+ indentLevel = Math.max(0, indentLevel - 1);
104
+ result += `${INDENT.repeat(indentLevel)}}\n`;
105
+ afterColon = false;
106
+ break;
107
+
108
+ case '(':
109
+ result += '(';
110
+ parenDepth++;
111
+ afterOpenParen = true;
112
+ break;
113
+
114
+ case ')':
115
+ parenDepth = Math.max(0, parenDepth - 1);
116
+ result += ')';
117
+ afterOpenParen = false;
118
+ break;
119
+
120
+ case ':':
121
+ result += ': ';
122
+ afterColon = true;
123
+ break;
124
+
125
+ case ',':
126
+ result += ', ';
127
+ afterColon = false;
128
+ break;
129
+
130
+ default:
131
+ if (firstToken) {
132
+ result += token;
133
+ firstToken = false;
134
+ } else if (afterOpenParen || parenDepth > 0) {
135
+ result += token;
136
+ } else if (afterColon) {
137
+ result += token;
138
+ afterColon = false;
139
+ } else {
140
+ result += `${INDENT.repeat(indentLevel)}${token}`;
141
+ }
142
+ break;
143
+ }
144
+ }
145
+
146
+ return result.trim();
147
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * GraphQL module — query builder, introspection, formatting, and validation.
3
+ */
4
+
5
+ export type {
6
+ GraphQLOperationType,
7
+ GraphQLVariable,
8
+ GraphQLField,
9
+ GraphQLArgument,
10
+ GraphQLFragment,
11
+ GraphQLQuery,
12
+ GraphQLSchemaType,
13
+ GraphQLSchemaField,
14
+ GraphQLSchemaArg,
15
+ GraphQLSchema,
16
+ GraphQLValidationError,
17
+ GraphQLValidationResult,
18
+ IntrospectionResponse,
19
+ } from './types';
20
+
21
+ export { QueryBuilder } from './builder';
22
+
23
+ export {
24
+ fetchSchema,
25
+ parseSchema,
26
+ getQueryFields,
27
+ getMutationFields,
28
+ getSubscriptionFields,
29
+ getTypeFields,
30
+ getTypesByKind,
31
+ findType,
32
+ unwrapTypeName,
33
+ isLeafType,
34
+ INTROSPECTION_QUERY,
35
+ } from './introspection';
36
+
37
+ export { formatQuery, minifyQuery } from './formatter';
38
+
39
+ export {
40
+ validateQuery,
41
+ validateQueryString,
42
+ validateAgainstSchema,
43
+ } from './validator';
@@ -0,0 +1,175 @@
1
+ /**
2
+ * GraphQL Schema Introspection — fetch, parse, and query a GraphQL schema.
3
+ */
4
+
5
+ import type {
6
+ GraphQLSchema,
7
+ GraphQLSchemaType,
8
+ GraphQLSchemaField,
9
+ IntrospectionResponse,
10
+ } from './types';
11
+
12
+ /** Full introspection query string */
13
+ const INTROSPECTION_QUERY = `
14
+ query IntrospectionQuery {
15
+ __schema {
16
+ queryType { name }
17
+ mutationType { name }
18
+ subscriptionType { name }
19
+ types {
20
+ kind
21
+ name
22
+ fields(includeDeprecated: true) {
23
+ name
24
+ description
25
+ args {
26
+ name
27
+ description
28
+ type {
29
+ kind
30
+ name
31
+ ofType { kind name ofType { kind name ofType { kind name } } }
32
+ }
33
+ defaultValue
34
+ }
35
+ type {
36
+ kind
37
+ name
38
+ ofType { kind name ofType { kind name ofType { kind name } } }
39
+ }
40
+ }
41
+ inputFields {
42
+ name
43
+ description
44
+ type {
45
+ kind
46
+ name
47
+ ofType { kind name ofType { kind name ofType { kind name } } }
48
+ }
49
+ defaultValue
50
+ }
51
+ enumValues(includeDeprecated: true) {
52
+ name
53
+ description
54
+ }
55
+ }
56
+ }
57
+ }
58
+ `;
59
+
60
+ /**
61
+ * Fetch the GraphQL schema from an endpoint via introspection.
62
+ */
63
+ export async function fetchSchema(
64
+ endpoint: string,
65
+ headers: Record<string, string> = {},
66
+ ): Promise<GraphQLSchema> {
67
+ const response = await fetch(endpoint, {
68
+ method: 'POST',
69
+ headers: {
70
+ 'Content-Type': 'application/json',
71
+ ...headers,
72
+ },
73
+ body: JSON.stringify({ query: INTROSPECTION_QUERY }),
74
+ });
75
+
76
+ if (!response.ok) {
77
+ throw new Error(`Introspection request failed: ${response.status} ${response.statusText}`);
78
+ }
79
+
80
+ const json = (await response.json()) as IntrospectionResponse;
81
+
82
+ if (json.errors && json.errors.length > 0) {
83
+ throw new Error(`Introspection errors: ${json.errors.map((e) => e.message).join(', ')}`);
84
+ }
85
+
86
+ if (!json.data?.__schema) {
87
+ throw new Error('No schema data returned from introspection query');
88
+ }
89
+
90
+ return json.data.__schema;
91
+ }
92
+
93
+ /**
94
+ * Parse a raw introspection response into our schema types.
95
+ */
96
+ export function parseSchema(schemaResponse: IntrospectionResponse): GraphQLSchema | null {
97
+ if (!schemaResponse.data?.__schema) return null;
98
+ return schemaResponse.data.__schema;
99
+ }
100
+
101
+ /**
102
+ * Get the available query fields from a schema.
103
+ */
104
+ export function getQueryFields(schema: GraphQLSchema): GraphQLSchemaField[] {
105
+ if (!schema.queryType?.name) return [];
106
+ const queryType = findType(schema, schema.queryType.name);
107
+ return queryType?.fields ?? [];
108
+ }
109
+
110
+ /**
111
+ * Get the available mutation fields from a schema.
112
+ */
113
+ export function getMutationFields(schema: GraphQLSchema): GraphQLSchemaField[] {
114
+ const name = schema.mutationType?.name;
115
+ if (!name) return [];
116
+ const mutationType = findType(schema, name);
117
+ return mutationType?.fields ?? [];
118
+ }
119
+
120
+ /**
121
+ * Get the available subscription fields from a schema.
122
+ */
123
+ export function getSubscriptionFields(schema: GraphQLSchema): GraphQLSchemaField[] {
124
+ const name = schema.subscriptionType?.name;
125
+ if (!name) return [];
126
+ const subscriptionType = findType(schema, name);
127
+ return subscriptionType?.fields ?? [];
128
+ }
129
+
130
+ /**
131
+ * Get the fields for a specific named type.
132
+ */
133
+ export function getTypeFields(schema: GraphQLSchema, typeName: string): GraphQLSchemaField[] {
134
+ const type = findType(schema, typeName);
135
+ return type?.fields ?? [];
136
+ }
137
+
138
+ /**
139
+ * Get all types of a specific kind from the schema.
140
+ */
141
+ export function getTypesByKind(
142
+ schema: GraphQLSchema,
143
+ kind: GraphQLSchemaType['kind'],
144
+ ): GraphQLSchemaType[] {
145
+ return schema.types.filter((t) => t.kind === kind);
146
+ }
147
+
148
+ /**
149
+ * Find a type by name in the schema.
150
+ */
151
+ export function findType(schema: GraphQLSchema, name: string): GraphQLSchemaType | undefined {
152
+ return schema.types.find((t) => t.name === name);
153
+ }
154
+
155
+ /**
156
+ * Get a human-readable type name, unwrapping NON_NULL and LIST wrappers.
157
+ */
158
+ export function unwrapTypeName(type: GraphQLSchemaType | undefined): string {
159
+ if (!type) return 'Unknown';
160
+ if (type.name) return type.name;
161
+ if (type.ofType) return unwrapTypeName(type.ofType);
162
+ return 'Unknown';
163
+ }
164
+
165
+ /**
166
+ * Check if a type is a leaf type (SCALAR or ENUM).
167
+ */
168
+ export function isLeafType(type: GraphQLSchemaType): boolean {
169
+ return type.kind === 'SCALAR' || type.kind === 'ENUM';
170
+ }
171
+
172
+ /**
173
+ * Export the introspection query string for custom use.
174
+ */
175
+ export { INTROSPECTION_QUERY };
@@ -0,0 +1,99 @@
1
+ /**
2
+ * GraphQL type definitions for the query builder, introspection, and validation.
3
+ */
4
+
5
+ /** Supported GraphQL operation types */
6
+ export type GraphQLOperationType = 'query' | 'mutation' | 'subscription';
7
+
8
+ /** A GraphQL variable definition */
9
+ export interface GraphQLVariable {
10
+ name: string;
11
+ type: string;
12
+ defaultValue?: string;
13
+ }
14
+
15
+ /** A field argument */
16
+ export interface GraphQLArgument {
17
+ name: string;
18
+ value: string;
19
+ }
20
+
21
+ /** A field within a selection set */
22
+ export interface GraphQLField {
23
+ name: string;
24
+ alias?: string;
25
+ arguments?: GraphQLArgument[];
26
+ subfields?: GraphQLField[];
27
+ fragments?: string[];
28
+ }
29
+
30
+ /** A named fragment definition */
31
+ export interface GraphQLFragment {
32
+ name: string;
33
+ onType: string;
34
+ fields: GraphQLField[];
35
+ }
36
+
37
+ /** A complete GraphQL query/mutation */
38
+ export interface GraphQLQuery {
39
+ operationType: GraphQLOperationType;
40
+ name?: string;
41
+ variables: GraphQLVariable[];
42
+ fields: GraphQLField[];
43
+ fragments: GraphQLFragment[];
44
+ }
45
+
46
+ /** Schema type information from introspection */
47
+ export interface GraphQLSchemaType {
48
+ name: string;
49
+ kind: 'SCALAR' | 'OBJECT' | 'INTERFACE' | 'UNION' | 'ENUM' | 'INPUT_OBJECT' | 'LIST' | 'NON_NULL';
50
+ fields?: GraphQLSchemaField[];
51
+ enumValues?: string[];
52
+ inputFields?: GraphQLSchemaField[];
53
+ ofType?: GraphQLSchemaType;
54
+ }
55
+
56
+ /** Schema field from introspection */
57
+ export interface GraphQLSchemaField {
58
+ name: string;
59
+ type: GraphQLSchemaType;
60
+ args: GraphQLSchemaArg[];
61
+ description?: string;
62
+ }
63
+
64
+ /** Schema argument from introspection */
65
+ export interface GraphQLSchemaArg {
66
+ name: string;
67
+ type: GraphQLSchemaType;
68
+ defaultValue?: string | null;
69
+ description?: string;
70
+ }
71
+
72
+ /** Full introspected schema */
73
+ export interface GraphQLSchema {
74
+ queryType?: { name: string };
75
+ mutationType?: { name: string | null };
76
+ subscriptionType?: { name: string | null };
77
+ types: GraphQLSchemaType[];
78
+ }
79
+
80
+ /** Validation error */
81
+ export interface GraphQLValidationError {
82
+ message: string;
83
+ line?: number;
84
+ column?: number;
85
+ }
86
+
87
+ /** Validation result */
88
+ export interface GraphQLValidationResult {
89
+ valid: boolean;
90
+ errors: GraphQLValidationError[];
91
+ }
92
+
93
+ /** Introspection response shape */
94
+ export interface IntrospectionResponse {
95
+ data?: {
96
+ __schema: GraphQLSchema;
97
+ };
98
+ errors?: Array<{ message: string }>;
99
+ }
@@ -0,0 +1,216 @@
1
+ /**
2
+ * GraphQL Query Validator — validate queries against known patterns and report errors.
3
+ *
4
+ * This is a structural/syntactic validator (not a full GraphQL spec validator).
5
+ * For full spec validation, pair with a server-side validation step.
6
+ */
7
+
8
+ import type {
9
+ GraphQLQuery,
10
+ GraphQLSchema,
11
+ GraphQLValidationError,
12
+ GraphQLValidationResult,
13
+ GraphQLField,
14
+ } from './types';
15
+
16
+ /**
17
+ * Validate a GraphQLQuery object for structural correctness.
18
+ */
19
+ export function validateQuery(query: GraphQLQuery): GraphQLValidationResult {
20
+ const errors: GraphQLValidationError[] = [];
21
+
22
+ // Check operation type
23
+ const validOps = ['query', 'mutation', 'subscription'];
24
+ if (!validOps.includes(query.operationType)) {
25
+ errors.push({
26
+ message: `Invalid operation type: "${query.operationType}". Must be one of: ${validOps.join(', ')}`,
27
+ });
28
+ }
29
+
30
+ // Check that the selection set is non-empty
31
+ if (query.fields.length === 0) {
32
+ errors.push({
33
+ message: 'Selection set is empty. At least one field is required.',
34
+ });
35
+ }
36
+
37
+ // Validate fields recursively
38
+ validateFields(query.fields, errors, []);
39
+
40
+ // Validate variables have names and types
41
+ for (const v of query.variables) {
42
+ if (!v.name) {
43
+ errors.push({ message: 'Variable definition is missing a name.' });
44
+ }
45
+ if (!v.type) {
46
+ errors.push({ message: `Variable "$${v.name}" is missing a type.` });
47
+ }
48
+ }
49
+
50
+ // Validate fragments
51
+ for (const f of query.fragments) {
52
+ if (!f.name) {
53
+ errors.push({ message: 'Fragment definition is missing a name.' });
54
+ }
55
+ if (!f.onType) {
56
+ errors.push({ message: `Fragment "${f.name}" is missing the "onType" condition.` });
57
+ }
58
+ if (f.fields.length === 0) {
59
+ errors.push({ message: `Fragment "${f.name}" has an empty selection set.` });
60
+ }
61
+ }
62
+
63
+ return { valid: errors.length === 0, errors };
64
+ }
65
+
66
+ /**
67
+ * Validate a raw GraphQL query string for basic syntax errors.
68
+ */
69
+ export function validateQueryString(queryString: string): GraphQLValidationResult {
70
+ const errors: GraphQLValidationError[] = [];
71
+ const trimmed = queryString.trim();
72
+
73
+ if (!trimmed) {
74
+ errors.push({ message: 'Query string is empty.' });
75
+ return { valid: false, errors };
76
+ }
77
+
78
+ // Check balanced braces
79
+ let braceDepth = 0;
80
+ let parenDepth = 0;
81
+ let inString = false;
82
+ let stringChar = '';
83
+
84
+ for (let i = 0; i < trimmed.length; i++) {
85
+ const char = trimmed[i];
86
+ const prev = i > 0 ? trimmed[i - 1] : '';
87
+
88
+ if (inString) {
89
+ if (char === stringChar && prev !== '\\') {
90
+ inString = false;
91
+ }
92
+ continue;
93
+ }
94
+
95
+ if (char === '"' || char === "'") {
96
+ inString = true;
97
+ stringChar = char;
98
+ continue;
99
+ }
100
+
101
+ if (char === '{') braceDepth++;
102
+ if (char === '}') {
103
+ braceDepth--;
104
+ if (braceDepth < 0) {
105
+ errors.push({ message: 'Unexpected closing brace "}"', column: i + 1 });
106
+ }
107
+ }
108
+ if (char === '(') parenDepth++;
109
+ if (char === ')') {
110
+ parenDepth--;
111
+ if (parenDepth < 0) {
112
+ errors.push({ message: 'Unexpected closing parenthesis ")"', column: i + 1 });
113
+ }
114
+ }
115
+ }
116
+
117
+ if (braceDepth > 0) {
118
+ errors.push({ message: `Missing ${braceDepth} closing brace(s) "}"` });
119
+ }
120
+ if (braceDepth < 0) {
121
+ errors.push({ message: `Unexpected ${Math.abs(braceDepth)} closing brace(s) "}"` });
122
+ }
123
+ if (parenDepth > 0) {
124
+ errors.push({ message: `Missing ${parenDepth} closing parenthesis ")"` });
125
+ }
126
+
127
+ // Check that the query starts with a valid keyword or brace
128
+ const startsWithKeyword = /^(query|mutation|subscription|fragment|\{)/.test(trimmed);
129
+ if (!startsWithKeyword) {
130
+ errors.push({
131
+ message: 'Query must start with "query", "mutation", "subscription", "fragment", or "{".',
132
+ });
133
+ }
134
+
135
+ return { valid: errors.length === 0, errors };
136
+ }
137
+
138
+ /**
139
+ * Validate a query against a known schema (field existence check).
140
+ */
141
+ export function validateAgainstSchema(
142
+ query: GraphQLQuery,
143
+ schema: GraphQLSchema,
144
+ ): GraphQLValidationResult {
145
+ const errors: GraphQLValidationError[] = [];
146
+
147
+ // First do structural validation
148
+ const structuralResult = validateQuery(query);
149
+ if (!structuralResult.valid) {
150
+ return structuralResult;
151
+ }
152
+
153
+ // Determine the root type
154
+ let rootTypeName: string | undefined;
155
+ if (query.operationType === 'query') {
156
+ rootTypeName = schema.queryType?.name;
157
+ } else if (query.operationType === 'mutation') {
158
+ rootTypeName = schema.mutationType?.name ?? undefined;
159
+ }
160
+
161
+ if (!rootTypeName) {
162
+ errors.push({
163
+ message: `Schema does not define a ${query.operationType} type.`,
164
+ });
165
+ return { valid: false, errors };
166
+ }
167
+
168
+ // Find the root type in schema
169
+ const rootType = schema.types.find((t) => t.name === rootTypeName);
170
+ if (!rootType?.fields) {
171
+ errors.push({ message: `Could not find type "${rootTypeName}" in schema.` });
172
+ return { valid: false, errors };
173
+ }
174
+
175
+ // Check each top-level field exists
176
+ for (const field of query.fields) {
177
+ const fieldName = field.name.startsWith('...') ? field.name.slice(3) : field.name;
178
+ const schemaField = rootType.fields.find((f) => f.name === fieldName);
179
+ if (!schemaField && !field.name.startsWith('...')) {
180
+ errors.push({
181
+ message: `Field "${fieldName}" does not exist on type "${rootTypeName}".`,
182
+ });
183
+ }
184
+ }
185
+
186
+ return { valid: errors.length === 0, errors };
187
+ }
188
+
189
+ /**
190
+ * Recursively validate field structure.
191
+ */
192
+ function validateFields(fields: GraphQLField[], errors: GraphQLValidationError[], path: string[]): void {
193
+ for (const field of fields) {
194
+ const fieldPath = [...path, field.name].join('.');
195
+
196
+ if (!field.name) {
197
+ errors.push({ message: `Empty field name at path: ${path.join('.') || '<root>'}` });
198
+ continue;
199
+ }
200
+
201
+ // Fragment spreads are valid
202
+ if (field.name.startsWith('...')) continue;
203
+
204
+ if (field.arguments) {
205
+ for (const arg of field.arguments) {
206
+ if (!arg.name) {
207
+ errors.push({ message: `Argument missing name in field "${fieldPath}".` });
208
+ }
209
+ }
210
+ }
211
+
212
+ if (field.subfields && field.subfields.length > 0) {
213
+ validateFields(field.subfields, errors, [...path, field.name]);
214
+ }
215
+ }
216
+ }