Skip to main content

Graphql server

executeGraphql

graphql-magic generates an execute.ts file for you, with this structure:

import knexConfig from "@/knexfile";
import { Context, User, execute } from "@smartive/graphql-magic";
import { randomUUID } from "crypto";
import { knex } from 'knex';
import { models } from "../config/models";

export const executeGraphql = async <T, V = undefined>(
body: {
query: string;
operationName?: string;
variables?: V;
options?: { email?: string };
}): Promise<{ data: T }> => {
const db = knex(knexConfig);
let user: User | undefined;
// TODO: get user

const result = await execute({
req: null as unknown as Context['req'],
body,
knex: db as unknown as Context['knex'],
locale: 'en',
locales: ['en'],
user,
models: models,
permissions: { ADMIN: true, UNAUTHENTICATED: true },
});
await db.destroy();

// https://github.com/vercel/next.js/issues/47447#issuecomment-1500371732
return JSON.parse(JSON.stringify(result)) as { data: T };
}

This is where you can set up your graphql server with

Graphql API

If you only need to execute graphql on the server (e.g. on next.js server components or server actions), you don't need a graphql endpoint. If you need client side querying, use executeGraphql to create a graphql endpoint, e.g. in src/app/api/graphql/route.ts:

export const POST = (req) => {
return await executeGraphql(req.body)
}

Custom resolvers

Sometimes you'll need a custom resolver, at the level of root queries, mutations or existing models. For that you need to create a additionalResolvers object.

export const additionalResolvers = {
// custom resolvers go here
}

Then feed it to graphql-magic's execute function:

const result = await execute({
...
additionalResolvers
})

Custom queries

For that you'll need to extend the Query model (and define any needed additional models), e.g.:

{
kind: 'object',
name: 'Stats',
fields: [
{
name: 'usersCount'
type: 'Int'
},
{
name: 'postsCount',
type: 'Int'
}
]
},
{
kind: 'object',
name: 'Query',
fields: [
// You'll need to define a custom resolver for this one
{
kind: 'custom',
name: 'stats',
type: 'Stats'
}
]
}

then implement the custom resolver as usual:

const additionalResolvers = {
Query: {
stats: (parent, args, ctx, schema) => {
// Implement custom resolver here
}
}
}

Custom mutations

For that you'll need to extend the Mutation model (and define any needed additional models), e.g.:

{
kind: 'input',
name: 'BulkDeleteWhereInput'
fields: [
{
kind: 'ID',
name: 'ids',
list: true
}
]
},
{
kind: 'object',
name: 'Mutation',
fields: [
// You'll need to define a custom resolver for this one
{
kind: 'custom',
name: 'bulkDelete',
args: [
{
kind: 'custom',
name: 'where',
type: 'BulkDeleteWhereInput'
}
]
type: 'Boolean'
}
]
}

then implement the custom resolver as usual:

const additionalResolvers = {
Mutation: {
bulkDelete: (parent, args, ctx, schema) => {
// Implement custom resolver here
}
}
}

Custom model resolvers

Sometimes you need to add a custom field to an existing model. For that, add a field of kind: 'custom' to the model.

{
kind: 'entity',
name: 'User',
fields: [
// ...
{
kind: 'custom',
name: 'isAdmin',
type: 'Boolean'
}
]
}

then implement the custom resolver as usual:

const additionalResolvers = {
User: {
isAdmin: (parent, args, ctx, schema) => {
// Implement custom resolver here
}
}
}

Mutation hooks

Sometimes you'll need some custom handling of mutations, before or after the change is committed to the database (e.g. for special data validation, or triggering cleanup work).

For this we can implement a global mutationHook function that will be called for all mutations with parameters describing the context:

import { MutationHook } from '@smartive/graphql-magic';

export const mutationHook: MutationHook = (model, action, when, data: { prev, input, normalizedInput, next }, ctx) => {
switch (model.name) {
// perform model specific tasks
}

// perform global tasks
}

Then feed it to graphql-magic's execute function:

const result = await execute({
...
mutationHook
})

The mutation hook function takes the following arguments:

  • model the model for the entity being mutated
  • action: can be 'create', 'update', 'delete' or 'restore'
  • when: either "before" or "after" the mutation is committed to the database
  • data containing the entity in various states:
    • prev: the previous entity (undefined in creation mutations)
    • input: input from the user
    • normalizedInput: input to feed to the database (e.g. including generated values such as id, createdAt...)
    • next: the full next entity after changes are applied