Medusa/developer documentation/Models

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

modules/example/src/models/Item.js
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:

PropertyTypeDefaultDescription
namestringRequiredName of the database table.
dynamicbooleanfalseWhen true, accepts undeclared fields and serializes them into a JSON options column.
formerlystringundefinedPrevious table name. When that table exists and the current one does not, it is renamed before models sync, keeping existing rows.
schemaobject{}Schema definition object. Usually defined via the schema() method.

Supported Data Types

Medusa maps schema type strings directly to Sequelize DataTypes:

Type StringUnderlying SQL TypeUsage Guidelines
"string"VARCHAR(255)Short text strings, identifiers, and slugs.
"text"TEXTUnbounded text, long descriptions, logs, or formatted notes.
"integer"INTEGER32-bit signed integers (-2,147,483,648 to 2,147,483,647).
"bigint"BIGINT64-bit integers. Always use this for Discord snowflakes (user IDs, guild IDs, channel IDs).
"float"FLOATFloating-point decimal numbers for balances, multipliers, and ratings.
"boolean"BOOLEANFlags representing true or false states.
"date"TIMESTAMP WITH TIME ZONETimestamps, expiration dates, and scheduling points.
"json"JSONArbitrary 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

PropertyTypeDefaultDescription
typestringRequiredData type identifier from the supported types table.
requiredbooleanfalseWhen true, sets allowNull: false. Rejects null or undefined values.
uniquebooleanfalseWhen true, creates a unique constraint on the column.
defaultanyundefinedDefault 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 or null if 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:

modules/example/src/models/CustomData.js
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 SequelizeUniqueConstraintError becomes an error with status = 409 and a clear message indicating which attribute conflicted.
  • Validation Failure: A SequelizeValidationError becomes an error with status = 400 indicating 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
});