2

I am making an application that shows information about different users, which is taken from third party API. I save this information in my own format with multiple tables in PostgreSQL to keep track of any changes to the data and provide history of changes (third party API only provides current data).

I want to use GraphQL, specifically Postgraphile to simplify backend development. But there is one use case which I can't find a way to implement with Postgraphile. Here is what I want to implement:

  1. User wants to see an updated information
  2. GraphQL mutation query is sent to the server, something like this:
mutation UpdateUserData($userid: Int) {
    updateUser(id: $userid) {
        field1,
        field2,
        relatedObjects {
            field3,
            filed3
        }
    }
}
  1. Server makes an API request to third party server, processes data, makes some calculations and updates the database
  2. Requested fields are returned to client

I know that this type of custom mutations can be implemented with database functions like PL/pgSQL or PLV8, but they can't make http requests and I already have most of the logic for data processing in Typescript, so would like to use it.

Is there a way to create a custom mutation that will call JavaScript function which has access to Node.js modules, interfaces and classes that I already created?

One solution that I think will work:

  1. Call REST endpoint on my server, like /update_user?id=$userid
  2. User data is loaded from third party API and database is updated
  3. Client receives response, like Update successful
  4. Normal GraphQL query is called to get the data

Is there a better way of satisfying this use case?

2 Answers2

0

This part is a bit hidden in the documentation, but the easiest way to add mutations written in JavaScript is the makeExtendSchemaPlugin.

Here you can define type definitions in SDL and implement resolvers in JS.

Herku
  • 7,198
  • 27
  • 36
0

What I want is to utilize extremely convenient Postgraphile queries for front-end, and to write my back-end logic in my framework of choice with my ORM of choice with their own workflows (so makeExtendSchemaPlugin is not and option), but reuse Postgraphile for return types of mutations.

In general, I rely on Postgraphile as the graphql server for my app, and put mutations under it's schema by stitching Postgraphile schema and my mutations schemas, with mutations resolvers delegating to Postgraphile schema. additionalGraphQLContextFromRequest provides DI container to my resolvers, so they have a way to call buisiness services. makeProcessSchemaPlugin is the place where stiching happens. codegen generates types so typescript complains when schema+resolvers+services doesn't add up.

Here is my setup:

- postgraphile.ts
- postgraphile.graphql
- types.ts
- services
  - base.graphql
  - document
    - service.ts
    - schema.graphql
    - resolver.ts
  - user
    - service.ts
    - schema.graphql
    - resolver.ts
  - ... other domains

postgraphile.ts set up as library:

export const setupPostgraphile = () => postgraphile(connString, schema, {
    additionalGraphQLContextFromRequest: async (req) => {
        // get DI cradle I previously set up in Fastify and put to resolvers context
        return {
            cradle: (req._fastifyRequest as FastifyRequest).server.cradle
        } as AppResolverContext;
    },
    appendPlugins: [
      MutationResolversSchemaPlugin
    ]
});

const MutationResolversSchemaPlugin = makeProcessSchemaPlugin((schema) => {
    // save schema file ourselves instead of using postgraphile plugin because
    // after stiching it will have duplicates of our types giving
    // '...type tried to redefine...' errors
    if (!isProd) {
        writeFileSync('./postgraphile.graphql', printSchema(schema));
    }

    const mutationResolvers: MutationResolvers<AppResolverContext> = {
        ...documentResolvers(schema),
        ...userResolvers(schema),
        // all other resolvers
    };

    return stitchSchemas({
        subschemas: [schema],
        // do NOT include postgraphile.schema, it will nullify Postgraphile resolvers
        typeDefs: loadFilesSync<GraphQLSchema>('./services/**/*.graphql'),
        resolvers: {
            Mutation: mutationResolvers
        }
    });
});

document/schema.graphql, note that Document is a Postgraphile type from postgraphile.graphql:

input DocumentInput {
    title: String
}

extend type Mutation {
    createDocument(doc: DocumentInput!): Document!
}

document/resolver.ts, types Query and QueryDocumentArgs are from codegen:

export const documentResolvers: AppMutationsSubresolver<
    'createDocument'
> = (postgraphileSchema) => ({
    createDocument: {
        selectionSet: '{ id }',
        resolve: async (parent, args, context, info) => {
            const result = await context.cradle.documentService.createDocument(args.doc);

            return delegateToSchema({
                schema: postgraphileSchema,
                operation: 'query',
                fieldName: 'document' as keyof Query['document'],
                args: {id: result.id} as QueryDocumentArgs,
                context,
                info
            });
        }
    }
});

document/service.ts:

export class DocumentService {
    // DI setting up bla bla bla
    
    async createDocument(dto: DocumentUpdateDTO) {
        // my perfectly normal service code
        ...

        await this.em.flush();

        // return something which holds 'id' of created object, it will be used
        // to query from Postgraphile
        return serialize(doc); 
    }
}

postgraphile.graphql is just a dump of Postgraphile schema (original, without my mutations) for IDE completions.

types.ts to have IDe blame e.g. when I forgot some mutation or misspel some type, types are from codegen:

export type AppResolverContext = {
    cradle: AppCradle; // my DI container type, I use awilix
}

// shorthand type for resolvers
export type AppMutationsSubresolver<
    MutationsSubset extends keyof MutationResolvers<AppResolverContext>
> = (postgraphileSchema: GraphQLSchema) => Pick<MutationResolvers<AppResolverContext>, MutationsSubset>

base.graphql:

type Mutation

Finally, doing mutation like this from front-end actually works, redirecting result to full-featured Postgraphile type:

mutation {
  createDocument(doc: {name: "ololo"}) {
    id, name
  }
}

Graphql infra got really awesome recently.

There seems to be good alternative way: instead of stiching, make a type merging gateway, making your mutations schemas returning stub types names after Postgraphile types, and instructing schema stiching how to merge Postgraphile types by IDs. I wanted to keep things explicit and non-gateway, and didn't try it.