graphql-compose-mongoose icon indicating copy to clipboard operation
graphql-compose-mongoose copied to clipboard

$near query not working (error in parsing)

Open ziedHamdi opened this issue 5 years ago • 10 comments

Hi,

I'm trying to execute a near query with the following filter : {"category":null,"viewer":"USER","location":{"$near":{"$geometry":{"type":"Point","coordinates":[10.3222671,36.88911649999999]},"$maxDistance":50000}}}

But I get the following error:

graphQl error :  Cannot read property 'forEach' of undefined
graphQl error stack:  TypeError: Cannot read property 'forEach' of undefined
    at castArraysOfNumbers (D:\Zied\work\weally\node_modules\mongoose\lib\schema\operators\helpers.js:25:7)
    at cast$geometry (D:\Zied\work\weally\node_modules\mongoose\lib\schema\operators\geospatial.js:41:7)
    at SingleNestedPath.cast$near (D:\Zied\work\weally\node_modules\mongoose\lib\schema\operators\geospatial.js:29:12)
    at SingleNestedPath.castForQuery (D:\Zied\work\weally\node_modules\mongoose\lib\schema\SingleNestedPath.js:197:20)
    at SingleNestedPath.SchemaType.castForQueryWrapper (D:\Zied\work\weally\node_modules\mongoose\lib\schematype.js:1326:17)
    at cast (D:\Zied\work\weally\node_modules\mongoose\lib\cast.js:288:39)
    at model.Query.Query.cast (D:\Zied\work\weally\node_modules\mongoose\lib\query.js:4607:12)
    at model.Query.<anonymous> (D:\Zied\work\weally\node_modules\mongoose\lib\query.js:2183:10)
    at model.Query._wrappedThunk [as _countDocuments] (D:\Zied\work\weally\node_modules\mongoose\lib\helpers\query\wrapThunk.js:16:8)
    at process.nextTick (D:\Zied\work\weally\node_modules\kareem\index.js:369:33)
    at process._tickCallback (internal/process/next_tick.js:61:11)

I logged the object you pass to cast$geometry :

function cast$geometry(val, self) {
  console.log( "geospacial cast$geometry Value : ", val )
  switch (val.$geometry.type) {
    case 'Polygon':
    case 'LineString':
    case 'Point':
      castArraysOfNumbers(val.$geometry.coordinates, self);
      break;
    default:
      // ignore unknowns
      break;
  }

  _castMinMaxDistance(self, val);

  return val;
}

And that object is completely different from what you seam to expect (it is displayed twice):

geospacial cast$geometry Value :  { '$geometry':
   { type: 'Point',
     'coordinates.0': 10.3222671,
     'coordinates.1': 36.88911649999999 },
  '$maxDistance': 50000 }
geospacial cast$geometry Value :  { '$geometry':
   { type: 'Point',
     'coordinates.0': 10.3222671,
     'coordinates.1': 36.88911649999999 },
  '$maxDistance': 50000 }

I'm using the latest versions of the lib:

    "graphql-compose": "^7.8.0",
    "graphql-compose-mongoose": "^7.3.1",

I upgraded from gcmongoose 7.1.1 since I had another bug : the query was correctly interpreted and logged by mongoose but there was a missing argument when querying : (the find method was called with two arguments instead of three which caused other errors)

Just for easier testing, I provide below the code I use to get to this issue: graph/complaint.js

const addComplaintByLocationSearch = function (resolverName) {
	const extendedResolver = ComplaintTC.getResolver(resolverName).addFilterArg({
		name: 'position',
		type: InputLatLngTC,
		description: 'Search around a given position (lat, lng) in priority',
		query: (query, value, resolveParams) => {
			// FIXME do localized query
			// query.name = new RegExp(value, 'i');
		},
	});
	extendedResolver.name = resolverName;
	ComplaintTC.addResolver(extendedResolver);
}


