import type { Attributes } from '@stacksjs/types' import { defineModel } from '@stacksjs/orm' import { makeHash } from '@stacksjs/security' // soon, these will be auto-imported import { schema } from '@stacksjs/validation' import { PASSWORD_MAX_LENGTH, PASSWORD_MIN_LENGTH } from '../password-policy' export default defineModel({ name: 'User', // defaults to the sanitized file name table: 'users', // defaults to the lowercase, plural name of the model name (or the name of the model file) primaryKey: 'id', // defaults to `id` autoIncrement: true, // defaults to true // Define composite indexes indexes: [ { name: 'users_email_name_index', columns: ['email', 'name'], }, ], traits: { useAuth: { usePasskey: true, }, // Not every app bills through the User model — leave `billable` // off by default here. An app that wants checkout()/ // createStripeUser()/activeSubscription() etc. (createBillableMethods // in orm/define-model.ts) should `buddy publish:model User` and // enable the trait on its own app/Models/User.ts override, alongside // the matching users.stripe_id guarantee-ALTER already provided by // ensureUsersAuthColumns() regardless of this flag. billable: false, useUuid: true, useTimestamps: true, // defaults to true, `timestampable` used as an alias useSocials: ['github'], useSearch: { displayable: ['id', 'name', 'email'], // the fields to become d (defaults to all fields) searchable: ['name', 'email'], // the fields to become searchable (defaults to all fields) sortable: ['created_at', 'updated_at'], // the fields to become sortable (defaults to all fields) filterable: [], // the fields to become filterable (defaults to all fields) // options: {}, // you may pass options to the search engine }, useSeeder: { // defaults to a count of 10, `seedable` used as an alias count: 10, }, useApi: { uri: 'users', // your-url.com/api/users routes: ['index', 'store', 'show'], // Registration is handled by the dedicated, rate-limited // `/register` action. The generated User CRUD surface exposes PII // and must only be available to authenticated operators. middleware: ['auth'], }, }, hasOne: ['Subscriber', 'Driver', 'Author'], hasMany: [ 'PersonalAccessToken', 'Customer', 'TeamMember', ], attributes: { name: { order: 2, fillable: true, validation: { rule: schema.string().required().min(5).max(100), message: { min: 'Name must have a minimum of 3 characters', max: 'Name must have a maximum of 255 characters', }, }, factory: faker => faker.person.fullName(), }, email: { unique: true, order: 1, fillable: true, validation: { rule: schema.string().email().required(), message: { required: 'Email is required', email: 'Email must be a valid email address', }, }, factory: faker => faker.internet.email(), }, password: { order: 3, hidden: true, fillable: true, validation: { rule: schema.string().required().min(PASSWORD_MIN_LENGTH).max(PASSWORD_MAX_LENGTH), message: { required: 'Password is required', min: `Password must have a minimum of ${PASSWORD_MIN_LENGTH} characters`, max: `Password must have a maximum of ${PASSWORD_MAX_LENGTH} characters`, }, }, // Must satisfy the rule above, or seeded users fail their own model's // validation. `123456` did once the minimum moved to 8 (#2226). factory: () => 'password1234', }, avatar: { order: 4, fillable: true, validation: { rule: schema.string().max(2048), message: { max: 'Avatar URL must be at most 2048 characters', }, }, factory: faker => faker.image.avatar(), }, }, get: { salutationName: (attributes: Attributes) => { return `Mr. ${attributes.name}` }, }, set: { password: async (attributes: Attributes) => { return await makeHash(attributes.password, { algorithm: 'bcrypt' }) }, }, dashboard: { highlight: true, }, } as const)