Mongoose Schema.index on multiple fields does not work with tests - javascript

I have created an uploadSchema (mongoose.Schema) with the fields (among the rest): key and bucket. each of them alone is not unique but together I want them to create a unique id.
in my code, I used the line (right after declaring the uploadSchema and right before the uploadModel):
uploadSchema.index({ key: 1, bucket: 1 }, { unique: true, background: true });
but then, in my tests (mocha and chai), the indexing is not enforced, and so I can create two instances with the same key and bucket (in my case).
for example, in my code:
await uploadModel.create({ key: testUpload.key, bucket: testUpload.bucket,
name: 'name1', ownerID: USER.id, parent: null }).should.eventually.exist;
and right after that:
await uploadModel.create({key: testUpload.key, bucket: testUpload.bucket,
name: 'name1', ownerID: USER.id, parent: null }).should.eventually.be.rejected;
does not throw the right error error:
AssertionError: expected promise to be rejected but it was fulfilled with { Object ($__, isNew, ...) }
Am I not using it correctly? Or is there a problem with indexing and testing?

Most likely you set autoIndex to false in your connection (which is recommended to do).
Either add it to you Schema:
let uploadSchema = mongoose.Schema({ ... }, {autoIndex: true});
But i would recommend just building the index yourself on the database, i think its the safest way around it.

so I figured it out!
Apparently, I used mongoose.connection.dropDatabase(); in my afterEach of the tests. That means that the indexes were reset each time.
So what I did was to recreate the indexes each time in my tests:
before(async () => {
// Remove files from DB
const collections = ['files', 'uploads'];
for (const i in collections) {
mongoose.connection.db.createCollection(collections[i], (err) => {});
}
await mongoose.connection.collections['files'].createIndex({ name: 1, parent: 1, ownerID: 1 }, { unique: true });
await mongoose.connection.collections['uploads'].createIndex({ key: 1, bucket: 1 }, { unique: true });
});
And in the beforeEach:
beforeEach(async () => {
const removeCollectionPromises = [];
for (const i in mongoose.connection.collections) {
removeCollectionPromises.push(mongoose.connection.collections[i].deleteMany({}));
}
await Promise.all(removeCollectionPromises);
});
afterEach is empty.
now it works :)

Related

How to seed roles and capabilities in MongoDB

