Skip to main content

stacks-models

Use when working with data models in Stacks — the defineModel() API, model attributes with validation and factories, relationships (hasOne/hasMany/belongsTo/belongsToMany), traits (useAuth, useUuid, useTimestamps, useSearch, useApi, billable, taggable, categorizable, commentable, likeable, observe), computed properties (get/set), model generation, and the 50+ built-in framework models. Covers model definitions and storage/framework/defaults/app/Models/.

Datos de origen

Repositorio
stacksjs/postline
Última actividad en el origen
3 de agosto de 2026 a las 19:34
Idioma detectado de SKILL.md
inglés
Estrellas
2
Forks
0

Opciones de instalación

De forma predeterminada está seleccionado el prompt que primero revisa el origen. Puedes cambiar a un comando directo o descargar una copia local.

Revisa los archivos de origen

Lee SKILL.md y los archivos complementarios que muestra SkillsMP antes de decidir si quieres instalarlo.

Mostrando SKILL.md

SKILL.md
Instrucciones de origen · Vista previa de solo lectura
name
stacks-models
description
Use when working with data models in Stacks — the defineModel() API, model attributes with validation and factories, relationships (hasOne/hasMany/belongsTo/belongsToMany), traits (useAuth, useUuid, useTimestamps, useSearch, useApi, billable, taggable, categorizable, commentable, likeable, observe), computed properties (get/set), model generation, and the 50+ built-in framework models. Covers model definitions 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 Models ## Key Paths - Your models: `app/Models/` (create it; it does not exist in a fresh project) - Built-in models: `storage/framework/defaults/app/Models/` (62 files, grouped into `commerce/`, `Content/`, `realtime/` and a flat top level) - `ModelOptions` / `Attribute` types: `storage/framework/core/types/src/model.ts` - Attribute presets: `storage/framework/types/attributes.ts` To customize a built-in model, create the same filename under `app/Models/` - `app/Models/User.ts` wins over the default. `buddy publish:model User` copies the default across as a starting point. ## Writing a model Everything - schema, validation, factory, relationships, behavior - is declared in one `defineModel()` call. Migrations are derived from this; you do not write the SQL. ```ts // app/Models/Product.ts import { defineModel } from '@stacksjs/orm' import { schema } from '@stacksjs/validation' export default defineModel({ name: 'Product', // defaults to the file name table: 'products', // defaults to lowercase plural of `name` primaryKey: 'id', // default autoIncrement: true, // default traits: { useUuid: true, useTimestamps: true, useApi: { uri: 'products', routes: ['index', 'store', 'show', 'update', 'destroy'] }, useSearch: { searchable: ['name'], filterable: ['status'] }, observe: true, }, belongsTo: ['Category'], hasMany: ['Review'], attributes: { name: { required: true, fillable: true, order: 1, validation: { rule: schema.string().min(3).max(100), message: { max: 'Name must have a maximum of 100 characters' }, }, factory: faker => faker.commerce.productName(), }, status: { required: true, fillable: true, default: 'draft', validation: { rule: schema.enum(['draft', 'published', 'archived']) }, factory: faker => faker.helpers.arrayElement(['draft', 'published', 'archived']), }, }, } as const) ``` `as const` is what the built-in models use - it narrows literal types so the generated model types stay precise. ### Attribute fields `validation.rule` is the only required key on an attribute. | Field | Effect | |---|---| | `required` | Value required; emits a `NOT NULL` column | | `nullable` | Explicit nullability override | | `default` | Column default (`string \| number \| boolean \| Date`) | | `unique` | Unique constraint | | `type` | Force the column type instead of inferring from the rule | | `order` | Column order in the table and in dashboard forms | | `fillable` | Allow mass assignment | | `guarded` | Block mass assignment | | `hidden` | Exclude from JSON serialization (passwords, tokens) | | `foreignKey` | Disable, infer, or configure the FK constraint | | `factory` | `(faker) => value`, used by seeders and tests | | `validation` | `{ rule, message? }` - `rule` from `schema`, `message` keyed by rule name | ### Traits | Trait | What it adds | |---|---| | `useUuid` | UUID column alongside the primary key | | `useTimestamps` (alias `timestampable`) | `created_at` / `updated_at`. On by default | | `useSoftDeletes` (alias `softDeletable`) | `deleted_at` plus soft-delete query scopes | | `useAuth` (alias `authenticatable`) | Auth columns; `{ usePasskey: true }` adds passkeys | | `useApi` | Generates REST actions and routes: `{ uri, routes, middleware? }` | | `useSearch` (alias `searchable`) | Search-engine indexing: `{ displayable, searchable, sortable, filterable }` | | `useSocials` | OAuth identities, e.g. `['github']` | | `useActivityLog` | Writes an `Activity` row per change | | `observe` | Emits `{model}:created` / `:updated` / `:deleted` events | | `billable` | Stripe methods (`checkout()`, `activeSubscription()`, ...) | | `taggable` / `categorizable` / `commentable` / `likeable` | Pivot tables and their relation methods | Also at the top level: `indexes: [{ name, columns, unique?, where? }]` for composite and partial-unique indexes, and `dashboard: { highlight: true }` to feature the model in the admin UI. `useApi` is an API capability, not a dashboard-view generator. Its generated routes are registered from the merged model registry. Framework defaults are loaded first, then recursive `app/Models/` definitions override matching model names. Protect non-public resources at the model: ```ts useApi: { uri: 'mail-preferences', routes: ['index', 'store', 'show', 'update', 'destroy'], middleware: ['auth'], } ``` Dashboard-specific endpoints may still use scoped Actions when their transport shape, authorization boundary, or aggregation differs from generic CRUD. Do not expose a sensitive model through unguarded generated routes just because a separate dashboard endpoint is protected. Generated store and update routes accept both spellings of every fillable attribute and each foreign key implied by `belongsTo`. Declaring Product as a belongs-to relation therefore accepts `productId` or `product_id` without duplicating that relationship column as an attribute. ### Relationships `hasOne`, `hasMany`, `belongsTo`, `belongsToMany`, `hasOneThrough`, `hasManyThrough`, `morphOne`, `morphMany`, `morphTo`, `morphToMany`, `morphedByMany`. Each takes an array of model names, or an object form when you need to name the foreign key. Use the named object form for a many-to-many relation that owns its pivot schema. It keeps the relation accessor, migration, pivot defaults, timestamps, and uniqueness in the model definition: ```ts belongsToMany: { tags: { model: 'Tag', table: 'taggable_models', foreignKey: 'taggable_id', relatedKey: 'tag_id', pivot: { columns: { taggable_type: { default: 'posts' }, }, timestamps: true, uniques: [['tag_id', 'taggable_id', 'taggable_type']], }, }, }, ``` An instance then exposes the named relation directly: ```ts const post = await Post.find(id) await post.tags().sync(tagIds) await post.tags().detach() ``` The legacy array form remains supported. Prefer the named form when the pivot has custom keys, columns, defaults, timestamps, or uniqueness. Run `buddy generate:migrations` after changing pivot metadata. ### Computed properties and scopes ```ts get: { fullName: (model) => `${model.firstName} ${model.lastName}`, }, set: { password: (value) => makeHash(value), }, scopes: { published: (query) => query.where('status', 'published'), }, ``` ## Workflow ```sh buddy make:model Product # scaffold app/Models/Product.ts buddy generate:migrations # diff models against the schema, emit SQL # review the generated file in database/migrations/ buddy migrate # apply it buddy migrate:fresh --seed # dev only: drop, re-migrate, seed ``` Models resolve at runtime through `createModel()` from `bun-query-builder` - there is no build step between editing a model and querying it. Only migrations need generating. ## Seeding Seed data is declared on the model, through the `useSeeder` trait plus the per-attribute `factory` functions: ```ts traits: { useSeeder: { count: 20, // Optional: pin specific rows over the generated ones. Keys use the // model's camelCase attribute names. fixtures: [ { name: 'Flagship Widget', status: 'published' }, ], }, }, ``` `buddy seed` walks every model carrying the trait and fills its table from the attribute factories. Nothing else is needed - no seeder files, no registration. ```bash buddy seed # every model with a useSeeder trait buddy seed --fresh # truncate each table first buddy seed --only Product,Review # just these models buddy seed --except User # everything but these buddy seed --include-defaults # framework built-ins too ``` A model with no `useSeeder` trait is never seeded. Auth and OAuth models are skipped on a non-fresh database so re-seeding cannot invalidate live sessions - pass `--allow-protected` to override. ## All 62 built-in models by category ### Users & Auth - **User** — name, email, password | traits: useAuth(passkey), useUuid, useTimestamps, useSocials(github) | hasOne: Subscriber, Driver, Author | hasMany: PersonalAccessToken, Customer - **Author** — name, email | belongsTo: User | hasMany: Post - **Customer** — name, email, phone, totalSpent, lastOrder, status, avatar | belongsTo: User | hasMany: Order, GiftCard, Review, Payment - **Driver** — name, phone, vehicleNumber, license, status | belongsTo: User | hasMany: DeliveryRoute - **Subscriber** — email, status, source | belongsTo: User | hasMany: SubscriberEmail ### Content - **Post** — title, content, poster, excerpt, views, publishedAt, status, isFeatured | belongsTo: Author | traits: categorizable, taggable, commentable | seeder: 20 - **Page** — similar to Post with taggable, categorizable - **Comment** — author info, approval, content fields - **Tag** — name(unique), slug(unique), description, postCount, color | seeder: 15 - **Category** — name, description, slug, imageUrl, isActive, parentCategoryId, displayOrder | hasMany: Product | seeder: 10 ### Commerce (20+ models) - **Product** — name(max100), description, price(min1), imageUrl, isAvailable, inventoryCount, preparationTime, allergens(JSON), nutritionalInfo(JSON) | belongsTo: Category, Manufacturer | hasMany: Review, ProductUnit, ProductVariant, LicenseKey, WaitlistProduct, Coupon | seeder: 10, dashboard: highlighted - **ProductVariant** — SKU, options, pricing - **ProductUnit** — unit-specific pricing - **Cart** — status(active|abandoned|converted|expired), totalItems, subtotal, taxAmount, discountAmount, total, expiresAt, currency(USD), notes | hasMany: CartItem | belongsTo: Customer, Coupon - **CartItem** — quantity(min1), unitPrice, totalPrice, taxRate, taxAmount, discountPercentage, productName, productSku | belongsTo: Cart - **Order** — status, totalAmount, taxAmount, discountAmount, deliveryFee, tipAmount, orderType(DINE_IN|TAKEOUT|DELIVERY), deliveryAddress, specialInstructions | hasMany: OrderItem, Payment | belongsTo: Customer, Coupon | observe: true | seeder: 20 - **OrderItem** — quantity(min1), price(min0), specialInstructions | belongsTo: Order, Product - **Coupon** — code(unique), discountType(fixed_amount|percentage), discountValue, minOrderAmount, usageLimit, usageCount, startDate, endDate | seeder: 15 - **GiftCard** — code(unique), initialBalance, currentBalance, currency, status, recipientEmail, isDigital, isReloadable, expiryDate | seeder: 20 - **Manufacturer** — manufacturer info - **Review** — rating(1-5), title, content(max2000), isVerifiedPurchase, isApproved, isFeatured, helpfulVotes, unhelpfulVotes | belongsTo: Product, Customer | seeder: 50 ### Shipping & Delivery - **ShippingMethod**, **ShippingRate** (weightFrom, weightTo, rate), **ShippingZone** - **DeliveryRoute** — driver, vehicle, stops, totalDistance | belongsTo: Driver - **DigitalDelivery** — name, downloadLimit, expiryDays, automaticDelivery - **LicenseKey** — key(XXXX-XXXX-XXXX-XXXX-XXXX), template, expiryDate, status ### Payments & Financial - **Payment** — amount, method(creditCard|debitCard|paypal|...), status(pending|completed|failed|refunded), currency, transactionId(unique) | belongsTo: Order, Customer | seeder: 50 - **PaymentMethod**, **PaymentProduct**, **PaymentTransaction** - **Subscription** — type, providerId, providerStatus, unitPrice - **Transaction** — standard transaction tracking - **TaxRate** — name, rate(0-100), type(VAT|GST|Sales Tax|Customs Duty), country, region, isDefault ### Engagement & Marketing - **Notification** — type, channel, recipient, subject, body, status(pending|sent|delivered|failed|read) | belongsTo: User | seeder: 30 - **Campaign** — name, type(email|sms|push|social|multi-channel), status, audienceSize, openRate, clickRate, budget | seeder: 10 - **Activity** — type, description, subjectType, subjectId, causer, properties(JSON), ipAddress | belongsTo: User | seeder: 50 - **EmailList**, **SocialPost**, **LoyaltyPoint** (walletId, points, source, expiryDate), **LoyaltyReward** ### System - **Job** — queue, payload, attempts, available_at, reserved_at | seeder: 15 - **FailedJob** — failed background jobs - **Error** — type, message, stack, status, additionalInfo | seeder: 10 - **Log** — application logs - **Request** — method, path, statusCode, durationMs, ipAddress, memoryUsage, userAgent, errorMessage | seeder: 50 - **Websocket** — connection tracking - **PrintDevice** — name, location, terminal, lastPing, printCount, isActive - **WaitlistProduct**, **WaitlistRestaurant** — waitlist tracking - **Receipt** — receipt records ## CLI Commands - `buddy make:model [name]` — scaffold a model in `app/Models/` - `buddy publish:model [name]` — copy a built-in model into `app/Models/` to override it - `buddy generate:migrations` — diff models against the schema and emit SQL - `buddy migrate` / `buddy migrate:fresh --seed` — apply migrations - `buddy make:migration [name]` — hand-write a migration instead
Ver en GitHub
Este SKILL.md es muy grande, por eso SkillsMP muestra aqui solo la primera seccion. Ver en GitHub