jest testing discord bot commands - javascript

So I have a file that I use module exports on and it has 4 fields among which an execute field that takes 2 args and is essentially a function. It doesn't return anything instead it uses discord.js and runs this message.channel.send('Pong');. I want to test this using jest
How do I:
1 - Make sure that the message.channel.send was called with 'Pong' as args
2 - How do I mock it so it doesnt actually call it (i just want to make sure that the text inside of it, like the actual argument is 'Pong' since calling it won't work due to the lack of a proper message object)
I can access the actual command and execute it but I am unsure as to how to check the contents of message.channel.send. The message object cannot be reconstructed by me so that might also need mocking.
I'm using discord.js but that shouldn't really matter.
I will also have to test commands that feature functions that do have returns so how should I go about them?

You can try this:
const Discord = require('discord.js')
// replace this with whatever the execute command is
// e.g. const ping = require('./commands/ping').execute
const ping = async (message, args) => {
message.channel.send('Pong')
}
// a counter so that all the ids are unique
let count = 0
class Guild extends Discord.Guild {
constructor(client) {
super(client, {
// you don't need all of these but I just put them in to show you all the properties that Discord.js uses
id: count++,
name: '',
icon: null,
splash: null,
owner_id: '',
region: '',
afk_channel_id: null,
afk_timeout: 0,
verification_level: 0,
default_message_notifications: 0,
explicit_content_filter: 0,
roles: [],
emojis: [],
features: [],
mfa_level: 0,
application_id: null,
system_channel_flags: 0,
system_channel_id: null,
widget_enabled: false,
widget_channel_id: null
})
this.client.guilds.cache.set(this.id, this)
}
}
class TextChannel extends Discord.TextChannel {
constructor(guild) {
super(guild, {
id: count++,
type: 0
})
this.client.channels.cache.set(this.id, this)
}
// you can modify this for other things like attachments and embeds if you need
send(content) {
return this.client.actions.MessageCreate.handle({
id: count++,
type: 0,
channel_id: this.id,
content,
author: {
id: 'bot id',
username: 'bot username',
discriminator: '1234',
bot: true
},
pinned: false,
tts: false,
nonce: '',
embeds: [],
attachments: [],
timestamp: Date.now(),
edited_timestamp: null,
mentions: [],
mention_roles: [],
mention_everyone: false
})
}
}
class Message extends Discord.Message {
constructor(content, channel, author) {
super(channel.client, {
id: count++,
type: 0,
channel_id: channel.id,
content,
author,
pinned: false,
tts: false,
nonce: '',
embeds: [],
attachments: [],
timestamp: Date.now(),
edited_timestamp: null,
mentions: [],
mention_roles: [],
mention_everyone: false
}, channel)
}
}
const client = new Discord.Client()
const guild = new Guild(client)
const channel = new TextChannel(guild)
// the user that executes the commands
const user = {id: count++, username: 'username', discriminator: '1234'}
describe('ping', () => {
it('sends Pong', async () => {
await ping(new Message('ping', channel, user))
expect(channel.lastMessage.content).toBe('Pong')
})
})
You also need to put testEnvironment: 'node' in your jest configuration (see this issue).
Edit
You can also use Discord.SnowflakeUtil.generate() to generate an id if you need to obtain things like the timestamp.

Related

Discordjs cooldown database with sequalize unique id error

I have made a cooldowns database with sequalize and sqlite3 for my discord bot. This is to add individual cooldowns for certain commands however I am getting an error that says "id must be unique" even when I turn off the unique: true to unique: false or even make a new database storage for the other command. Below is the full error.
node:internal/process/promises:246
triggerUncaughtException(err, true /* fromPromise */);
^
Error
at Database.<anonymous> (C:\Users\brade\Desktop\Bear battles\node_modules\sequelize\lib\dialects\sqlite\query.js:179:27)
at C:\Users\brade\Desktop\Bear battles\node_modules\sequelize\lib\dialects\sqlite\query.js:177:50
at new Promise (<anonymous>)
at Query.run (C:\Users\brade\Desktop\Bear battles\node_modules\sequelize\lib\dialects\sqlite\query.js:177:12)
at C:\Users\brade\Desktop\Bear battles\node_modules\sequelize\lib\sequelize.js:311:28
at async SQLiteQueryInterface.insert (C:\Users\brade\Desktop\Bear battles\node_modules\sequelize\lib\dialects\abstract\query-interface.js:308:21)
at async model.save (C:\Users\brade\Desktop\Bear battles\node_modules\sequelize\lib\model.js:2432:35)
at async Function.create (C:\Users\brade\Desktop\Bear battles\node_modules\sequelize\lib\model.js:1344:12) {
name: 'SequelizeUniqueConstraintError',
errors: [
ValidationErrorItem {
message: 'id must be unique',
type: 'unique violation',
path: 'id',
value: '250412979835764738',
origin: 'DB',
instance: cooldown {
dataValues: {
id: '250412979835764738',
expiry: 1655869677206,
command: 'hunt',
updatedAt: 2022-06-22T03:42:57.207Z,
createdAt: 2022-06-22T03:42:57.207Z
},
_previousDataValues: { id: undefined, expiry: undefined, command: undefined },
uniqno: 1,
_changed: Set(3) { 'id', 'expiry', 'command' },
_options: {
isNewRecord: true,
_schema: null,
_schemaDelimiter: '',
attributes: undefined,
include: undefined,
raw: undefined,
silent: undefined
},
isNewRecord: true
},
validatorKey: 'not_unique',
validatorName: null,
validatorArgs: []
}
],
parent: [Error: SQLITE_CONSTRAINT: UNIQUE constraint failed: cooldown.id] {
errno: 19,
code: 'SQLITE_CONSTRAINT',
sql: 'INSERT INTO `cooldown` (`id`,`expiry`,`command`,`createdAt`,`updatedAt`) VALUES ($1,$2,$3,$4,$5);'
},
original: [Error: SQLITE_CONSTRAINT: UNIQUE constraint failed: cooldown.id] {
errno: 19,
code: 'SQLITE_CONSTRAINT',
sql: 'INSERT INTO `cooldown` (`id`,`expiry`,`command`,`createdAt`,`updatedAt`) VALUES ($1,$2,$3,$4,$5);'
},
fields: [ 'id' ],
sql: 'INSERT INTO `cooldown` (`id`,`expiry`,`command`,`createdAt`,`updatedAt`) VALUES ($1,$2,$3,$4,$5);'
}
Here is the code for my daily command.
const { SlashCommandBuilder } = require('#discordjs/builders')
const ms = require('ms')
const { defaultColor } = require('../../command-imports')
module.exports = {
data: new SlashCommandBuilder()
.setName('daily')
.setDescription('Claim your daily reward.'),
async execute (interaction, Cooldowns) {
let getCooldown = await Cooldowns.findOne({where: {id: interaction.user.id}}) // Finds if user has cooldown
let cooldownTime = getCooldown?.expiry
if(getCooldown && cooldownTime > new Date().getTime()) { // If cooldown is active run this
return interaction.reply({content: `You are still under cooldown! Please wait **${ms(cooldownTime - new Date().getTime(), {long: true})}**`})
} else if (getCooldown) { // If cooldown is expired remove from db and run rest of code
Cooldowns.destroy({where: {id: interaction.user.id, command: 'daily'}})
}
const claimedDaily = {
color: defaultColor,
description: "You have recieved N/A from your daily reward"
}
interaction.reply({embeds: [claimedDaily]})
Cooldowns.create({ // Creates 5 minute cooldown for hunt command
id: interaction.user.id,
expiry: new Date().getTime() + (60000 * 5),
command: 'daily'
})
}
}
Here is my hunt command.
const { SlashCommandBuilder } = require('#discordjs/builders')
const { errorColor, defaultColor } = require('../../command-imports')
const ms = require('ms')
module.exports = {
data: new SlashCommandBuilder()
.setName('hunt')
.setDescription('Hunt for a chance at finding a bear.'),
async execute(interaction, Cooldowns, Economy) {
let getCooldown = await Cooldowns.findOne({where: {id: interaction.user.id, command: 'hunt'}}) // Finds if user has cooldown
let getUser = await Economy.findOne({where: {id: interaction.user.id}})
if(!getUser) {
getUser = await Economy.create({id: interaction.user.id, coins: 0})
}
let cooldownTime = getCooldown?.expiry
if(getCooldown && cooldownTime > new Date().getTime()) { // If cooldown is active run this
return interaction.reply({content: `You are still under cooldown! Please wait **${ms(cooldownTime - new Date().getTime(), {long: true})}**`})
} else if (getCooldown) { // If cooldown is expired remove from db and run rest of code
Cooldowns.destroy({where: {id: interaction.user.id, command: 'hunt'}})
}
let whichExecute = Math.floor(Math.random() * 8) + 1 // 7/8 Chance for coins 1/8 Chance for bears
if(whichExecute <= 7) {
let coinsFound = Math.floor(Math.random() * 10) + 1 // Picks random coin amount between 1 - 10
const nothingFound = {
color: errorColor,
description: `No bear was found however you found ${coinsFound} :coin: \n You have ${getUser.coins} :coin:` // Displays coins earned and total coins
}
interaction.reply({embeds: [nothingFound]})
await Economy.update({coins: getUser.coins + coinsFound}, {where: {id: interaction.user.id}}) // Updates user in db
Cooldowns.create({ // Creates 5 minute cooldown for hunt command
id: interaction.user.id,
expiry: new Date().getTime() + (60000 * 5),
command: 'hunt'
})
}
else if(whichExecute === 8) {
const bearFound = { // Displays bear found
color: defaultColor,
description: "You found placeholder_beartype :bear:;;"
}
interaction.reply({embeds: [bearFound]})
Cooldowns.create({ // Creates 5 minute cooldown for hunt command
id: interaction.user.id,
expiry: new Date().getTime() + (60000 * 5),
command: 'hunt'
})
}
}
}
I still get the error for unique id even though it should be stored with the same id but under a different command name. This error doesn't appear until I run both commands (doesn't matter the order) for example I do /hunt and then do /daily later on. Any help fixing this would be great if you have any questions or stuff I can clarify let me know.
Note: This code is written for Discord.js v13.7.0 and Sequelize v6
Improperly altering tables
<Sequelize>.sync() is not executed with alter or force
Sequelize, according to their v6 documentation, provides a function called sync(). This function is used to ensure that your models are up to date with the database. However, there is one caveat to this. If you execute sync() with no arguments, the database will not overwrite existing data. This is where your issue stems from. When you first defined the models, most likely you did the following two things:
Defined id as DataTypes.INTEGER
Set id to be a unique field
Due to these and you executing .sync() with no arguments, the database's tables will not be overwritten, therefore preserving the old unique fields. Also, if you attempt to store a Discord ID as an Integer, you may encounter an issue where Discord IDs are shortened or rounded.
Solutions
Drop the table
As a one time fix, you can manually drop the tables from sqlite3 using this command and rerun the bot without modifying sync() which will create the table with the right data:
DROP TABLE 'Cooldowns'
Fixing outdated tables
In order to fix the outdated tables, you have two options. However, be careful as these are destructive actions. You can execute the sync() command with the following arguments:
<Sequelize>.sync({ alter: true }); // Alters tables
<Sequelize>.sync({ force: true }); // Forcefully recreates tables
As stated before, be careful with these actions as they are destructive and cannot be reverted if you do not have backups.
Properly storing Discord IDs
All you need to do is store the Discord IDs as a DataTypes.STRING or DataTypes.TEXT. This will preserve the Snowflake form of the ID and prevent shortening.
sequelize.define("User", {
id: {
type: DataTypes.STRING, // Or .TEXT
unique: false
},
// ...
});

