Skip to content

defineModel

Declares a model. The data structure definition that drives database tables, API endpoints, UI forms, and permissions.

See Models concept for usage patterns.

Signature

typescript
import { defineModel, field } from 'rangka';

export default defineModel({
  name: 'invoice',
  label: 'Sales Invoice',
  naming: 'invoice_number',
  auditLog: true,
  traits: ['timestamped'],
  fields: {
    invoice_number: field.sequence({ prefix: 'INV-', digits: 5 }),
    customer: field.link('sales.customer', { required: true }),
    posting_date: field.date({ required: true }),
    total: field.decimal({ precision: 18, scale: 2, readOnly: true }),
    items: field.children('sales.invoice_item', { foreignKey: 'invoice_id' }),
  },
  indexes: [{ fields: ['customer', 'posting_date'] }, { fields: ['posting_date'], unique: false }],
});

ModelConfig

typescript
interface ModelConfig {
  name: string;
  label?: string;
  naming?: NamingConfig;
  scope?: string;
  auditLog?: boolean;
  fields: Record<string, FieldConfig>;
  indexes?: IndexConfig[];
  traits?: Trait[];
}

Top-Level Fields

FieldTypeDefaultDescription
namestringRequired. Model identifier. Combined with module as module.name for qualified name.
labelstringundefinedHuman-readable name. If omitted, derived from name.
namingstringundefinedField name to use as the record's display title.
scopeScopeConfigundefinedScope to auto-filter queries by the user's active scope value. Models without scope are global. See ScopeConfig below.
auditLogbooleanfalseTrack all field changes in an audit trail table.
fieldsRecord<string, FieldConfig>Required. Field definitions keyed by field name.
indexesIndexConfig[]undefinedDatabase indexes for query performance.
traitsTrait[]undefinedReusable behavior mixins.

ScopeConfig

typescript
type ScopeConfig = string | { name: string; field: string };

The string form uses the qualified model name directly (e.g., 'core.company'). The object form lets you specify a custom field name on the current model that holds the scope value.

typescript
scope: 'core.company'                          // string form: uses default field
scope: { name: 'core.company', field: 'branch' } // object form: custom field

Naming

typescript
type NamingConfig = string;

The naming field points to a field in your model that acts as the record's display title. For example, a customer model might use naming: 'name' so the customer's name shows up in links and references. An invoice model might use naming: 'invoice_number' where invoice_number is a field.sequence().

typescript
naming: 'invoice_number'; // uses the invoice_number field as the title
naming: 'name'; // uses the name field as the title

Traits

typescript
type Trait = 'timestamped' | 'soft_delete' | 'ledger';
TraitAdds FieldsBehavior
timestampedcreated_at: datetime, updated_at: datetime, created_by: link(core.user), updated_by: link(core.user)Auto-set on create/update
soft_deletearchived_at: datetimeDELETE becomes archive (sets archived_at). List and get queries exclude archived records by default. Updates and deletes on archived records are blocked.
ledger> Planned — not yet implemented. Will enforce immutability after posting and add reversal semantics for accounting entries.

For status fields and state transitions, define your own status enum field and use actions backed by services to handle transitions.

Indexes

typescript
interface IndexConfig {
  fields: string[];
  unique?: boolean;
}
FieldTypeDefaultDescription
fieldsstring[]Required. Ordered list of field names in the index.
uniquebooleanfalseEnforce uniqueness constraint.

Field Types

All fields (except relationship fields) extend BaseFieldOptions:

typescript
interface BaseFieldOptions {
  required?: boolean;
  label?: string;
  hidden?: boolean;
  readOnly?: boolean;
  searchable?: boolean;
  default?: unknown;
  validation?: ValidationConfig;
}
OptionTypeDefaultDescription
requiredbooleanfalseField must have a value on save.
labelstringundefinedDisplay label. Derived from field name if omitted.
hiddenbooleanfalseHide from UI (still exists in API).
readOnlybooleanfalseCannot be set via API. Typically set by hooks or computed fields.
searchablebooleanfalseInclude this field in search queries.
defaultunknownundefinedDefault value applied on create if field is empty.
validationValidationConfigundefinedDeclarative validation constraints (see below).

