polen
Version:
A framework for delightful GraphQL developer portals
328 lines • 11.8 kB
JavaScript
/**
* Layer 2: Schema Integration
*
* Bridge between GraphQL analysis and Polen's schema system.
* Resolves identifiers against the actual schema, extracts documentation,
* and generates reference URLs for navigation.
*/
import { getNamedType, isInputObjectType, isInterfaceType, isListType, isNonNullType, isObjectType, } from 'graphql';
import { analyze } from './analysis.js';
/**
* Polen's implementation of SchemaResolver
*/
export class PolenSchemaResolver {
schema;
routeConfig;
constructor(schema, routeConfig = {}) {
this.schema = schema;
this.routeConfig = routeConfig;
}
/**
* Resolve an identifier against the schema
*/
resolveIdentifier(identifier) {
const basePath = this.routeConfig.basePath || `/reference`;
switch (identifier.kind) {
case `Type`:
return this.resolveType(identifier, basePath);
case `Field`:
return this.resolveField(identifier, basePath);
case `Argument`:
return this.resolveArgument(identifier, basePath);
case `Variable`:
// Variables don't have schema resolution
return {
exists: true,
referenceUrl: `${basePath}#variables`,
documentation: {
typeInfo: `Variable`,
description: `Query variable: $${identifier.name}`,
},
};
case `Directive`:
return this.resolveDirective(identifier, basePath);
case `Fragment`:
// Fragments don't have schema resolution
return {
exists: true,
referenceUrl: `${basePath}#fragments`,
documentation: {
typeInfo: `Fragment`,
description: `Fragment: ${identifier.name}`,
},
};
default:
return null;
}
}
/**
* Get documentation for a schema path
*/
getDocumentation(schemaPath) {
if (schemaPath.length === 0)
return null;
const [typeName, fieldName, argName] = schemaPath;
if (!typeName)
return null;
const type = this.schema.getType(typeName);
if (!type)
return null;
// Type-level documentation
if (schemaPath.length === 1) {
return {
typeInfo: this.getTypeSignature(type),
description: type.description || undefined,
deprecated: `deprecationReason` in type
? {
reason: type.deprecationReason,
}
: undefined,
};
}
// Field-level documentation
if (fieldName && schemaPath.length === 2) {
const field = this.getFieldFromType(type, fieldName);
if (!field)
return null;
return {
typeInfo: this.getTypeSignature(field.type),
description: field.description || undefined,
deprecated: field.deprecationReason
? {
reason: field.deprecationReason,
}
: undefined,
};
}
// Argument-level documentation
if (fieldName && argName && schemaPath.length === 3) {
const field = this.getFieldFromType(type, fieldName);
if (!field || !(`args` in field))
return null;
const arg = field.args.find((a) => a.name === argName);
if (!arg)
return null;
return {
typeInfo: this.getTypeSignature(arg.type),
description: arg.description || undefined,
defaultValue: arg.defaultValue !== undefined
? String(arg.defaultValue)
: undefined,
};
}
return null;
}
/**
* Generate a reference URL for a schema path
*/
generateReferenceLink(schemaPath) {
const basePath = this.routeConfig.basePath || `/reference`;
const includeFragments = this.routeConfig.includeFragments !== false;
if (schemaPath.length === 0)
return basePath;
const [typeName, fieldName, argName] = schemaPath;
// Type reference
if (schemaPath.length === 1) {
return `${basePath}/${typeName}`;
}
// Field reference
if (fieldName && schemaPath.length === 2) {
const fragment = includeFragments ? `#${fieldName}` : ``;
return `${basePath}/${typeName}${fragment}`;
}
// Argument reference
if (fieldName && argName && schemaPath.length === 3) {
const fragment = includeFragments ? `#${fieldName}-${argName}` : ``;
return `${basePath}/${typeName}${fragment}`;
}
return `${basePath}/${typeName}`;
}
/**
* Check if a type exists in the schema
*/
typeExists(typeName) {
return !!this.schema.getType(typeName);
}
/**
* Get all available types for validation
*/
getAllTypes() {
return Object.keys(this.schema.getTypeMap())
.filter(name => !name.startsWith(`__`)); // Filter out introspection types
}
// Private helper methods
resolveType(identifier, basePath) {
const type = this.schema.getType(identifier.name);
return {
exists: !!type,
graphqlType: type || undefined,
referenceUrl: `${basePath}/${identifier.name}`,
documentation: type
? {
typeInfo: this.getTypeSignature(type),
description: type.description || undefined,
}
: undefined,
};
}
resolveField(identifier, basePath) {
if (!identifier.parentType) {
return {
exists: false,
referenceUrl: `${basePath}#${identifier.name}`,
};
}
const parentType = this.schema.getType(identifier.parentType);
if (!parentType) {
return {
exists: false,
referenceUrl: `${basePath}/${identifier.parentType}#${identifier.name}`,
};
}
const field = this.getFieldFromType(parentType, identifier.name);
return {
exists: !!field,
referenceUrl: `${basePath}/${identifier.parentType}#${identifier.name}`,
documentation: field
? {
typeInfo: this.getTypeSignature(field.type),
description: field.description || undefined,
deprecated: field.deprecationReason
? {
reason: field.deprecationReason,
}
: undefined,
}
: undefined,
deprecated: field?.deprecationReason
? {
reason: field.deprecationReason,
}
: undefined,
};
}
resolveArgument(identifier, basePath) {
const schemaPath = identifier.schemaPath;
if (schemaPath.length < 3) {
return {
exists: false,
referenceUrl: `${basePath}#${identifier.name}`,
};
}
const [typeName, fieldName] = schemaPath;
if (!typeName || !fieldName) {
return {
exists: false,
referenceUrl: `${basePath}#${identifier.name}`,
};
}
const parentType = this.schema.getType(typeName);
if (!parentType) {
return {
exists: false,
referenceUrl: `${basePath}/${typeName}#${fieldName}-${identifier.name}`,
};
}
const field = this.getFieldFromType(parentType, fieldName);
if (!field || !(`args` in field)) {
return {
exists: false,
referenceUrl: `${basePath}/${typeName}#${fieldName}-${identifier.name}`,
};
}
const arg = field.args.find((a) => a.name === identifier.name);
return {
exists: !!arg,
referenceUrl: `${basePath}/${typeName}#${fieldName}-${identifier.name}`,
documentation: arg
? {
typeInfo: this.getTypeSignature(arg.type),
description: arg.description || undefined,
defaultValue: arg.defaultValue !== undefined
? String(arg.defaultValue)
: undefined,
}
: undefined,
};
}
resolveDirective(identifier, basePath) {
const directive = this.schema.getDirective(identifier.name);
return {
exists: !!directive,
referenceUrl: `${basePath}/directives#${identifier.name}`,
documentation: directive
? {
typeInfo: `Directive`,
description: directive.description || undefined,
}
: undefined,
};
}
getFieldFromType(type, fieldName) {
if (isObjectType(type) || isInterfaceType(type)) {
return type.getFields()[fieldName] || null;
}
if (isInputObjectType(type)) {
return type.getFields()[fieldName] || null;
}
return null;
}
getTypeSignature(type) {
if (isNonNullType(type)) {
return `${this.getTypeSignature(type.ofType)}!`;
}
if (isListType(type)) {
return `[${this.getTypeSignature(type.ofType)}]`;
}
return getNamedType(type).name;
}
}
/**
* Create a schema resolver for Polen
*/
export const createPolenSchemaResolver = (schema, routeConfig) => {
return new PolenSchemaResolver(schema, routeConfig);
};
/**
* Perform schema-aware analysis of a GraphQL document
*/
export const analyzeWithSchema = (source, schema, routeConfig) => {
const analysis = analyze(source, {
schema,
validateAgainstSchema: true,
includePositions: true,
});
const resolver = createPolenSchemaResolver(schema, routeConfig);
const resolutions = new Map();
const schemaErrors = [];
// Resolve all identifiers against schema
for (const identifier of analysis.identifiers.all) {
const key = `${identifier.position.start}-${identifier.name}-${identifier.kind}`;
const resolution = resolver.resolveIdentifier(identifier);
if (resolution) {
resolutions.set(key, resolution);
// Add validation errors for non-existent identifiers
if (!resolution.exists && (identifier.kind === `Type` || identifier.kind === `Field`)) {
schemaErrors.push({
identifier,
message: `${identifier.kind} "${identifier.name}" does not exist in schema`,
severity: `error`,
});
}
// Add deprecation warnings
if (resolution.deprecated) {
schemaErrors.push({
identifier,
message: `${identifier.kind} "${identifier.name}" is deprecated: ${resolution.deprecated.reason}`,
severity: `warning`,
});
}
}
}
return {
analysis,
resolutions,
schemaErrors,
};
};
//# sourceMappingURL=schema-integration.js.map