Discord.js. Cannot add permissions to slash commands

Im trying to add permission to commands in such way, that only users with specific role can use it. At first im creating commands like its said in documentation.
Then Im trying to get each command and adding to them new permissions:
await commands.forEach(command => {
const permissions2 = [
{
id: guild.roles.everyone.id,
type: 'ROLE',
permission: false,
}
];
const permissions1 = [
{
id: botRole.id,
type: 'ROLE',
permission: true,
},
];
console.log(`Changing command ${command.id}`);
command.permissions.add(permissions2);
command.permissions.add(permissions1);
});
But whatever I do I get this error: TypeError [INVALID_TYPE]: Supplied permissions is not an Array of ApplicationCommandPermissionData.
Ive also tried running this code as shown in documentation but got same result:
await commands.forEach(command => {
...
console.log(`Changing command ${command.id}`);
command.permissions.add({permissions2});
command.permissions.add({permissions1});
});
Changing code to this helped:
const permissions2 = {
id: guild.roles.everyone.id,
type: 'ROLE',
permission: false,
};
const permissions1 = {
id: botRole.id,
type: 'ROLE',
permission: true,
};
let commandsList = await guild.commands.fetch();
await commandsList.forEach(slashCommand => {
console.log(`Changing command ${slashCommand.id}`);
//set the permissions for each slashCommand
guild.commands.permissions.add({
command: slashCommand.id,
permissions: [permissions1, permissions2]
});
});
I dont think you can use the forEach loop like you did.
commands.permissions#set consists of an object with the id of the command you want to edit, and an array with the permissions.
So you would have to rewrite your code to this:
//create the permissions objects
const permissions2 = {
id: guild.roles.everyone.id,
type: 'ROLE',
permission: false,
};
const permissions1 = {
id: botRole.id,
type: 'ROLE',
permission: true,
};
//loop through all the slashCommands
await commands.forEach(slashCommand => {
console.log(`Changing command ${slashCommand.id}`);
//set the permissions for each slashCommand
client.application.commands.permissions.set({command: slashCommand.id, permissions: [permissions1, permissions2]});
});
Read more about setting permissions for slashCommands here

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;