function wrapFindResolver(resolverName) {
	addComplaintByLocationSearch(resolverName)

	const findResolver = ComplaintTC.getResolver(resolverName);
	return findResolver.wrapResolve(next => async rp => {
		const filter = rp.args.filter;
		const entityId = filter.entityId;
		const position = filter.position;

		if (!entityId && position != null && position.lat != null) {
			filter.location = {
				$near: {
					$geometry: {
						type: "Point",
						coordinates: [position.lng, position.lat]
					},
					$maxDistance: 50000
				}
			}
			//saved as location in db
			filter.position = undefined
			console.log('########### args after : ', JSON.stringify( filter ) )
		}
		return next(rp)
	})

db/complaint.js (irrelevant fields removed)

const ComplainSchema = new Schema({
	entityGroupId: {type: String},
	entityId: {type: String},
	entity: {
		name: {type: String},
		kind: {type: String},
		groupTree: [GroupTreeItem],
	},
	categoryName: {type: String},
	orderId: String,
	location: PointSchema,
})

db/point.js

import mongoose from "mongoose";

export const PointSchema = new mongoose.Schema({
	type: {
		type: String,
		enum: ['Point'],
		required: true,
		default:'Point'
	},
	coordinates: {
		type: [Number],
		required: false
	}
});

The query I use is:

const COMPLAINTS = gql`
									query complaintConnection( $kind: String, $position: LatLngInput, $categoryName: String, $after: String){
										complaintConnection(after:$after, filter: { viewer: "USER", kind: $kind, category: $categoryName, position:$position }, first: 12, sort:POPULARITY_DESC) {
											count
											pageInfo {
												startCursor,
												endCursor,
												hasNextPage,
												hasPreviousPage
											},
											edges {
												cursor,
												node {
													_id, 
													entityId,
													stats {
														COMMENT {
															actionCount,
															userCount
														},
														USER_LIKE {
															userCount
														},
														SAME_ISSUE {
															actionCount,
															userCount,
															USER_ACTION_EXPECTED,
															USER_ACTION_REFUSED,
															SHOP_ACTION_EXPECTED,
															SHOP_ACTION_REFUSED,
															USER_SATISFIED
														},
													},
													entity {
														name
													},
													user {
														userId,
														userName,
														roleInShop
													},
													title,
													desc,
													video,
													attachments {
														url
													},
													mainAttachmentIndex,
													state,
													public,
													interactionState,
													interactionSpecifier,
													unanswered,
													createdAt,
													updatedAt
												}
											}
										}
									}`

ziedHamdi avatar Jan 14 '20 11:01 ziedHamdi

:tada: This issue has been resolved in version 7.3.2 :tada:

The release is available on:

Your semantic-release bot :package::rocket:

nodkz avatar Jan 21 '20 07:01 nodkz

Ok I upgraded to 7.3.3, but now I'm back to the error I had before upgrading to the 7.3.1 (should I post that in a separate issue?):

Mongoose: complaints.find({ location: { '$near': { '$geometry': { type: 'Point', coordinates: [ 10.3222671, 36.88911649999999 ] }, '$maxDistance': 50000 } }, '$or': [ { public: true, state: { '$in': [ 'OPEN', 'REFUSED' ] } }, { 'user.userId': '5e0df4ba796383107816fe42', state: { '$ne': 'DELETED' } } ] }, { limit: 13, sort: { popularity: -1 }, projection: { count: true, pageInfo: true, edges: true, __typename: true, _id: true, entityId: true, stats: true, entity: true, user: true, title: true, desc: true, video: true, attachments: true, mainAttachmentIndex: true, state: true, public: true, interactionState: true, interactionSpecifier: true, unanswered: true, createdAt: true, updatedAt: true, popularity: true } })
graphQl error :  $geoNear, $near, and $nearSphere are not allowed in this context
graphQl error stack:  MongoError: $geoNear, $near, and $nearSphere are not allowed in this context
    at Connection.<anonymous> (D:\Zied\work\weally\node_modules\mongodb-core\lib\connection\pool.js:443:61)
    at Connection.emit (events.js:182:13)
    at processMessage (D:\Zied\work\weally\node_modules\mongodb-core\lib\connection\connection.js:364:10)
    at Socket.<anonymous> (D:\Zied\work\weally\node_modules\mongodb-core\lib\connection\connection.js:533:15)
    at Socket.emit (events.js:182:13)
    at addChunk (_stream_readable.js:283:12)
    at readableAddChunk (_stream_readable.js:264:11)
    at Socket.Readable.push (_stream_readable.js:219:10)
    at TCP.onread (net.js:635:20)

executing the query that was logged by mongoose in the mongo console I get:

 db.complaints.find({ location: { '$near': { '$geometry': { type: 'Point', coordinates: [ 10.3222671, 36.88911649999999 ] }, '$maxDistance': 50000 } }, '$or': [ { public: true, state: { '$in': [ 'OPEN', 'REFUSED' ] } }, { 'user.userId': '5e0df4ba796383107816fe42',
 state: { '$ne': 'DELETED' } } ] }, { limit: 13, sort: { popularity: -1 }, projection: { count: true, pageInfo: true, edges: true, __typename: true, _id: true, entityId: true, stats: true, entity: true, user: true, title: true, desc: true, video: true, attachments:
 true, mainAttachmentIndex: true, state: true, public: true, interactionState: true, interactionSpecifier: true, unanswered: true, createdAt: true, updatedAt: true, popularity: true } })
Error: error: {
        "ok" : 0,
        "errmsg" : "Unsupported projection option: sort: { popularity: -1.0 }",
        "code" : 2,
        "codeName" : "BadValue"

On forums about this issue, I found I have to add an empty argument between the query and the second param: applying that makes the following query execute successfully in mongo terminal and give relevant results:

db.complaints.find({ 
  location: { '$near': { '$geometry': { type: 'Point', coordinates: [ 10.3222671, 36.88911649999999 ] }, 
    '$maxDistance': 50000 } }, '$or': [ { public: true, state: { '$in': [ 'OPEN', 'REFUSED' ] } }, { 'user.userId': 
    '5e0df4ba796383107816fe42',
    state: { '$ne': 'DELETED' } } ] 
  }, 
  {}, //here's the added argument
  { limit: 13, sort: { popularity: -1 }, projection: { count: true, pageInfo: true, edges: true, __typename: 
    true, _id: true, entityId: true, stats: true, entity: true, user: true, title: true, desc: true, video: true, 
    attachments: true, mainAttachmentIndex: true, state: true, public: true, interactionState: true, 
    interactionSpecifier: true, unanswered: true, createdAt: true, updatedAt: true, popularity: true 
  } 
})


... but I have no idea on what I should do on the gc-mongoose side to get that second argument inserted??? Additionally, I'm wondering why the error in gc-mongoose is different from the one in the terminal???

Just for completeness of the example, here's my model for the complaint.js entity

import mongoose, {Schema} from 'mongoose';
import UserInfoSchema from './fragment/userInfoFragment'
import {Actions} from "../procedures";
import {PointSchema} from "./geoPoint";

const GroupTreeItem = new Schema({
	id: {
		type: String,
		required: true
	},
	name: {
		type: String,
		required: true
	},
})

const ComplaintContribution = new Schema({
	state: {
		value: String,
		text: String
	},
	updatedAt: {
		type: Date,
		index: true
	},
}, {
	timestamps: {}
})

const ComplaintContributor = new Schema({
	user: UserInfoSchema,
	like: ComplaintContribution,
	join: ComplaintContribution,
	comment: ComplaintContribution,
})


const indexCreationCb = (err, db) => {
	console.log("After index creation : err: ", err, ", db : ", db)
}

const ComplainSchema = new Schema({
	entityGroupId: {type: String},
	entityId: {type: String},
	entity: {
		name: {type: String},
		kind: {type: String},
		groupTree: [GroupTreeItem],
	},
	categoryName: {type: String},
	orderId: String,
	location: PointSchema,
	contributors:[ComplaintContributor],

	billingDate: {
		type: Date,
		default: new Date()
	},
	issueDate: {
		type: Date,
		default: new Date()
	},
	issueRef: {type: String},
	solvedDate: {type: Date},
	user: {
		type: UserInfoSchema,
		required: true,
		index: true
	},
	title: {
		type: String,
		default: ""
	}, // standard types
	desc: {
		type: String,
		default: ""
	},
	video: {type: String},
	state: {
		required: true,
		type: String,
		enum: ["DRAFT", "OPEN", "SOLVED", "REFUSED", "DELETED"]
	},
	interactionState: {
		required: true,
		type: String,
		enum: [Actions.SHOP_ACTION_EXPECTED, Actions.SHOP_ACTION_REFUSED, Actions.USER_ACTION_EXPECTED, Actions.USER_ACTION_REFUSED, Actions.USER_SATISFIED],
		default: Actions.SHOP_ACTION_EXPECTED
	},
	interactionSpecifier: {
		type: String,
		enum: ["INFO", "PAYMENT", "REPLACEMENT"]
	},
	/**if the entity is of type "service" and is a paying member, the public variable will be false for a week before moving to a visible state.
	 * The non public state makes the issue visible only for its creator and the related entity members so they can communicate about it and find a solution. The state remains open for all the negociation, the stateNegocation explais who should do the next action
	 * The OPEN, REFUSED and CONFLICT states are considered as open issues, issues with other states are visible only to restricted viewers
	 */
	public: {
		type: Boolean,
		default: true
	},
	mainAttachmentIndex: {
		type: Number,
		default: 0
	},
	stats: {
		COMMENT: {
			actionCount: {
				type: Number,
				default: 0
			},
			userCount: {
				type: Number,
				default: 0
			}
		},
		USER_LIKE: {
			actionCount: {
				type: Number,
				default: 0
			},
			userCount: {
				type: Number,
				default: 0
			}
		},
		SAME_ISSUE: {
			actionCount: {
				type: Number,
				default: 0
			},
			userCount: {
				type: Number,
				default: 0
			},
			SHOP_ACTION_EXPECTED: {type: Number},
			USER_ACTION_EXPECTED: {type: Number},
			USER_ACTION_REFUSED: {type: Number},
			SHOP_ACTION_REFUSED: {type: Number},
			USER_SATISFIED: {type: Number},
			USER_QUIT: {type: Number}
		},
	},
	popularity: {
		type: Number,
		unique: true, // this is added to be able to sort with connection (it is pseudo unique since we keep a float value)
		default: null
	}, // this value is computed

	attachments: [{
		url: {
			type: String,
			required: true
		},
		thumbUrl: {
			type: String,
			required: true
		},
		name: String,
		mimeType: String,
		/**
		 * The attachmentId is used when the attachment is stored as a blob in mongodb
		 */
		attachmentId: Schema.Types.ObjectId
	}],
	removedAttachments: [{
		url: {
			type: String,
			required: true
		},
		thumbUrl: {
			type: String,
			required: true
		},
		name: String,
		mimeType: String,
		/**
		 * The attachmentId is used when the attachment is stored as a blob in mongodb
		 */
		attachmentId: Schema.Types.ObjectId,
	}],

	/**
	 * this field is used by the entity owner to see if he has answered to the user: as long as he didn't answer this field is visually marked as "unanswered"
	 */
	unanswered: {
		type: Boolean,
		default: true,
		index: true
	},
	createdAt: {
		type: Date,
		index: true
	},
	updatedAt: {
		type: Date,
		index: true
	},

}, {timestamps: {}})

mongoose.connection.collection('complaints').createIndex({
	entityId: 1,
}, indexCreationCb)
mongoose.connection.collection('complaints').createIndex({
	location: "2dsphere",
}, indexCreationCb)
mongoose.connection.collection('complaints').createIndex({
	_id:-1,
	public:1,
	state:1,
	location: "2dsphere"
}, indexCreationCb)
mongoose.connection.collection('complaints').createIndex({
	_id:-1,
	'user.userId':1,
	state:1,
	location	: "2dsphere"
}, indexCreationCb)
// mongoose.connection.collection('complaints').createIndex({
// 	location: "2dsphere"
// }, indexCreationCb)



export default mongoose.model('Complaint', ComplainSchema)

pointShema.js

import mongoose from "mongoose";

export const PointSchema = new mongoose.Schema({
	type: {
		type: String,
		enum: ['Point'],
		required: true,
		default:'Point'
	},
	coordinates: {
		type: [Number],
		required: false
	}
});

ziedHamdi avatar Jan 21 '20 12:01 ziedHamdi

It looks like that error somewhere between mongoose and mongodb. Try to manually write a working mongoose find query (not via shell).

After that please provide correct mongoose query here. And will be nice to see difference between your query and query generated by graphql-compose-mongoose.

nodkz avatar Jan 21 '20 12:01 nodkz

Hi Pavel,

Tried the following query via mongoose

			const result = await Complaint.find({
				location: {
					'$near': {
						'$geometry': {
							type: 'Point',
							coordinates: [10.3222671, 36.88911649999999]
						},
						'$maxDistance': 50000
					}
				},
				'$or': [{
					public: true,
					state: {'$in': ['OPEN', 'REFUSED']}
				}, {
					'user.userId': '5e0df4ba796383107816fe42',
					state: {'$ne': 'DELETED'}
				}]
			})

(which is a copy paste of the one in the logs of mongoose in the preceding comment)

That command logs the following way in mongoose logs (I formatted the json to make it readable):

				location: {
					'$near': {
						'$geometry': {
							type: 'Point',
							coordinates: [10.3222671, 36.88911649999999]
						},
						'$maxDistance': 50000
					}
				},
				'$or': [{
					public: true,
					state: {'$in': ['OPEN', 'REFUSED']}
				}, {
					'user.userId': '5e0df4ba796383107816fe42',
					state: {'$ne': 'DELETED'}
				}]
			}, {projection: {}}
)