I am new to working on a MongoDB and Docker, I am working on an application and couldn't find a more subtle way to seed my database using an npm run command. First I created a file called seed.js and then associated it to npm run seed command on the package.json file.
On the seed.js file I import Mongoose and the models but two things I will need to do is:
Create roles, if they don’t exist yet
Create capabilities, if they don’t exist yet and associate it to the
roles
The Roles that i want to create are:
admin (description: Administrator)
viewer (description: Viewer)
Capabilities
I need to check each endpoint of the Users service that should require authentication and create an adequate capability. Example: updateUser updates the user data. This could be done by the own user (so there must be an updateUserOwn capability) and by an administrator (that will have an updateUsers capability). I will have to analyse each endpoint and judge what is adequate but I cannot still find a way around getting the initial role and capabilities to the database.
UPDATE:
On the seeding itself, the updated solution works, but it requires lot of code and repetition that could probably be fixed by loops. I’d like to start creating the roles first which means creating an array with objects, with the data from the roles to be created. Each role has the fields role and description
const userRole = [{
role: admin
description: Administrator
},
{
role: viewer
description: Viewer
}]
The idea is that if the role exist it doesn't need to update but I don't know how do I loop through the array and create a role only if it doesn’t exist. Something like using updateOne, with the upsert: true option, but with the data on $setOnInsert as this will add the data only if a document is inserted.
I only need create and not update because in the future I’ll edit roles directly through the API. So, if a change was made on the admin role, for example, the seed will not overwrite it
During the loop, I'll need to create an associative array called rolesIds that will store the ObjectId of the created roles. It should result in something like this:
[
"admin": "iaufh984whrfj203jref",
"viewer": "r9i23jfeow9iefd0ew0",
]
Also each capability must have an array of roles it must be associated to. Example:
{
capability: "updateUsers",
description: "Update the data of all users",
roles: ["admin"]
}
How do I loop through the array on each element, prepare it to be inserted using the array with object IDs. Instead of roles: ["admin"]? something like roles: ["iaufh984whrfj203jref"], otherwise there’ll be a cast error. Remember each capability may be associated to more than one role, so I'll probably need to loop through them but I cannot find a way to create that logic.
Users Model
const userSchema = new mongoose.Schema(
{
.......
role: {
ref: "roles",
type: mongoose.Schema.Types.ObjectId,
},
);
module.exports = mongoose.model("User", userSchema);
Role Model:
const roles = new mongoose.Schema({
role: {
type: String,
required: true,
},
capabilities: [
{
type: mongoose.Schema.Types.ObjectId,
ref: "capabilities",
},
],
});
module.exports = mongoose.model("roles", roles);
Capabilities Model:
const capabilities = new mongoose.Schema({
capability: {
type: String,
required: true,
},
name: {
type: String,
},
});
module.exports = mongoose.model("capabilities", capabilities);
UPDATED: seed file:
const seedDB = async () => {
if (!process.env.DB_URI) {
throw new Error("Error connecting to MongoDB: DB_URI is not defined.");
}
try {
await mongoose.connect(process.env.DB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
useCreateIndex: true,
});
console.log("Connected to MongoDB");
const tasks = [
Capability.findOneAndUpdate(
{ name: "updateUserOwn" },
{ capability: "updateUser" },
{ upsert: true }
).exec(),
Capability.findOneAndUpdate(
{ name: "updateUsers" },
{ capability: "updateUser" },
{ upsert: true }
).exec(),
// Seed more...
];
const [updateUserOwn, updateUsers] = await Promise.all(tasks);
Role.bulkWrite([
{
updateOne: {
filter: { role: "Admin" },
update: { capabilities: [updateUsers] },
upsert: true,
},
},
{
updateOne: {
filter: { role: "Viewer" },
update: { capabilities: [updateUserOwn] },
upsert: true,
},
},
]);
console.log("seeded data", tasks);
} catch (error) {
console.log(`Error connecting to MongoDB: ${error}`);
}
};
seedDB();
You are on the right path overall.
Because capabilities are used as a reference you'd have to fetch or create them (get a ref) before assigning them to a role.
This could be your seed logic:
const tasks = [
Capability.findOneAndUpdate(
{ name: 'updateUserOwn' }, // matches or creates this capability
{ capability: 'updateUser' }, // adds this to the object
{ upsert: true, new: true } // `new` guarantees an object is always returned
}).exec(),
Capability.findOneAndUpdate(
{ name: 'updateUsers' },
{ capability: 'updateUser' },
{ upsert: true, new: true }
}).exec(),
// Seed more...
];
const [
updateUserOwn,
updateUsers,
] = await Promise.all(tasks);
// We can use bulk write for the second transaction so it runs in one go
await Role.bulkWrite([
{
updateOne: {
filter: { role: 'Admin' },
update: { capabilities: [updateUsers] },
upsert: true,
}
},
{
updateOne: {
filter: { role: 'Viewer' },
update: { capabilities: [updateUserOwn] },
upsert: true,
}
}
]);
We seed capabilities one by one using findOneAndUpdate so we can get a reference to each capability we intend to use on the roles
Then we use bulkWrite to seed the roles
I might have swapped the capabilities and their names but I hope you get the general idea
The seed would have been simpler if there weren't references involved - you could just use bulkWrite everything in one go, but in order to create object with inner references or add references to such object you first need to have the actual reference
You can create static mapping and loop through which would reduce the code a bit, and make things easier. This would also allow you to skip seeding items that already exist
Since capabilities are reused through roles I want to create them first, but it's no problem to alter the logic to first create roles and then capabilities, though it might not be as straight forward
Also each capability must have an array of roles it must be associated to.
This is called a "many to many" relationship (as roles also have an array of references to capabilities) which would only complicate logic. Are you sure you really need it - mongoose/monogo won't manage it automatically for you:
when you add a capability to a role you'd also need to sync and add the role inside capability.roles - manually
and the reverse - adding a role inside capability.roles you'd need to sync this and also manually add the capability to role.capabilities
the same thing for deleting capabilities or roles - manual cleanup
it can fail and would need to recover - e.g. a capability is added to role.capabilities but for some reason execution stopped and the role was not added to capability.roles - so the whole handling might need to be wrapped in a transaction
there are ways to cross reference roles and capabilities without have to have a "many to many" relationship
Here's a simple approach using save middleware to sync many to many relationships for create/update
Role.js
const mongoose = require('mongoose');
const roles = new mongoose.Schema({
role: {
type: String,
required: true,
},
description: String,
capabilities: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'capabilities',
},
],
});
roles.pre('save', async function save() {
// Doesn't need to run if there are no capabilities
if (!this.capabilities || this.capabilities.length === 0) return;
const Capability = mongoose.model('capabilities');
await Capability.updateMany(
{ _id: {$in: this.capabilities} },
// Adds only if it's missing
{ $addToSet: { roles: this._id }},
);
});
// Todo: similar logic to remove from capabilities if role is deleted
module.exports = mongoose.model("roles", roles);
Capability.js
const mongoose = require('mongoose');
const capabilities = new mongoose.Schema({
capability: {
type: String,
required: true,
},
description: {
type: String,
},
roles: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'roles',
}
]
});
capabilities.pre('save', async function save() {
if (!this.roles || this.roles.length === 0) return;
const Role = mongoose.model('roles');
await Role.updateMany(
{_id: {$in: this.roles}},
{$addToSet: {capabilities: this._id}},
);
})
// Todo: similar logic to remove from roles if capability is deleted
module.exports = mongoose.model("capabilities", capabilities);
Here's an update seed routine:
Seed.js
const mongoose = require('mongoose');
const Capability = require('./models/Capability');
const Role = require('./models/Role');
const CAPABILITIES = {
UPDATE_USERS: {
capability: 'updateUsers',
description: 'Update the data of all users',
},
VIEW_USERS: {
capability: 'viewUsers',
description: 'View public data of users',
},
UPDATE_OWN_RECORD: {
capability: 'updateUserOwn',
description: 'Update user own data',
}
}
const ROLES_TO_SEED = [
{
role: 'admin',
description: 'Administrator',
capabilities: [CAPABILITIES.UPDATE_USERS, CAPABILITIES.VIEW_USERS],
},
{
role: 'viewer',
description: 'Viewer',
capabilities: [CAPABILITIES.VIEW_USERS, CAPABILITIES.UPDATE_OWN_RECORD],
}
]
const seedDB = async () => {
await connectToDb();
await seedRoles();
};
const connectToDb = async () => {
if (!process.env.DB_URI) throw new Error('DB_URI is not defined.');
console.info('Connecting to database...');
await mongoose.connect(process.env.DB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
useCreateIndex: true,
useFindAndModify: false,
});
console.info('Connected \n');
}
const seedRoles = async () => {
console.log('Seeding Roles...');
// runs sequentially to skip creating duplicate capabilities
for (const role of ROLES_TO_SEED) {
await findOrCreateRole(role);
}
console.log('Complete \n');
}
const findOrCreateRole = async ({capabilities, role, ...defaults}) => {
console.info('Looking for role: ', role);
const fromDb = await Role.findOne({role}).exec();
if (fromDb) {
console.info('Role already exists skipping... \n');
return fromDb;
}
console.info('Role does not exist - creating new \n');
const doc = new Role({role, ...defaults});
// All capabilities (per role) can be created/found in parallel
const roleCapabilities = await Promise.all(capabilities.map(findOrCreateCapability));
doc.capabilities = roleCapabilities.map(c => c._id);
await doc.save();
console.info('Role created: ', role);
console.info('');
return doc;
}
const findOrCreateCapability = async ({capability, ...defaults}) => {
console.info('Looking for capability: ', capability);
let doc = await Capability.findOne({capability}).exec();
if (doc) {
console.info(`Capability ${capability} found - using existing...`);
}
else {
console.info(`Capability ${capability} does not exist - creating new`);
doc = new Capability({capability, ...defaults});
await doc.save();
}
return doc;
}
seedDB()
.then(() => {
console.info('Exiting...: ');
process.exit(0);
})
.catch(error => {
console.error('Seed failed');
console.error(error);
process.exit(1);
})
We have a dictionary of capabilities and a list of roles that we can map to db operations.
The idea is that each role should contain the full definition of a capability, it can be used to either find the existing capability or create it if it doesn't exist
For each role in the list we make a query to see if it exists.
When it exists we do nothing and move to the next role
When it doesn't exist we have all the data needed to create it and create/find any capabilities that it might need
When you figure out all the roles and capabilities of the application you just put them in the ROLES_TO_SEED and CAPABILITIES static mappings
The script relies on the above mentioned middleware modifications in models
And a small bonus
You don't need many to many relationship to match capabilities to the roles they are used in. Here's how you can aggregate that information if only the Role model have an array of capabilities (refs). Run this after the database is seeded:
const showCapabilitiesUsages = async () => {
const result = await Capability.aggregate([
{
$lookup: {
from: 'roles',
let: {searched: '$_id'},
pipeline: [
{
$match: {
$expr: {
$in: ['$$searched', '$capabilities']
}
}
}
],
as: 'roles'
}
}, {
$project: {
_id: 0,
capability: 1,
description: 1,
usedInRoles: {
$map: {
input: '$roles',
as: 'role',
in: '$$role.role',
}
}
}
}
]).exec();
console.log('Aggregate result: ', result);
}
You should get a result like:
Aggregate result: [
{
capability: 'updateUsers',
description: 'Update the data of all users',
usedInRoles: [ 'admin' ]
},
{
capability: 'viewUsers',
description: 'View public data of users',
usedInRoles: [ 'admin', 'viewer' ]
},
{
capability: 'updateUserOwn',
description: 'Update user own data',
usedInRoles: [ 'viewer' ]
}
]
Try something like this, it should would work:
const roles = [
{
name: 'admin',
description: 'Administrator',
},
{
name: 'viewer',
description: 'Viewer',
},
];
const capabilities = [
// Capabilities
{
name: 'createCapability',
description: 'Create a new capability',
roles: ['admin'],
},
{
name: 'deleteCapability',
description: 'Delete a capability',
roles: ['admin'],
}
// Roles
{
name: 'createRole',
description: 'Create a new role',
roles: ['admin'],
},
{
name: 'deleteRole',
description: 'Delete a role',
roles: ['admin'],
},
// Users
{
name: 'updateUser',
description: 'Update current user data',
roles: ['viewer'],
},
{
name: 'updateUsers',
description: 'Update the data from any user',
roles: ['admin'],
},
];
const seedRoles = async (roles) => {
if (0 == roles.length || !Array.isArray(roles)) {
return;
}
console.log('');
for (const role of roles) {
const savedRole = await Role.findOneAndUpdate(
{name: role.name},
{$setOnInsert: role},
{upsert: true, new: true, rawResult: true},
);
if (!savedRole) {
console.log(`Role “${savedRole.value.name}” already on database.`);
} else {
console.log(`Role “${savedRole.value.name}” added to database.`);
}
}
};
const seedCapabilities = async (capabilities) => {
if (0 == capabilities.length || !Array.isArray(capabilities)) {
return;
}
console.log('');
for (const capability of capabilities) {
const rolesToPush = capability.roles;
delete capability.roles;
const addedCapability = await Capability.findOneAndUpdate(
{name: capability.name},
{$setOnInsert: capability},
{upsert: true, new: true, rawResult: true},
);
if (!addedCapability) {
console.log(
`Capability “${addedCapability.value.name}” ` +
`already on database.`,
);
} else {
console.log(
`Capability “${addedCapability.value.name}” ` +
`added to database.`,
);
if (rolesToPush && Array.isArray(rolesToPush)) {
rolesToPush.forEach(async (role) => {
const roleToPush = await Role.findOne({name: role});
if (roleToPush) {
roleToPush.capabilities.push(addedCapability.value);
await roleToPush.save();
}
});
}
}
}
};
const seedDb = async (roles, capabilities, users) => {
try {
await seedRoles(roles);
await seedCapabilities(capabilities);
console.log('roles', roles);
} catch (error) {
console.error(error);
}
};
module.exports = seedDb;