ValidationConfig

typescript
interface ValidationConfig {
  format?: string;
  min?: number;
  max?: number;
  pattern?: string;
  minLength?: number;
  maxLength?: number;
  message?: string;
}
OptionTypeDescription
formatstringBuilt-in format validator (e.g., 'email', 'url', 'phone', 'uuid').
minnumberMinimum numeric value.
maxnumberMaximum numeric value.
patternstringRegex pattern the value must match.
minLengthnumberMinimum string length.
maxLengthnumberMaximum string length.
messagestringCustom error message when validation fails.

required remains a top-level field option. validation provides additional declarative constraints as data, not functions. The format field accepts any string so you can register custom format validators.


string

typescript
interface StringFieldConfig extends BaseFieldOptions {
  type: 'string';
  maxLength?: number;
  default?: string;
}
OptionTypeDefaultDescription
maxLengthnumberundefinedMaximum character length. Maps to VARCHAR(n).

text

typescript
interface TextFieldConfig extends BaseFieldOptions {
  type: 'text';
  default?: string;
}

Long-form text. Maps to TEXT column. No length limit.


int

typescript
interface IntFieldConfig extends BaseFieldOptions {
  type: 'int';
  default?: number;
}

Integer value. Maps to INTEGER.


decimal

typescript
interface DecimalFieldConfig extends BaseFieldOptions {
  type: 'decimal';
  precision?: number;
  scale?: number;
  default?: number;
}
OptionTypeDefaultDescription
precisionnumberundefinedTotal number of digits.
scalenumberundefinedDigits after decimal point.

Example: { type: 'decimal', precision: 18, scale: 2 } → max 9999999999999999.99


boolean

typescript
interface BooleanFieldConfig extends BaseFieldOptions {
  type: 'boolean';
  default?: boolean;
}

date

typescript
interface DateFieldConfig extends BaseFieldOptions {
  type: 'date';
  default?: string;
}

Date without time. Format: YYYY-MM-DD.


datetime

typescript
interface DatetimeFieldConfig extends BaseFieldOptions {
  type: 'datetime';
  default?: string;
}

Date with time. Stored as UTC. Format: ISO 8601.


enum

typescript
interface EnumFieldConfig extends BaseFieldOptions {
  type: 'enum';
  options: readonly string[];
  default?: string;
}
OptionTypeDescription
optionsreadonly string[]Required. Allowed values.

json

typescript
interface JsonFieldConfig extends BaseFieldOptions {
  type: 'json';
  default?: unknown;
}

Arbitrary JSON data. Maps to JSONB.


money

typescript
interface MoneyFieldConfig extends BaseFieldOptions {
  type: 'money';
}

Semantic field type. Stored as decimal. Marks the field as monetary so the frontend formats it as currency. The app layer handles currency awareness — no framework-level configuration needed.


code

typescript
interface CodeFieldConfig extends BaseFieldOptions {
  type: 'code';
  language: 'expression';
}
OptionTypeDescription
language'expression'Required. Code language for editor syntax highlighting.

computed

typescript
interface ComputedFieldConfig {
  type: 'computed';
  depends: string[];
  compute: (doc: Record<string, unknown>, ctx?: FrameworkContext) => unknown | Promise<unknown>;
}
OptionTypeDefaultDescription
dependsstring[]Required. Field names this computed field reads. Dot notation for child fields.
compute(doc, ctx?) => unknownRequired. Computation function.

Example:

typescript
fields: {
  items: field.children('sales.invoice_item', { foreignKey: 'invoice_id' }),
  total: field.computed({
    depends: ['items.amount'],
    compute: (doc) => (doc.items as any[]).reduce((sum, i) => sum + i.amount, 0),
  }),
  outstanding: field.computed({
    depends: ['total', 'paid_amount'],
    compute: (doc) => (doc.total || 0) - (doc.paid_amount || 0),
  }),
}

