graphql-compose / graphql-compose-mongoose

Mongoose model converter to GraphQL types with resolvers for graphql-compose https://github.com/nodkz/graphql-compose
MIT License
707 stars 94 forks source link

Issues with enum values on sort #147

Closed semiautomatix closed 5 years ago

semiautomatix commented 5 years ago

Good day

I'm receiving the response below when attempting to sort, I'm unsure whether this falls in the realm of graphql-compose-mongoose or graphql-compose.

{
  "errors": [
    {
      "message": "Variable \"$_v3_sort\" got invalid value { _id: 1 }; Expected type SortFindManyPurchaseOrderInput.",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "purchaseOrderPagination"
      ],
      "extensions": {
        "code": "INTERNAL_SERVER_ERROR",
        "exception": {
          "errors": [
            {
              "message": "Variable \"$_v3_sort\" got invalid value { _id: 1 }; Expected type SortFindManyPurchaseOrderInput.",
              "locations": []
            }
          ],
          "stacktrace": [
            "Error: Variable \"$_v3_sort\" got invalid value { _id: 1 }; Expected type SortFindManyPurchaseOrderInput.",
            "    at new CombinedError (E:\\tim\\Node Projects\\wh-express-babel\\node_modules\\graphql-tools\\dist\\stitching\\errors.js:82:28)",
            "    at Object.checkResultAndHandleErrors (E:\\tim\\Node Projects\\wh-express-babel\\node_modules\\graphql-tools\\dist\\stitching\\errors.js:98:15)",
            "    at CheckResultAndHandleErrors.transformResult (E:\\tim\\Node Projects\\wh-express-babel\\node_modules\\graphql-tools\\dist\\transforms\\CheckResultAndHandleErrors.js:9:25)",
            "    at E:\\tim\\Node Projects\\wh-express-babel\\node_modules\\graphql-tools\\dist\\transforms\\transforms.js:18:54",
            "    at Array.reduce (<anonymous>)",
            "    at applyResultTransforms (E:\\tim\\Node Projects\\wh-express-babel\\node_modules\\graphql-tools\\dist\\transforms\\transforms.js:17:23)",
            "    at E:\\tim\\Node Projects\\wh-express-babel\\node_modules\\graphql-tools\\dist\\stitching\\delegateToSchema.js:97:50",
            "    at step (E:\\tim\\Node Projects\\wh-express-babel\\node_modules\\graphql-tools\\dist\\stitching\\delegateToSchema.js:31:23)",
            "    at Object.next (E:\\tim\\Node Projects\\wh-express-babel\\node_modules\\graphql-tools\\dist\\stitching\\delegateToSchema.js:12:53)",
            "    at fulfilled (E:\\tim\\Node Projects\\wh-express-babel\\node_modules\\graphql-tools\\dist\\stitching\\delegateToSchema.js:3:58)",
            "    at <anonymous>",
            "    at process._tickCallback (internal/process/next_tick.js:188:7)"
          ]
        }
      }
    }
  ],
  "data": {
    "purchaseOrderPagination": null
  }
}

GraphQL query:

query {
  purchaseOrderMany(sort: _ID_ASC) {
    _id
  }
}

mongoose model:

// @ts-ignore
import mongoose from 'mongoose';

const Schema = mongoose.Schema;
const ObjectId = Schema.Types.ObjectId;

const schema = new Schema({
    orderNumber: { type: String, required: true, unique: true },
    supplier: { type: ObjectId, ref: 'Supplier', required: true },
    expectedDate: { type: Date, required: true },
    deliveredDate: { type: Date }, 
    receivedBy: { type: ObjectId, ref: 'User' },
    courier: { type: String },
    driver: { type: String },
    licenseNumber: { type: String },
    registrationNumber: { type: String },
    waybill: {
        number: { type: String },
        // document: { type: File }, // Document type
    },
    invoice: {
        number: { type: String },
        // document: { type: File }, // Document type
    },
    products: [{
        product: { type: ObjectId, ref: 'Product', required: true },
        expectedQuantity: { type: Number, required: true },
    }],
    expectedNoOfCartons: { type: Number },    
    cartons: [
        {
            cartonNumber: { type: Number, required: true, unique: true },
            carton: { type: ObjectId, ref: 'Carton', required: true, unique: true }, 
            notes: { type: String }
        }
    ],
    notes: { type: String },
    // documents: [Document],
    status: { 
        type: String, 
        enum: ['cancelled','expected','received','accepted'], 
        required: true,
        default: 'expected'
    },
    createdDate: { type: Date, default: Date.now, required: true },
    createdBy: { type: ObjectId, ref: 'User', required: true },   
    updatedDate: { type: Date },
    updatedBy: { type: ObjectId, ref: 'User' },         
    // hash: { type: String, required: true }
});