MongoDB Aggregate is not matching specific field

I'm new to Aggregation in MongoDB and I'm trying to understand the concepts of it by making examples.
I'm trying to paginate my subdocuments using aggregation but the returned document is always the overall values of all document's specific field.
I want to paginate my following field which contains an array of Object IDs.
I have this User Schema:
const UserSchema = new mongoose.Schema({
username: {
type: String,
unique: true,
required: true
},
firstname: String,
lastname: String,
following: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
}],
...
}, { timestamps: true, toJSON: { virtuals: true }, toObject: { getters: true, virtuals: true } });
Without aggregation, I am able to paginate following,
I have this route which gets the user's post by their username
router.get(
'/v1/:username/following',
isAuthenticated,
async (req, res, next) => {
try {
const { username } = req.params;
const { offset: off } = req.query;
let offset = 0;
if (typeof off !== undefined && !isNaN(off)) offset = parseInt(off);
const limit = 2;
const skip = offset * limit;
const user = await User
.findOne({ username })
.populate({
path: 'following',
select: 'profilePicture username fullname',
options: {
skip,
limit,
}
})
res.status(200).send(user.following);
} catch (e) {
console.log(e);
res.status(500).send(e)
}
}
);
And my pagination version using aggregate:
const following = await User.aggregate([
{
$match: { username }
},
{
$lookup: {
'from': User.collection.name,
'let': { 'following': '$following' },
'pipeline': [
{
$project: {
'fullname': 1,
'username': 1,
'profilePicture': 1
}
}
],
'as': 'following'
},
}, {
$project: {
'_id': 0,
'following': {
$slice: ['$following', skip, limit]
}
}
}
]);
Suppose I have this documents:
[
{
_id: '5fdgffdgfdgdsfsdfsf',
username: 'gagi',
following: []
},
{
_id: '5fgjhkljvlkdsjfsldkf',
username: 'kuku',
following: []
},
{
_id: '76jghkdfhasjhfsdkf',
username: 'john',
following: ['5fdgffdgfdgdsfsdfsf', '5fgjhkljvlkdsjfsldkf']
},
]
And when I test my route for user john: /john/following, everything is fine but when I test for different user which doesn't have any following: /gagi/following, the returned result is the same as john's following which aggregate doesn't seem to match user by username.
/john/following | following: 2
/kuku/following | following: 0
Aggregate result:
[
{
_id: '5fdgffdgfdgdsfsdfsf',
username: 'kuku',
...
},
{
_id: '5fgjhkljvlkdsjfsldkf',
username: 'gagi',
...
}
]
I expect /kuku/following to return an empty array [] but the result is same as john's. Actually, all username I test return the same result.
I'm thinking that there must be wrong with my implementation since I've only started exploring aggregation.
Mongoose uses a DBRef to be able to populate the field after it has been retrieved.
DBRefs are only handled on the client side, MongoDB aggregation does not have any operators for handling those.
The reason that aggregation pipeline is returning all of the users is the lookup's pipeline does not have a match stage, so all of the documents in the collection are selected and included in the lookup.
The sample document there is showing an array of strings instead of DBRefs, which wouldn't work with populate.
Essentially, you must decide whether you want to use aggregation or populate to handle the join.
For populate, use the ref as shown in that sample schema.
For aggregate, store an array of ObjectId so you can use lookup to link with the _id field.

