model.constant.ts

Defines the data shape (schema) inside the database. It ensures consistency in the data storage form and defines indexing for queries.
The constant file unifies the data rules of schema - api - fetch by being used by both the backend and frontend.
In the constant, the following data shapes should be defined in order.

Core Functions of Schema Definition

InputDroneInput

Data required to create a schema

ObjectDroneObject

Schema completed when data is created

LightLightDrone

Lightweight schema and class-based form used when multiple data is queried

FullDrone

Class-based complete form of data

InsightDroneInsight

Statistical data that can be extracted when data is queried

SummaryDroneSummary

Statistical data extracted periodically when data is monitored

querydroneQuery

Data that defines the method of querying and the query statement in advance

sortdroneSort

Sort key-sort value used when data is sorted

Importance of Schema Structure

These data shapes are used to create the domain and the overall schema shape. Finally, it reflects the data shape of mongoDB schema, statistics model, GraphQL, and RestAPI, and defines the find and sort form when performing a query, allowing you to design the entire schema structure in a single file.
Schema Structure

1. Model.Input

Overview
Defines the shape required for data creation in the decorated class form.

As shown in the example below, if you create a domain called drone and need name and websocket URI to create drone data, it can be declared as follows.
@Model.Input('DroneInput')
export class DroneInput {
  @Field.Prop(() => String) // String, Int, Boolean, Float, JSON...
  name: string;
  @Field.Prop(() => String, { default: 'ws://10.10.150.10:9091' })
  wsUri: string;
}
Usage Case
Fields declared as above can be used type-safely at the upper level as follows.

Document Creation in drone.document.ts

new this.Drone({'{ name: "myDrone", wsUri: "ws://10.10.150.10:9091" }'});

Data Creation Logic in drone.service.ts

this.createDrone({'{ name: "myDrone", wsUri: "ws://10.10.150.10:9091" }'});

Data Creation API Call in drone.store.ts

fetch.createDrone({'{ name: "myDrone", wsUri: "ws://10.10.150.10:9091" }'});
fetch.updateDrone("droneId",{'{ name: "myDrone", wsUri: "ws://10.10.150.10:9091" }'});

Usage in Drone.Template.ts & Drone.Util.ts

// Template.ts
st.do.setNameOnDrone("myDrone"); // set state of form for create or update
const droneForm = st.use.droneForm(); // droneForm.name
// Util.ts
const drone = st.use.drone(); // drone.name

2. Model.Object

Overview
Defines fields that are automatically created after data is created, and these fields are not modified through Model.Input and create or update.
Enter fields that are modified through separate API and service logic.

For example, status is an object field that is automatically added to all domains as the top-level state of data.
export const droneStatuses = ['active', 'offline', 'inactive'] as const;
export type DroneStatus = (typeof droneStatuses)[number];
@Model.Object('DroneObject')
export class DroneObject extends BaseModel(DroneInput) {
  @Field.Prop(() => String, { enum: droneStatuses, default: 'offline' })
  status: DroneStatus;
}
Usage Case
Fields declared as above can be used type-safely at the upper level as follows.

Document Lookup in drone.document.ts

const drone = await this.Drone.pickById("droneId"); // drone.status

Data Lookup in drone.service.ts

const drone = await this.getDrone("droneId"); // drone.status

Data Lookup in drone.store.ts

const drone = await fetch.drone("droneId"); // drone.status

Data Lookup in Drone.Util.ts

const drone = st.use.drone(); // drone.status

3. Model.Light

Overview
Light model is a lightweight version of the object by picking field names from the existing object without declaring fields through Field.Prop, and it helps you use data more conveniently by declaring convenience functions.

In addition, it helps you use data more conveniently by declaring convenience functions.

Performance Optimization Benefits

In the case of a list query that fetches multiple data at once, the number of data fields required in the UI is less, and to prevent overfetch, it is declared with light model by selecting only the necessary fields.
@Model.Light('LightDrone')
export class LightDrone extends Light(DroneObject, [
 'name',
 'status',
] as const) {
 isConnected() {
   return this.status !== 'offline';
 }
}
Usage Case
Fields declared as above can be used type-safely at the upper level as follows.

Data Lookup in drone.store.ts

const droneList = await fetch.droneList({'{ status: "active" }'});
// drone.name is possible, but drone.wsUri is not possible

Data Lookup in Drone.Util.ts

const droneMap = st.use.droneMap(); // Map<string, LightDrone>

4. Model.Full

full 모델은 Field.Prop을 통한 필드를 선언하지 않고, 기존의 object에서 편의함수 선언을 통해 도메인 데이터를 손쉽게 사용할 수 있습니다.
@Model.Full('Drone')
export class Drone extends Full(DroneObject, LightDrone) {
  static isDronesAllConnected(droneList: LightDrone[]) {
    return droneList.every((drone) => drone.isConnected());
  }
  isAvailable() {
    return this.isConnected() && this.wsUri.startsWith("ws://");
  }
}
Usage Case
Fields declared as above can be used type-safely at the upper level as follows.

Data Lookup in drone.store.ts

const drone = await fetch.drone("droneId"); // drone.isAvailable()

Data Lookup in Drone.Util.ts

const droneMap = st.use.droneMap(); // Map<string, LightDrone>
const droneList = [...droneMap.values()];
const isAllConnected = cnst.Drone.isDroneAllConnected(droneList);

5. Model.Insight

Insight model defines a schema that can query the total statistics of documents matching the query when the query is executed.
@Model.Insight('DroneInsight')
export class DroneInsight {
 @Field.Prop(() => Int, {"{ default: 0, accumulate: { $sum: 1 } }"})
 count: number;
}
Usage Case
Fields declared as above can be used type-safely at the upper level as follows.

Data Lookup in drone.service.ts

const droneInsight = await this.insight({"{ ...query }"}); // droneInsight.count

Data Lookup in drone.store.ts

const droneInsight = await fetch.droneInsight({"{ ...query }"}); // droneInsight.count

Data Lookup in Drone.Util.ts

const drone = st.use.droneInsight(); // droneInsight.count

6. Model.Summary

Summary model collects the total statistics of data every hour at 0 minutes 0 seconds.
@Model.Summary('DroneSummary')
export class DroneSummary {
 @Field.Prop(() => Int, {"{ min: 0, default: 0, query: { status: { $ne: 'inactive' } } }"})
 totalDrone: number;
}
Usage Case
Fields declared as above can be used type-safely at the upper level as follows.

Data Lookup in summary.service.ts

const summary = await this.getActiveSummary(); // summary.totalDrone

Data Lookup in summary.store.ts

const summary = await fetch.getActiveSummary(); // summary.totalDrone

Data Lookup in Summary.Util.ts

const summary = st.use.summary(); // summary.totalDrone

7. Model.Query

modelQuery is a detailed query statement that defines how to query the completed Full/Light/Insight model.
export const droneQuery = {"{
 ...baseQueries,
 byName: (name: string) => ({"{ name }"}),
}
Usage Case
Fields declared as above can be used type-safely at the upper level as follows.

Data Lookup in drone.document.ts

const drone = await this.findByName("myDrone"); // db.Drone | null
const drone = await this.findIdByName("myDrone"); // string | null
const drone = await this.pickByName("myDrone"); // db.Drone
const drone = await this.pickIdByName("myDrone"); // string
const drones = await this.listByName("myDrone"); // db.Drone[]
const droneIds = await this.listByName("myDrone"); // string[]
const droneCount = await this.countByName("myDrone"); // number
const droneExists = await this.existsByName("myDrone"); // string | null
const droneInsight = await this.insightByName("myDrone"); // db.DroneInsight

Data Lookup in drone.service.ts

const drone = await this.findByName("myDrone"); // db.Drone | null
const drone = await this.findIdByName("myDrone"); // string | null
const drone = await this.pickByName("myDrone"); // db.Drone
const drone = await this.pickIdByName("myDrone"); // string
const drones = await this.listByName("myDrone"); // db.Drone[]
const droneIds = await this.listByName("myDrone"); // string[]
const droneCount = await this.countByName("myDrone"); // number
const droneExists = await this.existsByName("myDrone"); // string | null
const droneInsight = await this.insightByName("myDrone"); // db.DroneInsight

8. Model.Sort

modelSort is a detailed sort statement that defines how to sort the completed Full/Light/Insight model.
export const droneSort = {"{
 ...baseSorts,
 alphabetical: {"{ name: 1 }"},
}
Usage Case
Fields declared as above can be used type-safely at the upper level as follows.

Data Lookup in drone.document.ts

const drone = await this.findByName("myDrone", { sort: "alphabetical" });
const drone = await this.findIdByName("myDrone", { sort: "alphabetical" });
const drone = await this.pickByName("myDrone", { sort: "alphabetical" });
const drone = await this.pickIdByName("myDrone", { sort: "alphabetical" });
const drones = await this.listByName("myDrone", { sort: "alphabetical" });
const droneIds = await this.listByName("myDrone", { sort: "alphabetical" });

Data Lookup in drone.service.ts

const drone = await this.findByName("myDrone", { sort: "alphabetical" });
const drone = await this.findIdByName("myDrone", { sort: "alphabetical" });
const drone = await this.pickByName("myDrone", { sort: "alphabetical" });
const drone = await this.pickIdByName("myDrone", { sort: "alphabetical" });
const drones = await this.listByName("myDrone", { sort: "alphabetical" });
const droneIds = await this.listByName("myDrone", { sort: "alphabetical" });

9.1. Advanced - Relation