and that command gives a successful result

To compare it with the command that throws an error here it is:

			complaints.find({
				location: {
					'$near': {
						'$geometry': {
							type: 'Point',
							coordinates: [10.3222671, 36.88911649999999]
						},
						'$maxDistance': 500000
					}
				},
				'$or': [{
					public: true,
					state: {'$in': ['OPEN', 'REFUSED']}
				}, {
					'user.userId': '5e0df4ba796383107816fe42',
					state: {'$ne': 'DELETED'}
				}]
			}, {
				limit: 13,
				sort: {popularity: -1},
				projection: {
					count: true,
					pageInfo: true,
					edges: true,
					__typename: true,
					_id: true,
					entityId: true,
					stats: true,
					entity: true,
					user: true,
					title: true,
					desc: true,
					video: true,
					attachments: true,
					mainAttachmentIndex: true,
					state: true,
					public: true,
					interactionState: true,
					interactionSpecifier: true,
					unanswered: true,
					createdAt: true,
					updatedAt: true,
					popularity: true
				}
			})

Here's the error:

graphQl error :  $geoNear, $near, and $nearSphere are not allowed in this context
graphQl error stack:  MongoError: $geoNear, $near, and $nearSphere are not allowed in this context
    at Connection.<anonymous> (D:\Zied\work\weally\node_modules\mongodb-core\lib\connection\pool.js:443:61)
    at Connection.emit (events.js:182:13)
    at processMessage (D:\Zied\work\weally\node_modules\mongodb-core\lib\connection\connection.js:364:10)
    at Socket.<anonymous> (D:\Zied\work\weally\node_modules\mongodb-core\lib\connection\connection.js:533:15)
    at Socket.emit (events.js:182:13)
    at addChunk (_stream_readable.js:283:12)
    at readableAddChunk (_stream_readable.js:264:11)
    at Socket.Readable.push (_stream_readable.js:219:10)
    at TCP.onread (net.js:635:20)
	

