Model
The model is at the hearth of this package. It boasts a lot of features, so they have been broken down into the following sections:
Creating Models
To create a model, you should first define your model class and define the getName method:
// User.js
import { Model } from '@upfrontjs/framework';
export default class User extends Model {
getName() {
return 'User';
}
}
// User.ts
import { Model } from '@upfrontjs/framework';
export default class User extends Model {
public override getName(): string {
return 'User';
}
}
Then you can call your model in various way, for example
// myScript.js
import User from '@Models/User';
User.find(1);
// or
User.make({ my: attributes });
// etc...
If you're passing attributes to the model you have to use the create method.
TIP (Typescript)
Typescript users may benefit from better typing support if they defined keys and their types on the models
export default class User extends Model {
public is_admin?: boolean;
public age?: number;
public name?: string;
public override getName(): string {
return 'User';
}
}
This will typehint keys on the model when accessing the above keys like user.age
and will get type hinted in various methods such as getAttribute where both the key, the default value and the return value will be type hinted.
Getters
primaryKey
The primaryKey
is a getter of the attribute name which is used to identify your model. The default value is 'id'
.
// User.js
import { Model } from '@upfrontjs/framework';
export default class User extends Model {
getName() {
return 'User';
}
get primaryKey() {
return 'id';
}
}
keyType
The keyType
is a getter that identifies what the type is of your primaryKey. Its value has to be either 'string'
or 'number'
with 'number'
being the default value. You should update this value to 'string'
if you're using a UUID or some custom string for the primary key as this is used when in the Factory and exists logic.
// User.ts
import { Model } from '@upfrontjs/framework';
export default class User extends Model {
public override getName(): string {
return 'User';
}
public get primaryKey(): 'string' {
return 'string';
}
}
exists
The exists
property is a getter on the model that returns a boolean which can be used to assert that the model has been persisted. It takes the primary key, timestamps and soft deletes into account.
Additional methods
is
The is
method compares the given model with the current model based on the getKey and getName
import User from '@Models/User';
import Shift from '@Models/Shift';
const user = User.make({ id: 1 });
const user2 = User.make({ id: 2 });
const shift = Shift.make({ id: 1 });
user.is(user); // true
user.is(user2); // false
user.is(shift); // false
isNot
The isNot
method is the inverse of the is method.
getKey
The getKey
method returns the value of the primary key from the model.
getKeyName
The getKeyName
method returns the primaryKey of the model.
getName
The getName
method expected to return the current class' name. For example in a class called User
it should return 'User'
.
Note: This is essential to add to every model as this is used throughout the framework.
DANGER
This value cannot be this.constructor.name
if you're minifying your code or in the minification options you haven't turned off the class rename or equivalent option.
TIP
If you turn of the class renaming when the code gets mangled by the minifier or bundler of your choice, this.constructor.name
would be an acceptable solution. This would allow you to have a base model you can extend from which can in turn implement the getName
that returns this.constructor.name
.
Bundlers/minifier options examples:
makestatic
The make
method instantiates your model while setting up attributes and relations. This will mass assign attributes to the model while respecting the guarding settings.
import User from '@Models/User';
const user = User.make({ name: 'User Name' }); // User
WARNING
Constructing a new class like new User({...})
is not acceptable. This will not overwrite your class fields with default values if the same key has been passed in due to how JavaScript first constructs the parent class and only then the subclasses. However, you can still use it to call instance methods. Furthermore, it will not cause unexpected results if using it with the setAttribute method or call methods that under the hood uses the setAttribute.
TIP
When creating an instance and passing in another instance of the model:
import User from '@Models/User';
import Shift from '@Models/Shift';
const user = User.make({ name: 'John Doe' });
const newUser = User.make(user);
It will clone the raw attributes and the relationships of the model.
replicate
The replicate
method copies the instance into a non-existent instance. Meaning primary key and the timestamps won't be copied.
import User from '@Models/User';
const user = User.factory().create();
user.getKey(); // 1
user.name; // 'the name'
user.getAttribute(user.getCreatedAtName()); // Date instance
const userCopy = user.replicate();
userCopy.getKey(); // undefined
userCopy.name; // 'the name'
userCopy.getAttribute(userCopy.getCreatedAtName()); // undefined
clone
The clone
method clones the instance in its current state. Meaning all changes to the query building, endpoint and attribute changes will be copied along. The result will match the original model but nothing is copied by reference.
import User from '@Models/User';
const user = User.factory().createOne({ myKey: 1 });
const userClone = user.clone();
user.is(userClone); // true
user.myKey = 2;
userClone.myKey === 1; // true
tap
The tap
method allows to use the model without affecting the model it is called on.
import User from '@Models/User';
const user = User.factory().createOne();
user.with('relation')
.select(['attribute1', 'attribute2'])
.tap(console.log) // the model logged out to the console
.tap(model => model.with('another-relation')) // will NOT add `another-relation` to the next query
.get();
when
The when
method calls the given closure when the first argument evaluates to a truthy value, allowing for changing the model conditionally without breaking the method chaining.
import User from '@Models/User';
User.make()
.when(true, user => user.setAttribute('test', 1))
.when(() => false, user.setAttribute('test', 2))
.getAttribute('test'); // 1
unless
The unless
method calls the given closure when the first argument evaluates to a falsy value, allowing for changing the model conditionally without breaking the method chaining.
import User from '@Models/User';
User.make()
.unless(false, user => user.setAttribute('test', 1))
.unless(() => true, user => user.setAttribute('test', 2))
.getAttribute('test'); // 1
factorystatic
The factory
is a method that returns a Factory instance. Optionally it takes a number argument which is a shorthand for the times' method.
import User from '@Models/User';
const user = User.factory().create(); // User
const users = User.factory(2).create(); // ModelCollection
allstaticasync
The all
method will initiate a request that returns a ModelCollection from the underlying get method.
import User from '@Models/User';
const users = await User.all(); // ModelCollection[User, ...]
saveasync
The save
method will update or save your model based on whether the model exists or not. If the model exists it will send a PATCH
request containing the changes, and the optionally passed in attributes. If the model does not exist it will send a POST
request. The method returns the same current user updated with the response data if any.
updateasync
The update
method sets the correct endpoint then initiates a patch request. If the model does not exists it will throw an error.
import User from '@Models/User';
const user = User.factory.createOne();
await user.update({ optionalExtra: 'data' });
findasync
The find
method sends a GET
request to the model endpoint supplemented with the given id. Available both static and non-statically.
import User from '@Models/User';
const user = await User.find('8934d792-4e4d-42a1-bb4b-45b34b1140b4');
findManyasync
The findMany
method similar to the find method sends a GET
request to the model endpoint but adds a whereKey constraint to the request, returning a ModelCollection. Available both static and non-statically.
import User from '@Models/User';
const users = await User.findMany([1, 2]); // ModelCollection[User, User]
refreshasync
The refresh
method updates all the attributes on the model by selecting the present attribute keys and setting the attributes from the response. This will reset any attribute changes.
import User from '@Models/User';
const user = await User.find(1);
user.name = 'new name';
user.getChanges(); // { name: 'new name' }
await user.refresh();
user.getChanges(); // {}