sequence

typescript
interface SequenceFieldConfig {
  type: 'sequence';
  prefix?: string;
  digits?: number;
}
OptionTypeDefaultDescription
prefixstringundefinedString prepended to the sequence number.
digitsnumberundefinedZero-pad to this many digits.

attachment

typescript
interface AttachmentFieldConfig extends BaseFieldOptions {
  type: 'attachment';
  accept?: string[];
  maxSize?: string;
}
OptionTypeDefaultDescription
acceptstring[]undefinedAllowed MIME types. Example: ['image/png', 'application/pdf']
maxSizestringundefinedMaximum file size. Example: '10mb', '500kb'

attachments

typescript
interface AttachmentsFieldConfig extends BaseFieldOptions {
  type: 'attachments';
  accept?: string[];
  maxSize?: string;
  maxCount?: number;
}
OptionTypeDefaultDescription
acceptstring[]undefinedAllowed MIME types.
maxSizestringundefinedMaximum size per file.
maxCountnumberundefinedMaximum number of files.

Relationship Fields

typescript
interface LinkFieldConfig extends BaseFieldOptions {
  type: 'link';
  model: string;
  nullable?: boolean;
}
OptionTypeDefaultDescription
modelstringRequired. Qualified model name (module.model).
nullablebooleanundefinedAllow null (no foreign key constraint).

Foreign key to another model. Stored as the referenced record's ID.


hasMany

typescript
interface HasManyFieldConfig {
  type: 'hasMany';
  model: string;
  foreignKey: string;
}
OptionTypeDescription
modelstringRequired. Qualified model name of the related records.
foreignKeystringRequired. Field on the related model pointing back to this model.

Virtual field. Does not create a column. Used for eager-loading via ?include=. Does not cascade delete — related records are independent and must be deleted separately.


children

typescript
interface ChildrenFieldConfig {
  type: 'children';
  model: string;
  foreignKey: string;
  fields?: Record<string, FieldConfig>;
}
OptionTypeDefaultDescription
modelstringRequired. Qualified model name for the child table.
foreignKeystringRequired. Field on the child model referencing this parent.
fieldsRecord<string, FieldConfig>undefinedInline field definitions for the child model (alternative to a separate schema file).

Parent-child relationship. Children are saved/deleted with the parent in a single transaction. Children always cascade delete with the parent — when a parent is deleted, all its children are deleted.


manyToMany

typescript
interface ManyToManyFieldConfig {
  type: 'manyToMany';
  model: string;
  through: string;
}
OptionTypeDescription
modelstringRequired. Qualified model name of the related model.
throughstringRequired. Junction table model name.

typescript
interface DynamicLinkFieldConfig extends BaseFieldOptions {
  type: 'dynamicLink';
  modelField: string;
}
OptionTypeDescription
modelFieldstringRequired. Name of another field on this model that stores the target model name.

Polymorphic reference. The target model is determined at runtime from another field's value.


tree

typescript
interface TreeFieldConfig extends BaseFieldOptions {
  type: 'tree';
  parentField: string;
  strategy: 'materialized_path' | 'nested_set' | 'closure_table';
}
OptionTypeDescription
parentFieldstringRequired. Field name that stores the parent reference.
strategystringRequired. Tree storage strategy.

Enables hierarchical queries (ancestors, descendants, subtree).

Extensions

Extensions add fields and hooks to models defined in other modules. Use defineExtension() to extend another module's model. Use defineHooks() to add hooks to your own model (same module).

typescript
import { defineExtension } from 'rangka';

export default defineExtension('sales.invoice', {
  fields: {
    tax_category: { type: 'link', model: 'tax.category' },
  },
  hooks: {
    beforeSave: async (doc, ctx) => {
      /* ... */
    },
  },
});

ExtensionConfig

typescript
interface ExtensionConfig {
  fields?: Record<string, FieldConfig>;
  hooks?: HooksConfig;
  actions?: Record<string, ActionConfig>;
}