feat(guide): port legacy guide (#10938)

* feat: initial attempt at porting legacy guide

* feat: completion of legacy guide backport

* chore: lockfile shenanigans

* fix: handle svgs

* fix: replace svg with mermaid integration

* chore: format

* chore: remove unnecssary bullet

* chore: cleanup code highlights

* chore: explicit return

* chore: move display components after interactive components in sidebar

* chore: voice

* top link should be installation
* add docs link to sidebar

* feat: subguide-based accent styles

* chore: don't list faq twice

* chore: mention display components in interactive components

* fix: remove unoccs/order rule from guide

* chore: redirect to legacy guide instead of /guide root

* refactor: use `<kbd>`

* refactor: more kbd use

* Update apps/guide/content/docs/legacy/app-creation/handling-events.mdx

Co-authored-by: Naiyar <137700126+imnaiyar@users.noreply.github.com>

* chore: fix typos

Co-authored-by: Qjuh <76154676+Qjuh@users.noreply.github.com>

* chore: fix typos

* chore: fix links regarding secret stores across coding platforms

* chore: fix typo

* chore: link node method directly

Co-authored-by: Qjuh <76154676+Qjuh@users.noreply.github.com>

* chore: typos

Co-authored-by: Vlad Frangu <me@vladfrangu.dev>

* chore: typo

Co-authored-by: Vlad Frangu <me@vladfrangu.dev>

* fix: prevent v14 changes from being listed twice

* chore: prefer relative links

* chore: missed link conversion

* chore: missed link conversion

* chore: fix link

* chore: remove legacy code highlight markers

* chore: rephrase and extend contributing guidelines

* feat(setup): suggest cli flag over dotenv package

* chore: move introduction in sidebar

better navigation experience if the 'next page' in intro refers to getting started vs. updating/faq

* fix: replace outdated link

* fix: update voice dependencies

* chore: update node install instructions

* fix: list in missing access callout

* chore: match bun env file format

* chore: restore ffmpeg disclaimer

* fix: lockfile conflict

* chore: action row typo

Co-authored-by: Vlad Frangu <me@vladfrangu.dev>

* chore: no longer use at-next for pino

---------

Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>
Co-authored-by: Qjuh <76154676+Qjuh@users.noreply.github.com>
Co-authored-by: Naiyar <137700126+imnaiyar@users.noreply.github.com>
Co-authored-by: Vlad Frangu <me@vladfrangu.dev>
This commit is contained in:
Souji
2025-07-08 15:01:50 +02:00
committed by GitHub
parent ee3ca6f7c6
commit bc6005f446
136 changed files with 11847 additions and 48 deletions

View File

@@ -0,0 +1,459 @@
---
title: Making a Currency System
---
A common feature of Discord bots is a currency system. It's possible to do everything in one object, but we can also abstract that in terms of _relations_ between objects. This is where the power of a RDBMS (Relational Database Management System) truly shines. Sequelize calls these _associations_, so we'll be using that term from now on.
## File overview
There will be multiple files: a DB init script, your models, and your bot script. In [the sequelize guide](./), we placed all of these in the same file. Having everything in one file isn't an ideal practice, so we'll correct that.
This time we'll have six files.
- `app.js` is where we'll keep the main bot code.
- `dbInit.js` is the initialization file for the database. We run this once and forget about it.
- `dbObjects.js` is where we'll import the models and create associations here.
- `models/Users.js` is the Users model. Users will have a currency attribute in here.
- `models/CurrencyShop.js` is the Shop model. The shop will have a name and a price for each item.
- `models/UserItems.js` is the junction table between the users and the shop. A junction table connects two tables. Our junction table will have an additional field for the amount of that item the user has.
## Create models
Here is an entity relation diagram of the models we'll be making:
<Mermaid
chart="
erDiagram
USER ||--o{ USERITEMS : owns
CURRENCYSHOP ||..o{ USERITEMS : offers
USER {
string user_id PK
string balance
}
USERITEMS
USERITEMS {
string user_id FK
string item_id FK
number amount
}
CURRENCYSHOP
CURRENCYSHOP {
string id PK
string name
number cost
}
"
/>
`Users` have a `user_id`, and a `balance`. Each `user_id` can have multiple links to the `UserItems` table, and each entry in the table connects to one of the items in the `CurrencyShop`, which will have a `name` and a `cost` associated with it.
To implement this, begin by making a `models` folder and create a `Users.js` file inside which contains the following:
```js title="models/Users.js" lineNumbers
module.exports = (sequelize, DataTypes) => {
return sequelize.define(
'users',
{
user_id: {
type: DataTypes.STRING,
primaryKey: true, // [!code word:primaryKey]
},
balance: {
type: DataTypes.INTEGER,
defaultValue: 0,
allowNull: false,
},
},
{
timestamps: false, // [!code word:timestamps]
},
);
};
```
Like you see in the diagram above, the Users model will only have two attributes: a `user_id` primary key and a `balance`. A primary key is a particular attribute that becomes the default column used when joining tables together, and it is automatically unique and not `null`.
Balance also sets `allowNull` to `false`, which means that both values have to be set in conjunction with creating a primary key; otherwise, the database would throw an error. This constraint guarantees correctness in your data storage. You'll never have `null` or empty values, ensuring that if you somehow forget to validate in the application that both values are not `null`, the database would do a final validation.
Notice that the options object sets `timestamps` to `false`. This option disables the `createdAt` and the `updatedAt` columns that sequelize usually creates for you. Setting `user_id` to primary also eliminates the `id` primary key that Sequelize usually generates for you since there can only be one primary key on a table.
Next, still in the same `models` folder, create a `CurrencyShop.js` file that contains the following:
```js title="models/CurrencyShop.js" lineNumbers
module.exports = (sequelize, DataTypes) => {
return sequelize.define(
'currency_shop',
{
name: {
type: DataTypes.STRING,
unique: true, // [!code word:unique]
},
cost: {
type: DataTypes.INTEGER,
allowNull: false,
},
},
{
timestamps: false,
},
);
};
```
Like the Users model, timestamps aren't needed here, so you can disable it. Unlike the Users model, however, the `unique` field is set to `true` here, allowing you to change the name without affecting the primary key that joins this to the next object. This gets generated automatically by sequelize since a primary key isn't set.
The next file will be `UserItems.js`, the junction table.
```js title="models/UserItems.js" lineNumbers
module.exports = (sequelize, DataTypes) => {
return sequelize.define(
'user_item',
{
user_id: DataTypes.STRING, // [!code word:user_id]
item_id: DataTypes.INTEGER, // [!code word:item_id]
amount: {
type: DataTypes.INTEGER,
allowNull: false,
default: 0,
},
},
{
timestamps: false,
},
);
};
```
The junction table will link `user_id` and the `id` of the currency shop together. It also contains an `amount` number, which indicates how many of that item a user has.
## Initialize database
Now that the models are defined, you should create them in your database to access them in the bot file. We ran the sync inside the `ready` event in the previous tutorial, which is entirely unnecessary since it only needs to run once. You can make a file to initialize the database and never touch it again unless you want to remake the entire database.
Create a file called `dbInit.js` in the base directory (_not_ in the `models` folder).
<Callout type="error" title="Attention! Security Risk!">
**Make sure you use version 5 or later of Sequelize!** As used in this guide, version 4 and earlier will pose a
security threat. You can read more about this issue on the [Sequelize issue
tracker](https://github.com/sequelize/sequelize/issues/7310).
</Callout>
```js title="dbInit.js" lineNumbers
const Sequelize = require('sequelize');
const sequelize = new Sequelize('database', 'username', 'password', {
host: 'localhost',
dialect: 'sqlite',
logging: false,
storage: 'database.sqlite',
});
const CurrencyShop = require('./models/CurrencyShop.js')(sequelize, Sequelize.DataTypes);
require('./models/Users.js')(sequelize, Sequelize.DataTypes);
require('./models/UserItems.js')(sequelize, Sequelize.DataTypes);
const force = process.argv.includes('--force') || process.argv.includes('-f');
sequelize
.sync({ force })
.then(async () => {
const shop = [
// [!code word:upsert]
CurrencyShop.upsert({ name: 'Tea', cost: 1 }),
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);
```
Here you pull the two models and the junction table from the respective model declarations, sync them, and add items to the shop.
A new function here is the `.upsert()` function. It's a portmanteau for **up**date or in**sert**. `upsert` is used here to avoid creating duplicates if you run this file multiple times. That shouldn't happen because `name` is defined as _unique_, but there's no harm in being safe. Upsert also has a nice side benefit: if you adjust the cost, the respective item should also have their cost updated.
<Callout>
Execute `node dbInit.js` to create the database tables. Unless you make a change to the models, you'll never need to
touch the file again. If you change a model, you can execute `node dbInit.js --force` or `node dbInit.js -f` to force
sync your tables. It's important to note that this **will** empty and remake your model tables.
</Callout>
## Create associations
Next, add the associations to the models. Create a file named `dbObjects.js` in the base directory, next to `dbInit.js`.
```js title="dbObjects.js" lineNumbers
const Sequelize = require('sequelize');
const sequelize = new Sequelize('database', 'username', 'password', {
host: 'localhost',
dialect: 'sqlite',
logging: false,
storage: 'database.sqlite',
});
const Users = require('./models/Users.js')(sequelize, Sequelize.DataTypes);
const CurrencyShop = require('./models/CurrencyShop.js')(sequelize, Sequelize.DataTypes);
const UserItems = require('./models/UserItems.js')(sequelize, Sequelize.DataTypes);
UserItems.belongsTo(CurrencyShop, { foreignKey: 'item_id', as: 'item' }); // [!code word:belongsTo]
Reflect.defineProperty(Users.prototype, 'addItem', {
value: async (item) => {
const userItem = await UserItems.findOne({
// [!code word: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', {
value: () => {
return UserItems.findAll({
// [!code word:findAll]
where: { user_id: this.user_id },
include: ['item'],
});
},
});
module.exports = { Users, CurrencyShop, UserItems };
```
Note that the connection object could be abstracted in another file and had both `dbInit.js` and `dbObjects.js` use that connection file, but it's not necessary to overly abstract things.
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 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.
## Application code
Create an `app.js` file in the base directory with the following skeleton code to put it together.
```js title="app.js" lineNumbers
const { Op } = require('sequelize');
const { Client, codeBlock, Collection, Events, GatewayIntentBits } = require('discord.js');
const { Users, CurrencyShop } = require('./dbObjects.js');
const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages] });
const currency = new Collection();
client.once(Events.ClientReady, (readyClient) => {
console.log(`Ready! Logged in as ${readyClient.user.tag}`);
});
client.on(Events.MessageCreate, async (message) => {
if (message.author.bot) return;
addBalance(message.author.id, 1);
});
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isChatInputCommand()) return;
const { commandName } = interaction;
// ...
});
client.login('your-token-goes-here');
```
Nothing special about this skeleton. You import the Users and CurrencyShop models from our `dbObjects.js` file and add a currency Collection. Every time someone talks, add 1 to their currency count. The rest is just standard discord.js code and a simple if/else command handler. A Collection is used for the `currency` variable to cache individual users' currency, so you don't have to hit the database for every lookup. An if/else handler is used here, but you can put it in a framework or command handler as long as you maintain a reference to the models and the currency collection.
### Helper methods
```js title="app.js" lineNumbers=5
// ...
const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages] });
const currency = new Collection();
async function addBalance(id, amount) {
// [!code ++:18]
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;
}
function getBalance(id) {
const user = currency.get(id);
return user ? user.balance : 0;
}
```
This defines the `addBalance()` helper function, since it'll be used quite frequently. A `getBalance()` function is also defined, to ensure that a number is always returned.
### Ready event data sync
```js title="app.js"
client.once(Events.ClientReady, async (readyClient) => {
const storedBalances = await Users.findAll(); // [!code ++:2]
storedBalances.forEach((b) => currency.set(b.user_id, b));
console.log(`Ready! Logged in as ${readyClient.user.tag}!`);
});
```
In the ready event, sync the currency collection with the database for easy access later.
### Show user balance
```js title="app.js"
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isChatInputCommand()) return;
const { commandName } = interaction;
if (commandName === 'balance') {
// [!code ++:5]
const target = interaction.options.getUser('user') ?? interaction.user;
return interaction.reply(`${target.tag} has ${getBalance(target.id)}💰`); // [!code word:getBalance]
}
});
```
Nothing tricky here. The `getBalance()` function is used to show either the author's or the mentioned user's balance.
### Show user inventory
```js title="app.js"
if (commandName === 'balance') {
// ...
} // [!code --]
} else if (commandName === 'inventory') { // [!code ++:9]
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((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!
### Transfer currency to another user
```js title="app.js"
if (commandName === 'balance') {
// ...
} else if (commandName === 'inventory') // [!code focus:16]
// ... /
} // [!code --]
} else if (commandName === 'transfer') { // [!code ++:13]
const currentAmount = getBalance(interaction.user.id);
const transferAmount = interaction.options.getInteger('amount');
const transferTarget = interaction.options.getUser('user');
if (transferAmount > currentAmount) return interaction.reply(`Sorry ${interaction.user}, you only have ${currentAmount}.`);
if (transferAmount <= 0) return interaction.reply(`Please enter an amount greater than zero, ${interaction.user}.`);
addBalance(interaction.user.id, - transferAmount); // [!code word:addBalance]
addBalance(transferTarget.id, transferAmount);
return interaction.reply(`Successfully transferred ${transferAmount}💰 to ${transferTarget.tag}. Your current balance is ${getBalance(interaction.user.id)}💰`); // [!code word:getBalance]
}
```
As a bot creator, you should always be thinking about how to make the user experience better. Good UX makes users less frustrated with your commands. If your inputs are different types, don't make them memorize which parameters come before the other.
`addBalance()` is used for both removing and adding currency. Since transfer amounts below zero are disallowed, it's safe to apply the transfer amount's additive inverse to their balance.
### Buying an item
```js title="app.js"
if (commandName === 'balance') {
// ...
} else if (commandName === 'inventory')
// ... /
} else if (commandName === 'transfer') {
// ...
} // [!code --]
} else if (commandName === 'buy') { // [!code ++:15]
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 > getBalance(interaction.user.id)) {
return interaction.reply(`You currently have ${getBalance(interaction.user.id)}, but the ${item.name} costs ${item.cost}!`);
}
const user = await Users.findOne({ where: { user_id: interaction.user.id } });
addBalance(interaction.user.id, -item.cost);
await user.addItem(item);
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.
### Display the shop
```js title="app.js"
if (commandName === 'balance') {
// ...
} else if (commandName === 'inventory')
// ... /
} else if (commandName === 'transfer') {
// ...
} else if (commandName === 'buy') { // [!code focus:8]
// ...
} // [!code --]
} else if (commandName === 'shop') { // [!code ++:4]
const items = await CurrencyShop.findAll(); // [!code word:findAll]
return interaction.reply(codeBlock(items.map(i => `${i.name}: ${i.cost}💰`).join('\n')));
}
```
There's nothing special here; just a regular `.findAll()` to get all the items in the shop and `.map()` to transform that data into something nice looking.
### Display the leaderboard
```js title="app.js"
if (commandName === 'balance') {
// ...
} else if (commandName === 'inventory')
// ... /
} else if (commandName === 'transfer') {
// ...
} else if (commandName === 'buy') {
// ...
} else if (commandName === 'shop') { // [!code focus:14]
// ...
} // [!code --]
} else if (commandName === 'leaderboard') { // [!code ++:11]
return interaction.reply(
codeBlock(
currency.sort((a, b) => b.balance - a.balance)
.filter(user => client.users.cache.has(user.user_id))
.first(10)
.map((user, position) => `(${position + 1}) ${(client.users.cache.get(user.user_id).tag)}: ${user.balance}💰`)
.join('\n'),
),
);
}
```
Nothing extraordinary here either. You could query the database for the top ten currency holders, but since you already have access to them locally inside the `currency` variable, you can sort the Collection and use `.map()` to display it in a friendly format. The filter is in case the users no longer exist in the bot's cache.