schema.set('toJSON', { virtuals: true });

export default mongoose.model('PurchaseOrder', schema);

graphql-compose-mongoose:

const customizationOptions = {}; // left it empty for simplicity, described below
import { composeWithMongoose } from 'graphql-compose-mongoose';
import { schemaComposer } from 'graphql-compose';
import * as jwt from 'jsonwebtoken';
import Cartons from '../models/cartons';
import Products from '../models/products';
import PurchaseOrders from '../models/purchase-orders';
import Suppliers from '../models/suppliers';
import Users from '../models/users';

const CartonsTC = composeWithMongoose(Cartons, customizationOptions);
const ProductsTC = composeWithMongoose(Products, customizationOptions);
const PurchaseOrdersTC = composeWithMongoose(PurchaseOrders, {
    resolvers: {
        findMany: {
            filter: {
                filterTypeName: 'FilterFindManyPurchaseOrderInput',
                operators: {
                    'expectedDate': ['gt','gte','lt','lte','ne','in[]','nin[]']
                }              
            },
        }
    }
});
const SuppliersTC = composeWithMongoose(Suppliers, customizationOptions);
const UsersTC = composeWithMongoose(Users, customizationOptions);

PurchaseOrdersTC.wrapResolverResolve('createOne', (next) => async (rp) => {
    // extend resolve params with hook
    rp.beforeRecordMutate = async (doc, resolveParams) => { 
        const { token } = rp.context;
        const decoded: any = jwt.verify(token, process.env.JWT_SECRET || '');
        const createdBy = await Users.findOne({ _id: decoded.sub });
        doc.createdBy = createdBy;      
        return doc;
    };

    return next(rp);
});

const updateBeforeRecordMutate = async (doc, resolveParams) => {
    const { token } = resolveParams.context;
    const decoded: any = jwt.verify(token, process.env.JWT_SECRET || '');
    const updatedBy = await Users.findOne({ _id: decoded.sub });
    doc.updatedBy = updatedBy;      
    doc.updatedDate = Date.now;
    // TODO: add code to check new state change to prevent invalid state changes
    return doc;
};

PurchaseOrdersTC.wrapResolverResolve('updateOne', (next) => async (rp) => {
    // extend resolve params with hook
    rp.beforeRecordMutate = updateBeforeRecordMutate;

    return next(rp);
});

PurchaseOrdersTC.wrapResolverResolve('updateById', (next) => async (rp) => {
    // extend resolve params with hook
    rp.beforeRecordMutate = updateBeforeRecordMutate;

    return next(rp);
});

PurchaseOrdersTC.addRelation(
    'createdBy',
    {
        resolver: () => UsersTC.getResolver('findById'),
        prepareArgs: {
            _id: (source) => source.createdBy,
        },
        projection: { createdBy: 1 }, // point fields in source object, which should be fetched from DB
    }
);

PurchaseOrdersTC.addRelation(
    'updatedBy',
    {
        resolver: () => UsersTC.getResolver('findById'),
        prepareArgs: {
            _id: (source) => source.updatedBy,
        },
        projection: { updatedBy: 1 }, // point fields in source object, which should be fetched from DB
    }
);

PurchaseOrdersTC.addRelation(
    'receivedBy',
    {
        resolver: () => UsersTC.getResolver('findById'),
        prepareArgs: {
            _id: (source) => source.receivedBy,
        },
        projection: { receivedBy: 1 }, // point fields in source object, which should be fetched from DB
    }
);

PurchaseOrdersTC.addRelation(
    'supplier',
    {
        resolver: () => SuppliersTC.getResolver('findById'),
        prepareArgs: {
            _id: (source) => source.supplier,
        },
        projection: { supplier: 1 }, // point fields in source object, which should be fetched from DB
    }
);

const PurchaseOrdersProductsTC = PurchaseOrdersTC.getFieldTC('products');

PurchaseOrdersProductsTC.addRelation(
    'product',
    {
        resolver: () => ProductsTC.getResolver('findById'),
        prepareArgs: {
            _id: (source) => source.product,
        },
        projection: { product: 1 }, // point fields in source object, which should be fetched from DB
    }
);

