import { AxiosRequestConfig, AxiosPromise, AxiosResponse } from 'axios'; import { Constructor } from '@matchlighter/common_library/mixins'; import { Model, AnyObject } from './model'; import { runInAction, action } from 'mobx'; import { urlJoin, toSnakeCase } from '@matchlighter/common_library/strings'; import { field } from './fields'; type HTTPMethod = 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; export const ApiContext = (target: Object, key: string, desc?: PropertyDescriptor) => { const mcls = target.constructor as typeof Model; mcls._meta['api_context_field'] = key; } export interface IApiContext { getApiPath(): string; } const sgetPrimaryKey = (inst: Model, v?) => { const model = inst._model; const fld_name = inst._model._meta['api_primary_key_field'] || 'id'; const fld = model.getField(fld_name); if (fld) { if (v !== undefined) inst[fld.propertyName] = v; return inst[fld.propertyName]; } return undefined; } // TODO Remove this when https://github.com/Microsoft/TypeScript/issues/17293 is resolved interface IRestModel { makeApiCall(options?: AxiosRequestConfig): AxiosPromise makeApiCall(method: HTTPMethod, options?: AxiosRequestConfig): AxiosPromise makeApiCall(method: HTTPMethod, url: string, options?: AxiosRequestConfig): AxiosPromise getApiName() getApiSubPath() decodeResponseData(response: AxiosResponse) } export const RestApiMixin = >(base: T): T & Constructor => { abstract class RestModel extends base implements IApiContext { static api_path = ''; allowCollectionAttachment() { return !!sgetPrimaryKey(this); } //#region API Helpers protected makeApiCall(options?: AxiosRequestConfig): AxiosPromise protected makeApiCall(method: HTTPMethod, options?: AxiosRequestConfig): AxiosPromise protected makeApiCall(method: HTTPMethod, url: string, options?: AxiosRequestConfig): AxiosPromise protected makeApiCall(...args) { let options: AxiosRequestConfig = {}; if (args.length) { if (typeof args[args.length - 1] != 'string') { options = args.pop(); } if (args.length == 2) { options.url = args[1]; options.method = args[0]; } else if (args.length == 1) { options.method = args[0]; } } options.url = urlJoin(this.getApiPath(), options.url); // return apiDesc.api.request(options); return null; } protected getApiName() { return toSnakeCase((this.constructor as typeof Model).model_name); } protected getApiSubPath() { return `${this.getApiName()}/${sgetPrimaryKey(this)}`; } getApiPath(): string { const meta = this._model._meta; const pathBits = []; if (meta['api_context_field']) { const context = this[meta['api_context_field']] as IApiContext; pathBits.unshift(context.getApiPath()); } pathBits.push(this.getApiSubPath()); return urlJoin(pathBits); } /** * Extract Model data from the API response. * Default implementation returns `response.data[Model.model_name] || response.data` */ protected decodeResponseData(response: AxiosResponse) { let rdata = response.data; rdata = (rdata && rdata[this.getApiName()]) || rdata; return rdata; } //#endregion //#region Actions /** * (Re-)fetch this object from the server. * Especially useful for initially loading a partial and fetching the complete Object later. */ async fetch() { if (!sgetPrimaryKey(this)) throw "Model must have an ID to be fetched"; const response = await this.makeApiCall(); const data = this.decodeResponseData(response); this.deserialize(data); return this; } /** * Make the actual API call to save this object * @param data A serialized representation of this object */ protected async do_save(data): Promise { const req_data = { [this.getApiName()]: data, } return await this.makeApiCall(sgetPrimaryKey(this) ? 'PUT' : 'POST', { data: req_data }) } private async saveAndUpdate(data) { const response = await this.do_save(data); const rdata = this.decodeResponseData(response) runInAction(() => { if (rdata['id']) sgetPrimaryKey(this, rdata['id']); // TODO Call deserialize()? this.deserialize(rdata); this.attachToCollections(); this._savedState = this.generateSnapshot(); }) } /** * Saves this object as it now stands, calling serialize() */ @action async save() { return this.saveAndUpdate(this.serialize()); } /** * Reverts this object to the last saved() snapshot. */ @action revert() { this.restoreSnapshot(this._savedState); } /** * Merges params with the result of serialize() and attempts to save. * If successful, merges params into the object by calling mergeUpdate(). */ @action async update(params) { const obj = Object.assign({}, this.serialize(), params); const result = await this.saveAndUpdate(obj); // Object.assign(this, params); return result; } @action async destroy(params?: any) { await this.makeApiCall('DELETE', { data: params, }); runInAction(() => { sgetPrimaryKey(this, null); this.detachFromCollections(); }) } @action destroyLocally() { this.detachFromCollections(); } //#endregion } return RestModel as any; } export class RestModel extends RestApiMixin(Model) { @field accessor id: number; }