Express: return is not showing array in api

So I have collections of Teacher, Classes. to understand my problem its necessary to understand my database. the structure is
const TeacherSchema = new Schema(
{
name: {
type: String,
required: true
},
email: {
type: String,
required: true
},
role: {
type: String,
default: "teacher"
},
userid: {
type: String,
required: true
},
password: {
type: String,
required: true
},
profileImage: {
type: String,
required: false
},
classes: [{
type:Schema.Types.ObjectId,
ref:'class'
}]//1 teacher will have multiple classes
},
{ timestamps: true }
);
and the class:
const ClassSchema = new Schema(
{
subject: {
type: String,
required: true
},
teacher:[{
type:Schema.Types.ObjectId,
ref: 'teacher'
}],//once class will have only one teacher
},
{ timestamps: true }
);
What i want to do is, I want to show teacher how many classes he/she has. here is my controller.
exports.getAllClass= async (req, res, next) => {
let teacherClasses
try{
teacherClasses= await Teacher.findById(req.params.id)//teacher database
arrayOfClass= teacherClasses.classes //getting the class it has array of objectID
arrayOfClass.forEach(async (classes)=>{
let classDB =await Class.findById(classes)
console.log(classDB.subject)// in the console it shows all the classes.
return res.status(200).json({
'name':classDB.subject //but here it only shows one class,the first class name
})
});
}catch(err){
console.log(err)
}
}
I have tried many thing, but cant reach to a proper solution. Can you tell me what did i do wrong here or is there any better approach? I want to show all the classes in the return. I am new in programming hence assigned in complex task, please help me.
You are using forEach on the arrayOfClass and within that forEach you return and send the response. So as soon as the first promise is resolved, the response is sent.
One simple way to do this is to use a for .. of loop and await each class-promise inside and keep track of each result. Once that's done you can send the response:
...
const classes = [];
for(const classId of arrayofClass) {
const classDB = await Class.findById(classId);
classes.push({ name: classDB.subject });
}
return res.json(classes);
Another way to do this is to use Promise.all() which might be better if you're dealing with a bigger dataset as is processes in parallel whereas the first solution processes in sequence:
...
const classPromises = await Promise.all(arrayofClass.map(classId => Class.findById(classId)));
const result = classPromises.map(classDB => ({
name: classDB.subject
}));
return res.json(result);