const PurchaseOrdersCartonsTC = PurchaseOrdersTC.getFieldTC('cartons');

PurchaseOrdersCartonsTC.addRelation(
    'carton',
    {
        resolver: () => CartonsTC.getResolver('findById'),
        prepareArgs: {
            _id: (source) => source.carton,
        },
        projection: { carton: 1 }, // point fields in source object, which should be fetched from DB
    }
);

schemaComposer.Query.addFields({
    // purchaseOrderBetweenDate: PurchaseOrdersTC.getResolver('findBetweenDate'),
    purchaseOrderById: PurchaseOrdersTC.getResolver('findById'),
    purchaseOrderByIds: PurchaseOrdersTC.getResolver('findByIds'),
    purchaseOrderOne: PurchaseOrdersTC.getResolver('findOne'),
    purchaseOrderMany: PurchaseOrdersTC.getResolver('findMany'),
    purchaseOrderCount: PurchaseOrdersTC.getResolver('count'),
    purchaseOrderConnection: PurchaseOrdersTC.getResolver('connection'),
    purchaseOrderPagination: PurchaseOrdersTC.getResolver('pagination'),
});

  schemaComposer.Mutation.addFields({
    purchaseOrderCreateOne: PurchaseOrdersTC.getResolver('createOne'),
    purchaseOrderCreateMany: PurchaseOrdersTC.getResolver('createMany'),
    purchaseOrderUpdateById: PurchaseOrdersTC.getResolver('updateById'),
    purchaseOrderUpdateOne: PurchaseOrdersTC.getResolver('updateOne'),
    purchaseOrderUpdateMany: PurchaseOrdersTC.getResolver('updateMany'),
    purchaseOrderRemoveById: PurchaseOrdersTC.getResolver('removeById'),
    purchaseOrderRemoveOne: PurchaseOrdersTC.getResolver('removeOne'),
    purchaseOrderRemoveMany: PurchaseOrdersTC.getResolver('removeMany'),
});

const graphqlSchema = schemaComposer.buildSchema();
export default graphqlSchema;

Node server is running apollo-server-express 2.2.2

app.tsx

// @ts-ignore
import dotenv from 'dotenv';
// @ts-ignore
import express from 'express';
import * as path from 'path';
// @ts-ignore
import logger from 'morgan';
import * as bodyParser from 'body-parser';
// @ts-ignore
import cors from 'cors';
import { ApolloServer } from 'apollo-server-express';
import schema from './graphql';

// change to current directory
process.chdir(__dirname);
dotenv.config();

// import routes from './routes';
import jwt from './_helpers/jwt';
import errorHandler from './_helpers/error-handler';
import routes from './controllers/users';

const app = express();
app.disable('x-powered-by');

// View engine setup
app.set('views', path.join(__dirname, '../views'));
app.set('view engine', 'pug');

app.use(logger('dev', {
  skip: () => app.get('env') === 'test'
}));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(express.static(path.join(__dirname, '../public')));
app.use(cors());

// use JWT auth to secure the api
app.use(jwt());

// Routes
// app.use('/', routes);
// api routes
app.use('/users', routes);

// The GraphQL endpoint
const server = new ApolloServer({
  schema,
  playground: {
    endpoint: '/graphql/play'
  },
  context: async ({req}) => { 
    const { authorization } = req.headers;
    return {
        token: authorization && authorization.split(' ')[1]
    } 
  }
});

server.applyMiddleware({ 
  app, 
  path: '/graphql'  
});

// global error handler
app.use(errorHandler);

export default app;

Any assistance in pointing me in the correct direction would be greatly appreciated.

semiautomatix commented 5 years ago

I've changed to express-graphql and the problem persists. So it appears to not be an issue with apollo.

semiautomatix commented 5 years ago

I'm using stitching with graphql-tools:

import { mergeSchemas } from 'graphql-tools';
import BinsSchema from './bins';
import CartonsSchema from './cartons';
import PurchaseOrdersSchema from './purchase-orders';
import ProductsSchema from './products';
import SuppliersSchema from './suppliers';
import UsersSchema from './users';

const schema = mergeSchemas({
  schemas: [
    BinsSchema,
    CartonsSchema,
    ProductsSchema,
    PurchaseOrdersSchema,
    SuppliersSchema,
      UsersSchema,
  ],
});

export default schema;

This is the source of the errors, what alternative do I have here?