Please notice that since I'm using a connection, and that the countDocuments that precedes the query doesn't throw an error Mongoose: complaints.countDocuments({ location: { '$near': { '$geometry': { type: 'Point', coordinates: [ 10.3222671, 36.88911649999999 ] }, '$maxDistance': 500000 } }, '$or': [ { public: true, state: { '$in': [ 'OPEN', 'REFUSED' ] } }, { 'user.userId': '5e0df4ba796383107816fe42', state: { '$ne': 'DELETED' } } ] }, {})

ziedHamdi avatar Jan 22 '20 11:01 ziedHamdi

I'm trying to add incrementally the arguments you are using in your mongoose query, byt as soon as I add:

, {
				limit: 13,
				sort: {popularity: -1},
			})

I get the error:

graphQl error :  Unsupported projection option: sort: { popularity: -1 }
graphQl error stack:  MongoError: Unsupported projection option: sort: { popularity: -1 }
    at Connection.<anonymous> (D:\Zied\work\weally\node_modules\mongodb-core\lib\connection\pool.js:443:61)
    at Connection.emit (events.js:182:13)
    at processMessage (D:\Zied\work\weally\node_modules\mongodb-core\lib\connection\connection.js:364:10)
    at Socket.<anonymous> (D:\Zied\work\weally\node_modules\mongodb-core\lib\connection\connection.js:533:15)
    at Socket.emit (events.js:182:13)
    at addChunk (_stream_readable.js:283:12)
    at readableAddChunk (_stream_readable.js:264:11)
    at Socket.Readable.push (_stream_readable.js:219:10)
    at TCP.onread (net.js:635:20)

