Skip to main content

stacks-orm

Use when working with the Stacks ORM - defining models with defineModel(), model relationships (hasOne, hasMany, belongsTo, belongsToMany, morphOne, hasManyThrough), attributes, traits, factories, computed properties, query building, transactions, or the 105 built-in models. Covers @stacksjs/orm, storage/framework/orm/, and storage/framework/defaults/app/Models/.

Source facts

Repository
stacksjs/wildloop
Last source activity
October 6, 2026 at 12:10
Detected SKILL.md language
English
Stars
2
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
stacks-orm
description
Use when working with the Stacks ORM - defining models with defineModel(), model relationships (hasOne, hasMany, belongsTo, belongsToMany, morphOne, hasManyThrough), attributes, traits, factories, computed properties, query building, transactions, or the 105 built-in models. Covers @stacksjs/orm, storage/framework/orm/, and storage/framework/defaults/app/Models/.
license
MIT
compatibility
Bun >= 1.3.0, TypeScript, SQLite >= 3.47.2
allowed-tools
Read Edit Write Bash Grep Glob
# Stacks ORM ## Key Paths - Core ORM package: `storage/framework/core/orm/src/` - ORM implementation: `storage/framework/orm/` - Model definitions: `storage/framework/defaults/app/Models/` (105 models) - Application models: `app/Models/` - Default model templates: `storage/framework/defaults/app/Models/` - ORM type globals: `storage/framework/types/orm-globals.d.ts` - Attribute types: `storage/framework/types/attributes.ts` (240+ attributes) - Model events: `storage/framework/types/events.ts` ## Source Files ``` orm/src/ ├── define-model.ts # defineModel() + buildEventHooks() + buildTraitMethods() ├── index.ts # Re-exports orm/src + db + subquery + transaction + types ├── db.ts # Database query builder bridge ├── subquery.ts # Subquery support ├── transaction.ts # transaction(), savepoint(), transactional() ├── model-types.ts # ModelRow<T>, NewModelData<T>, UpdateModelData<T> ├── types.ts # ORM type definitions ├── utils.ts # modelTableName, getRelations, getFillableAttributes, etc. ├── builder.ts # Query builder integration ├── generated/ # Auto-generated model types and table traits │ ├── types.ts │ ├── index.ts │ └── table-traits.ts └── traits/ ├── index.ts # Re-exports all trait creators ├── taggable.ts # createTaggableMethods() ├── categorizable.ts # createCategorizableMethods() ├── commentable.ts # createCommentableMethods() ├── billable.ts # createBillableMethods() ├── likeable.ts # createLikeableMethods() └── two-factor.ts # createTwoFactorMethods() ``` ## defineModel() API (define-model.ts) Wraps bun-query-builder's `createModel()` with Stacks-specific enhancements: - Event dispatching via `traits.observe` (emits `{model}:created`, `{model}:updated`, `{model}:deleted` via `@stacksjs/events`) - Trait methods (billable, taggable, categorizable, commentable, likeable, 2FA) - Raw definition access for generators (`getDefinition()`, `_isStacksModel`) ```typescript import { defineModel } from '@stacksjs/orm' export default defineModel({ name: 'Product', table: 'products', primaryKey: 'id', // default: 'id' autoIncrement: true, // default: true traits: { useUuid: true, // adds uuid column useTimestamps: true, // adds created_at, updated_at useAuth: { usePasskey: true }, // adds auth columns + passkey support useSocials: ['github'], // social login providers useSearch: { // search engine indexing displayable: ['name', 'email'], searchable: ['name', 'email'], sortable: ['name', 'created_at'], filterable: ['status'] }, useSeeder: { count: 10 }, // or just `true` (defaults to 10) useApi: { uri: 'products', routes: ['index', 'store', 'show', 'update', 'destroy'] }, categorizable: true, // adds category relations via categorizable table taggable: true, // adds tag relations via taggable table commentables: true, // adds comment relations (NOTE: plural 's' in key) billable: true, // adds Stripe integration methods likeable: true, // or { table?: string, foreignKey?: string } observe: true, // emit model events (true = all, or array: ['create','update','delete']) }, // Relationships hasOne: ['Subscriber'], hasMany: ['Post', 'Order'], belongsTo: ['User', 'Category'], belongsToMany: ['Tag'], hasOneThrough: ['Profile'], morphOne: 'Image', // or { model, morphName?, type?, id? } indexes: [ { name: 'idx_email', columns: ['email'] }, { name: 'idx_composite', columns: ['email', 'name'] } ], attributes: { name: { order: 1, fillable: true, required: true, unique: false, validation: { rule: schema.string().max(100), message: { max: 'Name is too long' } }, factory: (faker) => faker.lorem.word() }, price: { fillable: true, required: true, validation: { rule: schema.number().min(1) }, factory: (faker) => faker.datatype.number({ min: 100, max: 10000 }) }, status: { fillable: true, default: 'draft', validation: { rule: schema.enum(['draft', 'published', 'archived']) } }, password: { hidden: true, // excluded from JSON serialization guarded: true, // not mass-assignable } }, // Computed properties (accessors) get: { fullName: (attrs) => `${attrs.first_name} ${attrs.last_name}`, formattedPrice: (attrs) => `$${(attrs.price / 100).toFixed(2)}` }, // Mutators (setters) set: { email: (attrs) => attrs.email?.toLowerCase() }, // Model hooks (lifecycle callbacks) hooks: { afterCreate: (model) => { /* ... */ }, afterUpdate: (model) => { /* ... */ }, afterDelete: (model) => { /* ... */ }, }, dashboard: { highlight: true } // highlight in admin dashboard } as const) ``` ### How defineModel() Works Internally 1. `buildEventHooks(definition)` -- if `traits.observe` is truthy, creates `afterCreate`/`afterUpdate`/`afterDelete` hooks that lazy-import `@stacksjs/events` and call `dispatch()` 2. Merges event hooks with any user-defined hooks 3. Calls `createModel(defWithHooks)` from bun-query-builder (provides typed query methods) 4. `buildTraitMethods(definition)` -- checks each trait flag and creates method objects 5. Returns `Object.assign(baseModel, traitMethods, definition)` + `getDefinition()` + `_isStacksModel` ## Transactions (transaction.ts) ```typescript import { transaction, savepoint, transactional } from '@stacksjs/orm' // Basic transaction -- auto-commit on success, auto-rollback on error const result = await transaction(async (tx) => { await tx.insertInto('users').values({ name: 'John' }).execute() await tx.insertInto('profiles').values({ user_id: 1 }).execute() return 'success' }) // With options await transaction(callback, { retries: 3, isolation: 'serializable', // 'read committed' | 'repeatable read' | 'serializable' readOnly: false, onRollback: (error) => console.error(error), afterRollback: () => { /* cleanup */ } }) // Savepoints (nested transactions) await transaction(async (tx) => { await tx.insertInto('users').values({ name: 'Bob' }).execute() await savepoint(async (sp) => { await sp.insertInto('logs').values({ action: 'created' }).execute() // If this fails, only this savepoint rolls back }) }) // Decorator-style -- wraps function to auto-run in transaction const createUser = transactional(async (tx, name: string, email: string) => { const user = await tx.insertInto('users').values({ name }).returningAll().executeTakeFirst() await tx.insertInto('profiles').values({ user_id: user.id }).execute() return user }) await createUser('Alice', 'alice@example.com') // auto-wrapped ``` Both `transaction()` and `savepoint()` delegate to `db.transaction()` and `db.savepoint()` from `@stacksjs/database`. ### Transaction executor boundary Every query that must commit or roll back together must use the callback handle (`tx` or `sp`), including validation reads, pivot writes, and the final readback. Do not mix `Model.find()`, `Model.create()`, instance `update()` / `delete()`, or instance relation calls into a raw query-builder transaction. The model executor is a separate execution surface and is not rebound to the callback handle. On SQLite it may use a separate connection, so it cannot observe an uncommitted row written through `tx`. `runInTransactionScope()` buffers supported side effects until commit, but it does not rebind model queries. For a transaction-backed custom action, use the model definition as the schema and relationship source of truth, then execute the complete persistence workflow through `tx`. Read the created or updated row through `tx` before returning so a readback failure also rolls back the mutation. ## Trait Methods (traits/) ### Taggable (when `traits.taggable: true`) Uses `taggable` table with polymorphic `taggable_type` + `taggable_id` columns. - `Model._taggable.tags(id: number): Promise<any[]>` - `Model._taggable.tagCount(id: number): Promise<number>` -- uses `count(*)` - `Model._taggable.addTag(id, { name, description? }): Promise<any>` -- auto-generates slug - `Model._taggable.activeTags(id): Promise<any[]>` -- filters `is_active = true` - `Model._taggable.inactiveTags(id): Promise<any[]>` -- filters `is_active = false` - `Model._taggable.removeTag(id, tagId): Promise<void>` ### Categorizable (when `traits.categorizable: true`) Uses `categorizable` + `categorizable_models` pivot table. - `Model._categorizable.categories(id): Promise<any[]>` -- joins through pivot - `Model._categorizable.categoryCount(id): Promise<number>` - `Model._categorizable.addCategory(id, { name, description? }): Promise<any>` -- creates category if not exists, then links - `Model._categorizable.activeCategories(id): Promise<any[]>` - `Model._categorizable.inactiveCategories(id): Promise<any[]>` - `Model._categorizable.removeCategory(id, categoryId): Promise<void>` -- removes pivot link ### Commentable (when `traits.commentables: true`) Uses `comments` table with `commentables_id` + `commentables_type` columns. - `Model._commentable.comments(id): Promise<any[]>` - `Model._commentable.commentCount(id): Promise<number>` - `Model._commentable.addComment(id, { title, body }): Promise<any>` -- status defaults to `'pending'` - `Model._commentable.approvedComments(id): Promise<any[]>` -- status = `'approved'` - `Model._commentable.pendingComments(id): Promise<any[]>` -- status = `'pending'` - `Model._commentable.rejectedComments(id): Promise<any[]>` -- status = `'rejected'` ### Likeable (when `traits.likeable: true` or `{ table?, foreignKey? }`) Table defaults to `{tableName}_likes`, FK defaults to `{singular}_id`. - `Model._likeable.likes(id): Promise<any[]>` - `Model._likeable.likeCount(id): Promise<number>` - `Model._likeable.like(id, userId): Promise<any>` - `Model._likeable.unlike(id, userId): Promise<void>` - `Model._likeable.isLiked(id, userId): Promise<boolean>` ### Billable (when `traits.billable: true`) -- the configured payment driver All methods lazy-import `@stacksjs/payments`. Full reference: `stacks-payments`. - Provider-neutral (`config.payment.driver`): `paymentCustomer(model)`, `charge(model, money, paymentMethodId, options)`, `createPayment(model, money, options)`, `checkout(model, { mode, lines, successUrl, cancelUrl })`, `paymentMethods(model)`, `removePaymentMethod(model, providerId)`, `newSubscription(model, type, price)` -> `SubscriptionSummary`, `cancelSubscription(model, providerId, { atPeriodEnd })` (ownership-checked), `activeSubscription(model)` -> `{ subscription, providerSubscription }` - Stripe only (throw `PaymentUnsupportedError` under another driver): `createStripeUser`, `updateStripeUser`, `deleteStripeUser`, `createOrGetStripeUser`, `retrieveStripeUser`, `syncStripeCustomerDetails`, `setDefaultPaymentMethod`, `addPaymentMethod`, `updateSubscription`, `createSetupIntent`, `subscriptionHistory`, Connect methods - Local: `defaultPaymentMethod(model)`, `storeTransaction(model, productId, options)`, `transactionHistory(model)` ### Two-Factor Auth (when `traits.useAuth.useTwoFactor: true`)
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub