@samveys/orm
Dari packages/orm/README.md di repository framework.
Halaman ini belum diterjemahkan. Kamu sedang membaca versi bahasa Inggris.
Schema-first models for Samveys (design §8 to). It is built on @samveys/database and works on SQLite, PostgreSQL and MySQL (tested).
1. Define models
import { Model, col } from '@samveys/orm'
export class User extends Model.define('users', {
id: col.increments(),
name: col.string(80),
email: col.string().unique(),
isAdmin: col.boolean().default(false),
active: col.boolean().default(true),
createdAt: col.timestamp().autoCreate(),
updatedAt: col.timestamp().autoUpdate(),
}) {
posts = this.hasMany(() => Post)
static active = this.scope((query) => query.where('active', true))
}
export class Post extends Model.define('posts', {
id: col.uuid().primary(),
userId: col.integer().references('users', 'id', 'cascade'),
title: col.string(120),
publishedOn: col.date().nullable(),
}) {
author = this.belongsTo(() => User, { foreignKey: 'userId' })
}- Types come from the columns.
user.createdAtis aTemporal.Instant, andpost.publishedOnis aTemporal.PlainDate | null. A wrong attribute name inwhere(),orderBy()orwith()is atscerror (tested). - Primary keys.
col.uuid().primary()is a UUIDv7 generated on insert, so it sorts by creation time.col.increments()is an auto-increment integer. - Column names. Attribute names become snake_case column names (
createdAt→created_at). Override one with.column('name'). - Relations. These are
hasMany,hasOne,belongsToandbelongsToMany.hasMany/hasOnedefault to the foreign key<owner>Id.belongsTodefaults to<relation>Id.belongsToManydefaults to a pivot tablerole_userwith columnsrole_idanduser_id.
2. Query and load relations
import assert from 'node:assert/strict'
import { test } from 'node:test'
import { createDatabaseManager, defineDatabaseConfig, sqlite } from '@samveys/database'
import { diffSchema, renderMigration, useDatabase } from '@samveys/orm'
import { Post, User } from './models.ts'
test('models', async () => {
const db = createDatabaseManager(defineDatabaseConfig({ default: 'main', connections: { main: sqlite({ file: ':memory:' }) } }))
useDatabase(db)
// Build the tables straight from the models: the same path as `svs make:migration --diff`.
const diff = await diffSchema(db, [User, Post])
assert.equal(diff.createTables.length, 2)
assert.match(renderMigration(diff), /schema\.createTable\("users"/)
for (const { table, columns } of diff.createTables) {
db.schema.createTable(table, (t) => {
for (const column of columns) {
const built = column.type === 'string' ? t.string(column.name, column.length) : t[column.type](column.name)
if (column.nullable) built.nullable()
if (column.unique) built.unique()
if (column.default !== undefined) built.default(column.default)
if (column.primary && column.type !== 'increments') built.primary()
}
})
}
for (const statement of db.schema.toSQL()) await db.rawQuery(statement)
const ann = await User.create({ name: 'ann', email: 'ann@example.test' })
await ann.related('posts').create({ title: 'Hello' })
const users = await User.query().apply(User.active).with('posts').orderBy('name').get()
assert.deepEqual(users[0]?.posts.map((post) => post.title), ['Hello'])
const post = await Post.query().with('author').firstOrFail()
assert.equal(post.author?.name, 'ann')
await db.close()
})Lazy loading is forbidden in local and test. Reading a relation that was not loaded with with() throws SVS_E080, and the message names the model and the relation, e.g. User.posts was read but not loaded. In production the access is logged and the relation reads as empty. Load explicitly with await model.load('posts').
3. Write safely
Mass assignment. create() accepts validator output, an object written in code, or the result of Model.fill(input, allowList). Passing any or unknown is a type error. fill() keeps only the listed attributes, so an isAdmin field from a request body is dropped (tested).
const user = await User.create(User.fill(ctx.body, ['name', 'email']))Constraint errors are validation errors.
- A unique violation becomes
SVS_E083. - A foreign key violation becomes
SVS_E084.
Both are HTTP 422 with the field in errors, e.g. { "pointer": "#/body/email", "detail": "already taken" }, instead of a 500.
save() only writes changed attributes. Bulk query().update() and query().delete() skip timestamps.
4. Factories and generators
import { defineFactory } from '@samveys/orm'
export const users = defineFactory(User, ({ sequence }) => ({ name: `user ${sequence}`, email: `user${sequence}@example.test` }))
await users.createMany(3)
await users.state({ isAdmin: true }).create()Register orm() after database() in samveys.config.ts. It connects the models to the 'db' binding and adds two commands:
svs make:model Invoice --migration # app/models/invoice.ts + a create_invoices migration
svs make:migration add_paid_on --diff # compares app/models with the database, writes what is missing
svs migrate--diff writes one migration with the missing tables and columns. Columns that exist in the database but in no model are listed as warnings and never dropped.
5. Error codes
| Code | When |
|---|---|
SVS_E073 | findOrFail() / firstOrFail() found nothing (404) |
SVS_E080 | An unloaded relation was read (local and test) |
SVS_E081 | create(), make() or merge() got something that is not an object |
SVS_E082 | An unknown attribute or relation name, a reserved attribute name, or a missing primary key |
SVS_E083 | Duplicate value for a unique column (422) |
SVS_E084 | Foreign key violation (422) |