Decorators
TypeScript decorators for defining models and relationships.
Requires: TypeScript 5.0+ with TC39 Stage 3 decorators (experimentalDecorators: false)
@ClientModel
Register a model class.
Signature
@ClientModel(typeName: string)Parameters
| Parameter | Type | Description |
|---|---|---|
typeName | string | Type name - must match database table name exactly |
Type Name Convention: The typeName must match your database table name exactly (including capitalization) and be used consistently in relationship decorators.
// Drizzle: lowercase table names
export const task = sqliteTable('task', { ... })
@ClientModel('task') // Matches table 'task'
@ManyToOne('user', 'authorId') // Type 'user' matches @ClientModel('user')The TypeScript class name can be different - only the decorator string matters.
Example
// Drizzle table
export const user = sqliteTable('user', { ... })
// Client model
@ClientModel('user') // Must match table name 'user'
export class User extends Model {
@Property()
id = crypto.randomUUID()
@Property()
name!: string
}@Property
Mark a field as syncable.
Signature
@Property(options?: PropertyOptions)PropertyOptions Interface
Complete interface definition from implementation:
interface PropertyOptions {
// Field type (internal)
type?: 'primitive' | 'object' | 'collection'
// Relationship options (internal)
relation?: string
relationshipType?: 'o2o' | 'o2m' | 'm2o' | 'm2m' | 'ref'
fk?: string
relatedType?: string
// Field behavior
server?: boolean
validate?: (value: any) => boolean
computed?: boolean
// Default values (current v1.0)
defaultOn?: Array<'create' | 'save'>
defaultProvider?: (ctx: { t: string, id: string | null }) => any
}Note: type, relation, relationshipType, fk, and relatedType are set automatically by relationship decorators (@ManyToOne, @OneToMany, etc.). You typically only use server, validate, computed, defaultOn, and defaultProvider.
Options Reference
type (Internal)
type?: 'primitive' | 'object' | 'collection'Description: Field type classification. Set automatically:
'primitive'- Default for @Property'object'- Set by @ManyToOne, @OneToOne'collection'- Set by @OneToMany, @ManyToMany
User-facing: No (internal use only)
relation (Internal)
relation?: stringDescription: Name of inverse property on related model.
Example:
// Internally set by @ManyToOne('posts', 'authorId')
// relation = 'posts' (inverse collection on User)User-facing: No (set by relationship decorators)
relationshipType (Internal)
relationshipType?: 'o2o' | 'o2m' | 'm2o' | 'm2m' | 'ref'Description: Type of relationship cardinality:
'o2o'- One-to-one'o2m'- One-to-many'm2o'- Many-to-one'm2m'- Many-to-many'ref'- Reference (no inverse)
User-facing: No (set by relationship decorators)
fk (Internal)
fk?: stringDescription: Foreign key property name.
Example:
// Internally set by @ManyToOne('posts', 'authorId')
// fk = 'authorId'User-facing: No (set by relationship decorators)
relatedType (Internal)
relatedType?: stringDescription: Type name of related model (e.g., ‘User’, ‘Post’).
Example:
// For @OneToMany('author'), relatedType might be 'Issue'User-facing: No (set by relationship decorators)
server
server?: booleanDefault: false
Description: Mark field as server-managed (read-only on client).
Behavior:
- Client cannot write to this field
- Value comes from server only
- Useful for server timestamps, computed fields
Example:
@Property({ server: true })
serverCreatedAt?: number
@Property({ server: true })
serverUpdatedAt?: numbervalidate
validate?: (value: any) => booleanDescription: Validation function called before writes.
Behavior:
- Called when field is mutated
- Return
true= valid - Return
false= reject mutation
Example:
@Property({
validate: (email) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
})
email = ''
// Invalid writes are rejected
user.email = 'invalid' // Validation failscomputed
computed?: booleanDefault: false
Description: Mark as computed property (not stored in pool).
Behavior:
- Derived from other properties
- Not synced to server
- Not persisted in storage
- Use regular getters instead (recommended)
Example:
// Prefer getters (no decorator needed)
get wordCount(): number {
return this.content.split(' ').length
}
// Or use computed option
@Property({ computed: true })
get wordCount(): number {
return this.content.split(' ').length
}defaultOn (Current v1.0)
defaultOn?: Array<'create' | 'save'>Description: When to apply defaultProvider.
Values:
['create']- Only when creating new model (default)['save']- Only when updating existing model['create', 'save']- Both create and update
Example:
@Property({
defaultOn: ['create', 'save'],
defaultProvider: () => Date.now()
})
updatedAt = 0
// Re-computed on every create and savedefaultProvider (Current v1.0)
defaultProvider?: (ctx: { t: string, id: string | null }) => anyDescription: Function to generate default value.
Context:
t- Type name (e.g., ‘task’, ‘user’)id- Model ID (nullduring creation)
Example:
@Property({
defaultOn: ['create'],
defaultProvider: () => Date.now()
})
createdAt = 0
@Property({
defaultProvider: (ctx) => {
return `${ctx.t}-${crypto.randomUUID()}`
}
})
uniqueKey = ''v1.1+ Simplified API (Planned)
Planned: These simplified options are planned for v1.1+ and not yet available.
default (v1.1+)
default?: () => anyDescription: Shorthand for defaultProvider. Implies defaultOn: ['create'].
Example:
// v1.1+ (planned)
@Property({ default: () => Date.now() })
createdAt = 0
// Equivalent to v1.0:
@Property({
defaultOn: ['create'],
defaultProvider: () => Date.now()
})
createdAt = 0defaultOn Simplified (v1.1+)
defaultOn?: 'create' | 'update'Description: Simplified timing (single value instead of array).
Values:
'create'- Only on creation (default)'update'- On every save (including create)
Example:
// v1.1+ (planned)
@Property({ default: () => Date.now(), defaultOn: 'update' })
updatedAt = 0
// Or with initializer
@Property({ defaultOn: 'update' })
updatedAt = Date.now()Common Examples
// Required property
@Property()
title!: string
// Auto-generated with initializer
@Property()
id = crypto.randomUUID()
// Read-only from server
@Property({ server: true })
serverCreatedAt?: number
// With validation
@Property({
validate: (email) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
})
email = ''
// Current: Creation timestamp
@Property({
defaultOn: ['create'],
defaultProvider: () => Date.now()
})
createdAt = 0
// Current: Update timestamp
@Property({
defaultOn: ['create', 'save'],
defaultProvider: () => Date.now()
})
updatedAt = 0@ManyToOne
Define a many-to-one relationship (child → parent).
Signature
@ManyToOne(relatedType: string, fk?: string, inverse?: string)Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
relatedType | string | Yes | Type name from @ClientModel (e.g., ‘user’, ‘task’) |
fk | string | No | Foreign key field name on this model |
inverse | string | No | Inverse collection name on related model |
Type Name: Must match the @ClientModel('...') type of the related model, not the property name.
Example
@ClientModel('issue')
export class Issue extends Model {
@Property()
id = crypto.randomUUID()
@Property()
authorId?: string
// relatedType: 'user', fk: 'authorId', inverse: 'issues'
@ManyToOne('user', 'authorId', 'issues')
author?: User
}
@ClientModel('user')
export class User extends Model {
@Property()
id = crypto.randomUUID()
@OneToMany('issue', 'author')
issues = new Collection<Issue>()
}@OneToMany
Define a one-to-many relationship (parent → children).
Signature
@OneToMany(relatedType: string, inverse?: string)Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
relatedType | string | Yes | Type name from @ClientModel (e.g., ‘issue’, ‘task’) |
inverse | string | No | Inverse property name on related model |
Example
@ClientModel('team')
export class Team extends Model {
@Property()
id = crypto.randomUUID()
// relatedType: 'issue', inverse: 'team'
@OneToMany('issue', 'team')
issues = new Collection<Issue>()
}
@ClientModel('issue')
export class Issue extends Model {
@Property()
id = crypto.randomUUID()
@Property()
teamId!: string // Required FK
@ManyToOne('team', 'teamId', 'issues')
team?: Team
}@OneToOne
Define a one-to-one relationship.
Signature
@OneToOne(relation: string, fk?: string)Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
relation | string | Yes | Inverse property name on related model |
fk | string | No | Foreign key field name on this model |
Example
@ClientModel('user')
export class User extends Model {
@Property()
id = crypto.randomUUID()
@OneToOne('user')
profile?: Profile
}
@ClientModel('profile')
export class Profile extends Model {
@Property()
id = crypto.randomUUID()
@Property()
userId!: string // Required FK
@OneToOne('profile', 'userId')
user?: User
}@ManyToMany
Define a many-to-many relationship.
Signature
@ManyToMany(relation: string)Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
relation | string | Yes | Inverse property name on related model |
Example
@ClientModel('issue')
export class Issue extends Model {
@Property()
id = crypto.randomUUID()
@ManyToMany('issues')
labels = new Collection<Label>()
}
@ClientModel('label')
export class Label extends Model {
@Property()
id = crypto.randomUUID()
@ManyToMany('labels')
issues = new Collection<Issue>()
}@Reference
Define a reference relationship (no inverse).
Signature
@Reference(fk?: string)Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
fk | string | No | Foreign key field name on this model |
Example
@ClientModel('task')
export class Task extends Model {
@Property()
id = crypto.randomUUID()
@Property()
assigneeId?: string
@Reference('assigneeId')
assignee?: User
}TypeScript Configuration
tsconfig.json
{
"compilerOptions": {
"experimentalDecorators": false, // TC39 decorators
"useDefineForClassFields": false, // CRITICAL!
"strict": true
}
}React Native
// babel.config.js
module.exports = {
plugins: [
['@babel/plugin-proposal-decorators', { version: '2023-11' }]
]
}Web (Vite)
import { vitePluginTypescriptTranspile } from 'vite-plugin-typescript-transpile'
export default defineConfig({
plugins: [
vitePluginTypescriptTranspile(), // FIRST
react() // SECOND
]
})Alternative: Decorator-Free API
If you can’t use decorators:
import { Model, registerModel } from '@gluonic/client'
export class User extends Model {
id: string = ''
name: string = ''
}
registerModel(User, {
type: 'user',
properties: {
id: { type: 'primitive' },
name: { type: 'primitive' }
}
})See Decorator-Free Models for details.
See Also
- Model - Base model class
- LazyReference - Lazy reference class
- LazyCollection - Lazy collection class
- Defining Models Guide - Complete guide