UNPKG

@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
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