Sequelize findOrCreate(...).spread is not a function

I'm using the sequelize 6. When I'm runing findOrCreate().spread it says "findOrCreate(...).spread is not a function". Here is my code:
const response = await Response.findOrCreate({
where: {
participantId,
questionId,
},
defaults: responseNewDetail,
})
return res.status(200).send({ status: 0, data: response })
This is working fine, but it does not seperate the created status and the model value.
When I'm trying to use spread:
Response.findOrCreate({
where: {
participantId,
questionId,
},
defaults: responseNewDetail,
}).spread(function(response,created){
return res.status(200).send({ status: 0, data: response })
})
It says "Response.findOrCreate(...).spread is not a function".
This is the model file(response.js):
const { Sequelize } = require("sequelize")
module.exports = (sequelize, DataTypes) =>
sequelize.define(
"Response",
{
responseId: {
type: DataTypes.INTEGER,
primaryKey: true,
allowNull: false,
field: "Response_ID",
autoIncrement: true,
},
companyId: {
type: DataTypes.INTEGER,
allowNull: false,
field: "Company_ID",
},
...
)
Response model:
const ResponseModel = require("../models/response")
const Response = ResponseModel(sequelize, DataTypes)
Does anyone know what's wrong?
Since you're using await, you can change:
const response = await Response.findOrCreate({
where: {
participantId,
questionId,
},
defaults: responseNewDetail,
})
return res.status(200).send({ status: 0, data: response })
to
const [ response, created ] = await Response.findOrCreate({
where: {
participantId,
questionId,
},
defaults: responseNewDetail,
})
return res.status(200).send({ status: 0, data: response })
and all will be well.
This doesn't address the spread not a function thing though. For that, I only noticed this when I upgraded sequelize from an older version (I've been using sequelize since version 1). Your example of spread really should be working but for whatever reason it does not anymore (it doesn't for me either). Typically the only time I'm using spread instead of just awaiting it is when I want to defer execution of something. Right or wrong, I've begun to tackle that by just wrapping the await/async version in:
setImmediate(async () => {
// async things here
});
Hope I helped, sorry if I didn't.
eleborating Brian's Answer :-
findOrCreate returns two values first model instance and second created status,
so you can use
const [model,created] = ModelName.findOrCreate();
(Model Response in your case)
to get created status.

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