Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add extendWithModelMutations function to builder #43

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

timhuff
Copy link
Collaborator

@timhuff timhuff commented Feb 15, 2018

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
Copy link
Collaborator Author

timhuff commented Feb 15, 2018

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
Copy link
Contributor

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
Copy link
Collaborator Author

timhuff commented Mar 12, 2018

@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
Copy link
Collaborator Author

timhuff commented Jun 17, 2018

@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
Copy link

SkeLLLa commented Jul 3, 2018

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
Copy link

DaKaZ commented Dec 19, 2019

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants