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 Tasks.
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.removeSee 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 // truecollection 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) // falseIn 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.