@democratize-quality/mcp-server 1.1.2 → 1.1.3

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.
@@ -2,6 +2,21 @@ const ToolBase = require('../base/ToolBase');
2
2
  const https = require('https');
3
3
  const http = require('http');
4
4
  const { URL } = require('url');
5
+ const fs = require('fs').promises;
6
+ const path = require('path');
7
+
8
+ // Try to load GraphQL for SDL parsing
9
+ let graphql;
10
+ try {
11
+ graphql = require('graphql');
12
+ if (!graphql.buildSchema || !graphql.introspectionFromSchema || !graphql.getIntrospectionQuery) {
13
+ console.warn('GraphQL loaded but required functions not available');
14
+ graphql = null;
15
+ }
16
+ } catch (error) {
17
+ console.warn('GraphQL library not available, SDL files will not be supported:', error.message);
18
+ graphql = null;
19
+ }
5
20
 
6
21
  // Try to load faker.js for enhanced sample data generation
7
22
  let faker;
@@ -42,7 +57,7 @@ try {
42
57
  // Input schema for the API planner tool
43
58
  const apiPlannerInputSchema = z.object({
44
59
  schemaUrl: z.string().optional(),
45
- schemaContent: z.string().optional(),
60
+ schemaPath: z.string().optional(),
46
61
  schemaType: z.enum(['openapi', 'swagger', 'graphql', 'auto']).optional(),
47
62
  apiBaseUrl: z.string().optional(),
48
63
  includeAuth: z.boolean().optional(),
@@ -69,9 +84,9 @@ class ApiPlannerTool extends ToolBase {
69
84
  type: "string",
70
85
  description: "URL to fetch the API schema/documentation (e.g., OpenAPI spec URL, GraphQL introspection endpoint)"
71
86
  },
72
- schemaContent: {
87
+ schemaPath: {
73
88
  type: "string",
74
- description: "Direct schema content as JSON/YAML string (alternative to schemaUrl)"
89
+ description: "File path to a local schema file (.graphql, .json, .yaml, .yml). GraphQL SDL files (.graphql, .gql) are automatically converted to introspection JSON. For large files, this is the recommended approach as it reads the complete file without truncation."
75
90
  },
76
91
  schemaType: {
77
92
  type: "string",
@@ -173,9 +188,11 @@ class ApiPlannerTool extends ToolBase {
173
188
  // Fetch or parse schema
174
189
  let schemaData;
175
190
  if (params.schemaUrl) {
176
- schemaData = await this._fetchSchema(params.schemaUrl);
177
- } else if (params.schemaContent) {
178
- schemaData = this._parseSchemaContent(params.schemaContent, options.schemaType);
191
+ schemaData = await this._fetchSchema(params.schemaUrl, options.schemaType);
192
+ } else if (params.schemaPath) {
193
+ schemaData = await this._readSchemaFromFile(params.schemaPath, options.schemaType);
194
+ } else {
195
+ throw new Error('Either schemaUrl or schemaPath must be provided. For local schema files, use schemaPath parameter.');
179
196
  }
180
197
 
181
198
  if (!schemaData) {
@@ -233,7 +250,16 @@ class ApiPlannerTool extends ToolBase {
233
250
  }
234
251
  }
235
252
 
236
- async _fetchSchema(url) {
253
+ async _fetchSchema(url, schemaType = 'auto') {
254
+ // If schemaType is 'graphql' or if URL suggests GraphQL endpoint, perform introspection
255
+ const isGraphQL = schemaType === 'graphql' ||
256
+ url.toLowerCase().includes('graphql') ||
257
+ url.toLowerCase().includes('/graph');
258
+
259
+ if (isGraphQL) {
260
+ return await this._fetchGraphQLSchema(url);
261
+ }
262
+
237
263
  return new Promise((resolve, reject) => {
238
264
  const urlObj = new URL(url);
239
265
  const client = urlObj.protocol === 'https:' ? https : http;
@@ -278,8 +304,160 @@ class ApiPlannerTool extends ToolBase {
278
304
  req.end();
279
305
  });
280
306
  }
307
+
308
+ /**
309
+ * Fetch GraphQL schema via introspection query
310
+ * GENERIC - Works with any GraphQL endpoint
311
+ */
312
+ async _fetchGraphQLSchema(url) {
313
+ return new Promise((resolve, reject) => {
314
+ const urlObj = new URL(url);
315
+ const client = urlObj.protocol === 'https:' ? https : http;
316
+
317
+ const introspectionQuery = this._getGraphQLIntrospectionQuery();
318
+ const postData = JSON.stringify({
319
+ query: introspectionQuery
320
+ });
321
+
322
+ const options = {
323
+ hostname: urlObj.hostname,
324
+ port: urlObj.port,
325
+ path: urlObj.pathname + urlObj.search,
326
+ method: 'POST',
327
+ headers: {
328
+ 'Content-Type': 'application/json',
329
+ 'Accept': 'application/json',
330
+ 'User-Agent': 'Democratize-Quality-MCP-Server/1.0',
331
+ 'Content-Length': Buffer.byteLength(postData)
332
+ }
333
+ };
281
334
 
335
+ const req = client.request(options, (res) => {
336
+ let data = '';
337
+
338
+ res.on('data', (chunk) => {
339
+ data += chunk;
340
+ });
341
+
342
+ res.on('end', () => {
343
+ try {
344
+ const response = JSON.parse(data);
345
+
346
+ // Check for GraphQL errors
347
+ if (response.errors) {
348
+ reject(new Error(`GraphQL introspection failed: ${response.errors[0].message}`));
349
+ return;
350
+ }
351
+
352
+ // Return the introspection result
353
+ resolve(response);
354
+ } catch (error) {
355
+ reject(new Error(`Failed to parse GraphQL response: ${error.message}`));
356
+ }
357
+ });
358
+ });
359
+
360
+ req.on('error', (error) => {
361
+ reject(new Error(`Failed to fetch GraphQL schema: ${error.message}`));
362
+ });
363
+
364
+ req.setTimeout(30000, () => {
365
+ req.destroy();
366
+ reject(new Error('GraphQL introspection timeout'));
367
+ });
368
+
369
+ req.write(postData);
370
+ req.end();
371
+ });
372
+ }
373
+
374
+ /**
375
+ * Read schema from local file system
376
+ */
377
+ async _readSchemaFromFile(filePath, schemaType = 'auto') {
378
+ try {
379
+ // Resolve absolute path
380
+ const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath);
381
+
382
+ // Check file exists
383
+ try {
384
+ await fs.access(absolutePath);
385
+ } catch (error) {
386
+ throw new Error(`Schema file not found: ${absolutePath}`);
387
+ }
388
+
389
+ // Read file content
390
+ const content = await fs.readFile(absolutePath, 'utf-8');
391
+ const ext = path.extname(filePath).toLowerCase();
392
+
393
+ // Handle .graphql files
394
+ if (ext === '.graphql' || ext === '.gql') {
395
+ // Check if content is GraphQL SDL or introspection JSON
396
+ const trimmedContent = content.trim();
397
+
398
+ // If it starts with '{', it's likely JSON introspection result
399
+ if (trimmedContent.startsWith('{')) {
400
+ try {
401
+ return JSON.parse(content);
402
+ } catch (error) {
403
+ throw new Error(`GraphQL file contains invalid JSON: ${error.message}`);
404
+ }
405
+ }
406
+
407
+ // Otherwise it's SDL (Schema Definition Language) - convert it
408
+ console.log(`📝 Detected GraphQL SDL in ${path.basename(filePath)}, converting to introspection JSON...`);
409
+
410
+ if (!graphql) {
411
+ throw new Error(
412
+ `GraphQL SDL detected but 'graphql' package is not installed.\n` +
413
+ `Please install it: npm install graphql\n` +
414
+ `Or convert manually: npx graphql-cli introspect ${filePath}`
415
+ );
416
+ }
417
+
418
+ try {
419
+ // Build schema from SDL
420
+ const schema = graphql.buildSchema(content);
421
+
422
+ // Generate introspection query result
423
+ const introspectionQuery = graphql.getIntrospectionQuery();
424
+ const introspectionResult = graphql.introspectionFromSchema(schema);
425
+
426
+ // Create output filename (e.g., schema.graphql -> schema.json)
427
+ const baseName = path.basename(filePath, ext);
428
+ const outputPath = path.join(path.dirname(absolutePath), `${baseName}.json`);
429
+
430
+ // Save introspection JSON file
431
+ await fs.writeFile(outputPath, JSON.stringify(introspectionResult, null, 2));
432
+ console.log(`✅ Introspection JSON saved to: ${outputPath}`);
433
+
434
+ return introspectionResult;
435
+ } catch (error) {
436
+ throw new Error(
437
+ `Failed to convert GraphQL SDL to introspection JSON: ${error.message}\n` +
438
+ `Please check that your SDL syntax is valid.`
439
+ );
440
+ }
441
+ }
442
+
443
+ // Parse JSON or YAML content
444
+ return this._parseSchemaContent(content, schemaType);
445
+ } catch (error) {
446
+ // Re-throw with original message if it's already formatted
447
+ if (error.message.includes('GraphQL SDL') || error.message.includes('Schema file not found')) {
448
+ throw error;
449
+ }
450
+ throw new Error(`Failed to read schema file: ${error.message}`);
451
+ }
452
+ }
453
+
454
+ /**
455
+ * Parse schema content (JSON/YAML) - Internal method used by _fetchSchema and _readSchemaFromFile
456
+ * Note: SDL conversion is handled in _readSchemaFromFile before reaching this method
457
+ */
282
458
  _parseSchemaContent(content, schemaType) {
459
+ const trimmedContent = content.trim();
460
+
283
461
  try {
284
462
  // Try JSON first
285
463
  return JSON.parse(content);
@@ -1470,10 +1648,848 @@ class ApiPlannerTool extends ToolBase {
1470
1648
  };
1471
1649
  }
1472
1650
 
1651
+ // ============================================================================
1652
+ // GRAPHQL SUPPORT - GENERIC IMPLEMENTATION
1653
+ // ============================================================================
1654
+
1655
+ /**
1656
+ * Standard GraphQL introspection query (works with ANY GraphQL endpoint)
1657
+ * Follows GraphQL specification: https://spec.graphql.org/October2021/#sec-Introspection
1658
+ */
1659
+ _getGraphQLIntrospectionQuery() {
1660
+ return `
1661
+ query IntrospectionQuery {
1662
+ __schema {
1663
+ queryType { name }
1664
+ mutationType { name }
1665
+ subscriptionType { name }
1666
+ types {
1667
+ kind
1668
+ name
1669
+ description
1670
+ fields(includeDeprecated: true) {
1671
+ name
1672
+ description
1673
+ args {
1674
+ name
1675
+ description
1676
+ type { ...TypeRef }
1677
+ defaultValue
1678
+ }
1679
+ type { ...TypeRef }
1680
+ isDeprecated
1681
+ deprecationReason
1682
+ }
1683
+ inputFields {
1684
+ name
1685
+ description
1686
+ type { ...TypeRef }
1687
+ defaultValue
1688
+ }
1689
+ interfaces { ...TypeRef }
1690
+ enumValues(includeDeprecated: true) {
1691
+ name
1692
+ description
1693
+ isDeprecated
1694
+ deprecationReason
1695
+ }
1696
+ possibleTypes { ...TypeRef }
1697
+ }
1698
+ directives {
1699
+ name
1700
+ description
1701
+ locations
1702
+ args {
1703
+ name
1704
+ description
1705
+ type { ...TypeRef }
1706
+ defaultValue
1707
+ }
1708
+ }
1709
+ }
1710
+ }
1711
+
1712
+ fragment TypeRef on __Type {
1713
+ kind
1714
+ name
1715
+ ofType {
1716
+ kind
1717
+ name
1718
+ ofType {
1719
+ kind
1720
+ name
1721
+ ofType {
1722
+ kind
1723
+ name
1724
+ ofType {
1725
+ kind
1726
+ name
1727
+ ofType {
1728
+ kind
1729
+ name
1730
+ }
1731
+ }
1732
+ }
1733
+ }
1734
+ }
1735
+ }
1736
+ `;
1737
+ }
1738
+
1473
1739
  async _generateGraphQLTestPlan(schema, options) {
1474
- // GraphQL implementation would be added here
1475
- // For now, return a placeholder
1476
- throw new Error('GraphQL schema analysis not yet implemented');
1740
+ // Parse schema (generic - works with any GraphQL schema)
1741
+ const parsedSchema = this._parseGraphQLSchema(schema);
1742
+
1743
+ // Discover operations (generic)
1744
+ const operations = this._discoverGraphQLOperations(parsedSchema);
1745
+
1746
+ const testPlan = {
1747
+ summary: {
1748
+ title: 'GraphQL API Test Plan',
1749
+ type: 'graphql',
1750
+ baseUrl: options.apiBaseUrl || 'https://graphql.example.com/graphql',
1751
+ totalQueries: operations.queries.length,
1752
+ totalMutations: operations.mutations.length,
1753
+ totalSubscriptions: operations.subscriptions.length,
1754
+ totalScenarios: 0,
1755
+ totalEndpoints: 1 // GraphQL typically has single endpoint
1756
+ },
1757
+ sections: []
1758
+ };
1759
+
1760
+ // Generate test scenarios for queries (generic)
1761
+ for (const query of operations.queries) {
1762
+ const section = this._generateGraphQLQueryTestSection(query, parsedSchema, options);
1763
+ testPlan.sections.push(section);
1764
+ testPlan.summary.totalScenarios += section.scenarios.length;
1765
+ }
1766
+
1767
+ // Generate test scenarios for mutations (generic)
1768
+ for (const mutation of operations.mutations) {
1769
+ const section = this._generateGraphQLMutationTestSection(mutation, parsedSchema, options);
1770
+ testPlan.sections.push(section);
1771
+ testPlan.summary.totalScenarios += section.scenarios.length;
1772
+ }
1773
+
1774
+ // Generate test scenarios for subscriptions if requested
1775
+ if (options.testCategories.includes('subscriptions') && operations.subscriptions.length > 0) {
1776
+ for (const subscription of operations.subscriptions) {
1777
+ const section = this._generateGraphQLSubscriptionTestSection(subscription, parsedSchema, options);
1778
+ testPlan.sections.push(section);
1779
+ testPlan.summary.totalScenarios += section.scenarios.length;
1780
+ }
1781
+ }
1782
+
1783
+ testPlan.content = this._generateMarkdownContent(testPlan);
1784
+ return testPlan;
1785
+ }
1786
+
1787
+ /**
1788
+ * Parse GraphQL introspection result into usable structure
1789
+ * GENERIC - Works with any GraphQL schema following the spec
1790
+ */
1791
+ _parseGraphQLSchema(introspectionResult) {
1792
+ // Handle both direct introspection result and wrapped result
1793
+ const schemaData = introspectionResult.__schema || introspectionResult.data?.__schema;
1794
+
1795
+ if (!schemaData) {
1796
+ throw new Error('Invalid GraphQL introspection result - missing __schema');
1797
+ }
1798
+
1799
+ const schema = {
1800
+ queryType: schemaData.queryType?.name,
1801
+ mutationType: schemaData.mutationType?.name,
1802
+ subscriptionType: schemaData.subscriptionType?.name,
1803
+ types: {},
1804
+ directives: schemaData.directives || []
1805
+ };
1806
+
1807
+ // Index all types by name (skip internal GraphQL types starting with __)
1808
+ for (const type of schemaData.types) {
1809
+ if (!type.name.startsWith('__')) {
1810
+ schema.types[type.name] = {
1811
+ kind: type.kind,
1812
+ name: type.name,
1813
+ description: type.description,
1814
+ fields: type.fields || [],
1815
+ inputFields: type.inputFields || [],
1816
+ enumValues: type.enumValues || [],
1817
+ interfaces: type.interfaces || [],
1818
+ possibleTypes: type.possibleTypes || []
1819
+ };
1820
+ }
1821
+ }
1822
+
1823
+ return schema;
1824
+ }
1825
+
1826
+ /**
1827
+ * Discover all queries, mutations, and subscriptions from schema
1828
+ * GENERIC - Extracts operations from any GraphQL schema
1829
+ */
1830
+ _discoverGraphQLOperations(schema) {
1831
+ const operations = {
1832
+ queries: [],
1833
+ mutations: [],
1834
+ subscriptions: []
1835
+ };
1836
+
1837
+ // Find Query type operations
1838
+ if (schema.queryType && schema.types[schema.queryType]) {
1839
+ const queryType = schema.types[schema.queryType];
1840
+ operations.queries = (queryType.fields || []).map(field => ({
1841
+ name: field.name,
1842
+ description: field.description,
1843
+ args: field.args || [],
1844
+ returnType: field.type,
1845
+ isDeprecated: field.isDeprecated,
1846
+ deprecationReason: field.deprecationReason
1847
+ }));
1848
+ }
1849
+
1850
+ // Find Mutation type operations
1851
+ if (schema.mutationType && schema.types[schema.mutationType]) {
1852
+ const mutationType = schema.types[schema.mutationType];
1853
+ operations.mutations = (mutationType.fields || []).map(field => ({
1854
+ name: field.name,
1855
+ description: field.description,
1856
+ args: field.args || [],
1857
+ returnType: field.type,
1858
+ isDeprecated: field.isDeprecated,
1859
+ deprecationReason: field.deprecationReason
1860
+ }));
1861
+ }
1862
+
1863
+ // Find Subscription type operations
1864
+ if (schema.subscriptionType && schema.types[schema.subscriptionType]) {
1865
+ const subscriptionType = schema.types[schema.subscriptionType];
1866
+ operations.subscriptions = (subscriptionType.fields || []).map(field => ({
1867
+ name: field.name,
1868
+ description: field.description,
1869
+ args: field.args || [],
1870
+ returnType: field.type,
1871
+ isDeprecated: field.isDeprecated,
1872
+ deprecationReason: field.deprecationReason
1873
+ }));
1874
+ }
1875
+
1876
+ return operations;
1877
+ }
1878
+
1879
+ /**
1880
+ * Generate realistic value for GraphQL scalar type
1881
+ * GENERIC - Handles built-in scalars and custom scalars using field name semantics
1882
+ */
1883
+ _generateGraphQLScalarValue(scalarType, fieldName = '') {
1884
+ const scalarName = scalarType.name;
1885
+
1886
+ // ===== BUILT-IN SCALARS (GraphQL Spec - these 5 are always present) =====
1887
+ switch (scalarName) {
1888
+ case 'String':
1889
+ return this._generateStringValue({ type: 'string' }, fieldName);
1890
+
1891
+ case 'Int':
1892
+ return this._generateNumericValue({ type: 'integer' }, fieldName);
1893
+
1894
+ case 'Float':
1895
+ return this._generateNumericValue({ type: 'number' }, fieldName);
1896
+
1897
+ case 'Boolean':
1898
+ return this._generateBooleanValue({}, fieldName);
1899
+
1900
+ case 'ID':
1901
+ // Generic ID generation
1902
+ if (faker) {
1903
+ return faker.string.alphanumeric(16);
1904
+ }
1905
+ return `id_${Math.random().toString(36).substr(2, 9)}`;
1906
+ }
1907
+
1908
+ // ===== COMMON CUSTOM SCALAR PATTERNS (Generic Detection) =====
1909
+ const lowerScalar = scalarName.toLowerCase();
1910
+
1911
+ // Date/Time scalars (various naming conventions)
1912
+ if (lowerScalar.includes('date') || lowerScalar.includes('time')) {
1913
+ if (lowerScalar.includes('date') && !lowerScalar.includes('time')) {
1914
+ return this._generateStringValue({ format: 'date' }, fieldName);
1915
+ }
1916
+ return this._generateStringValue({ format: 'date-time' }, fieldName);
1917
+ }
1918
+
1919
+ // URL/URI scalars
1920
+ if (lowerScalar.includes('url') || lowerScalar.includes('uri')) {
1921
+ return this._generateStringValue({ format: 'uri' }, fieldName);
1922
+ }
1923
+
1924
+ // JSON scalars
1925
+ if (lowerScalar.includes('json')) {
1926
+ return { example: 'json_data' };
1927
+ }
1928
+
1929
+ // Email scalars
1930
+ if (lowerScalar.includes('email')) {
1931
+ return this._generateStringValue({ format: 'email' }, fieldName);
1932
+ }
1933
+
1934
+ // HTML/Markdown scalars
1935
+ if (lowerScalar.includes('html')) {
1936
+ return '<p>Sample HTML content</p>';
1937
+ }
1938
+
1939
+ if (lowerScalar.includes('markdown')) {
1940
+ return '# Sample Markdown\n\nThis is sample content.';
1941
+ }
1942
+
1943
+ // ===== FALLBACK: Use field name semantic analysis =====
1944
+ // This is the SECRET WEAPON - works for ANY custom scalar!
1945
+ // We analyze the field name to generate appropriate data
1946
+ return this._generateStringValue({ type: 'string' }, fieldName);
1947
+ }
1948
+
1949
+ /**
1950
+ * Recursively generate sample data from GraphQL type
1951
+ * GENERIC - Handles all GraphQL type kinds with circular reference protection
1952
+ */
1953
+ _generateSampleFromGraphQLType(type, schema, fieldName = '', visited = new Set(), depth = 0) {
1954
+ // Prevent infinite recursion
1955
+ const MAX_DEPTH = 5;
1956
+ if (depth > MAX_DEPTH) {
1957
+ return null;
1958
+ }
1959
+
1960
+ // Circular reference detection
1961
+ const typeKey = type.name ? `${type.name}_${fieldName}` : `${type.kind}_${fieldName}`;
1962
+ if (visited.has(typeKey)) {
1963
+ return null;
1964
+ }
1965
+
1966
+ switch (type.kind) {
1967
+ case 'NON_NULL':
1968
+ // Unwrap non-null and continue
1969
+ return this._generateSampleFromGraphQLType(type.ofType, schema, fieldName, visited, depth);
1970
+
1971
+ case 'LIST':
1972
+ // Generate array with 1-2 sample items
1973
+ visited.add(typeKey);
1974
+ const itemValue = this._generateSampleFromGraphQLType(type.ofType, schema, fieldName, visited, depth + 1);
1975
+ visited.delete(typeKey);
1976
+ return itemValue !== null ? [itemValue] : [];
1977
+
1978
+ case 'SCALAR':
1979
+ return this._generateGraphQLScalarValue(type, fieldName);
1980
+
1981
+ case 'ENUM':
1982
+ // Pick first enum value
1983
+ const enumType = schema.types[type.name];
1984
+ if (enumType?.enumValues && enumType.enumValues.length > 0) {
1985
+ return enumType.enumValues[0].name;
1986
+ }
1987
+ return 'ENUM_VALUE';
1988
+
1989
+ case 'OBJECT':
1990
+ case 'INPUT_OBJECT':
1991
+ visited.add(typeKey);
1992
+ const objValue = this._generateGraphQLObject(type, schema, visited, depth + 1);
1993
+ visited.delete(typeKey);
1994
+ return objValue;
1995
+
1996
+ case 'INTERFACE':
1997
+ // For interfaces, generate fields from the interface itself
1998
+ visited.add(typeKey);
1999
+ const interfaceValue = this._generateGraphQLObject(type, schema, visited, depth + 1);
2000
+ visited.delete(typeKey);
2001
+ return interfaceValue;
2002
+
2003
+ case 'UNION':
2004
+ // Pick first possible type
2005
+ const unionType = schema.types[type.name];
2006
+ if (unionType?.possibleTypes && unionType.possibleTypes.length > 0) {
2007
+ const firstType = unionType.possibleTypes[0];
2008
+ return this._generateSampleFromGraphQLType(firstType, schema, fieldName, visited, depth + 1);
2009
+ }
2010
+ return null;
2011
+
2012
+ default:
2013
+ return null;
2014
+ }
2015
+ }
2016
+
2017
+ /**
2018
+ * Generate sample object with all fields
2019
+ * GENERIC - Works with any GraphQL object type
2020
+ */
2021
+ _generateGraphQLObject(type, schema, visited, depth) {
2022
+ const typeDefinition = schema.types[type.name];
2023
+ if (!typeDefinition) {
2024
+ return {};
2025
+ }
2026
+
2027
+ const obj = {};
2028
+ const fields = typeDefinition.fields || typeDefinition.inputFields || [];
2029
+
2030
+ // Limit fields at deep nesting levels to prevent bloat
2031
+ const maxFieldsAtDepth = depth > 3 ? 3 : (depth > 2 ? 5 : 10);
2032
+ const fieldsToGenerate = fields.slice(0, maxFieldsAtDepth);
2033
+
2034
+ for (const field of fieldsToGenerate) {
2035
+ // Use field name for semantic data generation
2036
+ const value = this._generateSampleFromGraphQLType(
2037
+ field.type,
2038
+ schema,
2039
+ field.name,
2040
+ visited,
2041
+ depth
2042
+ );
2043
+
2044
+ if (value !== null) {
2045
+ obj[field.name] = value;
2046
+ }
2047
+ }
2048
+
2049
+ return obj;
2050
+ }
2051
+
2052
+ /**
2053
+ * Build GraphQL query string with variables
2054
+ * GENERIC - Constructs valid GraphQL query/mutation syntax
2055
+ */
2056
+ _buildGraphQLQueryString(operation, args, schema, operationType = 'query') {
2057
+ const operationName = this._capitalize(operation.name);
2058
+
2059
+ // Build variable definitions
2060
+ const variableDefinitions = [];
2061
+ const argumentsList = [];
2062
+
2063
+ for (const arg of args) {
2064
+ const varName = arg.name;
2065
+ const typeString = this._getGraphQLTypeString(arg.type);
2066
+ variableDefinitions.push(`$${varName}: ${typeString}`);
2067
+ argumentsList.push(`${arg.name}: $${varName}`);
2068
+ }
2069
+
2070
+ // Build field selection (simplified - select first level fields)
2071
+ const returnType = this._unwrapType(operation.returnType);
2072
+ const fields = this._buildFieldSelection(returnType, schema, 0);
2073
+
2074
+ // Construct query string
2075
+ let queryString = operationType;
2076
+ if (variableDefinitions.length > 0) {
2077
+ queryString += ` ${operationName}(${variableDefinitions.join(', ')})`;
2078
+ } else {
2079
+ queryString += ` ${operationName}`;
2080
+ }
2081
+
2082
+ queryString += ' {\n';
2083
+ if (argumentsList.length > 0) {
2084
+ queryString += ` ${operation.name}(${argumentsList.join(', ')}) `;
2085
+ } else {
2086
+ queryString += ` ${operation.name} `;
2087
+ }
2088
+ queryString += fields;
2089
+ queryString += '\n}';
2090
+
2091
+ return queryString;
2092
+ }
2093
+
2094
+ /**
2095
+ * Build field selection for GraphQL query
2096
+ * GENERIC - Selects appropriate fields based on return type
2097
+ */
2098
+ _buildFieldSelection(type, schema, depth = 0) {
2099
+ const MAX_DEPTH = 2;
2100
+ if (depth > MAX_DEPTH) {
2101
+ return '';
2102
+ }
2103
+
2104
+ const typeDefinition = schema.types[type.name];
2105
+ if (!typeDefinition || !typeDefinition.fields || typeDefinition.fields.length === 0) {
2106
+ return '';
2107
+ }
2108
+
2109
+ let selection = '{\n';
2110
+ const indent = ' '.repeat(depth + 2);
2111
+
2112
+ // Select up to 5 fields to keep queries manageable
2113
+ const fieldsToSelect = typeDefinition.fields.slice(0, 5);
2114
+
2115
+ for (const field of fieldsToSelect) {
2116
+ const fieldType = this._unwrapType(field.type);
2117
+ const fieldTypeDefinition = schema.types[fieldType.name];
2118
+
2119
+ // If field is scalar or enum, just select it
2120
+ if (!fieldTypeDefinition || fieldType.kind === 'SCALAR' || fieldType.kind === 'ENUM') {
2121
+ selection += `${indent}${field.name}\n`;
2122
+ } else if (fieldType.kind === 'OBJECT' && depth < MAX_DEPTH) {
2123
+ // If field is object, recursively build selection
2124
+ const subSelection = this._buildFieldSelection(fieldType, schema, depth + 1);
2125
+ if (subSelection) {
2126
+ selection += `${indent}${field.name} ${subSelection}`;
2127
+ } else {
2128
+ selection += `${indent}${field.name}\n`;
2129
+ }
2130
+ }
2131
+ }
2132
+
2133
+ selection += ' '.repeat(depth + 1) + '}';
2134
+ return selection;
2135
+ }
2136
+
2137
+ /**
2138
+ * Get GraphQL type string (e.g., "String!", "[ID!]!")
2139
+ * GENERIC - Constructs type strings following GraphQL syntax
2140
+ */
2141
+ _getGraphQLTypeString(type) {
2142
+ if (type.kind === 'NON_NULL') {
2143
+ return this._getGraphQLTypeString(type.ofType) + '!';
2144
+ }
2145
+ if (type.kind === 'LIST') {
2146
+ return '[' + this._getGraphQLTypeString(type.ofType) + ']';
2147
+ }
2148
+ return type.name;
2149
+ }
2150
+
2151
+ /**
2152
+ * Unwrap type to get base type (remove NON_NULL and LIST wrappers)
2153
+ */
2154
+ _unwrapType(type) {
2155
+ if (type.kind === 'NON_NULL' || type.kind === 'LIST') {
2156
+ return this._unwrapType(type.ofType);
2157
+ }
2158
+ return type;
2159
+ }
2160
+
2161
+ /**
2162
+ * Capitalize first letter
2163
+ */
2164
+ _capitalize(str) {
2165
+ return str.charAt(0).toUpperCase() + str.slice(1);
2166
+ }
2167
+
2168
+ /**
2169
+ * Generate test section for GraphQL query
2170
+ * GENERIC - Creates test scenarios for any GraphQL query operation
2171
+ */
2172
+ _generateGraphQLQueryTestSection(query, schema, options) {
2173
+ const scenarios = [];
2174
+
2175
+ // Happy path scenario
2176
+ const happyPathScenario = {
2177
+ title: `${query.name} - Happy Path`,
2178
+ method: 'POST',
2179
+ endpoint: '/graphql',
2180
+ description: query.description || `Test successful execution of ${query.name} query`,
2181
+ data: {
2182
+ query: this._buildGraphQLQueryString(query, query.args, schema, 'query'),
2183
+ variables: this._generateGraphQLVariables(query.args, schema)
2184
+ },
2185
+ headers: {
2186
+ 'Content-Type': 'application/json'
2187
+ },
2188
+ expect: {
2189
+ status: 200,
2190
+ body: {
2191
+ data: {
2192
+ [query.name]: this._generateExpectedGraphQLResponse(query.returnType, schema)
2193
+ }
2194
+ }
2195
+ }
2196
+ };
2197
+
2198
+ if (options.includeAuth) {
2199
+ happyPathScenario.headers['Authorization'] = 'Bearer {{auth_token}}';
2200
+ }
2201
+
2202
+ scenarios.push(happyPathScenario);
2203
+
2204
+ // Error scenarios
2205
+ if (options.includeErrorHandling) {
2206
+ // Missing required arguments
2207
+ if (query.args.some(arg => arg.type.kind === 'NON_NULL')) {
2208
+ scenarios.push({
2209
+ title: `${query.name} - Missing Required Arguments`,
2210
+ method: 'POST',
2211
+ endpoint: '/graphql',
2212
+ description: 'Test error handling when required arguments are missing',
2213
+ data: {
2214
+ query: this._buildGraphQLQueryString(query, query.args, schema, 'query'),
2215
+ variables: {}
2216
+ },
2217
+ headers: {
2218
+ 'Content-Type': 'application/json'
2219
+ },
2220
+ expect: {
2221
+ status: 200,
2222
+ body: {
2223
+ errors: [
2224
+ {
2225
+ message: 'string'
2226
+ }
2227
+ ]
2228
+ }
2229
+ }
2230
+ });
2231
+ }
2232
+
2233
+ // Invalid field selection
2234
+ scenarios.push({
2235
+ title: `${query.name} - Invalid Field Selection`,
2236
+ method: 'POST',
2237
+ endpoint: '/graphql',
2238
+ description: 'Test error handling for invalid field in query',
2239
+ data: {
2240
+ query: `query { ${query.name} { invalidField } }`,
2241
+ variables: this._generateGraphQLVariables(query.args, schema)
2242
+ },
2243
+ headers: {
2244
+ 'Content-Type': 'application/json'
2245
+ },
2246
+ expect: {
2247
+ status: 200,
2248
+ body: {
2249
+ errors: [
2250
+ {
2251
+ message: 'string'
2252
+ }
2253
+ ]
2254
+ }
2255
+ }
2256
+ });
2257
+ }
2258
+
2259
+ // Authentication error
2260
+ if (options.includeAuth) {
2261
+ scenarios.push({
2262
+ title: `${query.name} - Unauthorized Access`,
2263
+ method: 'POST',
2264
+ endpoint: '/graphql',
2265
+ description: 'Test access without authentication',
2266
+ data: {
2267
+ query: this._buildGraphQLQueryString(query, query.args, schema, 'query'),
2268
+ variables: this._generateGraphQLVariables(query.args, schema)
2269
+ },
2270
+ headers: {
2271
+ 'Content-Type': 'application/json'
2272
+ },
2273
+ expect: {
2274
+ status: [200, 401],
2275
+ body: {
2276
+ errors: [
2277
+ {
2278
+ message: 'string'
2279
+ }
2280
+ ]
2281
+ }
2282
+ }
2283
+ });
2284
+ }
2285
+
2286
+ return {
2287
+ title: `Query: ${query.name}`,
2288
+ description: query.description || `Test cases for ${query.name} query`,
2289
+ scenarios: scenarios
2290
+ };
2291
+ }
2292
+
2293
+ /**
2294
+ * Generate test section for GraphQL mutation
2295
+ * GENERIC - Creates test scenarios for any GraphQL mutation operation
2296
+ */
2297
+ _generateGraphQLMutationTestSection(mutation, schema, options) {
2298
+ const scenarios = [];
2299
+
2300
+ // Happy path scenario
2301
+ const happyPathScenario = {
2302
+ title: `${mutation.name} - Happy Path`,
2303
+ method: 'POST',
2304
+ endpoint: '/graphql',
2305
+ description: mutation.description || `Test successful execution of ${mutation.name} mutation`,
2306
+ data: {
2307
+ query: this._buildGraphQLQueryString(mutation, mutation.args, schema, 'mutation'),
2308
+ variables: this._generateGraphQLVariables(mutation.args, schema)
2309
+ },
2310
+ headers: {
2311
+ 'Content-Type': 'application/json'
2312
+ },
2313
+ expect: {
2314
+ status: 200,
2315
+ body: {
2316
+ data: {
2317
+ [mutation.name]: this._generateExpectedGraphQLResponse(mutation.returnType, schema)
2318
+ }
2319
+ }
2320
+ }
2321
+ };
2322
+
2323
+ if (options.includeAuth) {
2324
+ happyPathScenario.headers['Authorization'] = 'Bearer {{auth_token}}';
2325
+ }
2326
+
2327
+ scenarios.push(happyPathScenario);
2328
+
2329
+ // Error scenarios
2330
+ if (options.includeErrorHandling) {
2331
+ // Invalid input data
2332
+ scenarios.push({
2333
+ title: `${mutation.name} - Invalid Input Data`,
2334
+ method: 'POST',
2335
+ endpoint: '/graphql',
2336
+ description: 'Test error handling with invalid input data',
2337
+ data: {
2338
+ query: this._buildGraphQLQueryString(mutation, mutation.args, schema, 'mutation'),
2339
+ variables: this._generateInvalidGraphQLVariables(mutation.args, schema)
2340
+ },
2341
+ headers: {
2342
+ 'Content-Type': 'application/json'
2343
+ },
2344
+ expect: {
2345
+ status: 200,
2346
+ body: {
2347
+ errors: [
2348
+ {
2349
+ message: 'string'
2350
+ }
2351
+ ]
2352
+ }
2353
+ }
2354
+ });
2355
+ }
2356
+
2357
+ // Authorization error for mutations
2358
+ if (options.includeAuth) {
2359
+ scenarios.push({
2360
+ title: `${mutation.name} - Unauthorized Mutation`,
2361
+ method: 'POST',
2362
+ endpoint: '/graphql',
2363
+ description: 'Test mutation without proper authorization',
2364
+ data: {
2365
+ query: this._buildGraphQLQueryString(mutation, mutation.args, schema, 'mutation'),
2366
+ variables: this._generateGraphQLVariables(mutation.args, schema)
2367
+ },
2368
+ headers: {
2369
+ 'Content-Type': 'application/json'
2370
+ },
2371
+ expect: {
2372
+ status: [200, 401, 403],
2373
+ body: {
2374
+ errors: [
2375
+ {
2376
+ message: 'string'
2377
+ }
2378
+ ]
2379
+ }
2380
+ }
2381
+ });
2382
+ }
2383
+
2384
+ return {
2385
+ title: `Mutation: ${mutation.name}`,
2386
+ description: mutation.description || `Test cases for ${mutation.name} mutation`,
2387
+ scenarios: scenarios
2388
+ };
2389
+ }
2390
+
2391
+ /**
2392
+ * Generate test section for GraphQL subscription
2393
+ * GENERIC - Creates test scenarios for any GraphQL subscription operation
2394
+ */
2395
+ _generateGraphQLSubscriptionTestSection(subscription, schema, options) {
2396
+ const scenarios = [];
2397
+
2398
+ // Happy path scenario
2399
+ const happyPathScenario = {
2400
+ title: `${subscription.name} - Happy Path`,
2401
+ method: 'POST',
2402
+ endpoint: '/graphql',
2403
+ description: subscription.description || `Test successful subscription to ${subscription.name}`,
2404
+ data: {
2405
+ query: this._buildGraphQLQueryString(subscription, subscription.args, schema, 'subscription'),
2406
+ variables: this._generateGraphQLVariables(subscription.args, schema)
2407
+ },
2408
+ headers: {
2409
+ 'Content-Type': 'application/json'
2410
+ },
2411
+ expect: {
2412
+ status: 200,
2413
+ body: {
2414
+ data: {
2415
+ [subscription.name]: this._generateExpectedGraphQLResponse(subscription.returnType, schema)
2416
+ }
2417
+ }
2418
+ }
2419
+ };
2420
+
2421
+ if (options.includeAuth) {
2422
+ happyPathScenario.headers['Authorization'] = 'Bearer {{auth_token}}';
2423
+ }
2424
+
2425
+ scenarios.push(happyPathScenario);
2426
+
2427
+ return {
2428
+ title: `Subscription: ${subscription.name}`,
2429
+ description: subscription.description || `Test cases for ${subscription.name} subscription`,
2430
+ scenarios: scenarios
2431
+ };
2432
+ }
2433
+
2434
+ /**
2435
+ * Generate variables object for GraphQL operation
2436
+ * GENERIC - Creates realistic variable values based on argument types
2437
+ */
2438
+ _generateGraphQLVariables(args, schema) {
2439
+ const variables = {};
2440
+
2441
+ for (const arg of args) {
2442
+ const argType = this._unwrapType(arg.type);
2443
+ variables[arg.name] = this._generateSampleFromGraphQLType(argType, schema, arg.name, new Set(), 0);
2444
+ }
2445
+
2446
+ return variables;
2447
+ }
2448
+
2449
+ /**
2450
+ * Generate invalid variables for error testing
2451
+ * GENERIC - Creates intentionally invalid data for negative testing
2452
+ */
2453
+ _generateInvalidGraphQLVariables(args, schema) {
2454
+ const variables = {};
2455
+
2456
+ for (const arg of args) {
2457
+ const argType = this._unwrapType(arg.type);
2458
+
2459
+ // Generate wrong type of data based on expected type
2460
+ if (argType.kind === 'SCALAR') {
2461
+ switch (argType.name) {
2462
+ case 'String':
2463
+ variables[arg.name] = 12345; // Wrong type
2464
+ break;
2465
+ case 'Int':
2466
+ case 'Float':
2467
+ variables[arg.name] = 'not_a_number'; // Wrong type
2468
+ break;
2469
+ case 'Boolean':
2470
+ variables[arg.name] = 'not_a_boolean'; // Wrong type
2471
+ break;
2472
+ case 'ID':
2473
+ variables[arg.name] = { invalid: 'object' }; // Wrong type
2474
+ break;
2475
+ default:
2476
+ variables[arg.name] = null;
2477
+ }
2478
+ } else {
2479
+ variables[arg.name] = null;
2480
+ }
2481
+ }
2482
+
2483
+ return variables;
2484
+ }
2485
+
2486
+ /**
2487
+ * Generate expected response structure for GraphQL operation
2488
+ * GENERIC - Creates expected response based on return type
2489
+ */
2490
+ _generateExpectedGraphQLResponse(returnType, schema) {
2491
+ const unwrappedType = this._unwrapType(returnType);
2492
+ return this._generateSampleFromGraphQLType(unwrappedType, schema, '', new Set(), 0);
1477
2493
  }
1478
2494
 
1479
2495
  _generateMarkdownContent(testPlan) {