This guide works through the basics of defining your models. We will consider an example of three models: Employee
, Task
and Team
. Each Employee
belongs to one Team
, and an Employee
may have multiple Task
s.
You probably also will need to check the default API format that is assumed for these examples.
The most straight forward way to implement the three models is as below.
import { Model, Field, ToOne, ToMany, Resource } from '@ngx-api-orm/core';
@Model()
export class Employee extends Resource {
@Field()
public id: number;
@Field()
public firstName: string;
@Field()
public lastName: string;
@ToOne(Team)
public team: ToOneRelation<Employee, Task>;
@ToMany(Task):
public tasks: ToManyRelation<Employee, Task>;
}
@Model()
export class Task extends Resource {
@Field()
public id: number;
@Field()
public title: string;
}
@Model()
export class Team extends Resource {
@Field()
public id: number;
@Field()
public teamName: string;
}
For such models, the following methods will available:
Team.fetch
, Team.collection
, Team.find
, Team.template
.team.save
, team.update
, team.delete
.employee.team.set
, employee.team.remove
, employee.team.sync
.employee.workItems.add
, employee.workItems.remove
See the API docs for detailed specifications of these functions. If you need to know which requests are made exactly and how to change this, see the extendability guide. Some more examples on how to use these methods are included below in the final part of this document.
A few things to keep in mind:
Resource
.@Field()
decorator to link a field to a plain property in the API response.id
field is required and also requires @Field()
. It can be a string
or number
.@ToOne(TRelated)
and @ToMany(TRelated)
to mark fields as to-one and to-many relationships respectively.ToOneRelation<THost, TRelated>
or ToManyRelation<THost, TRelated>
.Perhaps your model isn't perfectly matching your API response. Some easy fixes can be done in the model. The following model below works with this default response example with property mismatches.
@Model()
export class Employee extends Resource {
@Field()
public id: number;
@Field('givenName')
public firstName: string;
@Field('familyName')
public lastName: string;
@ToOne(Team)
public team: ToOneRelation<Employee, Task>;
@ToMany(Task, 'assignments'):
public workItems: ToManyRelation<Employee, Task>;
}
@Model({name: 'WorkItem'}) /* Putting 'work-items' has the same effect'. */
export class Task extends Resource {
@Field()
public id: number;
@Field()
public title: string;
}
@Model()
export class Team extends Resource {
@Field()
public id: number;
@Field()
public teamName: string;
}
A few things to notice:
@Field('givenName') public firstName: string;
will link employee.firstName
to employee.givenName
when parsing the API response.@ToMany(Task, 'assignments'): public workItems
will look for a assignments
property in the API response and treat it as the contents for, in this case, the to-many relationship with Task
.@Model({name: 'WorkItem'})
matches the localTask
model to a WorkItem
resource in the API. This results in for example:Task.fetch()
using the url /work-items/
.employeeWithId2.tasks.add( ... )
using the url /employees/2/work-items
.work-itemS
. If this were a to-one relation, the url would have singular, i.e. work-item
. You can add business logic to your class declarations.
@Model()
export class Employee extends Resource {
@Field()
public id: number;
...
...
public fullName: string;
/* Custom request using built-in fetch */
public async static fetchWithId(id: number): Promise<Employee> {
const options: HttpClientOptions = {
params: new HttpParams().set('id', id)
url: '/special-route-override'
}
const result = await this.fetch(options)
return result[0];
}
/* Initialization logic: it is better to leave the constructor alone. */
public onInit(rawInstance: RawInstanceTemplate<Employee>): void {
this.fullName = rawInstance.firstName + ' ' + rawInstance.lastName
}
public complain(): void {
console.log('My days are too long...');
}
}
Note that
constructor
unless you know what you're doing. Overloading it wrongly will result in it not working correctly with Angular's dependency injection. Any logic you'd normally put in the constructor can probably go into onInit
.There are two ways to create a new instance of your model.
/* First method, using a raw instance template */
const template = Employee.template(); // gets a RawInstanceTemplate<Employee> object
template.firstName = 'John';
template.lastName = 'Williams';
const localInstance = new Employee(employeeTemplate);
/* Second method, using no template */
const localInstance = new Employee();
localInstance.firstName = 'John';
localInstance.lastName = 'Williams';
/* This will not work */
const error = new Employee({firstName: 'John', lastName: 'Williams'})
team
and tasks
. When passing along a object template, it is required that all fields (the ones that are decorated with Model
, ToOne
and ToMany
are present.The local instance can now be saved.
localInstance.id === undefined // true
Employee.collection().length === 0 // true
const savedInstance = await localInstance.save()
savedInstance.id === undefined // false: it should get an id from the API.
Employee.collection().length === 0 // false: only instances that have an id are added to the internal collection.
Employee.collection().length === 1 // true.
Employee.collection()[0] === savedInstance // true
collection
gets the list of available local instances. Only the instances that have an id are included, i.e. unsaved local instances are not in this list.We can update and delete instances as follows.
/* updating */
console.log(employee.firstName) // 'John'
employee.firstName = 'Johnny'
await localInstance.update() // only sends the fields that are updated, in this case 'firstName'
employee.lastName = 'Bravo'
await localInstance.update() // only sends the fields that are updated, in this case 'lastName'
/* deleting */
await employee.delete();
Employee.collection().includes(employee) // false
In our example, our Employee
instance has a to-one relation with Team
and a to-many relation with Task
. The related instances of Team
and Task
are stored in ToOneRelation
and ToMany
containers, respectively. Because there is only one Team
instance related to an Employee
instance, the ToOneRelation
container is Object
-like, whereas ToManyRelation
container is Array
-like.
These are some operations with to-one relations involved.
/* to-one relations */
employee.team.length // undefined: it's not array like
const team = employee.team.instance // accessing the instance reference of class Team.
await employee.team.set( someOtherTeam )
employee.team.instance === team // false
employee.team.instance === someOtherTeam // true
await employee.team.remove()
employee.team.instance === null // true
/* also possible to directly set the instance reference */
employee.team.instance = someOtherTeam;
await employee.team.sync(); // effectively the same as .set(someOtherTeam)
employee.team.instance = null;
await employee.team.sync(); // effectively the same as .set(null)
ToOneRelation
container can be access as .instance
.Team.collection()
is invariant under the above operations..sync
.These are some operations with to-many relations involved.
/* to-many relations */
employee.team.length // defined, a number.
const team = employee.team[2] // accessing one of related instances of class Task
await employee.tasks.add( assignment )
employee.tasks.includes(assignment) // true
await employee.tasks.remove( oldAssignment)
employee.tasks.includes(oldAssignment) // false
/* WARNING: NOT YET IMPLEMENTED */
employee.tasks.pop(); // this is coming in the next release!
employee.tasks.push( ..assignments ); // this is coming in the next release!
await employee.tasks.sync(); // this is coming in the next release!
If you need to know or change to which HTTP verbs the actions add, remove, delete, update, save, fetch
are linked, now would be a good time to check out the extendability guide.