UNPKG

polen

Version:

A framework for delightful GraphQL developer portals

328 lines 11.8 kB
/** * 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