Include order is relevant on scope but not on finder options

Depending on the order I place the includes in the scope, sequelize won't fetch one of the includes I requested. Oddly, if instead of a scope I put the options directly in the finder options (findOne() in this case), both requests work correctly. Why is this happening?
const Sequelize = require('sequelize');
const sequelize = new Sequelize({ dialect: 'sqlite', storage: 'db.sqlite' });
const Foo = sequelize.define('foo', { name: Sequelize.STRING });
const Bar = sequelize.define('bar', { name: Sequelize.STRING });
Foo.belongsToMany(Bar, { through: 'foo_bars', foreignKey: 'fooId' });
Foo.belongsTo(Bar, { foreignKey: 'barId', as: 'whatever' });
const includeOrder1 = { include: [{ model: Bar, as: 'whatever' }, Bar] };
const includeOrder2 = { include: [Bar, { model: Bar, as: 'whatever' }] };
Foo.addScope('test1', includeOrder1);
Foo.addScope('test2', includeOrder2);
const logGotWhatever = obj => console.log('Got whatever: ' + !!obj.whatever);
sequelize.sync()
.then(() => Bar.create({ name: 'The Bar' }).then(bar => {
return Foo.create({ name: 'The Foo', barId: bar.id }).then(foo => foo.addBar(bar));
}))
.then(() => Foo.findOne(includeOrder1).then(logGotWhatever))
.then(() => Foo.findOne(includeOrder2).then(logGotWhatever))
.then(() => Foo.scope('test1').findOne().then(logGotWhatever))
.then(() => Foo.scope('test2').findOne().then(logGotWhatever));
After running npm install sequelize sqlite3, the code above outputs:
Got whatever: true
Got whatever: true
Got whatever: false
Got whatever: true
Although I expected true in all four cases.
I'm using the most recent (non-beta) version of sequelize at the moment: 4.42.0
This is indeed a bug, which was indirectly resolved by PR #9735 in 2018-10-28, which changed how includes are dealt with (both in scopes and in finder options) and is available in v5.0.0-beta.14 and above.
Running the code above with npm install sequelize#next sqlite3 yields:
Got whatever: true
Got whatever: true
Got whatever: true
Got whatever: true
As it should.
This fix probably will not be backported to v4 because it involved breaking changes on how includes work (although it isn't exactly a catastrophic change, is is technically a breaking change).

Is there a way to get rid of [Object: null prototype] in GraphQL

I'm trying to make one-to-many relationship database with Mongoose and GraphQL.
Whenever I insert the data to GraphQL mutation argument, I will get [Object: null prototype] error.
I notice the object will have [Object: null prototype] in front of it when I tried to console.log for debug purpose.
I have tried many ways, tried to map() args or even to use replace() but no luck. All I have been getting is "args.ingredient.map/replace is not a function"
I have test hard coded method by changing the args for example:
args.category = '5c28c79af62fad2514ccc788'
args.ingredient = '5c28c8deb99a9d263462a086'
Surprisingly it works with this method. I assume the input cannot be an object but just an ID.
Refer below for actual results.
Resolvers
Query: {
recipes: async (root, args, { req }, info) => {
return Recipe.find({}).populate('ingredient category', 'name createdAt').exec().then(docs => docs.map(x => x))
},
},
Mutation: {
addRecipe: async (root, args, { req }, info) => {
// args.category = '5c28c79af62fad2514ccc788'
// args.ingredient = '5c28c8deb99a9d263462a086'
// console.log(args.map(x => x))
return Recipe.create(args)
}
}
TypeDef
extend type Mutation {
addRecipe(name: String!, direction: [String!]!, ingredient: [IngredientInput], category: [CategoryInput]): Recipe
}
type Recipe {
id: ID!
name: String!
direction: [String!]!
ingredient: [Ingredient!]!
category: [Category!]!
}
input IngredientInput {
id: ID!
}
input CategoryInput {
id: ID!
}
Models
const recipeSchema = new mongoose.Schema({
name: String,
direction: [String],
ingredient: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Ingredient' }],
category: { type: mongoose.Schema.Types.ObjectId, ref: 'Category' }
}, {
timestamps: true // createdAt, updateAt
})
const Recipe = mongoose.model('Recipe', recipeSchema)
This is the result I console log the args when inserting the data
{
name: 'Butter Milk Chicken TEST2',
direction: [ 'Step1', 'Step2', 'Step3' ],
ingredient:[[Object: null prototype] { id: '5c28c8d6b99a9d263462a085' }],
category: [[Object: null prototype] { id: '5c28c79af62fad2514ccc788' }]
}
I assume I need to get something like this
{
name: 'Butter Milk Chicken TEST2',
direction: [ 'Step1', 'Step2', 'Step3' ],
args.category = ['5c28c79af62fad2514ccc788']
args.ingredient = ['5c28c8ccb99a9d263462a083', '5c28c8d3b99a9d263462a084', '5c28c8d6b99a9d263462a085']
}
You can do something like below,and [Object: null prototype] would disappear
const a = JSON.parse(JSON.stringify(args));
args.category is
[[Object: null prototype] { id: '5c28c79af62fad2514ccc788' }],
JSON.parse(JSON.stringify(args.category) would be { id: '5c28c79af62fad2514ccc788' }
Normally, when passing InputTypes as an argument, I solve it using destructuring, like this:
addRecipe: async (root, { ...args }, { req }, info) => {
// args.category = '5c28c79af62fad2514ccc788'
// args.ingredient = '5c28c8deb99a9d263462a086'
// console.log(args.map(x => x))
return Recipe.create(args)
}
Try destructuring assignment with the args parameter. Your problem happens because args is an object that holds the mutation arguments. After destructuring it, you're gonna be able to access your argument directly:
Mutation: {
addRecipe: async (root, { args }, { req }, info) => {
return Recipe.create(args)
}
}
In the playground I changed in settings request credentials from omit to include and it worked, "request.credentials": "include" I hope it helps.
We had this problem. We were looking to query a service object in the database that had a price on it.
Expected Result:
service: {
price: 9999
}
However, we accidentally queried “services” (instead of “service”) which gave us an array of prices (with only one price) like so:
[ [Object: null prototype] { price: 9.99 } ]
This was caused by a bad query.
Once we changed the query to “service” (instead of “services”) the data came back as expected without the null prototype.
We use Prisma as our ORM though but perhaps you are querying for recipes when you should be querying for recipe.

Categories

Resources