Designing a schema follows the basic principles of MongoDB schema design.
9.1.1. Scalar Embedded style
If you want to add detailed information without any special scaling or purpose, you can declare a Scalar model and embed the schema as a field.
@Model.Scalar('DronePhysicalState')
export class DronePhysicalState {
 @Field.Prop(() => [Float], { default: [0, 0, 0] })
 rpy: [number, number, number];
 @Field.Prop(() => [Float], { default: [0, 0, 0] })
 position: [number, number, number];
 @Field.Prop(() => [Float], { default: [0, 0, 0] })
 velocity: [number, number, number];
}
import { DronePhysicalState } from '../_lfg/lfg.constant';
// ... //
@Model.Object('DroneObject')
export class DroneObject extends BaseModel(DroneInput) {
 @Field.Prop(() => DronePhysicalState)
 physicalState: DronePhysicalState;
}
9.1.2. Reference ID style
For relationships that require separate collection separation, it is possible to declare fields by referencing ObjectID.
@Model.Input('MissionInput')
export class MissionInput {
 @Field.Prop(() => ID, {"{ ref: 'drone' }"})
 drone: string;
}
9.1.3. Resolved Reference style
For relationships that require separate collection separation, it is possible to declare fields by referencing ObjectID.
import { LightMission } from '../mission/mission.constant';
// ... //
@Model.Object('DroneObject')
export class DroneObject extends BaseModel(DroneInput) {
 @Field.Prop(() => LightMission, { nullable: true })
 mission: LightMission | null;
}
Constant file imports can cause circular references. It is necessary to design import as a one-way reference structure, and it is not possible to import from index.ts or barrel file.
import {"{ LightMission }"} from "../cnst_";
Correct Example
import {"{ LightMission }"} from "../mission/mission.constant";

9.2. Advanced - Field Types

Advanced Field Types
9.2.1. Field.Prop
It is a field that can be used in both backend and frontend as a general schema field declaration.
9.2.2. Field.Hidden
It is a field that can be used freely in the backend, but is restricted in the frontend due to security issues.
9.2.3. Field.Secret
It is a field that is declared when a field that is stored once in the database and has a long query/change cycle or contains sensitive security information is declared. In general, all queries are restricted in the query.
9.2.4. Field.Resolve
It is a field that is used when there is a need to pre-calculate information needed for display on the frontend without being stored in the database.

9.3. Advanced - Field Options

Various options that can be used together with Field.Prop, defining the characteristics and validation of the field.
nullable:boolean:false

Field value required, if nullable: true, data can be saved even if it is not present

@Field.Prop(()=> String, { default: "untitled" }) title: string;
ref:string:-

Option for reference ID field, can specify reference collection name(e.g. drone)

@Field.Prop(()=> ID, { ref: "drone" }) drone: string;
refPath:string:-

Option for reference ID field, if reference collection is variable, can specify reference collection name by referring to other field values

@Field.Prop(()=> ID, { refPath: "rootType" }) root: string; @Field.Prop(()=> String, { enum: ["drone", "mission"] }) rootType: string;
default:any:-

Default value set when no value is entered, must be set for all fields except Model.Input

@Field.Prop(()=> String, { default: "untitled" }) title: string;
type:email | password | url:-

Preset setting, default validation or example value is set

@Field.Prop(()=> String, { type: "email" }) email: string;
immutable:boolean:false

Option that cannot be changed after document creation, after creation, changes to the field are not possible

@Field.Prop(()=> ID, { immutable: true }) creator: string;
min:number:-

If the field's model is Int or Float, the minimum value can be limited in the save step

@Field.Prop(()=> Int, { min: 0 }) progress: number;
max:number:-

If the field's model is Int or Float, the maximum value can be limited in the save step

@Field.Prop(()=> Float, { min: 0, max: 1 }) ratio: number;
enum:any[]:-

If the field is enum, the const array can be specified to limit values other than the save step

@Field.Prop(()=> String, { enum: ["active", "inactive"] }) status: "active" | "inactive";
minlength:number:-

If the field's model is String, the minimum length of the string can be limited

@Field.Prop(()=> String, { minlength: 2 }) name: string;
maxlength:number:-

If the field's model is String, the maximum length of the string can be limited

@Field.Prop(()=> String, { maxlength: 30 }) title: string;
query:object:-

Used in Model.Summary fields, the value obtained by performing the query is saved as the field's value

@Field.Prop(() => Int, { min: 0, default: 0, query: { status: { $ne: 'inactive' } } }) totalDrone: number;
accumulate:object:-

Used in Model.Insight fields, the value calculated in the aggregation step is saved as the field's value

@Field.Prop(() => Int, { default: 0, accumulate: { $sum: 1 } }) count: number;
example:any:-

Can set the value that appears as an example when creating API Docs

@Field.Prop(()=> String, { example: "contact@akanjs.com" }) email: string;
of:any:-

If the field's model is Map, the key value of the Map is string, and the model specified by of is set as the value type of the schema

@Field.Prop(()=> Map, { of: Date }) readAts: Map<string, Dayjs>;
validate:any(value, model) => boolean:-

Can check validation in the save step.(unstable)

@Field.Prop(()=> String, { validate: (value)=> value.includes("@") }) email: string;
Released under the MIT License
Official Akan.js Consulting onAkansoft
Copyright © 2025 Akan.js. All rights reserved.
System managed bybassman