ActiveModel
A Proxy-based model with runtime data integrity control (fillable, hidden, protected, readonly) and type checks — for DTOs from external sources.
TS base classes for working with data structures — reactive fields, integrity control, declarative validation.
How do you build a data model where every property can be intercepted on read, write, and delete — without hand-writing getters/setters for each field? ActiveModel wraps the instance in a Proxy and intercepts get/set/deleteProperty/has/ownKeys. On top of that, the @ActiveField() decorator declaratively describes each field's behavior — no wrapper classes or manual Object.defineProperty calls needed:
import { ActiveModel, ActiveField } from '@alt-point/active-models'
class Product extends ActiveModel {
@ActiveField() name: string = ''
// setter normalizes the input, getter computes a display value on the fly
@ActiveField({
setter: (model, prop, value) => Math.round(Number(value) * 100) / 100,
getter: (model, prop, target) => `$${target[prop].toFixed(2)}`
})
price: number = 0
}
const product = Product.create({ name: 'Mouse', price: 19.999 })
product.price // '$20.00' — 19.999 went in, the getter hands back a display-ready stringMore: Decorators example, @ActiveField() options reference.
How do you keep external data (an API response, for example) from silently overwriting a protected field, deleting a required attribute, or adding stray keys that shouldn't be there? The fillable, readonly, protected, and hidden options give fine-grained, per-field control:
class User extends ActiveModel {
// set once at creation (factory or constructor), read-only from then on
@ActiveField({ readonly: true, attribute: () => crypto.randomUUID() })
id!: string
// delete user.role throws
@ActiveField({ protected: true })
role: string = 'guest'
// excluded from Object.keys()/JSON.stringify(), still directly accessible
@ActiveField({ hidden: true })
internalNotes: string = ''
}
const user = User.create({ id: 'ignored-from-outside', role: 'admin' })
user.id // the generated uuid, not 'ignored-from-outside'
user.id = 'x' // throws - the field was already set
delete user.role // throws - the field is protected from deletion
JSON.stringify(user) // internalNotes is absent from the resultMore: @ActiveField() options reference, including a comparison table between readonly and fillable: false.
TypeScript only checks types at compile time — data coming from external sources (an API, localStorage, a WebSocket) arrives at runtime with no such guarantee. validator runs on every attempted write and throws if the value doesn't match the expected shape; factory automatically wraps nested structures in their own ActiveModel, keeping validation intact at any depth:
enum OrderStatus { New = 'new', Paid = 'paid', Shipped = 'shipped' }
class Address extends ActiveModel {
@ActiveField() city: string = ''
}
class Order extends ActiveModel {
@ActiveField({
validator: (model, prop, value) => {
if (!Object.values(OrderStatus).includes(value)) {
throw new Error(`Invalid status: ${value}`)
}
}
})
status: OrderStatus = OrderStatus.New
// the nested object automatically becomes an Address instance with its own validation
@ActiveField({ factory: Address })
shippingAddress?: Address
}
const order = Order.create({ status: 'paid', shippingAddress: { city: 'Berlin' } })
order.shippingAddress instanceof Address // true
order.status = 'not-a-real-status' // throws Error: Invalid status: not-a-real-statusMore: Advanced features — validators and factory.
How do you find out that a specific field changed, was nulled out, or was deleted — without wrapping every assignment in your own code? The beforeSetValue, afterSetValue, nulling, and beforeDeletingAttribute events are subscribed to right in the decorator via on/once, while touched/created are instance-level events available through model.emitter:
import { EventType } from '@alt-point/active-models'
class Invoice extends ActiveModel {
@ActiveField({
on: {
afterSetValue: ({ prop, value, oldValue }) => {
console.log(`${prop}: ${oldValue} → ${value}`)
},
nulling: ({ prop }) => console.log(`${prop} was explicitly cleared`)
}
})
total: number | null = 0
static beforeFill (model: any) {
model.emitter.on(EventType.created, () => console.log('invoice fully built'))
model.emitter.on(EventType.touched, () => console.log('invoice changed'))
}
}
const invoice = Invoice.create({ total: 100 })
// invoice fully built
invoice.total = 250
// total: 100 → 250
// invoice changed
invoice.total = null
// total: 250 → null
// invoice changed
// total was explicitly clearedMore: Model lifecycle — every event from creation to deletion, with a diagram.
yarn add @alt-point/active-modelsnpm install --save @alt-point/active-modelsbun add @alt-point/active-models@ActiveField() options reference — every option on its own, with examplesnew/create/fill, serialization, mapTo()node:httpРусская версия: /