objection-graphql icon indicating copy to clipboard operation
objection-graphql copied to clipboard

Add `extendWithModelMutations` function to builder

Open timhuff opened this issue 6 years ago • 6 comments

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 avatar Feb 15 '18 22:02 timhuff

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.

timhuff avatar Feb 15 '18 22:02 timhuff

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).

nasushkov avatar Mar 11 '18 16:03 nasushkov

@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 avatar Mar 12 '18 13:03 timhuff

@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.

timhuff avatar Jun 17 '18 14:06 timhuff

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)

SkeLLLa avatar Jul 03 '18 10:07 SkeLLLa

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!

DaKaZ avatar Dec 19 '19 16:12 DaKaZ