I was under mongoose 5.6.7, migrating to the 5.8.9 version I get the same error

But as I mentioned in the first post, adding an extra argument in the middle with the projection fixes the issue :

			const result = await Complaint.find({
				location: {
					'$near': {
						'$geometry': {
							type: 'Point',
							coordinates: [10.3222671, 36.88911649999999]
						},
						'$maxDistance': 50000
					}
				},
				'$or': [{
					public: true,
					state: {'$in': ['OPEN', 'REFUSED']}
				}, {
					'user.userId': '5e0df4ba796383107816fe42',
					state: {'$ne': 'DELETED'}
				}]
			}, {
				count: true,
				pageInfo: true,
				edges: true,
				__typename: true,
				_id: true,
				entityId: true,
				stats: true,
				entity: true,
				user: true,
				title: true,
				desc: true,
				video: true,
				attachments: true,
				mainAttachmentIndex: true,
				state: true,
				public: true,
				interactionState: true,
				interactionSpecifier: true,
				unanswered: true,
				createdAt: true,
				updatedAt: true,
				popularity: true
			}, {
				limit: 13,
				sort: {popularity: -1},
			})

When I look to the API, I see that in fact, there are supposed to be only the projection as a second argument, the sort, item count etc... are in the third argument if I understand it right: https://mongoosejs.com/docs/api/model.html#model_Model.find