semiautomatix commented 5 years ago

Ok, the alternative was quite obvious:

import { schemaComposer } from 'graphql-compose';

import * as Bins from './bins';
import * as Cartons from './cartons';
import * as PurchaseOrders from './purchase-orders';
import * as Products from './products';
import * as Suppliers from './suppliers';
import * as Users from './users';

schemaComposer.Query.addFields({
  ...Bins.queries,
  ...Cartons.queries,
  ...Products.queries,
  ...PurchaseOrders.queries,
  ...Suppliers.queries,
  ...Users.queries
});

schemaComposer.Mutation.addFields({
  ...Bins.mutations,
  ...Cartons.mutations,
  ...Products.mutations,
  ...PurchaseOrders.mutations,
  ...Suppliers.mutations,
  ...Users.mutations,
});

const graphqlSchema = schemaComposer.buildSchema();
export default graphqlSchema;
semiautomatix commented 5 years ago

graphql-tools mergeSchemas has known issues with enums, best to avoid!

nodkz commented 5 years ago

Thanks for info!

PS. if you already have typeDefs and resolvers in graphql-tools format, then you may use the following methods:

schemaComposer.addTypeDefs(`
  type Post {
    id: Int!
    title: String
    votes: Int
  }

  enum Sort {
    ASC 
    DESC
  }
`);

schemaComposer.addResolveMethods({
  Query: {
    posts: () => Post.find(),
  },
  Post: {
    votes: (post) => Vote.getFor(post.id),
  },
});

So stitching graphql-tools schemas and graphql-compose schemas maybe done via schemaComposer methods. With graphql-compose you will able to progromatically stitch types as you wish in more imperative way via addFields, remove and etc... methods.

JaosnHsieh commented 5 years ago

I encountered exactly the same problem. Since my project's graphql-tool generated schema has other settings ( dataloader, graphql-middlewares.....) so i cannot merge with graphql-compose-mongoose generated schema by schemaComposer.addTypeDefs and schemaComposer.addResolveMethods smoothly.

Instead of trying to merge schema by either mergedSchema from graphql-tools or schemaComposer.addTypeDefs and schemaComposer.addResolveMethods from 'graphl-compose-mongoose, I eventually start another apollo-server and bindip to only localhost and use introspectSchema provided by graphql-tools as below:


const getRemoteMergedSchema = async oldGraphqlSchema => {
  const link = new HttpLink({ uri: `http://${host}:${port}`, fetch });
  const introspectionResult = await introspectSchema(link);

  const remoteGraphqlSchema = await makeRemoteExecutableSchema({
    schema: introspectionResult,
    link: new HttpLink({ uri: `http://${host}:${port}`, fetch }),
  });

  const mergedSchema = mergeSchemas({
    schemas: [oldGraphqlSchema, remoteGraphqlSchema],
  });
  return mergedSchema;
};

const startApolloServer = schema =>
  new Promise((resolve, reject) => {
    const server = new ApolloServer({
      schema,
    });

    // config 'host' for limiting access permission to only localhost machine
    server.listen({ port, host }, async err => {
      if (err) {
        console.log(chalk.red(err));
        reject(err);
      }
      console.log(
        chalk.green(`Mongoose schema apollo server is listening on ${port}`),
      );
      resolve();
    });
  });

export const startServerAndGetRemoteMergedSchema = async oldGraphqlSchema => {
  try {
    const autoGenSchema = await getAutoGenSchema();
    await startApolloServer(autoGenSchema);
    const mergedSchema = await getRemoteMergedSchema(oldGraphqlSchema);
    return mergedSchema;
  } catch (err) {
    console.log(err);
  }
};
nodkz commented 5 years ago

@JaosnHsieh please open a new issue with small schema demo with midllewares. I'l think how it can be implemented. I will have free time at the end of month.

Globally I'm thinking to make framework from graphql-compose with middlewares (on top of exising middlewares eg shield), auth and DataLoader.

Your example will help me provide better solution for different use cases. Tnx

JaosnHsieh commented 5 years ago

@JaosnHsieh please open a new issue with small schema demo with midllewares. I'l think how it can be implemented. I will have free time at the end of month.

Globally I'm thinking to make framework from graphql-compose with middlewares (on top of exising middlewares eg shield), auth and DataLoader.

Your example will help me provide better solution for different use cases. Tnx

just opened a new issue and an issue reproduce repo thanks for your reply, awesome project!