View File

@@ -0,0 +1,355 @@
---
title: Sequelize
---
Sequelize is an object-relational-mapper, which means you can write a query using objects and have it run on almost any other database system that Sequelize supports.
### Why use an ORM?
The main benefit of using an ORM like Sequelize is that it allows you to write code that virtually looks like native JavaScript. As a side benefit, an ORM will enable you to write code that can run in almost every database system. Although databases generally adhere very closely to SQL, they each have their slight nuances and differences. You can create a database-agnostic query using an ORM that works on multiple database systems.
## A simple tag system
For this tutorial, we will create a simple tag system that will allow you to add a tag, output a tag, edit a tag, show tag info, list tags, and delete a tag.
To begin, you should install Sequelize into your discord.js project. We will explain SQlite as the first storage engine and show how to use other databases later. Note that you will need Node 7.6 or above to utilize the `async/await` operators.
### Installing and using Sequelize
Create a new project folder and run the following:
```sh tab="npm"
npm install discord.js sequelize sqlite3
```
```sh tab="yarn"
yarn add discord.js sequelize sqlite3
```
```sh tab="pnpm"
pnpm install discord.js sequelize sqlite3
```
```sh tab="bun"
bun add discord.js sequelize sqlite3
```
<Callout type="error" title="Attention! Security Risk!">
**Make sure you use version 5 or later of Sequelize!** As used in this guide, version 4 and earlier will pose a
security threat. You can read more about this issue on the [Sequelize issue
tracker](https://github.com/sequelize/sequelize/issues/7310).
</Callout>
<Callout>
This section will still work with any provider supported by sequelize. We recommend PostgreSQL for larger applications.
Do note that "large" here should be interpreted as absolutely massive. Sqlite is used at scale by many companies and products you use every single day. The slight overhead should not be noticable for the application of a Discord bot at all unless you are dealing with super complicated queries or are using specific features that do not exist in sqlite.
You can find out if sqlite might be a good choice for your project (it very likely is) by reading [their own article](https://www.sqlite.org/whentouse.html) on the topic.
</Callout>
After you have installed discord.js and Sequelize, you can start with the following skeleton code. The comment labels will tell you where to insert code later on.
```js title="sequelize-example.js"
// Require Sequelize
const Sequelize = require('sequelize');
// Require the necessary discord.js classes
const { Client, Events, GatewayIntentBits } = require('discord.js');
// Create a new client instance
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
// When the client is ready, run this code (only once)
client.once(Events.ClientReady, (readyClient) => {
console.log(`Ready! Logged in as ${readyClient.user.tag}`);
});
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isChatInputCommand()) return;
const { commandName } = interaction;
// ...
});
// Login to Discord with your client's token
client.login('your-token-goes-here');
```
### Connection information
The first step is to define the connection information. It should look something like this:
```js title="sequelize-example.js"
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
// [!code ++:7]
const sequelize = new Sequelize('database', 'user', 'password', {
host: 'localhost',
dialect: 'sqlite',
logging: false,
// SQLite only
storage: 'database.sqlite',
});
```
- `host` tells Sequelize where to look for the database. For most systems, the host will be localhost, as the database usually resides with the application. If you have a remote database, however, then you can set it to that connection address. Otherwise, don't touch this unless you know what you're doing.
- `dialect` refers to the database engine you are going to use. For this tutorial, it will be sqlite.
- `logging` enables verbose output from Sequelizeuseful for when you are trying to debug. You can disable it by setting it to `false`.
- `storage` is a sqlite-only setting because sqlite is the only database that stores all its data to a single file.
### Creating the model
In any relational database, you need to create tables to store your data. This simple tag system will use four fields. The table in the database will look something like this:
| name | description | username | usage_count |
| --------- | -------------- | -------- | ----------- |
| bob | is the best | bob | 0 |
| tableflip | (╯°□°)╯︵ ┻━┻ | joe | 8 |
To do that in Sequelize, define a model based on this structure below the connection information, as shown below, after the `sequelize` initalisation.
```js title="sequelize-example.js"
// ...
const sequelize = new Sequelize('database', 'user', 'password', {
host: 'localhost',
dialect: 'sqlite',
logging: false,
storage: 'database.sqlite',
});
// [!code ++:21] [!code focus:24]
/*
* equivalent to: CREATE TABLE tags(
* name VARCHAR(255) UNIQUE,
* description TEXT,
* username VARCHAR(255),
* usage_count INT NOT NULL DEFAULT 0
* );
*/
// [!code word:define]
const Tags = sequelize.define('tags', {
name: {
type: Sequelize.STRING,
unique: true,
},
description: Sequelize.TEXT,
username: Sequelize.STRING,
usage_count: {
type: Sequelize.INTEGER,
defaultValue: 0,
allowNull: false,
},
});
// ...
```
The model mirrors very closely what the database defines. There will be a table with four fields called `name`, `description`, `username`, and `usage_count`.
`sequelize.define()` takes two parameters. `'tags'` are passed as the name of our table, and an object that represents the table's schema in key-value pairs. Keys in the object become the model's attributes, and the values describe the attributes.
- `type` refers to what kind of data this attribute should hold. The most common types are number, string, and date, but other data types are available depending on the database.
- `unique: true` will ensure that this field will never have duplicated entries. Duplicate tag names are disallowed in this database.
- `defaultValue` allows you to set a fallback value if there's no initial value during the insert.
- `allowNull` is not all that important, but this will guarantee in the database that the attribute is never unset. You could potentially set it to be a blank or empty string, but it has to be _something_.
<Callout>
`Sequelize.STRING` vs. `Sequelize.TEXT`: In most database systems, the string's length is a fixed length for
performance reasons. Sequelize defaults this to 255. Use STRING if your input has a max length, and use TEXT if it
does not. For sqlite, there is no unbounded string type, so it will not matter which one you pick.
</Callout>
### Syncing the model
Now that your structure is defined, you need to make sure the model exists in the database. To make sure the bot is ready and all the data you might need has arrived, add this line in your code.
```js title="sequelizeexample.ts"
client.once(Events.ClientReady, (readyClient) => {
Tags.sync(); // [!code ++] [!code word:sync]
console.log(`Logged in as ${readyClient.user.tag}!`);
});
```
The table does not get created until you `sync` it. The schema you defined before was building the model that lets Sequelize know how the data should look. For testing, you can use `Tags.sync({ force: true })` to recreate the table every time on startup. This way, you can get a blank slate each time.
### Adding a tag
After all this preparation, you can now write your first command! Let's start with the ability to add a tag.
```js title="sequelize-example.js"
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isChatInputCommand()) return;
const { commandName } = interaction;
// [!code ++:21]
if (commandName === 'addtag') {
const tagName = interaction.options.getString('name');
const tagDescription = interaction.options.getString('description');
try {
// equivalent to: INSERT INTO tags (name, description, username) values (?, ?, ?);
const tag = await Tags.create({
name: tagName,
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.');
}
}
});
```
`Tags.create()` uses the models that you created previously. The `.create()` method inserts some data into the model. You are going to insert a tag name, description, and the author name into the database.
The `catch (error)` section is necessary for the insert because it will offload checking for duplicates to the database to notify you if an attempt to create a tag that already exists occurs. The alternative is to query the database before adding data and checking if a result returns. If there are no errors or no identical tag is found, only then would you add the data. Of the two methods, it is clear that catching the error is less work for you.
Although `if (error.name === 'SequelizeUniqueConstraintError')` was mostly for doing less work, it is always good to handle your errors, especially if you know what types of errors you will receive. This error comes up if your unique constraint is violated, i.e., duplicate values are inserted.
<Callout type="warn">
Do not use catch for inserting new data. Only use it for gracefully handling things that go wrong in your code or
logging errors.
</Callout>
### Fetching a tag
Next, let's fetch the inserted tag.
```js title="sequelize-example.js"
if (commandName === 'addtag') {
// ...
} // [!code --]
} else if (command === 'tag') { // [!code ++:15]
const tagName = interaction.options.getString('name');
// equivalent to: SELECT * FROM tags WHERE name = 'tagName' LIMIT 1;
const tag = await Tags.findOne({ where: { name: tagName } }); // [!code word:findOne]
if (tag) {
// equivalent to: UPDATE tags SET usage_count = usage_count + 1 WHERE name = 'tagName';
tag.increment('usage_count'); // [!code word:increment]
return interaction.reply(tag.get('description')); // [!code word:get]
}
return interaction.reply(`Could not find tag: ${tagName}`);
}
```
This is your first query. You are finally doing something with your data; yay!
`.findOne()` is how you fetch a single row of data. The `where: { name: tagName }` makes sure you only get the row with the desired tag. Since the queries are asynchronous, you will need to use `await` to fetch it. After receiving the data, you can use `.get()` on that object to grab the data. If no data is received, then you can tell the user that the query returned no data.
### Editing a tag
```js title="sequelize-example.js"
if (commandName === 'addtag') {
// ...
} else if (command === 'tag') { // [!code focus:16]
// ...
} // [!code --]
} else if (command === 'edittag') { // [!code ++:13]
const tagName = interaction.options.getString('name');
const tagDescription = interaction.options.getString('description');
// equivalent to: UPDATE tags (description) values (?) WHERE name='?';
const affectedRows = await Tags.update({ description: tagDescription }, { where: { name: tagName } }); // [!code word:update]
if (affectedRows > 0) {
return interaction.reply(`Tag ${tagName} was edited.`);
}
return interaction.reply(`Could not find a tag with name ${tagName}.`);
}
```
It is possible to edit a record by using the `.update()` function. An update returns the number of rows that the `where` condition changed. Since you can only have tags with unique names, you do not have to worry about how many rows may change. Should you get that the query didn't alter any rows, you can conclude that the tag did not exist.
### Display info on a specific tag
```js title="sequelize-example.js"
if (commandName === 'addtag') {
// ...
} else if (command === 'tag') {
// ...
} else if (command === 'edittag') { // [!code focus:15]
// ...
} // [!code --]
} else if (commandName == 'taginfo') { // [!code ++:12]
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}`);
}
```
This section is very similar to the previous command, except you will be showing the tag metadata. `tag` contains your tag object. Notice two things: firstly, it is possible to access the object's properties without the `.get()` function. This is because the object is an instance of a Tag, which you can treat as an object and not just a row of data.
Second, you can access a property that was not defined explicitly, `createdAt`. This is because Sequelize automatically adds that column to all tables. Passing another object into the model with `{ createdAt: false }` can disable this feature, but in this case, it was useful to have.
### Listing all tags
The next command will enable you to fetch a list of all the created tags.
```js title="sequelize-example.js"
if (commandName === 'addtag') {
// ...
} else if (command === 'tag') {
// ...
} else if (command === 'edittag') {
// ...
} else if (commandName == 'taginfo') { // [!code focus:10]
// ...
} // [!code --]
} else if (command === 'showtags') { // [!code ++:7]
// equivalent to: SELECT name FROM tags;
const tagList = await Tags.findAll({ attributes: ['name'] }); // [!code word:attributes]
const tagString = tagList.map(t => t.name).join(', ') || 'No tags set.';
return interaction.reply(`List of tags: ${tagString}`);
}
```
Here, you can use the `.findAll()` method to grab all the tag names. Notice that instead of having `where`, the optional field, `attributes`, is set. Setting attributes to name will let you get _only_ the names of tags. If you tried to access other fields, like the tag author, you would get an error.
If left blank, it will fetch _all_ of the associated column data. It will not affect the results returned, but from a performance perspective, you should only grab the necessary data. If no results return, `tagString` will default to 'No tags set'.
### Deleting a tag
```js title="sequelize-example.js"
if (commandName === 'addtag') {
// ...
} else if (command === 'tag') {
// ...
} else if (command === 'edittag') {
// ...
} else if (commandName == 'taginfo') {
// ...
} else if (command === 'showtags') { // [!code focus:11]
// ...
}// [!code --]
} else if (command === 'deletetag') { // [!code ++:9]
const tagName = interaction.options.getString('name');
// equivalent to: DELETE from tags WHERE name = ?;
const rowCount = await Tags.destroy({ where: { name: tagName } }); // [!code word:destroy]
if (!rowCount) return interaction.reply('That tag doesn\'t exist.');
return interaction.reply('Tag deleted.');
}
```
`.destroy()` runs the delete operation. The operation returns a count of the number of affected rows. If it returns with a value of 0, then nothing was deleted, and that tag did not exist in the database in the first place.