Vincit / objection-graphql

GraphQL schema generator for objection.js
MIT License
307 stars 34 forks source link

Add `extendWithModelMutations` function to builder #43

Open timhuff opened 6 years ago

timhuff commented 6 years ago

Similar to my other pull request, this is just a proof of concept. I'll add tests if we want to move forward. What this enables a user to do is add a mutations static object property to their objection model that defines mutations that should be added to the schema. This is currently not compatible with extendWithMutations, though it'd be fairly easy to make it so.

timhuff commented 6 years ago

On my fork of this I added a feature that you might be interested in but wasn't sure if it was appropriate for the pull request.

nasushkov commented 6 years ago

Hi @timhuff, Do you think it's a good idea to add mutations in objection models as static properties? I think doing so you are violating the separation of concerns a little since you are extending your models with functionality which is very specific to the API implementation (GraphQL in that case).

timhuff commented 6 years ago

@nasushkov That's a fair point but for me it's a matter of organization. Could the same question be asked of this library in general? We already have the models pretty tied up in the API implementation via the json schema. When building out mutations, it's nice to have the mutations right there in the model. I've been working with this patch quite a bit and I would say it's been an improvement. Functionally, it's not much different than having the functions pulled out to dedicated files (they're static functions, after all) but it results in a lot less "jumping around the file tree". If you're writing a User_create mutation, you have the definition of the user sitting right there (along with any custom methods on the model, etc).

timhuff commented 6 years ago

@nasushkov That graphqlConfig property might be a good place to define mutations as well. I maintain that it's of great benefit to have them defined on the model. In my codebase, I've even been able to create general mutations by using polymorphism.

SkeLLLa commented 6 years ago

I'm also looking forward to have this feature. It will be much easier to define mutations in model, especially when you have lots of models (like in my case)

DaKaZ commented 4 years ago

FWIW, we put our mutations IN our models without any major code changes. Let me explain... I may need to publish a blog on this ;) I realize this thread is pretty old, but perhaps this could help someone.

First, we have a common class that all models extend named BaseModel. In base model we have two important functions: mutations() and buildEager - the latter handles the graph relationships

  static buildEager(depth: number = 3): string {
    if (!this.relationMappings) {
      return '[]';
    }

    if (depth <= 1) {
      return `[${Object.keys(this.relationMappings).join(',')}]`;
    }

    const eager = [];
    Object.keys(this.relationMappings).forEach((key: string) => {
      const realModelClass = resolveModel(this.relationMappings[key].modelClass, this.modelPaths, `${this.tableName}.buildEager`);
      eager.push(`${key}.${realModelClass.buildEager(depth - 1)}`);
    });

    return `[${eager.join(',')}]`;
  }

  static get mutations(): Array<MutationObjType> {
    const authMiddleware = require('../../functions/_dsf/authMiddleware').default;

    const updateGqlFields = jsonSchemaUtils.jsonSchemaToGraphQLFields(this.jsonSchema, { exclude: ['createdAt', 'updatedAt'] });
    const createGqlFields = jsonSchemaUtils.jsonSchemaToGraphQLFields(this.jsonSchema, { exclude: [this.idColumn, 'createdAt', 'updatedAt'] });
    const primaryKeyField = jsonSchemaUtils.jsonSchemaToGraphQLFields(
      this.jsonSchema,
      { include: [this.idColumn] }
    );

    const createMutation = {
      docs_actionTitle: `Create a new ${this.tableName}`,
      mutationName: `${this.tableName}Create`,
      docs_actionDescription:
        `Use this mutation to create a new ${this.tableName}`,
      inputFieldTitle: 'input',
      argumentName: `${this.tableName}CreateType`,
      inputFields: createGqlFields,
      resolver: authMiddleware(async (root: *, input: GraphQLNonNull): Promise<BaseModel> => {
        const updatableKeys: Array<string> = Object.keys(createGqlFields);
        try {
          const object = await this
            .query()
            .allowInsert(JSON.stringify(updatableKeys))
            .insertAndFetch(pick(input.input, ...updatableKeys));
          return await object.$query().eager(this.buildEager());
        } catch (err) {
          this.handleError(err);
          throw (err);
        }
      }, {
        modelClass: this
      })
    };

    const updateMutation = {
      docs_actionTitle: `Update a ${this.tableName}`,
      mutationName: `${this.tableName}Update`,
      docs_actionDescription:
        `Use this mutation to update a ${this.tableName}`,
      inputFieldTitle: 'input',
      argumentName: `${this.tableName}UpdateType`,
      inputFields: updateGqlFields,
      resolver: authMiddleware(async (root: *, input: GraphQLNonNull): Promise<BaseModel> => {
        const updatableKeys: Array<string> = Object.keys(updateGqlFields);
        try {
          const updating = await this
            .query()
            .findById(input.input[this.idColumn]);
          const updated = await updating
            .$query()
            .patchAndFetch(pick(input.input, ...updatableKeys));
          return await updated.$query().eager(this.buildEager());
        } catch (err) {
          this.handleError(err);
          throw (err);
        }
      }, {
        modelClass: this,
      })
    };

    const deleteMutation = {
      docs_actionTitle: `Delete a ${this.tableName}`,
      mutationName: `${this.tableName}Delete`,
      docs_actionDescription:
        `Use this mutation to delete a ${this.tableName}`,
      inputFieldTitle: this.idColumn,
      inputField: primaryKeyField,
      resolver: authMiddleware(async (root: *, primaryKey: GraphQLNonNull): Promise<BaseModel> => {
        try {
          const object = await this
            .query()
            .findById(primaryKey[this.idColumn]);
          if (!object) {
            throw new Error(`${this.tableName} does not exist`);
          }
          await object.destroy();
          return object;
        } catch (err) {
          this.handleError(err);
          throw (err);
        }
      }, {
        modelClass: this
      })
    };

    return [createMutation, updateMutation, deleteMutation];
  }

(note: we use FLOW so you will see all the typing in there).

Separately, we have a schema.js file that looks like:

import { builder } from 'objection-graphql';
import buildMutations from '../../lib/utils';
import allModels from '../../models';
import authMiddleware from './authMiddleware';

const rootSchema = builder().allModels(allModels);

const schema = rootSchema
  .extendWithMiddleware(authMiddleware)
  .extendWithMutations(buildMutations(rootSchema))
  .build(true);

export default schema;

and the buildMutations looks like:

function buildMutations(schema: GraphQLSchema): GraphQLObjectType {
  const fields = {};

  const { models } = schema;

  Object.keys(models).forEach((modelName: string) => {
    const klass = models[modelName].modelClass;

    const mutationObjs = klass.mutations;

    Array.from(mutationObjs).forEach((mutationObj: MutationObjType) => {
      fields[mutationObj.mutationName] = {
        description: mutationObj.docs_actionTitle,
        type: (mutationObj.outputType)
          ? mutationObj.outputType
          // eslint-disable-next-line no-underscore-dangle
          : schema._typeForModel(models[modelName]),
        args: buildArgs(mutationObj),
        resolve: mutationObj.resolver
      };
    });
  });

  return new GraphQLObjectType({
    name: 'Mutations',
    description: 'Domain API actions',
    fields
  });
}

And Viola, you now have create, update and delete mutations for EVERY model!