Skip to Content

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

ParameterTypeDescription
typeNamestringType 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?: string

Description: 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?: string

Description: Foreign key property name.

Example:

// Internally set by @ManyToOne('posts', 'authorId') // fk = 'authorId'

User-facing: No (set by relationship decorators)


relatedType (Internal)

relatedType?: string

Description: 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?: boolean

Default: 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?: number

validate

validate?: (value: any) => boolean

Description: 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 fails

computed

computed?: boolean

Default: 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 save

defaultProvider (Current v1.0)

defaultProvider?: (ctx: { t: string, id: string | null }) => any

Description: Function to generate default value.

Context:

  • t - Type name (e.g., ‘task’, ‘user’)
  • id - Model ID (null during 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?: () => any

Description: 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 = 0

defaultOn 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

ParameterTypeRequiredDescription
relatedTypestringYesType name from @ClientModel (e.g., ‘user’, ‘task’)
fkstringNoForeign key field name on this model
inversestringNoInverse 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

ParameterTypeRequiredDescription
relatedTypestringYesType name from @ClientModel (e.g., ‘issue’, ‘task’)
inversestringNoInverse 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

ParameterTypeRequiredDescription
relationstringYesInverse property name on related model
fkstringNoForeign 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

ParameterTypeRequiredDescription
relationstringYesInverse 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

ParameterTypeRequiredDescription
fkstringNoForeign 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

Last updated on