Models
Declare relational database tables, data types, and asynchronous CRUD operations.
Models define SQL database tables managed by Sequelize. When extending Models, Medusa automatically creates or alters the corresponding table on startup, translates database constraints into HTTP status codes, and provides promise-based CRUD methods.
Models reside in src/models/ and load from every *.js file located directly in that directory.
Base Class Definition
import { Models } from "#medusa/modules";
export class ItemModel extends Models {
constructor(medusa) {
super(medusa, {
name: "example_items",
dynamic: false
});
}
schema() {
return {
guildId: { type: "bigint", required: true },
userId: { type: "bigint", required: true },
identifier: { type: "string", required: true, unique: true },
name: { type: "string", required: true },
description: { type: "text" },
quantity: { type: "integer", default: 1 },
price: { type: "float", default: 0.0 },
metadata: { type: "json" },
active: { type: "boolean", default: true },
expiresAt: { type: "date" }
};
}
}Constructor Options
The configuration object passed to super(medusa, meta) accepts the following properties:
| Property | Type | Default | Description |
|---|---|---|---|
name | string | Required | Name of the database table. |
dynamic | boolean | false | When true, accepts undeclared fields and serializes them into a JSON options column. |
formerly | string | undefined | Previous table name. When that table exists and the current one does not, it is renamed before models sync, keeping existing rows. |
schema | object | {} | Schema definition object. Usually defined via the schema() method. |
Supported Data Types
Medusa maps schema type strings directly to Sequelize DataTypes:
| Type String | Underlying SQL Type | Usage Guidelines |
|---|---|---|
"string" | VARCHAR(255) | Short text strings, identifiers, and slugs. |
"text" | TEXT | Unbounded text, long descriptions, logs, or formatted notes. |
"integer" | INTEGER | 32-bit signed integers (-2,147,483,648 to 2,147,483,647). |
"bigint" | BIGINT | 64-bit integers. Always use this for Discord snowflakes (user IDs, guild IDs, channel IDs). |
"float" | FLOAT | Floating-point decimal numbers for balances, multipliers, and ratings. |
"boolean" | BOOLEAN | Flags representing true or false states. |
"date" | TIMESTAMP WITH TIME ZONE | Timestamps, expiration dates, and scheduling points. |
"json" | JSON | Arbitrary JSON objects, arrays, and nested structures. |
Field Definitions
A field in schema() can be declared as an object with constraint options or shorthand as a type string:
schema() {
return {
shorthandField: "string",
detailedField: {
type: "string",
required: true,
unique: true,
default: "initial"
}
};
}Field Properties
| Property | Type | Default | Description |
|---|---|---|---|
type | string | Required | Data type identifier from the supported types table. |
required | boolean | false | When true, sets allowNull: false. Rejects null or undefined values. |
unique | boolean | false | When true, creates a unique constraint on the column. |
default | any | undefined | Default column value applied when creating new records without specifying this field. |
CRUD Methods
Models expose asynchronous methods called directly or from within handlers:
create(values)
Creates and persists a single record in the database.
- Parameters:
values(object) - Key-value map of attributes matching the schema. - Returns:
Promise<ModelInstance>- The created Sequelize instance. - Errors: Throws 400 validation error if required fields are missing; throws 409 conflict error if unique constraints are violated.
const record = await model.create({
guildId: "100000000000000000",
userId: "200000000000000000",
identifier: "item-sword-01",
name: "Iron Sword",
quantity: 5,
price: 49.99
});get(where)
Retrieves the first record matching the query criteria.
- Parameters:
where(object) - Query filter conditions. - Returns:
Promise<ModelInstance | null>- The matching record instance ornullif none found.
const item = await model.get({ identifier: "item-sword-01" });
if (item) {
console.log(item.name, item.quantity);
}list(where)
Retrieves all records matching query filter conditions.
- Parameters:
where(object, optional) - Query filter conditions. Defaults to{}(all records). - Returns:
Promise<Array<ModelInstance>>- Array of matching record instances.
const guildItems = await model.list({ guildId: "100000000000000000", active: true });count(where)
Counts records matching query filter conditions.
- Parameters:
where(object, optional) - Query filter conditions. Defaults to{}(all records). - Returns:
Promise<number>- Number of matching records.
const open = await model.count({ status: "open" });edit(where, values)
Updates attributes on records matching the query criteria.
- Parameters:
where(object) - Filter conditions identifying records to update.values(object) - New attribute values to apply.
- Returns:
Promise<[number]>- Array containing the number of affected rows. - Errors: Throws translated errors if constraints are violated.
await model.edit({ identifier: "item-sword-01" }, { quantity: 10, active: false });delete(where)
Deletes records matching query filter conditions.
- Parameters:
where(object) - Filter conditions identifying records to remove. - Returns:
Promise<number>- Number of records deleted.
const deletedCount = await model.delete({ identifier: "item-sword-01" });Dynamic Fields
When a model sets dynamic: true in its constructor, fields passed to create() or edit() that are not explicitly defined in schema() are stored automatically in a JSON options column:
export class CustomDataModel extends Models {
constructor(medusa) {
super(medusa, { name: "custom_data", dynamic: true });
}
schema() {
return {
id: { type: "string", required: true, unique: true }
};
}
}await model.create({
id: "entry_1",
customAttributeA: "value",
customAttributeB: 42
});The database stores customAttributeA and customAttributeB inside options, eliminating the need to alter table definitions for arbitrary user data.
Error Translation
Medusa intercepts Sequelize database exceptions and normalizes them into standard errors with an HTTP status property:
- Unique Constraint: A
SequelizeUniqueConstraintErrorbecomes an error withstatus = 409and a clear message indicating which attribute conflicted. - Validation Failure: A
SequelizeValidationErrorbecomes an error withstatus = 400indicating which fields failed requirement checks.
When called from an HTTP route, these errors automatically respond with the corresponding HTTP status code and { error: message } payload.
Direct Sequelize Model Access
The initialized Sequelize model is accessible through this.model. You can execute arbitrary Sequelize queries:
const total = await model.model.count({ where: { active: true } });
const rows = await model.model.findAll({
where: { guildId },
order: [["price", "DESC"]],
limit: 10
});