Skip to main content

Data Model

The data model in libs/upp-data/src/modules/model/ provides a schema-driven object system with hierarchical relationships, change tracking, deep copy, and server synchronization. This page covers the class hierarchy, schema definition, relationship management, the ViewObject pattern, and the storage lifecycle.

Class Hierarchy

classDiagram
BaseObject <|-- RelatedObject
RelatedObject <|-- DataObject
DataObject <|-- BaseClass~T~
BaseClass~T~ <|-- "Product, Ticket, Place, ..."
DataObject *-- ViewObject~T~

class BaseObject {
+table: string
+objid: string | null
+OnCommit: Observable
+OnUpdated: Observable
+IsLoaded: boolean
+IsVolatile: boolean
+DoCommit(force): Promise~boolean~
+ForceUpdate()
+ForceRefresh()
#OnUpdate(info): boolean
#_OnUpdate(info): void
}

class RelatedObject {
+_children: map
+_chldlist: map
+SetChild(target, child, relation)
+AddChild(target, child, relation)
+DelChild(target, child, relation)
+OnResolve()
}

class DataObject {
+OnRefresh: Observable
+OnChildChanged: Observable
+View: ViewObject | null
+ToInsert: boolean
+ToUpdate: boolean
+Copy(store): DataObject
+Overwrite(): DataObject
+patchValue(dst, prop, value): boolean
}

class ViewObject~T~ {
+Proxy: this & T
+OnViewRefresh: Observable
+OnDataRefresh: Observable
+doRegister(): void
+OnDestroy(): void
}

BaseObject

The abstract root of the hierarchy. Manages:

  • Identity: table (entity type), objid (server ID), _uuid (client-generated UUID before server assignment).
  • Change tracking: Flags _toInsert, _toUpdate, _toDelete, _toCommit, _inCommit track the object's synchronization state.
  • Loading state: _isLoaded indicates whether data has been received from the server. WaitLoaded() returns a Promise that resolves when the object is loaded.
  • Commit pipeline: Commit recursively collects all changes (this object + children), DoCommit() sends them to the sync service.
  • Volatility: Volatile objects (_isVolatile = true) are not persisted to the server — they exist only in-memory for temporary computations.

RelatedObject

Adds parent-child relationship management:

  • _children: A map where each key is a relation name. Values are either a single RelatedObject (1:1) or a map of RelatedObject (1:N).
  • _chldlist: An array mirror of _children for efficient iteration over 1:N relations.
  • _related: Bidirectional relationship tracking between objects, using > (outgoing) and < (incoming) direction markers.

Key methods:

MethodPurpose
SetChild(target, child, relation)Set a 1:1 child. Replaces any previous child for this relation.
AddChild(target, child, relation)Add a child to a 1:N relation.
DelChild(target, child, relation)Remove a child from a relation.
SetChildren(target, children, relation)Bulk-add children to a 1:N relation.
OnResolve()Called when the object receives a server-assigned objid. Updates all references.

DataObject

The practical base for all domain entities. Adds:

  • OnRefresh: Subject that emits when the object's data changes, propagating up through the object graph via UpRefresh().
  • OnChildChanged: Subject that emits the relation name when children are added/removed.
  • View property: Lazy-initialized ViewObject using WeakRef for garbage-collection-friendly caching.
  • Deep copy: Copy() / _Copy() creates a full clone of the object and its children. Overwrite() transfers changes from a copy back to the original.
  • patchValue(dst, property, value): Updates a property only if the value actually changed, and records the change for commit.

BaseClass<T>

A generic base generated from a schema definition. Handles:

  • Dynamic property definition based on schema fields
  • Automatic type conversion between client types and database types
  • Schema-based relationship wiring (1:1 and 1:N)
  • Change getter that serializes fields for server commit
  • _OnUpdate(info) that deserializes server data into typed properties

Schema Definition

Every domain entity is defined by a schema object:

const productSchema = {
name: 'PRODUCT',
fields: [
{ name: 'title', type: 'string' },
{ name: 'price', type: 'number' },
{ name: 'image', type: 'file' },
{ name: 'status', type: 'string', default: 'AC' },
{ name: 'categoryId', type: 'objid' }
],
relate: [
{
direction: '>',
target: 'CATEGORY',
by: 'category',
name: 'category',
reason: 'categoryId',
child: false,
class: Category
}
]
} as const;

Field Types

TypeTypeScriptDatabaseNotes
stringstring | nullVARCHARDirect mapping
numbernumber | nullDECIMAL/INTParsed with parseFloat()
booleanboolean | null'0'/'1'String conversion for DB
dateDate | nullDATETIMEMySQL format conversion
fileurlfile | nullVARCHAR (path)URL ↔ base64 conversion
objidstring | nullINT (FK)Can also hold DataObject reference

Creating a Class from Schema

const _ProductClass = CreateObject(productSchema);
if (!_ProductClass) {
throw new Error("Failed to create BaseClass for 'PRODUCT'");
}

