@jamx-framework/graphql
v2.0.0
Published
JAMX Framework — GraphQL adapter
Maintainers
Readme
@jamx-framework/graphql
Descripción
Módulo de GraphQL para JAMX Framework. Proporciona una API completa para construir, ejecutar y gestionar esquemas GraphQL, incluyendo soporte para DataLoader (carga por lotes), paginación, y registro de resolvers. Permite crear servidores GraphQL type-safe que se integran perfectamente con el ecosistema JAMX.
Cómo funciona
El módulo implementa las capacidades principales de GraphQL:
- Schema Builder: Construye esquemas GraphQL a partir de clases y decoradores de JAMX
- Resolver Registry: Registra y gestiona resolvers para campos y tipos
- Executor: Ejecuta queries y mutations contra el schema
- DataLoader: Carga por lotes para evitar el problema N+1
- Pagination: Utilidades para paginación cursor-based y offset-based
Componentes principales
- src/schema-builder.ts: Clase
SchemaBuilderque construye esquemas GraphQL desde tipos TypeScript - src/executor.ts: Clase
Executorque ejecuta queries y mutations - src/resolver-registry.ts:
ResolverRegistryque gestiona resolvers de campos - src/dataloader.ts: Implementación de DataLoader para batch loading
- src/pagination.ts: Utilidades de paginación (Connection, Edge, PageInfo)
- src/types.ts: Tipos compartidos (GraphQLType, Resolver, etc.)
- src/index.ts: Punto de exportación
Uso básico
import { SchemaBuilder, Executor, ResolverRegistry } from '@jamx-framework/graphql';
// 1. Definir tipos GraphQL
const builder = new SchemaBuilder();
const UserType = builder.objectType('User', {
fields: (t) => ({
id: t.field({ type: 'ID' }),
name: t.field({ type: 'String' }),
email: t.field({ type: 'String' }),
}),
});
const QueryType = builder.objectType('Query', {
fields: (t) => ({
user: t.field({
type: UserType,
args: { id: t.arg({ type: 'ID' }) },
resolve: async (parent, args, ctx) => {
return await ctx.db.users.findById(args.id);
},
}),
}),
});
// 2. Construir schema
const schema = builder.build();
// 3. Registrar resolvers
const registry = new ResolverRegistry();
registry.register('Query.user', async (parent, args, ctx) => {
return await ctx.db.users.findById(args.id);
});
// 4. Ejecutar query
const executor = new Executor(schema, registry);
const result = await executor.execute(`
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
`, { id: '123' }, { db });
console.log('Resultado GraphQL:', result);Ejemplos
Schema completo con mutations
import { SchemaBuilder } from '@jamx-framework/graphql';
const builder = new SchemaBuilder();
// Input types
const CreateUserInput = builder.input('CreateUserInput', {
fields: (t) => ({
name: t.field({ type: 'String' }),
email: t.field({ type: 'String' }),
password: t.field({ type: 'String' }),
}),
});
// User type
const UserType = builder.objectType('User', {
fields: (t) => ({
id: t.field({ type: 'ID' }),
name: t.field({ type: 'String' }),
email: t.field({ type: 'String' }),
createdAt: t.field({ type: 'String' }),
}),
});
// Query
builder.queryType({
fields: (t) => ({
users: t.field({
type: ['User'],
resolve: async (_, __, ctx) => await ctx.db.users.findAll(),
}),
user: t.field({
type: UserType,
args: { id: t.arg({ type: 'ID' }) },
resolve: async (_, args, ctx) => await ctx.db.users.findById(args.id),
}),
}),
});
// Mutation
builder.mutationType({
fields: (t) => ({
createUser: t.field({
type: UserType,
args: { input: t.arg({ type: CreateUserInput }) },
resolve: async (_, { input }, ctx) => {
const user = await ctx.db.users.insert(input);
return user;
},
}),
deleteUser: t.field({
type: 'Boolean',
args: { id: t.arg({ type: 'ID' }) },
resolve: async (_, { id }, ctx) => {
await ctx.db.users.delete(id);
return true;
},
}),
}),
});
const schema = builder.build();DataLoader para evitar N+1
import { DataLoader } from '@jamx-framework/graphql';
// Crear DataLoader para cargar usuarios por IDs en batch
const userLoader = new DataLoader<string, User>(async (ids) => {
// Esta función se llama una vez con todos los IDs necesarios
const users = await db.users.findAll({ where: { id: ids } });
const userMap = new Map(users.map(u => [u.id, u]));
return ids.map(id => userMap.get(id) ?? null);
});
// En el resolver:
const userType = builder.objectType('User', {
fields: (t) => ({
id: t.field({ type: 'ID' }),
name: t.field({ type: 'String' }),
// Relación que usa DataLoader
posts: t.field({
type: ['Post'],
resolve: async (user, _, ctx) => {
// Esto se cachea automáticamente por usuario
return await ctx.postLoader.load(user.id);
},
}),
}),
});Paginación con cursor
import { paginate, Connection, Edge } from '@jamx-framework/graphql';
// En el resolver:
const usersConnection: Connection<User> = await paginate(
await db.users.findAll({ where: { active: true } }),
{ first: 10, after: cursor },
(user) => user.id // cursor field
);
return {
edges: usersConnection.edges.map(edge => ({
cursor: edge.cursor,
node: edge.node,
})),
pageInfo: {
hasNextPage: usersConnection.hasNext,
hasPrevPage: usersConnection.hasPrev,
startCursor: usersConnection.edges[0]?.cursor,
endCursor: usersConnection.edges[usersConnection.edges.length - 1]?.cursor,
},
totalCount: usersConnection.total,
};Integración con JAMX Container
import { Container } from '@jamx-framework/core';
import { Executor } from '@jamx-framework/graphql';
// Registrar en contenedor
Container.registerSingleton('graphqlExecutor', () => {
const schema = buildSchema(); // tu función que construye el schema
const registry = new ResolverRegistry();
// registrar resolvers...
return new Executor(schema, registry);
});
// Usar en controlador
const executor = Container.resolve<Executor>('graphqlExecutor');
const result = await executor.execute(query, variables, context);
res.json(result);Flujo interno
- Schema Building:
SchemaBuilderprocesa definiciones de tipos, queries y mutations - Type Resolution: Convierte tipos TypeScript a tipos GraphQL (String, Int, ID, Object, etc.)
- Resolver Registration:
ResolverRegistryasocia funciones a campos específicos - Execution:
Executorrecibe una query, la parsea, resuelve campos y ejecuta resolvers - DataLoader: Cachea requests de batch para evitar llamadas repetidas a BD
- Pagination: Utilidades para convertir resultados a formato Connection de GraphQL
- Error Handling: Los errores en resolvers se capturan y formatean según GraphQL spec
API Reference (Resumen)
SchemaBuilder
objectType(name, config): Define un tipo objetoinputType(name, config): Define un tipo de inputinterfaceType(name, config): Define una interfazenumType(name, values): Define un enumunionType(name, types): Define una uniónqueryType(config): Define el tipo Query raízmutationType(config): Define el tipo Mutation raízbuild(): Construye el schema final
Executor
constructor(schema, registry, context?)execute(query, variables?, context?): Ejecuta una queryexecuteSubscription(...): Ejecuta una suscripción (si está soportado)
ResolverRegistry
register(fieldPath, resolver): Registra un resolver para un campoget(fieldPath): Obtiene el resolver para un campoclear(): Limpia todos los resolvers
DataLoader
load(key): Carga un dato por clave (batch automático)loadMany(keys): Carga múltiples datosclear(key): Limpia cache para una claveclearAll(): Limpia todo el cache
Pagination
paginate<T>(data, options, cursorField): Pagina resultadosConnection<T>,Edge<T>,PageInfo: Tipos de paginación
Performance Considerations
- DataLoader: Reduce queries N+1 a 1 query batch
- Resolver caching: Los resultados de DataLoader se cachean por request
- Schema validation: El schema se valida una vez al construir, no en cada query
- Lazy loading: Los campos se resuelven solo cuando se solicitan
- Batching: DataLoader agrupa requests en un solo batch por tick de event loop
Configuration Options
const executor = new Executor(schema, registry, {
// Contexto global para todos los resolvers
db,
cache,
auth,
// Límites
maxDepth: 10, // profundidad máxima de query
maxComplexity: 1000, // complejidad máxima
fieldLimit: 100, // máximo campos por query
// Formato de errores
formatError: (err) => ({
message: err.message,
locations: err.locations,
path: err.path,
}),
});Testing
Tests en packages/graphql/tests/unit/:
pnpm testCubre:
- Construcción de schemas
- Resolución de campos
- DataLoader batch loading
- Paginación
- Manejo de errores
- Variables y fragments
Compatibility
- Compatible con GraphQL SDL (Specification)
- Funciona con Node.js 18+
- Integra con cualquier base de datos a través de JAMX db
- Soporta subscriptions (parcialmente, requiere WebSocket)
CLI Integration
jamx graphql:playground: Abre GraphQL Playground en devjamx graphql:introspect: Genera schema SDLjamx graphql:validate <query>: Valida una query contra el schema
Best Practices
- Usar DataLoader para relaciones y queries N+1
- Limitar profundidad de queries para evitar DoS
- Validar queries en producción (no ejecutar queries arbitrarias)
- Cachear resultados de queries costosas
- Documentar schema con descripciones en campos
- Usar paginación para listas grandes
- Manejar errores consistentemente en resolvers
This GraphQL module provides a powerful, type-safe way to build GraphQL APIs in JAMX applications, with built-in support for batching, pagination, and seamless integration with the rest of the framework.
