polen
Version:
A framework for delightful GraphQL developer portals
361 lines • 13.9 kB
JavaScript
import { parse, validate, visit, } from 'graphql';
/**
* Default GraphQL document analyzer implementation
*/
export class DefaultGraphQLAnalyzer {
/**
* Parse a GraphQL document string into an AST
*/
parse(source) {
try {
return parse(source, {
noLocation: false, // We need location info for positioning
});
}
catch (error) {
throw new Error(`Failed to parse GraphQL document: ${error instanceof Error ? error.message : `Unknown error`}`);
}
}
/**
* Validate a GraphQL document against a schema
*/
validateAgainstSchema(ast, schema) {
return [...validate(schema, ast)];
}
/**
* Extract all identifiers from a GraphQL AST
*/
extractIdentifiers(ast, config = {}) {
const identifiers = [];
const errors = [];
const schema = config.schema;
// Context tracking during traversal
let currentOperationType;
let currentOperationName;
let currentFragment;
let selectionPath = [];
let parentTypes = [];
// Helper function to create context
const createContext = () => {
return {
operationType: currentOperationType,
operationName: currentOperationName,
inFragment: currentFragment,
selectionPath: [...selectionPath],
};
};
visit(ast, {
OperationDefinition: {
enter: (node) => {
currentOperationType = node.operation;
currentOperationName = node.name?.value;
selectionPath = [];
// Set the root type based on operation type
if (schema) {
if (node.operation === `query` && schema.getQueryType()) {
parentTypes = [schema.getQueryType().name];
}
else if (node.operation === `mutation` && schema.getMutationType()) {
parentTypes = [schema.getMutationType().name];
}
else if (node.operation === `subscription` && schema.getSubscriptionType()) {
parentTypes = [schema.getSubscriptionType().name];
}
else {
parentTypes = [];
}
}
else {
// Fallback to default root type names
if (node.operation === `query`) {
parentTypes = [`Query`];
}
else if (node.operation === `mutation`) {
parentTypes = [`Mutation`];
}
else if (node.operation === `subscription`) {
parentTypes = [`Subscription`];
}
else {
parentTypes = [];
}
}
},
leave: () => {
currentOperationType = undefined;
currentOperationName = undefined;
parentTypes = [];
},
},
FragmentDefinition: {
enter: (node) => {
currentFragment = node.name.value;
selectionPath = [];
parentTypes = [node.typeCondition.name.value];
// Add fragment name as identifier
this.addIdentifier(identifiers, {
name: node.name.value,
kind: `Fragment`,
position: this.getPosition(node.name),
schemaPath: [node.name.value],
context: createContext(),
});
// Add type condition as identifier
this.addIdentifier(identifiers, {
name: node.typeCondition.name.value,
kind: `Type`,
position: this.getPosition(node.typeCondition.name),
schemaPath: [node.typeCondition.name.value],
context: createContext(),
});
},
leave: () => {
currentFragment = undefined;
parentTypes = [];
},
},
Field: {
enter: (node) => {
const fieldName = node.name.value;
const parentType = parentTypes[parentTypes.length - 1];
selectionPath.push(fieldName);
this.addIdentifier(identifiers, {
name: fieldName,
kind: `Field`,
position: this.getPosition(node.name),
parentType,
schemaPath: parentType ? [parentType, fieldName] : [fieldName],
context: createContext(),
});
// Track parent type for nested selections
// Resolve the field's return type from schema if available
let pushedType = false;
if (schema && parentType) {
const type = schema.getType(parentType);
if (type && (`getFields` in type)) {
const fields = type.getFields();
const field = fields[fieldName];
if (field) {
// Get the base type name (unwrap NonNull and List wrappers)
let fieldType = field.type;
while (fieldType.ofType) {
fieldType = fieldType.ofType;
}
if (fieldType.name) {
parentTypes.push(fieldType.name);
pushedType = true;
}
}
}
}
else if (this.isObjectField(fieldName)) {
// Fallback to inference if no schema
const inferredType = this.inferReturnType(parentType, fieldName);
parentTypes.push(inferredType);
pushedType = true;
} // Store whether we pushed a type for this field
;
node._pushedType = pushedType;
},
leave: (node) => {
selectionPath.pop();
// Remove parent type if we added one in enter
if (node._pushedType) {
parentTypes.pop();
}
},
},
Argument: {
enter: (node) => {
const argName = node.name.value;
const parentType = parentTypes[parentTypes.length - 1];
const fieldName = selectionPath[selectionPath.length - 1];
this.addIdentifier(identifiers, {
name: argName,
kind: `Argument`,
position: this.getPosition(node.name),
parentType,
schemaPath: parentType && fieldName
? [parentType, fieldName, argName]
: [argName],
context: createContext(),
});
},
},
VariableDefinition: {
enter: (node) => {
this.addIdentifier(identifiers, {
name: node.variable.name.value,
kind: `Variable`,
position: this.getPosition(node.variable.name),
schemaPath: [node.variable.name.value],
context: createContext(),
});
// Also add the type reference
const typeName = this.extractTypeName(node.type);
if (typeName) {
this.addIdentifier(identifiers, {
name: typeName,
kind: `Type`,
position: this.getTypePosition(node.type),
schemaPath: [typeName],
context: createContext(),
});
}
},
},
Directive: {
enter: (node) => {
this.addIdentifier(identifiers, {
name: node.name.value,
kind: `Directive`,
position: this.getPosition(node.name),
schemaPath: [node.name.value],
context: createContext(),
});
},
},
});
return this.createIdentifierMap(identifiers, errors);
}
/**
* Perform complete analysis of a GraphQL document
*/
analyze(source, config = {}) {
try {
const ast = this.parse(source);
const identifiers = this.extractIdentifiers(ast, config);
let validationErrors = [];
if (config.validateAgainstSchema && config.schema) {
validationErrors = this.validateAgainstSchema(ast, config.schema);
}
return {
ast,
identifiers,
isValid: validationErrors.length === 0,
errors: validationErrors,
};
}
catch (error) {
return {
ast: { kind: `Document`, definitions: [] },
identifiers: this.createIdentifierMap([], [{
message: error instanceof Error ? error.message : `Unknown parsing error`,
severity: `error`,
}]),
isValid: false,
errors: [],
};
}
}
// Private helper methods
addIdentifier(identifiers, identifier) {
identifiers.push(identifier);
}
getPosition(node) {
const loc = node.loc;
if (!loc) {
return { start: 0, end: 0, line: 1, column: 1 };
}
return {
start: loc.start,
end: loc.end,
line: loc.startToken.line,
column: loc.startToken.column,
};
}
getTypePosition(node) {
// For type nodes, we need to extract the base type name position
// This is a simplified implementation
return this.getPosition(node);
}
extractTypeName(typeNode) {
// Recursively extract the base type name from wrapped types (NonNull, List)
if (typeNode.kind === `NamedType`) {
return typeNode.name.value;
}
if (typeNode.kind === `NonNullType` || typeNode.kind === `ListType`) {
return this.extractTypeName(typeNode.type);
}
return null;
}
isObjectField(fieldName) {
// This is only used as a fallback when no schema is available
// Common patterns for object-returning fields
const objectFieldPatterns = [
`user`,
`users`,
`post`,
`posts`,
`comment`,
`comments`,
`profile`,
`settings`,
`organization`,
`project`,
`target`,
`member`,
`members`,
`node`,
`nodes`,
`edge`,
`edges`,
];
return objectFieldPatterns.some(pattern => fieldName.toLowerCase().includes(pattern));
}
inferReturnType(parentType, fieldName) {
// Simplified type inference - in real implementation would use schema
if (fieldName === `user`)
return `User`;
if (fieldName === `posts`)
return `Post`;
if (fieldName === `comments`)
return `Comment`;
return fieldName.charAt(0).toUpperCase() + fieldName.slice(1);
}
createContext(operationType, operationName, inFragment, selectionPath = []) {
return {
operationType,
operationName,
inFragment,
selectionPath: [...selectionPath],
};
}
createIdentifierMap(identifiers, errors) {
const byPosition = new Map();
const byKind = new Map();
for (const identifier of identifiers) {
// Index by position
byPosition.set(identifier.position.start, identifier);
// Group by kind
if (!byKind.has(identifier.kind)) {
byKind.set(identifier.kind, []);
}
byKind.get(identifier.kind).push(identifier);
}
return {
byPosition,
byKind,
errors,
all: identifiers,
};
}
}
/**
* Default analyzer instance
*/
export const analyzer = new DefaultGraphQLAnalyzer();
/**
* Convenience function to analyze a GraphQL document
*/
export const analyze = (source, config) => {
return analyzer.analyze(source, config);
};
/**
* Convenience function to extract identifiers from a GraphQL document
*/
export const extractIdentifiers = (source, config) => {
const ast = analyzer.parse(source);
return analyzer.extractIdentifiers(ast, config);
};
//# sourceMappingURL=analysis.js.map