Backend vs Frontend Models
Models exist on both sides of an Oidis application. They share the same decorator system (@Entity, @Property) and base class (BaseModel<T>), but have different capabilities and serve different roles.
Side-by-Side Comparison
| Aspect | Backend (Models/) | Frontend (Gui/Models/) |
|---|---|---|
| Base class | BaseModel<T> from commons | BaseModel<T> from commons, or a hub-gui model (User, Group, …) |
@Entity | Required — maps to MongoDB collection | Required for DB-mapped models; omitted for FE-only models |
| DB access | Full (Find, Save, Delete, Update) on MongoDB | Cross-tier data flows through connectors. Local-only models with IndexedDbContext get full Find / Save / Refresh / Remove against the browser's IndexedDB — see IndexedDB. |
| Converters | Handle both DB reads and LCP receives | Primarily handle LCP deserialization (dataTranslation = true) |
| Server-only fields | noJSON: true keeps a field out of ToJSON() / ToInterface() | Not applicable |
| Location | source/.../Models/ | source/.../Gui/Models/ |
| Converters location | source/.../Models/Converters/ | source/.../Gui/Models/Converters/ |
Backend Models
Backend models are the source of truth. They define the database schema, handle persistence, and manage data integrity.
// source/typescript/Io/Oidis/YourProject/Models/ProductRecord.ts
import { Entity, Property } from "@io-oidis-commons/Io/Oidis/Commons/Primitives/Decorators.js";
import { BaseModel } from "@io-oidis-commons/Io/Oidis/Commons/Primitives/BaseModel.js";
@Entity("products")
export class ProductRecord extends BaseModel<ProductRecord> {
@Property({type: String})
public declare Code : string;
@Property({type: ProductGroup, required: false, converter: new ProductGroupToRefConverter()})
public declare Group : ProductGroup;
@Property({type: Number, required: false})
public declare TaxRate : number;
// Server-only: excluded from ToJSON() / ToInterface()
@Property({type: String, required: false, noJSON: true})
public declare InternalNote : string;
}
Backend models can:
- Query MongoDB:
ProductRecord.Find({...}),ProductRecord.FindById($id) - Persist data:
record.Save(),record.Update("TaxRate")($setof the current value of one property) - Soft delete:
record.Delete() - Validate:
record.Validate(),record.ValidateDeep() - Serialize for transport:
record.ToJSON()
Frontend Models
Frontend models that mirror a backend MongoDB collection do not talk to MongoDB directly — the data flows through connectors via LCP. The exception is models whose context is IndexedDbContext: those are persisted locally in the browser's IndexedDB and expose the same CRUD surface (Find, Save, Refresh, Remove) on the frontend. See IndexedDB for the open lifecycle and version handling.
// source/typescript/Io/Oidis/YourProject/Gui/Models/ProductRecord.ts
import { Entity, Property } from "@io-oidis-commons/Io/Oidis/Commons/Primitives/Decorators.js";
import { BaseModel } from "@io-oidis-commons/Io/Oidis/Commons/Primitives/BaseModel.js";
@Entity("products")
export class ProductRecord extends BaseModel<ProductRecord> {
@Property({type: String})
public declare Code : string;
@Property({type: ProductGroup, required: false, converter: new ProductGroupToRefConverter()})
public declare Group : ProductGroup;
@Property({type: Number, required: false})
public declare TaxRate : number;
// InternalNote is NOT here — it is excluded via noJSON on the backend
}
Key differences:
- Uses the same commons
BaseModel<T>, but lives inGui/Models/and is compiled for the browser - No
InternalNotefield — it was excluded from serialization on the backend - Converters use the
dataTranslationpattern (always receiving serialized objects) - No direct MongoDB access — backend data flows through
asyncInvoke()on connectors. Models withIndexedDbContextinstead persist locally in the browser (IndexedDB).
Frontend-Only Models
Some frontend models don't map to a database collection at all. They extend hub-gui base models and add project-specific fields:
// Frontend-only model — NO @Entity
import { User as HubUser } from "@io-oidis-hub-gui/.../Models/User.js";
export class User extends HubUser {
// Project-specific fields added on top of hub's User model
@Property({type: String, required: false})
public declare CompanyName : string;
@Property({type: String, required: false})
public declare ICO : string;
@Property({type: Boolean, required: false})
public declare IsProducer : boolean;
}
Rules for FE-only models:
- No
@Entity— They don't own a MongoDB collection - Extend hub-gui base — Inherit standard fields from the hub model
- Add project fields — Extend with application-specific properties
- Never persist directly — All CRUD operations go through backend connectors
Converter Differences
Backend Converter
Handles both contexts — DB reads (stored id string) and LCP receives (serialized object):
// Models/Converters/ProductGroupToRefConverter.ts
export class ProductGroupToRefConverter extends BaseToRefConverter {
public async from($value : any, $context : IContext) : Promise<ProductGroup> {
if ($context.dataTranslation) {
return ProductGroup.From($value); // LCP: reconstruct from object
}
return ProductGroup.FindById($value); // DB: load by id
}
}
Frontend Converter
Primarily handles LCP deserialization, but maintains the same pattern for consistency:
// Gui/Models/Converters/ProductGroupToRefConverter.ts
export class ProductGroupToRefConverter extends BaseToRefConverter {
public async from($value : any, $context : IContext) : Promise<ProductGroup> {
if ($context.dataTranslation) {
return ProductGroup.From($value); // LCP: reconstruct from object
}
return ProductGroup.FindById($value); // Hit only on the IndexedDB persistence path
}
}
The code looks nearly identical, but the frontend converter lives in Gui/Models/Converters/ and imports the frontend version of ProductGroup.
Data Flow
┌─────────────────────────────────────────────────────────────┐
│ Frontend │
│ │
│ Controller → Connector.asyncInvoke("ListRecords") │
│ │ │
│ ▼ LCP │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Backend │ │
│ │ │ │
│ │ @Extern ListRecords() │ │
│ │ → BE Model: ProductRecord.Find({...}) │ │
│ │ → MongoDB query (converter: id → instance) │ │
│ │ → record.ToJSON() (serialization) │ │
│ │ → Response (JSON) │ │
│ └────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ LCP response │
│ FE Connector receives serialized data │
│ → FE Model converter (dataTranslation = true) │
│ → Model.From(data) → typed FE model instance │
│ → Controller updates UI │
└─────────────────────────────────────────────────────────────┘
When to Skip Backend Models
For simple data entities (e.g. registries, lookup tables), a dedicated backend model is not required. The backend DataManager can import and use the frontend model directly, because both sides share the same @Entity and @Property decorator system and the same BaseModel<T> base class.
When a backend model IS needed:
- The model extends a complex hub base class (e.g.
UserextendsHub.DAO.Models.User) - Server-only fields are required (
noJSON: true) - Custom backend-specific logic (hooks, computed properties, aggregation helpers)
When a backend model is NOT needed:
- Simple CRUD entities with no server-only fields
- All properties are shared between frontend and backend
- The backend only performs basic
Find,SyncWith,Save,Removeoperations
In this case, define the model only in Gui/Models/ and import it in the backend DataManager:
// Utils/DataManager.ts — backend imports FE model directly
import { Company } from "../Gui/Models/Company.js";
@Extern()
public async SaveCompany($data : Company) : Promise<void> {
let model : Company = await Company.FindById($data.Id);
if (ObjectValidator.IsEmptyOrNull(model)) {
delete $data.Id;
model = new Company();
}
model.SyncWith($data);
await model.Save();
}
This approach simplifies maintenance — there is only one model definition to keep in sync, and SyncWith works seamlessly because the same @Property declarations are used on both sides.
SyncWith($data) copies all properties present in $data, including fields the user must not control (owner, organization,
flags). The @Extern method is callable by every user allowed to call it, so mark such fields protected: true or assign
the permitted fields explicitly when the input comes from a client — see
Protected Properties.
The User model is a notable exception. It extends the Hub's User base class which contains complex authentication, session, and permission logic. Always create a backend User model in Models/User.ts that extends @io-oidis-hub/...Hub/DAO/Models/User.js.
Checklist: Adding a New Model
- Decide if a backend model is needed — see criteria above; most simple entities only need a FE model
- Frontend model —
@Entity,@Propertydeclarations inGui/Models/, converters inGui/Models/Converters/ - Backend model (if needed) — Mirror in
Models/, addnoJSONfor sensitive fields andprotectedfor fields only BE code may write, converters inModels/Converters/ - Backend manager — Import the model (FE or BE) and implement CRUD with
@Extern()methods - ForceCompile — Add import for the FE model; Builder auto-adds controller references
- Connector — Add typed methods in the GUI connector wrapping
asyncInvoke()
See also: Models Overview | Converters | Validators | IndexedDB