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,_inCommittrack the object's synchronization state. - Loading state:
_isLoadedindicates whether data has been received from the server.WaitLoaded()returns a Promise that resolves when the object is loaded. - Commit pipeline:
Commitrecursively 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 singleRelatedObject(1:1) or a map ofRelatedObject(1:N)._chldlist: An array mirror of_childrenfor efficient iteration over 1:N relations._related: Bidirectional relationship tracking between objects, using>(outgoing) and<(incoming) direction markers.
Key methods:
| Method | Purpose |
|---|---|
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 viaUpRefresh().OnChildChanged: Subject that emits the relation name when children are added/removed.Viewproperty: Lazy-initializedViewObjectusingWeakReffor 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)
Changegetter 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
| Type | TypeScript | Database | Notes |
|---|---|---|---|
string | string | null | VARCHAR | Direct mapping |
number | number | null | DECIMAL/INT | Parsed with parseFloat() |
boolean | boolean | null | '0'/'1' | String conversion for DB |
date | Date | null | DATETIME | MySQL format conversion |
file | urlfile | null | VARCHAR (path) | URL ↔ base64 conversion |
objid | string | null | INT (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:
- Validates the schema structure
- Creates a class extending
BaseClass<T> - Defines getter/setter for every field (with type validation)
- Defines getter/setter for 1:1 relations and
Add*/Del*methods for 1:N relations - 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:
- Updates
_children['categoryId']with the object reference - Creates a bidirectional relation entry (
>on this,<on category) - Updates
this.Info['categoryId']with the category'sobjid - 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
Proxygetter returns a JavaScriptProxythat mergesViewObjectproperties withDataObjectproperties, so consumers see a single unified interface - Reactive refresh:
OnViewRefreshandOnDataRefreshobservables notify the UI when data changes - Deferred rendering:
DoRefreshView()batches UI updates viaclockService.OnRefreshTickto avoid excessive re-renders - Subscription management:
_subscriptionsarray is cleaned up inOnDestroy()
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
- Client creates a new object:
new Product(null, dataService, {}). Theobjidisnull; a_uuidis auto-generated._toInsertis set totrue. - The object is added to the central
dataStorageviadata.store.AddObject(this). - Parent-child relations are established via
SetChild()/AddChild().
Commit to Server
DoCommit()callsCommitwhich recursively collects all changes via_CommitMap().- Each change includes:
relation(identity),entrynfo(field values + action),requires(dependencies). - Changes are sorted by dependency order.
- The sync service sends them to the server. The server responds with
objidassignments for new objects.
Server Update Received
OnChange(change)is called with server data.OnUpdate(info)compares with current state; if different, applies via_OnUpdate(info).- For new objects,
OnResolve()updates the storage key from_uuidtoobjid. DoRefresh()emits on_onRefreshand propagates up viaUpRefresh().
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:
- Transfers data from copy to original
- Reparents all child relationships
- Processes children recursively
- Removes the copy from storage
- Emits
OnOverwriteon the original