JSPM

@jamx-framework/graphql

2.0.0
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 6
  • Score
    100M100P100Q67609F
  • License MIT

JAMX Framework — GraphQL adapter

Package Exports

  • @jamx-framework/graphql

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:

  1. Schema Builder: Construye esquemas GraphQL a partir de clases y decoradores de JAMX
  2. Resolver Registry: Registra y gestiona resolvers para campos y tipos
  3. Executor: Ejecuta queries y mutations contra el schema
  4. DataLoader: Carga por lotes para evitar el problema N+1
  5. Pagination: Utilidades para paginación cursor-based y offset-based

Componentes principales

  • src/schema-builder.ts: Clase SchemaBuilder que construye esquemas GraphQL desde tipos TypeScript
  • src/executor.ts: Clase Executor que ejecuta queries y mutations
  • src/resolver-registry.ts: ResolverRegistry que 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

  1. Schema Building: SchemaBuilder procesa definiciones de tipos, queries y mutations
  2. Type Resolution: Convierte tipos TypeScript a tipos GraphQL (String, Int, ID, Object, etc.)
  3. Resolver Registration: ResolverRegistry asocia funciones a campos específicos
  4. Execution: Executor recibe una query, la parsea, resuelve campos y ejecuta resolvers
  5. DataLoader: Cachea requests de batch para evitar llamadas repetidas a BD
  6. Pagination: Utilidades para convertir resultados a formato Connection de GraphQL
  7. Error Handling: Los errores en resolvers se capturan y formatean según GraphQL spec

API Reference (Resumen)

SchemaBuilder

  • objectType(name, config): Define un tipo objeto
  • inputType(name, config): Define un tipo de input
  • interfaceType(name, config): Define una interfaz
  • enumType(name, values): Define un enum
  • unionType(name, types): Define una unión
  • queryType(config): Define el tipo Query raíz
  • mutationType(config): Define el tipo Mutation raíz
  • build(): Construye el schema final

Executor

  • constructor(schema, registry, context?)
  • execute(query, variables?, context?): Ejecuta una query
  • executeSubscription(...): Ejecuta una suscripción (si está soportado)

ResolverRegistry

  • register(fieldPath, resolver): Registra un resolver para un campo
  • get(fieldPath): Obtiene el resolver para un campo
  • clear(): Limpia todos los resolvers

DataLoader

  • load(key): Carga un dato por clave (batch automático)
  • loadMany(keys): Carga múltiples datos
  • clear(key): Limpia cache para una clave
  • clearAll(): Limpia todo el cache

Pagination

  • paginate<T>(data, options, cursorField): Pagina resultados
  • Connection<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 test

Cubre:

  • 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 dev
  • jamx graphql:introspect: Genera schema SDL
  • jamx graphql:validate <query>: Valida una query contra el schema

Best Practices

  1. Usar DataLoader para relaciones y queries N+1
  2. Limitar profundidad de queries para evitar DoS
  3. Validar queries en producción (no ejecutar queries arbitrarias)
  4. Cachear resultados de queries costosas
  5. Documentar schema con descripciones en campos
  6. Usar paginación para listas grandes
  7. 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.