Reputation: 125
I am trying to $push
an Object
into a nested array, however it doesn't seem to be working. I am not sure what I am doing wrong.
My database is like this:
{
customers: {
name: String,
address: String,
proj_managers: [
{
name: String,
username: String,
projects: [
name: String,
tags: [
{
tag_no: Number,
tag_id: String,
time: String,
product_id: String,
urls: [
url: String,
count: Number
],
gps: String,
deactivate: Boolean
}
]
]
}
]
}
}
So what I want to do is $push
an array of tags into tags
, for a specific project
. My backend uses GraphQL
:
/index.js
// GraphQL schema
import schema from './schema'
// Mongoose Models
import Customer from './models/Customer'
import Manager from './models/Manager'
// Access to GraphQL API
app.use('/graphql', bodyParser.json(), graphqlExpress({ schema, context: { Customer, Manager } }))
app.use('/graphiql', graphiqlExpress({ endpointURL: '/graphql' }))
/schema/index.js
import { bundle } from 'graphql-modules'
import { makeExecutableSchema } from 'graphql-tools'
// GraphQL Modules
import Manager from './Manager'
import Customer from './Customer'
import ProjectManager from './ProjectManager'
import Project from './Project'
import Tag from './Tag'
import URL from './URL'
const modules = [Manager, Customer, ProjectManager, Project, Tag, URL]
export default makeExecutableSchema(bundle(modules))
/schema/Project.js
const schema = `
type Project {
_id: ID,
name: String!,
description: String,
tags: [Tag],
deactivate: Boolean!
}
input TagInput {
tagNo: Int!,
tagId: String!,
time: String,
productId: String,
url1: String,
url2: String,
gps: String
}
`
const queries = `
projects(customerUsername: String!): [Project],
project(projectID: ID!): Project
`
const mutations = `
editProject(id: ID!, name: String, description: String, deactivate: Boolean, customerUsername: String!, pmUsername: String!): String,
addProject(name: String!, description: String, customerID: ID!, pmUsername: String!): String,
pushTags(customerID: String!, username: String!, projectID: ID!, isManager: Boolean!, tags: [TagInput]!): String
`
const pushTags = async (root, { tags, customerID, username, projectID, isManager }, { Customer }) => {
let result = ''
let query = { _id: customerID }
let update = {}
let ts = []
let options = {
arrayFilters: [
{ 'a.username': username },
{ 'b._id': projectID }
]
}
tags.forEach(tag => {
if (isManager) {
ts.push({
tag_no: tag.tagNo,
tag_id: tag.tagId,
time: new Date(),
product_id: tag.productId,
urls: [
{ url: tag.url1, count: 0 },
{ url: tag.url2, count: 0 }
],
gps: tag.gps,
deactivate: false
})
} else {
update = {
$set: {
'proj_managers.$[a].projects.$[b].tags': {
product_id: tag.productId,
urls: [
{ url: tag.url1 },
{ url: tag.url2 }
],
gps: tag.gps
}
}
}
}
})
if (isManager) {
update = {
$push: {
'proj_managers.$[a].projects.$[b].tags': {
$each: ts
}
}
}
result = await Customer.update(query, update, options)
}
return result.ok && result.nModified ? 'Success' : 'Failed'
}
const resolvers = {
queries: {
projects,
project
},
mutations: {
addProject,
editProject,
pushTags
}
}
export default () => ({
schema,
queries,
mutations,
resolvers
})
The tags that are being sent to pushTags
mutation is:
[
{
"tagNo":"1",
"tagId":"02F9AMCGA38O7L",
"productId":"",
"url1":"",
"url2":"",
"gps":""
},{
"tagNo":"2",
"tagId":"028MFL6EV5L904",
"productId":"",
"url1":"",
"url2":"",
"gps":""
},{
"tagNo":"3",
"tagId":"02XDWCIL6W2IIX",
"productId":"",
"url1":"",
"url2":"",
"gps":""
}
];
The Document
{
"_id": ObjectId("5b0216f1cf14851f18e4312b"),
"deactivate": false,
"name": "Razer",
"address": "201 3rd Street, Suite 900 San Francisco, CA 94103 USA",
"phone_no": "0987654321",
"proj_managers": [
{
"deactivate": false,
"_id": ObjectId("5b021750cf14851f18e4312c"),
"name": "Liang-Shih Lin",
"username": "troopy",
"phone_no": "0987654321",
"password": "$2b$10$eOVoRkfmkHQyHkc6XaDanunUuyi0EFy.oZ.dRgKJYxBciMLYUVy0W",
"projects": [
{
"deactivate": false,
"_id": ObjectId("5b0217d4cf14851f18e4312d"),
"name": "Razer Godzilla",
"description": "A Godzilla Mouse",
"tags": [ ]
}
]
}
],
"__v": 0
}
I have tried using findByIdAndUpdate
, updateOne
, using a forEach()
helper function to loop through the tags and $push
it into the database one by one, but nothing seems to be working. I thought it could be my arrayFilters
, I changed b._id
to b.name
but that didn't work either.
I tried using this in the mongo
shell, with this query:
db.customers.update({ _id: "5afe642ed42aee261cb3292e" }, { $push: { "proj_managers.$[a].projects.$[b].tags": { tag_no: 1, tag_id: "0476F06A594980", time: "2018-05-20T23:18:18.824Z", product_id: "xr235Yf4", urls: [{url: "example.com", count: 0}, {url: "example2.com", count: 0}], gps: "", deactivate: false} } }, { arrayFilters: [{ "a.username": "joliver" }, { "b.id": "5b01367b6d053860e90e0f9f" }] })
The result:
WriteResult({
"nMatched": 0,
"nUpserted": 0,
"nModified": 0
})
If you want to take a look at the whole project, here is the link
Upvotes: 4
Views: 3805
Reputation: 151102
What you missed in your attempts is that arrayFilters
entries do not "autocast" like other properties in mongoose operations based on what value the "schema" has. This is because there is nothing there which actually ties the condition to a specific detail in the defined schema, or at least as far as the current mongoose release processes it.
Therefore if you match to an _id
within arrayFilters
, you need to actually "cast" the ObjectId
value yourself where the source comes from a "string":
let updated = await Customer.findOneAndUpdate(
{
"_id": "5b0216f1cf14851f18e4312b", //<-- mongoose can autocast these
"proj_managers": {
"$elemMatch": {
"username": "troopy",
"projects._id": "5b0217d4cf14851f18e4312d" //<-- here as well
}
}
},
{
"$push": {
"proj_managers.$[a].projects.$[b].tags": { "$each": tags }
}
},
{
"new": true,
// But not in here
"arrayFilters": [
{ "a.username": "troopy" },
{ "b._id": ObjectId("5b0217d4cf14851f18e4312d") } // <-- Cast manually
]
}
);
And then you get the result you should. Cutting it down a bit just for demonstration:
{
"_id": "5b0216f1cf14851f18e4312b",
"name": "Bill",
"address": "1 some street",
"proj_managers": [
{
"projects": [
{
"tags": [
{
"_id": "5b0239cc0a7a34219b0efdab",
"tagNo": 1,
"tagId": "02F9AMCGA38O7L",
"productId": "",
"url1": "",
"url2": "",
"gps": ""
},
{
"_id": "5b0239cc0a7a34219b0efdaa",
"tagNo": 2,
"tagId": "028MFL6EV5L904",
"productId": "",
"url1": "",
"url2": "",
"gps": ""
},
{
"_id": "5b0239cc0a7a34219b0efda9",
"tagNo": 3,
"tagId": "02XDWCIL6W2IIX",
"productId": "",
"url1": "",
"url2": "",
"gps": ""
}
],
"_id": "5b0217d4cf14851f18e4312d",
"name": "Razer Godzilla"
}
],
"_id": "5b021750cf14851f18e4312c",
"name": "Ted",
"username": "troopy"
}
],
"__v": 0
}
So the main thing here is to import the ObjectId
method from Types.ObjectId
and actually cast any strings you have. Input from external requests are typically "strings".
So for now, whenever you want such values in combination with matches for the positional filtered $[<identifier>]
operator and arrayFilters
just remember to actually "cast types".
Note that using $elemMatch
here for the same matching criteria on the array is not actually a "requirement" but probably should always be considered best practice. The reason being that whilst the arrayFilters
conditions will actually decide the selection of what actually gets altered, backing that up with a "query" condition to ensure the same conditions do exist on the array simply makes sure the document never even gets considered, and this actually reduces processing overhead.
Also note that because you are using a unique _id
value within each array member inherent to mongoose schema, then you can essentially "get away with":
let wider = await Customer.findOneAndUpdate(
{ "_id": "5b0216f1cf14851f18e4312b" },
{ "$push": {
"proj_managers.$[].projects.$[b].tags": { "$each": extra }
}},
{
"new": true,
"arrayFilters": [
{ "b._id": ObjectId("5b0217d4cf14851f18e4312d") }
]
}
);
So actually using the positional all $[]
instead and as mentioned simply skipping the other conditions in favor of the "unique" ObjectId
values. It seems lighter, but it actually adds a few cpu cycles by unnecessarily checking potentially various array paths, not to mention the match on the document itself if the array simply did not meet the other conditions.
I also can't really leave this without the "caveat" where even though modern MongoDB releases will support this, it's still really not advisable to have nested arrays. Yes it's possible to update them with modern features, but it's still far more complicated to "query" them than using a much flatter array structure or even completely flattened data in a separate collection, depending on the needs.
There's more description at Updating a Nested Array with MongoDB and Find in Double Nested Array MongoDB, but in most cases it really is true that the perceived "organizing" of structuring as "nested" actually does not exist, and it's really more of a hindrance.
And the full listing to demonstrate the working update:
const { Schema, Types: { ObjectId } } = mongoose = require('mongoose');
const uri = 'mongodb://localhost/test';
mongoose.Promise = global.Promise;
mongoose.set('debug', true);
const tagSchema = new Schema({
tagNo: Number,
tagId: String,
productId: String,
url1: String,
url2: String,
gps: String
})
const projectSchema = new Schema({
name: String,
tags: [tagSchema]
})
const projManagerSchema = new Schema({
name: String,
username: String,
projects: [projectSchema]
});
const customerSchema = new Schema({
name: String,
address: String,
proj_managers: [projManagerSchema]
});
const Customer = mongoose.model('Customer', customerSchema);
const log = data => console.log(JSON.stringify(data, undefined, 2));
(async function() {
try {
const conn = await mongoose.connect(uri);
await Promise.all(Object.entries(conn.models).map(([k,m]) => m.remove()));
await Customer.create({
_id: "5b0216f1cf14851f18e4312b",
name: 'Bill',
address: '1 some street',
proj_managers: [
{
_id: "5b021750cf14851f18e4312c",
name: "Ted",
username: "troopy",
projects: [
{
_id: "5b0217d4cf14851f18e4312d",
name: "Razer Godzilla",
tags: [ ]
}
]
}
]
});
const tags = [
{
"tagNo":"1",
"tagId":"02F9AMCGA38O7L",
"productId":"",
"url1":"",
"url2":"",
"gps":""
},{
"tagNo":"2",
"tagId":"028MFL6EV5L904",
"productId":"",
"url1":"",
"url2":"",
"gps":""
},{
"tagNo":"3",
"tagId":"02XDWCIL6W2IIX",
"productId":"",
"url1":"",
"url2":"",
"gps":""
}
];
const extra = [{
"tagNo":"4",
"tagId":"02YIVGMFZBC9OI",
"productId":"",
"url1":"",
"url2":"",
"gps":""
}];
let cust = await Customer.findOne({
"_id": "5b0216f1cf14851f18e4312b",
"proj_managers": {
"$elemMatch": {
"username": "troopy",
"projects._id": "5b0217d4cf14851f18e4312d"
}
}
});
log(cust);
let updated = await Customer.findOneAndUpdate(
{
"_id": "5b0216f1cf14851f18e4312b",
"proj_managers": {
"$elemMatch": {
"username": "troopy",
"projects._id": "5b0217d4cf14851f18e4312d"
}
}
},
{
"$push": {
"proj_managers.$[a].projects.$[b].tags": { "$each": tags }
}
},
{
"new": true,
"arrayFilters": [
{ "a.username": "troopy" },
{ "b._id": ObjectId("5b0217d4cf14851f18e4312d") }
]
}
);
log(updated);
let wider = await Customer.findOneAndUpdate(
{ "_id": "5b0216f1cf14851f18e4312b" },
{ "$push": {
"proj_managers.$[].projects.$[b].tags": { "$each": extra }
}},
{
"new": true,
"arrayFilters": [
{ "b._id": ObjectId("5b0217d4cf14851f18e4312d") }
]
}
);
log(wider);
mongoose.disconnect();
} catch(e) {
console.error(e)
} finally {
process.exit()
}
})()
Upvotes: 5