Repository navigation
Home
Welcome to the documentation of the custom ORM that is used for the Huddle 3.0 client.
The Wikipedia definition of an ORM:
Object–relational mapping (ORM) in computer science is a technique for converting data between type systems using object-oriented programming languages. This creates, in effect, a "virtual object database" that can be used from within the programming language.
Loose translated to fit our use case:
We use the ORM to fetch data from an API, (automatically) transform it into classes predefined by us, and then use that shared data throughout our application using useful retrieval methods provided by the ORM.
We will look at a few problems that are often encountered when creating a large Vue production application, and how an ORM can help solves these issues. Do note that all these issues can be solved using different methods and do not necessarily require an ORM, but it definitely makes life a lot easier.
Large applications, especially client side rendered ones, tend to make lots of different network calls. ORMs usually provide an interface to handle network calls, minimising duplicate code and providing error handling. After making a network call, the response usually needs to be parsed to a predefined model to allow for proper typing. ORMs also provide features to automate this process, further minimising the required code even more. Read more about making API calls using the ORM here.
Each model (i.e. a User or a Post) needs to have it data saved somewhere. There are countless of ways of doing this, for example fetching and then storing the data in a component itself, or creating (Pinia) stores for each model and creating some sort of array to store the data in.
This in itself means duplicate code, although it admittedly does provide a more transparent way for developers to see where data is stored compared to an efficiently written ORM.
However, one of the key features and real powers of an ORM lie in the reusable functions to retrieve and manipulate data stored in the ORM. For example, instead of having to write a function yourself that returns a filtered and ordered collection of data, you use the simple to use functions that the ORM provides to handle these cases. This provides multiple benefits, such as less duplicate code, less complexity for the developer and only one place where the logic of these functions resides, making it easy to change or add functions to the ORM when need be. Read more about retrieval and manipulation functions here.
An ORM also provides an opinionated but structured way of placing code. For example, instead of building "queries" to retrieve data in your Vue components or store getters, an ORM encourages you to use a repository for these kinds of actions and services for defining specific network calls. This separates the logic (data handling or network calls) from your views (the components). More on repositories and services here.
Depending on the nature of the application, using shared data within the application can be of great value for minimising the amount of network calls made and thus improving the speed of the application. Our client often shares data between components and pages, and using an ORM makes this very, very easy. With Pinia, you'd have to import a specific store and calling useStore on that store inside of a computed before being able to access its data and functions. The ORM provides an even easier way of doing this, simple by importing the model that you need and.. well, that's it.
At the time of writing, the battle really was between using Pinia ORM and writing our own ORM. There are multiple reasons for writing our own ORM, and the list goes as follows:
- Speed - Because we only write the features that we need, we greatly reduce the size of the library thus improving loading times.
- Maintainability - We are not impacted by (breaking) changes from an external library.
- Core - The ORM resides at the core of the application, and having complete control over this part is essential and should not be delegated and trusted to an external library.
- Flexibility - We can easily extend and change the working of the ORM whenever we deicide to.
- Understandable - Because the code is small and is written using our own standards, we ensure that everybody can always understand and extend the code.
- Persistence - We will not be impacted in the scenario where the external library is no longer continued and supported (especially in future versions of Vue).
- Pure TypeScript - Our own ORM is not linked to Pinia or dependent on external libraries (other than Vue), and is written in pure TypeScript.
- Compared to Pinia ORM - Pinia ORM does not contain functions to make API calls out of the box and need to be written ourselves.
The reasons can be summed up in one sentence:
The core of our application should be managed by ourselves, independent of external libraries and factors, and should be fast, understandable and easily extended or changed whenever we want to.
First you'll have to add the package to your project using NPM or Yarn.
npm
npm i huddle-record
or
Yarn
yarn add huddle-record
Then you'll have to import the setupRecord function to setup the API.
Important: this step is only necessary if you are planning to use services.
main.ts
import axios from "axios";
import { setupRecord } from "huddle-record/lib/src/main";
setupRecord({
api: {
axiosInstance: axios.create({
baseURL: "https://my-json-server.typicode.com/typicode/demo/", // the default URL API calls will be made to
},
source: "", // optional, the default resource key your elements may be nested under in the response body
debug: { // optional, to log requests and responses
logRequest: false;
logRequestHeaders: false;
logRequestBody: false;
logResponseHeaders: false;
logResponseBody: false;
logResponseStatus: false;
},
}
});
API debugging can be enabled to automatically log network calls that are being made.
The ORM consists of three parts: the models, the repositories and the services.
Models are basic TypeScript classes that define the attributes of a model (i.e. a User).
Repositories contain the data for each model within your application (imagine a database for your browser), and provides reusable functions to retrieve and manipulate data.
Services provide reusable functions to make network calls and (automatically) parsing the response data to models and storing those in the repositories.
Each model require one repository and optionally a service if you are planning on using the build-in API of Record.
A model should reside in the "models" directory and should use the same name as its backend counterpart. They are written as normal TypeScript classes and should instantiate a repository and a service. Note: a service is only needed if network calls are going to be made on the model.
user.ts
import { reactive } from "vue";
import Model from "@/orm/model";
import UserService from "@/services/userService";
import UserRepository from "@/repositories/userRepository";
export default class User extends Model {
constructor(map: UserInterface) {
super(User);
this.id = map["id"];
this.firstname = map["firstname"];
this.lastname = map["lastname"];
this.description = map["description"];
}
// @ts-ignore
static store: UserRepository<User> = reactive(
new UserRepository<User>(),
this
);
static api: UserService<User> = new UserService<User>(this, this.store, {
route: 'users',
});
}
interface UserInterface {
id?: number;
firstname?: string;
lastname?: string;
description?: string;
}
export default interface User extends UserInterface {}
The interface should define the fields of a User and the constructor should reference these fields.
The static api can be omitted if you are not planning on using Services. If you are however using Services, a route parameter can be provided to specify what route API calls on this model should be made to. Additionally, a source parameter can also be provided to specify what the resource key your elements may be nested under in the response body (default '').
Accessors and mutators can be added to a model to create virtual properties.
For this example we want to combine the firstname and lastname for a User, and to keep things as DRY as possible we want to write this only once. We would write this accessor as follows:
user.ts
export default class User extends Model {
constructor(map: UserInterface) {
super(User);
this.id = map["id"];
this.firstname = map["firstname"];
this.lastname = map["lastname"];
this.description = map["description"];
}
get fullName() {
return `${this.firstname} ${this.lastname}`;
}
}
This can then be called like:
const user = User.store.first();
console.log(user.fullName); // Dolores Umbridge
You might also want to prefix someones lastname with (for example) Mr or Mrs.
We could do this like:
export default class User extends Model {
constructor(map: UserInterface) {
super(User);
this.id = map["id"];
this.firstname = map["firstname"];
this.lastname = map["lastname"];
this.description = map["description"];
}
prefix(prefix: string) {
return `${prefix} ${this.lastname}`;
}
}
This can then be called like:
const user = User.store.first();
console.log(user.prefix('Mrs'); // Mrs Umbridge
Mutators can be used to manipulate the data of a model. For example, we might want our lastname to always be in capital letters.
We could achieve this by writing the following code:
export default class User extends Model {
_lastname: string;
constructor(map: UserInterface) {
super(User);
this.id = map["id"];
this.firstname = map["firstname"];
this.lastname = map["lastname"];
this.description = map["description"];
}
// set lastname to uppercase
set lastname(value: string) {
this._lastname = value.toUpperCase();
}
// get lastname
get lastname(): string {
return this._lastname;
}
}
Let's say a User has a Profile, where Profile is an actual model instead of just a simple object.
To add this to your model, use:
user.ts
import Profile from "@/models/Profile";
export default class User extends Model {
constructor(map: UserInterface) {
...
this.profile = map["profile"] ? new Profile(map["profile"]) : null;
}
...
}
interface UserInterface {
...
profile?: Profile | null;
}
export default interface User extends UserInterface {}
Expanding on the documentation above in Fields that are another model, this is how you can define that a field inside your model is an array of some other model.
For this example, we will assume that a User has multiple hobbies, that are an array of the Hobby model:
user.ts
import Hobby from "@/models/Hobby";
export default class User extends Model {
constructor(map: UserInterface) {
...
this.hobbies = map["hobbies"]
? map["hobbies"].map(
(hobby) => new Hobby(hobby)
) : null;
}
...
}
interface UserInterface {
...
hobbies?: Hobby[] | null;
}
export default interface User extends UserInterface {}
A repository should reside in the "repositories" directory and should use the naming convention "{ModelName}Repository" (i.e. UserRepository).
userRepository.ts
import Repository from "@/libs/orm/repository";
import User from "../user";
export default class UserRepository<T extends User> extends Repository<T> {
// here goes your custom functions
}
Returns: instance|null
Use the find function to retrieve a single instance from the store
User.store.find(1);
Returns the first user that has an id of 1
Returns: instance[]
Use the all function to retrieve all the instances in the store
User.store.all();
Returns all instances
Returns: instance|null
Use the first function to retrieve the first instance in the store
User.store.first();
Returns the first user
Returns: instance[]|null, parameters: amount = 1
Use the take function to retrieve the first n amount of items from the store
User.store.take(5);
Returns the first five users
Returns: instance|null
Use the last function to retrieve the last instance in the store
User.store.last();
Returns the last user
Returns: number
Use the count function to retrieve the amount of instances in the store
User.store.count();
Return the amount of instances in the store as integer
Returns: boolean
Use the exists function to see whether a instance exists in the stores based on an id
User.store.exists(1);
Return whether a User with id 1 exists in the store
Queries are chainable functions that allow you to get a finely grained set of data that suits your needs.
Returns: instance[]
Use the where function to retrieve instances from the store based on a condition
User.store.query()
.where('firstname', 'John')
.get();
Returns all users with field firstname 'John'
By default it will use QueryOperator.EQUAL (===) to match the key to the value.
Instead, other logical operators can also be used. A full list of possible operators are:
import QueryOperator from "src/orm/enums/queryOperator";
User.store.query().where('firstname', QueryOperator.EQUAL, 'John').get();
User.store.query().where('firstname', QueryOperator.NOT_EQUAL, 'John').get();
User.store.query().where('age', QueryOperator.GREATER_THAN, '17').get();
User.store.query().where('age', QueryOperator.GREATER_THAN_OR_EQUAL, '18').get();
User.store.query().where('age', QueryOperator.LESS_THAN, '18').get();
User.store.query().where('age', QueryOperator.LESS_THAN_OR_EQUAL, '17').get();
User.store.query().where('lastname', QueryOperator.IN, '[Jackson, Obel]').get();
User.store.query().where('lastname', QueryOperator.NOT_IN', '[Swift, Bowie]').get();
User.store.query().where('gender', QueryOperator.CONTAINS, 'M').get();
User.store.query().where('gender', QueryOperator.DOES_NOT_CONTAIN, 'F').get();
User.store.query().where('image', QueryOperator.IS_NULL).get();
User.store.query().where('image', QueryOperator.IS_NOT_NULL).get();
Returns: instance[]
Need a more advanced where that is (optionally) based on more than one field?
User.store.query()
.where((user: User) => {
return user.firstname === 'John' && user.lastname === 'Cena';
})
.get();
Returns all users with field firstname 'John' and field lastname 'Cena'
Returns: instance[]
You can also add or conditions to your wheres, allowing you to receive a record if it matches X or Y.
User.store.query()
.where('id', 1')
.orWhere('firstname', 'Johnny')
.get();
Returns all users where id is 1 or firstname is Johnny
Returns: instance[]
You can group where and orWhere functions together to make more complex queries:
import Query from "huddle-record/lib/src/orm/query";
User.store.query()
.where('age', 18)
.where((user: User, query: Query<User>) => {
query.where('role', 'admin').orWhere('role', 'moderator');
})
.get();
Returns all users that are aged 18 and where their role is admin OR moderator
Returns: instance[]
Use the orderBy function to order a a set of data to a specific key and direction.
User.store.query()
.orderBy('firstname')
.get();
Returns "Adam", "Bernard", "Charlie"
An order can be specified in the second argument by passing asc or desc:
User.store.query()
.orderBy('firstname', 'desc')
.get();
Returns "Charlie", "Bernard", "Adam"
Functions can also be passed to the orderBy function, for example:
User.store.query()
.orderBy((user: User) => user.profile.city)
.get();
Returns users ordered by the city inside their profile data
Other functions instead of get that can be used on .query are:
first() returns: instance|null
last() returns: instance|null
limit() returns: instance[]|null, parameters: amount = 1
exists() returns: boolean
count() returns: number
Queries also support datasets, for example:
User.store.query({dataset: 'friends'}).where('lastname', 'Jones').get();
To see what "dataset" is about, see Working with different datasets.
To create a new instance of a model, the save function can be called:
Returns: instance|instance[]|null
User.store.save({
id: 1,
firstname: "Leroy",
});
This returns a new User instance and automatically adds it to the store
Alternatively, a User can be manually created and passed into the save function:
UserCreateManually.vue
const user = new User({
id: 1,
firstname: "Leroy",
});
User.store.save(user);
This automatically adds a User into the store
Additional config can be passed to the save function:
UserCreateWithConfig.vue
User.store.save({
id: 1,
firstname: "Leroy",
}, {
save: true, // automatically persists data to the store
replace: true, // automatically overwrites existing data in the store by making a new model and replacing the existing one
update: false, // overwrite only the data supplied, updating the existing model instead of replacing the existing one
dataset: 'all', // the key to specify the dataset
});
To see what "dataset" is about, see Working with different datasets.
Existing data can be updated using the update method
Returns: instance|null
UserUpdate.vue
User.store.update(4, {
title: "Daemon",
});
This updates the User with id 4, setting the title to "Daemon"
A dataset can also be applied: UserUpdateWithConfig.vue
User.store.update(4, {
firstname: "Daemon",
}, {
dataset: 'all', // the key to specify the dataset
});
To see what "dataset" is about, see Working with different datasets.
Updating data can also be combined with where functions:
Returns: instance|instance[]|null
UpdatingUsersUsingWhere.vue
User.store.query().where('lastname', QueryOperator.IN, ['Truss', 'Johnson']).update({
minister: false,
});
This updates all Users that have a lastname of Truss or Johnson, setting the minister boolean to false
You can delete data from the store in a myriad of ways.
Returns: void
Deleting data from the store by passing in an id:
User.store.delete(123);
Will delete the User with id 123
Returns: void
Deleting data from the store by passing an array of ids:
User.store.delete([123, 124]);
Will delete Users with an id of 123 or 124
Returns: void
Delete data by passing in a key value pair:
User.store.delete('firstname', 'Johnny');
Will delete Users with firstname Johnny
Returns: void
Delete data by using where functions:
User.store.query().where('age', QueryOperator.GREATER_THAN, 12).where('age', QueryOperator.LESS_THAN, 18).delete();
Will delete Users between the age of 13 and 17
Returns: void
Or even clear out all data from the store:
User.store.clear();
Will delete all User data from the store
By default all data is inserted, retrieved and manipulated from and in one big dataset (default: all). In some cases you might however want to work with different datasets, for example when you don't want to store all users in one dataset but for example two datasets: 'friends' and 'co-workers'. In this example you don't want to retrieve all users when doing User.store.all();.
For all repository functions, a configuration object with a dataset can be passed to specify what the name of the dataset is. For example, you might do:
User.store.save({
firstname: 'Rupert',
lastname: 'Grint',
}, {
dataset 'friends',
});
To store Rupert in the friends dataset.
When you now call:
User.store.all();
Rupert will not be returned. If you however do:
User.store.all({dataset: 'friends'});
Rupert will be returned! :)
This configuration can be passed to all repository functions and queries using:
User.store.query({dataset: 'friends'}).where('firstname', 'Rupert').get();
Although the functions listed above this section provide you ample ways of handling data, sometimes you might need custom logic that the ORM doesn't offer. For example, retrieving all users but returning just the last three.
You can add a function to the UserRepository itself to handle this:
userRepository.ts
import Repository from "@/libs/orm/repository";
import User from "../user";
export default class UserRepository<T extends User> extends Repository<T> {
lastThreeUsers(): User[] {
return this.data.all().slice(-3);
}
}
Then in your Vue component, you can call this function using:
const users: User[] = User.store.lastThreeUsers();
Services provide an API layer to your application and offers reusable functions to make network calls and (automatically) parsing the response data to models and storing those in the repositories.
A service should reside in the "services" directory and should use the naming convention "{ModelName}Service" (i.e. UserService).
userService.ts
import Api from "@/libs/orm/api";
import User from "../models/User";
export default class UserService<T extends User> extends Api<T> {
// here goes your custom functions
}
As with repositories, a service contains useful functions out of the box to help you fetch data.
An example of fetching data:
UserIndex.vue
import User from "@/models/User";
import { onMounted } from "vue";
onMounted(() => {
User.api
.get("users")
.then(() => {
//
})
.catch(() => {
//
});
});
By default, the ORM tries to parse the response from the backend into instances of the model. For example, this API call will automatically parse the response into a list of users (or a single user if the response is not an array) and save them in the ORM. This means that you can immediately access the users using one of the ORM retrieval functions. By default, the ORM looks for the data key in the response as input that will be parsed to instances of models.
Things like automatic parsing can be configured per call to suit your use case.
Configuration options look as follows:
User.api.get("users", {
config: {}, // AxiosRequestConfig
replace: true, // boolean
update: false, // boolean
save: true, // boolean
source: "data", // string or null
dataset: 'all', // the key to specify the dataset
});
To see what "dataset" is about, see Working with different datasets.
The AxiosRequestConfig can be found here, but is usually used to pass either params or data to the endpoint.
save automatically persists the fetched data to the store
If save is true, replace will replace existing data found in the store with the newly fetched data by replacing the existing model with a newly instantiated one
update also replaces existing data, but only the data that has been supplied. This setting updates the existing model instead of replacing it with a new model
source is the resource key your elements may be nested under in the response body.
A POST request can be made using User.api.post and the data can be (optionally) automatically persisted in the store.
UserIndex.vue
import User from "@/models/User";
import { onMounted } from "vue";
onMounted(() => {
User.api
.post("users", {
firstname: 'Johnny',
lastname: 'Harris',
})
.then(() => {
//
})
.catch(() => {
//
});
});
A POST request will be made to "users" and automatically persist it in the store
As third optional parameter for the post() method additional configuration can be given, see configuration instructions.
A PUT request can be made using User.api.put and the data can (optionally) automatically update the existing record in the store. If no record exists (again, optionally) a new record will be instantiated.
UserIndex.vue
import User from "@/models/User";
import { onMounted } from "vue";
onMounted(() => {
User.api
.put("users", {
id: 3,
firstname: 'Pamela',
lastname: 'Anderson',
})
.then(() => {
//
})
.catch(() => {
//
});
});
A PUT request will be made to "users" and automatically update/create the record in the store
As third optional parameter for the put() method additional configuration can be given, see configuration instructions.
The exact same instructions as Updating data using PUT can be followed to perform a PATCH request, except the method that should be called is User.api.patch() instead of User.api.put().
A DELETE request can be made by using the User.api.delete method and the data can (optionally) automatically be deleted from the store.
UserIndex.vue
import User from "@/models/User";
import { onMounted } from "vue";
onMounted(() => {
User.api
.delete("users", {
delete: 3,
})
.then(() => {
//
})
.catch(() => {
//
});
});
This will make a DELETE request to "users" and automatically delete the user id with 3 from the store`
A delete key must be provided if you want to automatically delete the record from the store.
An array of ids is also accepted (i.e. id: [1, 2, 3]) and if you want to delete by key value pair a deleteValue can be given in which case the delete key will function as the model field name, i.e.:
User.api.delete("users", {
delete: 'id',
deleteValue: 2,
});
A HEAD request can be made using User.api.head.
UserIndex.vue
import User from "@/models/User";
import { onMounted } from "vue";
onMounted(() => {
User.api
.head("users")
.then(() => {
//
})
.catch(() => {
//
});
});
This will make a HEAD request to "users"
By default all data is inserted, retrieved and manipulated from and in one big dataset (default: all). In some cases you might however want to work with different datasets, for example when you don't want to store all users in one dataset but for example two datasets: 'friends' and 'co-workers'. In this example you don't want to retrieve all users when doing User.store.all();.
For all service functions, a configuration object with a dataset can be passed to specify what the name of the dataset is. For example, you might do:
await User.api.get('users', {
dataset: 'friends'
});
To automatically save the users in the response to the friends dataset.
When you now call:
User.store.all();
Rupert will not be returned. If you however do:
User.store.all({dataset: 'friends'});
Rupert will be returned! :)
This configuration can be passed to all service functions.
Although the functions listed above this section provide you ample ways of interacting with an API, sometimes you might need custom logic that the ORM doesn't offer. For example, filtering out users that have the name Dirk before sending the data to the API. Obviously.
You can add a function to the UserService itself to handle this:
userService.ts
import Api from "@/libs/orm/api";
import User from "../models/User";
export default class UserService<T extends User> extends Api<T> {
saveFilteredDirkCourses(users: User[]) {
return new Promise((resolve, reject) => {
const filteredUsers = users.filter((user) => user.firstname === 'Dirk');
this.post("users", filteredUsers)
.then((res) => {
resolve(res);
})
.catch((err) => {
reject(err);
});
});
}
}
Then in your Vue component, you can call this function using:
const users: User[] = User.store.all();
User.api.saveFilteredDirkCourses(users);
In theory, a custom service function does not need to be wrapped inside a promise, but it is considered a best practice.
Also, this.post() is used in the example code because we are storing new users, which would be a POST.
All methods that can be called in the service are: get, post, put, patch and delete.
In Fields that are another model we talked about having a relation inside a model, for example a list of hobbies (Hobby[]) inside a User.
This method works when you want to access user.hobbies, for example:
const user: User = User.store.first();
console.log(user.hobbies);
However, these hobbies can only be accessed through user, which means that if we want to use that data elsewhere it wouldn't be possible. You might also have different relations on the user which data you might not always need, which would bloat the model. This could create performance issues down the line when you are working with a lot users (that each have a LOT of data).
When we define relations on our models, the ORM will automatically normalize (parse) data for those relations to their own stores (repositories), allowing them to then be used wherever we want. By moving the data from the model to the models' own store, we keep the data on the models small. Then by defining a getter for each relation we only access the data for that relation whenever we need to.
Let's look at some examples.
Imagine having a User, which has a relation with exactly one Profile. This is what is called a one to one relationship, as one user has one profile.
We would define this on the User model as follows:
user.ts
export default class User extends Model {
constructor(map: UserInterface) {
super(User);
this.id = map["id"];
this.firstname = map["firstname"];
this.lastname = map["lastname"];
this.description = map["description"];
this.profile = map["profile"];
}
get profile(): Profile | null {
return this.belongsTo<Profile>(Profile, this.profile_id);
}
set profile(value: any) {
this.setupRelation<Profile>(Profile, value);
}
// @ts-ignore
static store: UserRepository<User> = reactive(
new UserRepository<User>(),
this
);
static api: UserService<User> = new UserService<User>(this, this.store, {
route: 'users',
});
}
interface UserInterface {
id?: number;
firstname?: string;
lastname?: string;
description?: string;
profile?: Profile | null;
}
export default interface User extends UserInterface {}
Let's go back once again to the example of User having hobbies (Hobby[]). This is what is called a one to many relationship, as one user has multiple hobbies.
We would define this on the User model as follows:
export default class User extends Model {
constructor(map: UserInterface) {
super(User);
this.id = map["id"];
this.firstname = map["firstname"];
this.lastname = map["lastname"];
this.description = map["description"];
this.hobbies = map["hobbies"];
}
get hobbies(): Hobby[] | null {
return this.hasMany<Hobby>(Hobby, "user_id");
}
set hobbies(value: any) {
this.setupRelation<Hobby>(Hobby, value);
}
// @ts-ignore
static store: UserRepository<User> = reactive(
new UserRepository<User>(),
this
);
static api: UserService<User> = new UserService<User>(this, this.store, {
route: 'users',
});
}
interface UserInterface {
id?: number;
firstname?: string;
lastname?: string;
description?: string;
hobbies?: Hobby[] | null;
}
export default interface User extends UserInterface {}
Hooks give you the opportunity to add custom functionality at certain points in the lifecycle of a model. This allows you to manipulate data before (or after) it's inserted or to fire your own custom code when, for example, a model gets deleted.
Supported lifecycle methods are:
beforeCreate()afterCreate()beforeUpdate()afterUpdate()beforeDelete()afterDelete()
user.ts
export default class User extends Model {
constructor(map: UserInterface) {
super(User);
this.id = map["id"];
this.firstname = map["firstname"];
}
beforeCreate(map: any) {
// allows you to read and manipulate the data inside the map before it's inserted
}
}
user.ts
export default class User extends Model {
constructor(map: UserInterface) {
super(User);
this.id = map["id"];
this.firstname = map["firstname"];
}
afterCreate(instance: User) {
// receives an instance of the created User
}
}
user.ts
export default class User extends Model {
constructor(map: UserInterface) {
super(User);
this.id = map["id"];
this.firstname = map["firstname"];
}
beforeUpdate(map: any) {
// allows you to read and manipulate the data inside the map before it's updated
}
}
user.ts
export default class User extends Model {
constructor(map: UserInterface) {
super(User);
this.id = map["id"];
this.firstname = map["firstname"];
}
afterUpdate(map: any) {
// receives an instance of the updated User
}
}
user.ts
export default class User extends Model {
constructor(map: UserInterface) {
super(User);
this.id = map["id"];
this.firstname = map["firstname"];
}
beforeDelete() {
// execute code that need's to be fired when a User is deleted
}
}
user.ts
export default class User extends Model {
constructor(map: UserInterface) {
super(User);
this.id = map["id"];
this.firstname = map["firstname"];
}
beforeDelete() {
// execute code that need's to be fired when a User no longer exists in the store
}
}