export class Product extends _ProductClass {
constructor(objid: string | null, data: dataService, objoptions: ObjectOptions = {}) {
super(productSchema, objid, data, objoptions);
}

Copy(store: Array<DataObject> = []): Product {
return this._Copy(store) as Product;
}

get IsActive(): boolean {
return this.status === 'AC';
}
}

The CreateObject() factory:

  1. Validates the schema structure
  2. Creates a class extending BaseClass<T>
  3. Defines getter/setter for every field (with type validation)
  4. Defines getter/setter for 1:1 relations and Add*/Del* methods for 1:N relations
  5. Overridden properties get a __base__ prefix for internal access

Relationships

1:1 Relations (direction >)

A child-to-parent reference. The child stores the parent's objid in a field.

// Schema definition
{ direction: '>', target: 'CATEGORY', by: 'category', name: 'category', reason: 'categoryId', child: false }

This generates:

// Getter — returns the related Category or null
get category(): Category | null { ... }

// Setter — sets the relation and marks object for update
set category(value: Category | null) { ... }

When category is set, the framework automatically:

  1. Updates _children['categoryId'] with the object reference
  2. Creates a bidirectional relation entry (> on this, < on category)
  3. Updates this.Info['categoryId'] with the category's objid
  4. Marks the object as _toUpdate

1:N Relations (direction <)

A parent-to-children reference. The parent maintains a collection.

// Schema definition
{ direction: '<', target: 'TICKETPRODUCT', by: 'ticket', name: 'product', reason: 'products', child: true }

This generates:

// Getter — returns array of children
get products(): TicketProduct[] { ... }

// Add method
AddProduct(child: DataObject): void { ... }

// Delete method
DelProduct(child: DataObject): void { ... }

The _chldlist Pattern

For 1:N relations, _children[target] stores a map keyed by objref (either objid or _uuid). The _chldlist[target] is a flat array mirror updated on every add/delete, used for efficient iteration:

// Iterate over all products of a ticket
for (const product of ticket._chldlist['products']) {
console.log(product.title);
}

// Or via the generated getter
for (const product of ticket.products) {
console.log(product.title);
}

Bidirectional Tracking

Every relation is tracked in both directions via _related: Map<RelatedObject, Set<string>>:

  • > fieldName — outgoing: this object references the other
  • < fieldName — incoming: the other object references this

This bidirectional tracking enables OnResolve() to update all references when an object receives its server objid, and UpRefresh() to propagate change notifications upward through the graph.

ViewObject Pattern

ViewObject<T> wraps a DataObject to provide:

  • Computed properties derived from the underlying data
  • Proxy access: The Proxy getter returns a JavaScript Proxy that merges ViewObject properties with DataObject properties, so consumers see a single unified interface
  • Reactive refresh: OnViewRefresh and OnDataRefresh observables notify the UI when data changes
  • Deferred rendering: DoRefreshView() batches UI updates via clockService.OnRefreshTick to avoid excessive re-renders
  • Subscription management: _subscriptions array is cleaned up in OnDestroy()
class ProductView extends ViewObject<Product> {
constructor(product: Product, data: dataService) {
super(product, data);
}

doRegister(): void {
this._subscriptions.push(
this.dataobject.OnRefresh.subscribe(() => this.DoRefreshView())
);
}

get formattedPrice(): string {
return `${this.object.price?.toFixed(2) ?? '0.00'}`;
}
}

WeakRef Caching

DataObject.View uses WeakRef to cache the ViewObject:

get View(): ViewObject<DataObject> | null {
let view = this._objectviewref?.deref() ?? null;
if (view == null) {
view = this.viewProxy;
if (view) {
this._objectviewref = new WeakRef(view);
}
}
return view;
}

This ensures the ViewObject is garbage-collected when no component holds a reference, while avoiding recreation on every access.

Storage Lifecycle

Object Creation

  1. Client creates a new object: new Product(null, dataService, {}). The objid is null; a _uuid is auto-generated. _toInsert is set to true.
  2. The object is added to the central dataStorage via data.store.AddObject(this).
  3. Parent-child relations are established via SetChild() / AddChild().

Commit to Server

  1. DoCommit() calls Commit which recursively collects all changes via _CommitMap().
  2. Each change includes: relation (identity), entrynfo (field values + action), requires (dependencies).
  3. Changes are sorted by dependency order.
  4. The sync service sends them to the server. The server responds with objid assignments for new objects.

Server Update Received

  1. OnChange(change) is called with server data.
  2. OnUpdate(info) compares with current state; if different, applies via _OnUpdate(info).
  3. For new objects, OnResolve() updates the storage key from _uuid to objid.
  4. DoRefresh() emits on _onRefresh and propagates up via UpRefresh().

Deep Copy and Overwrite

Used for editing flows where changes should be applied atomically:

// Create a working copy
const copy = ticket.Copy();

// Modify the copy
copy.products[0].quantity = 5;

// Apply changes back to original
copy.Overwrite();

Overwrite() recursively:

  1. Transfers data from copy to original
  2. Reparents all child relationships
  3. Processes children recursively
  4. Removes the copy from storage
  5. Emits OnOverwrite on the original