ziedHamdi avatar Jan 22 '20 11:01 ziedHamdi

Since in any case, I will have to wait for a new version of gc-mongoose to fix the issue, is there a way I can execute my query manually (with mongoose or mongodb) and give the result to gc-mongoose so I can still benefit from the other services as the transforming of the table to a graphql connection compatible result result? Kind of a "visitor" GOF pattern to execute the query

ziedHamdi avatar Jan 22 '20 12:01 ziedHamdi

Hi Pavel,

Did you please have the time to look to this issue? I don't know how to workaround it except by rewriting the connection logic my self, which is worth waiting for your answer :)

You can direct me to the part that handles the query to try to patch it and push

ziedHamdi avatar Jan 29 '20 11:01 ziedHamdi

Sorry for the late response. Here is quite difficult to find the root of the problem. Anyway it somewhere in mongoose.

gc-connection uses under the hood findMany method from gc-mongoose which uses the following helpers: https://github.com/graphql-compose/graphql-compose-mongoose/blob/f819521650e8ed7c528acf1190500907ed04eec9/src/resolvers/findMany.js#L53-L59

So if you take a look at how helpers work – they chain query something like this:

query = model.find();
query = query.where({ ...your where condition...})
query = query.skip(...value from graphql request...);
query = query.limit(...value from graphql request...);
query = query.sort(...value from graphql request...);
query = query.select(...value from graphql request...);  // projection fields
const res = await query.exec();

So there is no way in gc-mongoose to use find with 3 args as you suggested above. The final find operation constructed inside mongoose.

Anyway you may write your own resolver with your custom logic like in this issue https://github.com/graphql-compose/graphql-compose-mongoose/issues/88

nodkz avatar Feb 04 '20 13:02 nodkz

Hi Pavel,

No problem for the response delay, I know you must be bombarded with questions. Thanks for answering.

If I attempt to create my own resolver I would have to rewrite the connection logic. Is there a way I can only write a second 'findManyHacked' resolver to do the query the way I want, and then call composeWithConnection(ComplaintActionUserStateTC, { findResolverName: 'findManyHacked', countResolverName: 'countHacked',

so that I can still benefit from the connection logic?

ziedHamdi avatar Feb 06 '20 09:02 ziedHamdi

Yep, findResolverName options exactly for this purpose was introduced. I forgot about it 😊

nodkz avatar Feb 06 '20 11:02 nodkz