Skip to content

Latest commit

 

History

History
305 lines (228 loc) · 10.9 KB

models.md

File metadata and controls

305 lines (228 loc) · 10.9 KB

Models

Models define your data structure. Each model becomes a database table.

Minimal Example

{
  "name": "customer",
  "fields": {
    "name":  { "type": "string", "required": true },
    "email": { "type": "email", "unique": true }
  }
}

This creates a customers table with id, name, email, created_at, updated_at, created_by, updated_by, active columns.

Field Types

Type SQLite PostgreSQL MySQL Description
string TEXT VARCHAR(n) VARCHAR(n) Short text. Use max for length.
text TEXT TEXT TEXT Long text
integer INTEGER INTEGER INTEGER Whole number. min/max for range.
decimal REAL DECIMAL(18,n) DECIMAL(18,n) Number with decimals. precision for decimal places.
boolean INTEGER BOOLEAN BOOLEAN True/false
date TEXT DATE DATE Date only
datetime TEXT TIMESTAMPTZ DATETIME Date + time
selection TEXT VARCHAR(50) VARCHAR(50) Enum. Requires options array.
email TEXT VARCHAR(255) VARCHAR(255) Email (validated)
uuid TEXT UUID CHAR(36) UUID v4 (auto-validated)
ip TEXT INET VARCHAR(45) IPv4 or IPv6 address
ipv6 / ip:v6 TEXT INET VARCHAR(45) Strict IPv6 only
year INTEGER SMALLINT YEAR 4-digit year (1900-2100)
color TEXT VARCHAR(30) VARCHAR(30) CSS color (#hex, rgb, hsl)
currency REAL DECIMAL(18,n) DECIMAL(18,n) Money amount with currency resolution
vector TEXT VECTOR(n) JSON Embedding vector. Requires dimensions.
binary BLOB BYTEA LONGBLOB Raw binary data
json:object TEXT JSONB JSON JSON object (validated as object)
json:array TEXT JSONB JSON JSON array (validated as array)
many2one TEXT UUID CHAR(36) FK to another model. Requires model.
one2many - - - Reverse FK. Requires model + inverse. No column created.
many2many - - - Junction table. Requires model.
json TEXT JSONB JSON Arbitrary JSON data
file TEXT VARCHAR(500) VARCHAR(500) File path/URL
computed - - - Virtual field. Requires computed expression. No column created.

Field Options

Option Type Description
required bool NOT NULL constraint
unique bool UNIQUE constraint
default any Default value. Use "now" for current timestamp.
max int Max length (string) or max value (integer)
min int Min value (integer)
precision int Decimal places (decimal)
max_size string Max file size, e.g. "5MB" (file)
label string Display label for UI
options string[] Enum values (selection)
model string Related model name (many2one, one2many, many2many)
inverse string Inverse FK field name (one2many)
computed string Computation expression (computed). Uses expr-lang/expr engine with full function library (68+ built-in + go-json stdlib). Supports arithmetic, string functions, ternary, aggregates over child collections (sum("lines.subtotal")).
auto bool Auto-set value (datetime)
storage string Storage hint override (e.g. "bigint", "smallint", "numeric", "longtext")
scale int Decimal scale (alias for precision in decimal/currency)
hidden bool Exclude from auto-generated views
display_field string Override target model's title_field for many2one display
currency_field string Field name containing currency code (for currency type)
dimensions int Vector dimensions (required for vector type)
number_format object Number formatting config (thousand_separator, decimal_separator, symbol, symbol_position)
models string[] Allowed morph target models (for morph_to type)
morph string Morph name identifier (for polymorphic relations)

Model Options

Control auto-generated columns and behavior at the model level:

{
  "name": "order",
  "version": true,
  "timestamps": true,
  "timestamps_by": true,
  "soft_deletes": true,
  "tenant_scoped": true,
  "fields": { ... }
}
Option Type Default Columns Generated
version bool false version INTEGER NOT NULL DEFAULT 1 — optimistic locking
timestamps bool true created_at, updated_at
timestamps_by bool true created_by, updated_by
soft_deletes bool false deleted_at (nullable datetime)
soft_deletes_by bool false deleted_by (UUID FK)
tenant_scoped bool true tenant_id (when tenant.enabled = true). Set false for shared tables.

Optimistic Locking (version: true)

When enabled, every update requires the client to send the current version in the request body. The engine checks WHERE version = ? and increments it atomically. If another user modified the record first, the update returns HTTP 409 Conflict with "record has been modified by another user".

# Read record (version: 3)
curl http://localhost:8080/api/order/abc123

# Update with version check
curl -X PUT http://localhost:8080/api/order/abc123 \
  -H "Content-Type: application/json" \
  -d '{"name": "Updated", "version": 3}'
# → Success: version becomes 4

# Stale update (version 3 already changed)
curl -X PUT http://localhost:8080/api/order/abc123 \
  -d '{"name": "Conflict", "version": 3}'
# → 409 Conflict

Soft Deletes (soft_deletes: true)

When enabled, DELETE sets deleted_at = NOW() and active = false instead of removing the row. Records with deleted_at IS NOT NULL are excluded from FindAll, Count, and Sum queries.

Active vs Soft Deletes

These are two separate concepts:

  • active — A business field. An inactive record still appears in lists but cannot be used in selections/dropdowns. User-controlled toggle.
  • soft_deletes — Recycle bin. A deleted record is hidden from all queries. Can be restored.

The repository provides two sets of query methods:

Method Filter Use Case
FindAll / FindByID / Count / Sum / Paginate Excludes soft-deleted (deleted_at IS NULL) Default queries
FindAllActive / FindActive / CountActive / SumActive / PaginateActive active = true + excludes soft-deleted Dropdowns, selections, active-only views

Auto-generated Columns

Every model automatically gets:

Column Type Description
id UUID Primary key (auto-generated)
active boolean Business active flag (default true)
created_at datetime Set on creation (when timestamps: true, default)
updated_at datetime Set on every update (when timestamps: true, default)
created_by UUID FK User who created the record (when timestamps_by: true, default)
updated_by UUID FK User who last updated (when timestamps_by: true, default)
version integer Optimistic lock counter (only when version: true)
deleted_at datetime Soft delete timestamp (only when soft_deletes: true)
deleted_by UUID FK User who deleted the record (only when soft_deletes_by: true)

You never need to define these in your JSON.

Relationships

many2one (FK)

"customer_id": { "type": "many2one", "model": "customer", "required": true }

Creates a UUID foreign key column.

one2many (Reverse FK)

"lines": { "type": "one2many", "model": "order_line", "inverse": "order_id" }

No column created. Resolved by querying order_lines WHERE order_id = this.id.

many2many (Junction Table)

"tags": { "type": "many2many", "model": "tag" }

Creates a junction table model_tags with both FKs.

Polymorphic Relations (Morphs)

Five morph types allow a model to relate to multiple different model types through a single relation.

morph_to — "belongs to any model"

"commentable": { "type": "morph_to", "models": ["post", "video"] }

Creates two columns: commentable_type (VARCHAR) + commentable_id (VARCHAR(36)). models is optional — if omitted, any model is allowed.

morph_one — "has one polymorphic child"

"avatar": { "type": "morph_one", "model": "image", "morph": "imageable" }

No column created. Queries child table: WHERE imageable_type = 'user' AND imageable_id = this.id.

morph_many — "has many polymorphic children"

"comments": { "type": "morph_many", "model": "comment", "morph": "commentable" }

No column created. Same as morph_one but returns array.

morph_to_many — "many-to-many polymorphic (parent side)"

"tags": { "type": "morph_to_many", "model": "tag", "morph": "taggable" }

Creates junction table taggables with columns: id, tag_id, taggable_id, taggable_type.

morph_by_many — "many-to-many polymorphic (inverse side)"

"posts": { "type": "morph_by_many", "model": "post", "morph": "taggable" }

No table created — uses the same junction table from morph_to_many.

Morph Map (Type Aliasing)

Configure in bitcode.toml to use short aliases instead of model names:

[morph_map]
post = "p"
video = "v"

Or in module.json:

{ "morph_map": { "post": "p", "video": "v" } }

Default: model name used as-is (no config needed).

Bridge API

await bitcode.db.morphAttach("post", postId, "tags", [tagId1, tagId2]);
await bitcode.db.morphDetach("post", postId, "tags", [tagId1]);
await bitcode.db.morphSync("post", postId, "tags", [tagId1, tagId2, tagId3]);

REST API

POST /api/v1/{module}/{model}/:id/:relation/attach  { "ids": ["1", "2"] }
POST /api/v1/{module}/{model}/:id/:relation/detach   { "ids": ["1"] }
POST /api/v1/{module}/{model}/:id/:relation/sync     { "ids": ["1", "2", "3"] }

Limitations

  • No automatic cascade delete (use model events for cleanup)
  • No morph-aware record rules (parent record rules not applied during morph_to loading)
  • MongoDB: morph relations loaded via standalone methods, not eager loading (planned for Phase 6C)

Record Rules (Row-Level Security)

"record_rules": [
  { "groups": ["sales.user"],    "domain": [["created_by", "=", "{{user.id}}"]] },
  { "groups": ["sales.manager"], "domain": [] }
]
  • sales.user group: can only see records they created
  • sales.manager group: can see all records (empty domain = no filter)

Record rules are enforced automatically when auth: true on the API.

Model Inheritance

{
  "name": "vip_customer",
  "inherit": "customer",
  "fields": {
    "vip_level": { "type": "selection", "options": ["gold", "platinum"] },
    "discount_rate": { "type": "decimal", "default": 0.1 }
  }
}

Adds fields to the parent model's table.

Indexes

"indexes": [
  ["customer_id", "order_date"],
  ["status"]
]

Creates database indexes for query performance.