Skip to content

Commit

Permalink
refactor(sequelize): Tweak code for consistency (discordjs#933)
Browse files Browse the repository at this point in the history
  • Loading branch information
Renal-Of-Loon authored Oct 26, 2021
1 parent db39710 commit c1abfa3
Show file tree
Hide file tree
Showing 6 changed files with 90 additions and 43 deletions.
9 changes: 8 additions & 1 deletion code-samples/sequelize/currency/13/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,15 @@ Reflect.defineProperty(currency, 'add', {
/* eslint-disable-next-line func-name-matching */
value: async function add(id, amount) {
const user = currency.get(id);

if (user) {
user.balance += Number(amount);
return user.save();
}

const newUser = await Users.create({ user_id: id, balance: amount });
currency.set(id, newUser);

return newUser;
},
});
Expand All @@ -35,6 +38,7 @@ Reflect.defineProperty(currency, 'getBalance', {
client.once('ready', async () => {
const storedBalances = await Users.findAll();
storedBalances.forEach(b => currency.set(b.user_id, b));

console.log(`Logged in as ${client.user.tag}!`);
});

Expand All @@ -50,13 +54,15 @@ client.on('interactionCreate', async interaction => {

if (commandName === 'balance') {
const target = interaction.options.getUser('user') || interaction.user;

return interaction.reply(`${target.tag} has ${currency.getBalance(target.id)}💰`);
} else if (commandName === 'inventory') {
const target = interaction.options.getUser('user') || interaction.user;
const user = await Users.findOne({ where: { user_id: target.id } });
const items = await user.getItems();

if (!items.length) return interaction.reply(`${target.tag} has nothing!`);

return interaction.reply(`${target.tag} currently has ${items.map(t => `${t.amount} ${t.item.name}`).join(', ')}`);
} else if (commandName === 'transfer') {
const currentAmount = currency.getBalance(interaction.user.id);
Expand All @@ -73,6 +79,7 @@ client.on('interactionCreate', async interaction => {
} else if (commandName === 'buy') {
const itemName = interaction.options.getString('item');
const item = await CurrencyShop.findOne({ where: { name: { [Op.like]: itemName } } });

if (!item) return interaction.reply('That item doesn\'t exist.');
if (item.cost > currency.getBalance(interaction.user.id)) {
return interaction.reply(`You don't have enough currency, ${interaction.user}`);
Expand All @@ -82,7 +89,7 @@ client.on('interactionCreate', async interaction => {
currency.add(interaction.user.id, -item.cost);
await user.addItem(item);

interaction.reply(`You've bought a ${item.name}`);
return interaction.reply(`You've bought a ${item.name}`);
} else if (commandName === 'shop') {
const items = await CurrencyShop.findAll();
return interaction.reply(Formatters.codeBlock(items.map(i => `${i.name}: ${i.cost}💰`).join('\n')));
Expand Down
2 changes: 2 additions & 0 deletions code-samples/sequelize/currency/13/dbInit.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ sequelize.sync({ force }).then(async () => {
CurrencyShop.upsert({ name: 'Coffee', cost: 2 }),
CurrencyShop.upsert({ name: 'Cake', cost: 5 }),
];

await Promise.all(shop);
console.log('Database synced');

sequelize.close();
}).catch(console.error);
46 changes: 25 additions & 21 deletions code-samples/sequelize/currency/13/dbObjects.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,30 @@ const UserItems = require('./models/UserItems.js')(sequelize, Sequelize.DataType

UserItems.belongsTo(CurrencyShop, { foreignKey: 'item_id', as: 'item' });

/* eslint-disable-next-line func-names */
Users.prototype.addItem = async function(item) {
const useritem = await UserItems.findOne({
where: { user_id: this.user_id, item_id: item.id },
});

if (useritem) {
useritem.amount += 1;
return useritem.save();
}

return UserItems.create({ user_id: this.user_id, item_id: item.id, amount: 1 });
};

/* eslint-disable-next-line func-names */
Users.prototype.getItems = function() {
return UserItems.findAll({
where: { user_id: this.user_id },
include: ['item'],
});
};
Reflect.defineProperty(Users.prototype, 'addItem', {
/* eslint-disable-next-line func-name-matching */
value: async function addItem(item) {
const userItem = await UserItems.findOne({
where: { user_id: this.user_id, item_id: item.id },
});

if (userItem) {
userItem.amount += 1;
return userItem.save();
}

return UserItems.create({ user_id: this.user_id, item_id: item.id, amount: 1 });
},
});

Reflect.defineProperty(Users.prototype, 'getItems', {
/* eslint-disable-next-line func-name-matching */
value: function getItems() {
return UserItems.findAll({
where: { user_id: this.user_id },
include: ['item'],
});
},
});

module.exports = { Users, CurrencyShop, UserItems };
11 changes: 11 additions & 0 deletions code-samples/sequelize/tags/sequelize.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,52 +58,63 @@ client.on('interactionCreate', async interaction => {
description: tagDescription,
username: interaction.author.username,
});

return interaction.reply(`Tag ${tag.name} added.`);
} catch (error) {
if (error.name === 'SequelizeUniqueConstraintError') {
return interaction.reply('That tag already exists.');
}

return interaction.reply('Something went wrong with adding a tag.');
}
} else if (commandName === 'tag') {
const tagName = interaction.options.getString('name');

// equivalent to: SELECT * FROM tags WHERE name = 'tagName' LIMIT 1;
const tag = await Tags.findOne({ where: { name: tagName } });

if (tag) {
// equivalent to: UPDATE tags SET usage_count = usage_count + 1 WHERE name = 'tagName';
tag.increment('usage_count');
return interaction.reply(tag.get('description'));
}

return interaction.reply(`Could not find tag: ${tagName}`);
} else if (commandName === 'edittag') {
const tagName = interaction.options.getString('name');
const tagDescription = interaction.options.getString('description');

// equivalent to: UPDATE tags (descrption) values (?) WHERE name = ?;
const affectedRows = await Tags.update({ description: tagDescription }, { where: { name: tagName } });

if (affectedRows > 0) {
return interaction.reply(`Tag ${tagName} was edited.`);
}

return interaction.reply(`Could not find a tag with name ${tagName}.`);
} else if (commandName === 'taginfo') {
const tagName = interaction.options.getString('name');

// equivalent to: SELECT * FROM tags WHERE name = 'tagName' LIMIT 1;
const tag = await Tags.findOne({ where: { name: tagName } });

if (tag) {
return interaction.reply(`${tagName} was created by ${tag.username} at ${tag.createdAt} and has been used ${tag.usage_count} times.`);
}

return interaction.reply(`Could not find tag: ${tagName}`);
} else if (commandName === 'showtags') {
const tagList = await Tags.findAll({ attributes: ['name'] });
const tagString = tagList.map(t => t.name).join(', ') || 'No tags set.';

return interaction.reply(`List of tags: ${tagString}`);
} else if (commandName === 'removetag') {
// equivalent to: DELETE from tags WHERE name = ?;
const tagName = interaction.options.getString('name');
const rowCount = await Tags.destroy({ where: { name: tagName } });

if (!rowCount) return interaction.reply('That tag did not exist.');

return interaction.reply('Tag deleted.');
}
});
Expand Down
11 changes: 11 additions & 0 deletions guide/sequelize/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,12 +170,14 @@ try {
description: tagDescription,
username: interaction.user.username,
});

return interaction.reply(`Tag ${tag.name} added.`);
}
catch (error) {
if (error.name === 'SequelizeUniqueConstraintError') {
return interaction.reply('That tag already exists.');
}

return interaction.reply('Something went wrong with adding a tag.');
}
```
Expand All @@ -199,11 +201,14 @@ const tagName = interaction.options.getString('name');

// equivalent to: SELECT * FROM tags WHERE name = 'tagName' LIMIT 1;
const tag = await Tags.findOne({ where: { name: tagName } });

if (tag) {
// equivalent to: UPDATE tags SET usage_count = usage_count + 1 WHERE name = 'tagName';
tag.increment('usage_count');

return interaction.reply(tag.get('description'));
}

return interaction.reply(`Could not find tag: ${tagName}`);
```

Expand All @@ -220,9 +225,11 @@ const tagDescription = interaction.options.getString('description');

// equivalent to: UPDATE tags (description) values (?) WHERE name='?';
const affectedRows = await Tags.update({ description: tagDescription }, { where: { name: tagName } });

if (affectedRows > 0) {
return interaction.reply(`Tag ${tagName} was edited.`);
}

return interaction.reply(`Could not find a tag with name ${tagName}.`);
```

Expand All @@ -237,9 +244,11 @@ const tagName = interaction.options.getString('name');

// equivalent to: SELECT * FROM tags WHERE name = 'tagName' LIMIT 1;
const tag = await Tags.findOne({ where: { name: tagName } });

if (tag) {
return interaction.reply(`${tagName} was created by ${tag.username} at ${tag.createdAt} and has been used ${tag.usage_count} times.`);
}

return interaction.reply(`Could not find tag: ${tagName}`);
```

Expand All @@ -255,6 +264,7 @@ The next command will enable you to fetch a list of all the created tags.
// equivalent to: SELECT name FROM tags;
const tagList = await Tags.findAll({ attributes: ['name'] });
const tagString = tagList.map(t => t.name).join(', ') || 'No tags set.';

return interaction.reply(`List of tags: ${tagString}`);
```

Expand All @@ -268,6 +278,7 @@ Here, you can use the `.findAll()` method to grab all the tag names. Notice that
const tagName = interaction.options.getString('name');
// equivalent to: DELETE from tags WHERE name = ?;
const rowCount = await Tags.destroy({ where: { name: tagName } });

if (!rowCount) return interaction.reply('That tag did not exist.');

return interaction.reply('Tag deleted.');
Expand Down
54 changes: 33 additions & 21 deletions guide/sequelize/currency.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,10 @@ sequelize.sync({ force }).then(async () => {
CurrencyShop.upsert({ name: 'Coffee', cost: 2 }),
CurrencyShop.upsert({ name: 'Cake', cost: 5 }),
];

await Promise.all(shop);
console.log('Database synced');

sequelize.close();
}).catch(console.error);
```
Expand Down Expand Up @@ -156,27 +158,31 @@ const UserItems = require('./models/UserItems.js')(sequelize, Sequelize.DataType

UserItems.belongsTo(CurrencyShop, { foreignKey: 'item_id', as: 'item' });

/* eslint-disable-next-line func-names */
Users.prototype.addItem = async function(item) {
const userItem = await UserItems.findOne({
where: { user_id: this.user_id, item_id: item.id },
});

if (userItem) {
userItem.amount += 1;
return userItem.save();
}
Reflect.defineProperty(Users.prototype, 'addItem', {
/* eslint-disable-next-line func-name-matching */
value: async function addItem(item) {
const userItem = await UserItems.findOne({
where: { user_id: this.user_id, item_id: item.id },
});

if (userItem) {
userItem.amount += 1;
return userItem.save();
}

return UserItems.create({ user_id: this.user_id, item_id: item.id, amount: 1 });
};
return UserItems.create({ user_id: this.user_id, item_id: item.id, amount: 1 });
},
});

/* eslint-disable-next-line func-names */
Users.prototype.getItems = function() {
return UserItems.findAll({
where: { user_id: this.user_id },
include: ['item'],
});
};
Reflect.defineProperty(Users.prototype, 'getItems', {
/* eslint-disable-next-line func-name-matching */
value: function getItems() {
return UserItems.findAll({
where: { user_id: this.user_id },
include: ['item'],
});
},
});

module.exports = { Users, CurrencyShop, UserItems };
```
Expand All @@ -185,7 +191,7 @@ Note that the connection object could be abstracted in another file and had both

Another new method here is the `.belongsTo()` method. Using this method, you add `CurrencyShop` as a property of `UserItem` so that when you do `userItem.item`, you get the respectively attached item. You use `item_id` as the foreign key so that it knows which item to reference.

You then add some prototypes to the User object to finish up the junction: add items to users, and get their current inventory. The code inside should be somewhat familiar from the last tutorial. `.findOne()` is used to get the item if it exists in the user's inventory. If it does, increment it; otherwise, create it.
You then add some methods to the `Users` object to finish up the junction: add items to users, and get their current inventory. The code inside should be somewhat familiar from the last tutorial. `.findOne()` is used to get the item if it exists in the user's inventory. If it does, increment it; otherwise, create it.

Getting items is similar; use `.findAll()` with the user's id as the key. The `include` key is for associating the CurrencyShop with the item. You must explicitly tell Sequelize to honor the `.belongsTo()` association; otherwise, it will take the path of the least effort.

Expand Down Expand Up @@ -247,12 +253,15 @@ Reflect.defineProperty(currency, 'add', {
/* eslint-disable-next-line func-name-matching */
value: async function add(id, amount) {
const user = currency.get(id);

if (user) {
user.balance += Number(amount);
return user.save();
}

const newUser = await Users.create({ user_id: id, balance: amount });
currency.set(id, newUser);

return newUser;
},
});
Expand Down Expand Up @@ -283,6 +292,7 @@ In the ready event, sync the currency collection with the database for easy acce

```js
const target = interaction.options.getUser('user') ?? interaction.user;

return interaction.reply(`${target.tag} has ${currency.getBalance(target.id)}💰`);
```
Expand All @@ -298,6 +308,7 @@ const user = await Users.findOne({ where: { user_id: target.id } });
const items = await user.getItems();

if (!items.length) return interaction.reply(`${target.tag} has nothing!`);

return interaction.reply(`${target.tag} currently has ${items.map(i => `${i.amount} ${i.item.name}`).join(', ')}`);
```
This is where you begin to see the power of associations. Even though users and the shop are different tables, and the data is stored separately, you can get a user's inventory by looking at the junction table and join it with the shop; no duplicated item names that waste space!
Expand Down Expand Up @@ -330,6 +341,7 @@ You'd ideally want to allow users to do both `!transfer 5 @user` and `!transfer
```js
const itemName = interaction.options.getString('item');
const item = await CurrencyShop.findOne({ where: { name: { [Op.like]: itemName } } });

if (!item) return interaction.reply(`That item doesn't exist.`);
if (item.cost > currency.getBalance(interaction.user.id)) {
return interaction.reply(`You currently have ${currency.getBalance(interaction.user.id)}, but the ${item.name} costs ${item.cost}!`);
Expand All @@ -339,7 +351,7 @@ const user = await Users.findOne({ where: { user_id: interaction.user.id } });
currency.add(interaction.user.id, -item.cost);
await user.addItem(item);

interaction.reply(`You've bought: ${item.name}.`);
return interaction.reply(`You've bought: ${item.name}.`);
```
For users to search for an item without caring about the letter casing, you can use the `$iLike` modifier when looking for the name. Keep in mind that this may be slow if you have millions of items, so please don't put a million items in your shop.
Expand Down

0 comments on commit c1abfa3

Please sign in to comment.