@herlinus/coloquent
Version:
Library for retrieving model objects from a JSON-API, with a fluent syntax inspired by Laravel Eloquent.
387 lines • 13.1 kB
JavaScript
import { Builder } from "./Builder";
import { Map } from "./util/Map";
import { PaginationStrategy } from "./PaginationStrategy";
import DateFormatter from "php-date-formatter";
import { SaveResponse } from "./response/SaveResponse";
import { ToManyRelation } from "./relation/ToManyRelation";
import { ToOneRelation } from "./relation/ToOneRelation";
import { Reflection } from "./util/Reflection";
import { AxiosHttpClient } from "./httpclient/axios/AxiosHttpClient";
export class Model {
constructor() {
this.readOnlyAttributes = [];
this.type = typeof this;
this.relations = new Map();
this.attributes = new Map();
this.dates = {};
/*if (!Model._httpClient[this.getJsonApiBaseUrl()]) {
Model._httpClient[this.getJsonApiBaseUrl()] = new AxiosHttpClient();
}*/
this.initHttpClient();
}
static getJsonApiBaseUrl() { return ''; }
static get httpClient() {
return this._httpClient[this.getJsonApiBaseUrl()] ? this._httpClient[this.getJsonApiBaseUrl()] : this._httpClient[''];
}
static set httpClient(value) {
this._httpClient[this.getJsonApiBaseUrl()] = value;
}
static getJsonApiBaseType() {
if (this.JsonApiBaseType_) {
return this.JsonApiBaseType_;
}
return this.name;
}
getJsonApiBaseType() {
if (this.constructor.JsonApiBaseType_) {
return this.constructor.JsonApiBaseType_;
}
return this.constructor.name;
}
initHttpClient() {
this.constructor.httpClient.setBaseUrl(this.getJsonApiBaseUrl());
}
/**
* Get a {@link Builder} instance from a {@link Model} instance
* so you can query without having a static reference to your specific {@link Model}
* class.
*/
query() {
return this.constructor.query();
}
/**
* Get a {@link Builder} instance from a static {@link Model}
* so you can start querying
*/
static query() {
return new Builder(this);
}
static get(page) {
return new Builder(this)
.get(page);
}
static first() {
return new Builder(this)
.first();
}
static find(id) {
return new Builder(this)
.find(id);
}
static with(attribute) {
return new Builder(this)
.with(attribute);
}
static limit(limit) {
return new Builder(this)
.limit(limit);
}
static where(attribute, value) {
return new Builder(this)
.where(attribute, value);
}
static orderBy(attribute, direction) {
return new Builder(this)
.orderBy(attribute, direction);
}
static option(queryParameter, value) {
return new Builder(this)
.option(queryParameter, value);
}
serialize() {
let attributes = {};
for (let key in this.attributes.toArray()) {
if (this.readOnlyAttributes.indexOf(key) == -1) {
attributes[key] = this.attributes.get(key);
}
}
let relationships = {};
for (let key in this.relations.toArray()) {
if (this.relations.hasChanged(key)) {
let relation = this.relations.get(key);
if (relation instanceof Model) {
relationships[key] = this.serializeToOneRelation(relation);
}
else if (relation instanceof Array) {
relationships[key] = this.serializeToManyRelation(relation);
}
else if (relation === null) {
relationships[key] = { data: null };
}
}
}
let payload = {
data: {
type: this.getJsonApiType(),
attributes,
relationships
}
};
if (this.hasId) {
payload['data']['id'] = this.id;
}
return payload;
}
serializeRelatedModel(model) {
return {
type: model.getJsonApiType(),
id: model.id
};
}
serializeToOneRelation(model) {
return {
data: this.serializeRelatedModel(model),
};
}
serializeToManyRelation(models) {
return {
data: models.map((model) => this.serializeRelatedModel(model))
};
}
beforeSave() {
return {};
}
save() {
if (!this.hasId) {
return this.create();
}
const preprocessResult = this.beforeSave();
let payload = this.serialize();
return this.constructor.httpClient
.patch(this.getJsonApiType() + '/' + this.id, payload, preprocessResult)
.then((response) => {
const idFromJson = response.getData().data.id;
this.setApiId(idFromJson);
return new SaveResponse(response, this.constructor, response.getData());
}, (response) => {
throw response;
});
}
create() {
const preprocessResult = this.beforeSave();
let payload = this.serialize();
return this.constructor.httpClient
.post(this.getJsonApiType(), payload, preprocessResult)
.then((response) => {
const idFromJson = response.getData().data.id;
this.setApiId(idFromJson);
return new SaveResponse(response, this.constructor, response.getData());
}, function (response) {
throw response;
});
}
delete() {
if (!this.hasId) {
throw new Error('Cannot delete a model with no ID.');
}
return this.constructor.httpClient
.delete(this.getJsonApiType() + '/' + this.id)
.then(function () { });
}
/**
* @return A {@link Promise} resolving to:
*
* * the representation of this {@link Model} instance in the API if this {@link Model} has an ID and this ID can
* be found in the API too
* * `undefined` if this {@link Model} instance has no ID
* * `null` if there _is_ an ID, but a {@link Model} with this ID cannot be found in the backend
*/
fresh() {
let model = (new this.constructor);
let builder = model
.query()
.with(this.getRelationsKeys());
if (this.getApiId()) {
return builder
.find(this.getApiId())
.then((response) => {
let model = response.getData();
return model;
}, (response) => {
throw response;
});
}
else {
return Promise.resolve(undefined);
}
}
getRelations() {
return this.relations.toArray();
}
getRelationsKeys(parentRelationName) {
let relationNames = [];
for (let key in this.relations.toArray()) {
let relation = this.getRelation(key);
if (parentRelationName) {
relationNames.push(parentRelationName + '.' + key);
}
else {
relationNames.push(key);
}
if (Array.isArray(relation)) {
relation.forEach((model) => {
relationNames = [...relationNames, ...model.getRelationsKeys(key)];
});
}
else if (relation) {
relationNames = [...relationNames, ...relation.getRelationsKeys(key)];
}
}
return relationNames;
}
/**
* Allows you to get the current HTTP client (AxiosHttpClient by default), e.g. to alter its configuration.
* @returns {HttpClient}
*/
static getHttpClient() {
return this.httpClient;
}
/**
* Allows you to use any HTTP client library, as long as you write a wrapper for it that implements the interfaces
* HttpClient, HttpClientPromise and HttpClientResponse.
* @param httpClient
*/
static setHttpClient(httpClient) {
this.httpClient = httpClient;
}
getJsonApiType() {
return this.jsonApiType;
}
populateFromResource(resource, original) {
this.id = resource.id;
for (let key in resource.attributes) {
this.setAttribute(key, resource.attributes[key], original);
}
}
static getPageSize() {
return this.pageSize;
}
static getPaginationStrategy() {
return this.paginationStrategy;
}
static getPaginationPageNumberParamName() {
return this.paginationPageNumberParamName;
}
static getPaginationPageSizeParamName() {
return this.paginationPageSizeParamName;
}
static getPaginationOffsetParamName() {
return this.paginationOffsetParamName;
}
static getPaginationLimitParamName() {
return this.paginationLimitParName;
}
getRelation(relationName) {
return this.relations.get(relationName);
}
setRelation(relationName, value, orginal) {
this.relations.set(relationName, value, orginal);
}
getAttributes() {
return this.attributes.toArray();
}
getAttribute(attributeName) {
if (this.isDateAttribute(attributeName)) {
return this.getAttributeAsDate(attributeName);
}
return this.attributes.get(attributeName);
}
getAttributeAsDate(attributeName) {
if (!Date.parse(this.attributes.get(attributeName))) {
throw new Error(`Attribute ${attributeName} cannot be cast to type Date`);
}
return new Date(this.attributes.get(attributeName));
}
isDateAttribute(attributeName) {
return this.dates.hasOwnProperty(attributeName);
}
setAttribute(attributeName, value, original) {
if (this.isDateAttribute(attributeName)) {
if (!Date.parse(value)) {
throw new Error(`${value} cannot be cast to type Date`);
}
value = Model.getDateFormatter().parseDate(value, this.dates[attributeName]);
}
this.attributes.set(attributeName, value, original);
}
/**
* We use a single instance of DateFormatter, which is stored as a static property on Model, to minimize the number
* of times we need to instantiate the DateFormatter class. By using this getter a DateFormatter is instantiated
* only when it is used at least once.
*
* @returns DateFormatter
*/
static getDateFormatter() {
if (!Model.dateFormatter) {
Model.dateFormatter = new DateFormatter();
}
return Model.dateFormatter;
}
getApiId() {
return this.id;
}
setApiId(id) {
this.id = id;
}
hasMany(relatedType, relationName) {
if (typeof relationName === 'undefined') {
relationName = Reflection.getNameOfNthMethodOffStackTrace(new Error(), 2);
}
return new ToManyRelation(relatedType, this, relationName);
}
hasOne(relatedType, relationName) {
if (typeof relationName === 'undefined') {
relationName = Reflection.getNameOfNthMethodOffStackTrace(new Error(), 2);
}
return new ToOneRelation(relatedType, this, relationName);
}
get hasId() {
return this.id !== undefined
&& this.id !== null
&& this.id !== '';
}
isDirty() {
return this.attributes.isDirty() || this.relations.isDirty();
}
getChangedRelations() {
const chandeRelations = [];
for (let key in this.relations.toArray()) {
if (this.relations.hasChanged(key)) {
chandeRelations.push(key);
}
}
return chandeRelations;
}
resetAttributes() {
this.attributes.reset();
}
resetRelationships() {
this.relations.reset();
}
}
/**
* @type {number} the page size
*/
Model.pageSize = 50;
/**
* @type {PaginationStrategy} the pagination strategy
*/
Model.paginationStrategy = PaginationStrategy.OffsetBased;
/**
* @type {string} The number query parameter name. By default: 'page[number]'
*/
Model.paginationPageNumberParamName = 'page[number]';
/**
* @type {string} The size query parameter name. By default: 'page[size]'
*/
Model.paginationPageSizeParamName = 'page[size]';
/**
* @type {string} The offset query parameter name. By default: 'page[offset]'
*/
Model.paginationOffsetParamName = 'page[offset]';
/**
* @type {string} The limit query parameter name. By default: 'page[limit]'
*/
Model.paginationLimitParName = 'page[limit]';
Model._httpClient = { '': new AxiosHttpClient() };
//# sourceMappingURL=Model.js.map