Created
September 2, 2019 11:53
-
-
Save sp90/3e15bd9aa97f77d4908c8b1b1b373034 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { of, BehaviorSubject } from 'rxjs'; | |
import { fromFetch } from 'rxjs/fetch'; | |
import { switchMap, catchError } from 'rxjs/operators'; | |
interface CustomMethodInterface { | |
name: string; | |
endpoint: string; | |
method: 'GET' | 'POST' | 'PATCH' | 'DELETE'; | |
} | |
interface ModelInterface { | |
idName: string; | |
endpoint: string; | |
validator?: Function; | |
customEndpoints?: { | |
find?: string; | |
findOne?: string; | |
createOne?: string; | |
findOneAndUpdate?: string; | |
findOneAndRemove?: string; | |
}; | |
customMethods?: CustomMethodInterface[]; | |
HTTPinterceptors?: { | |
request?: Function; | |
}; | |
} | |
interface FindSettingsInterface { | |
sort?: any; | |
pagination?: any; | |
customEndpoint?: string; | |
} | |
const originalPagination = { | |
offset: 0, | |
limit: 10 | |
}; | |
class FindInstance { | |
pagination = originalPagination; | |
requestOptions: any = { | |
asQueryParam: true | |
}; | |
data = []; | |
subject = new BehaviorSubject([]); | |
constructor(private _model: any, private query: any, private sort: any) { | |
if (_model.customEndpoints && _model.customEndpoints.find) { | |
this.requestOptions.customEndpoint = _model.customEndpoints.find; | |
} | |
} | |
run() { | |
let query = this.newQuery(this.sort, this.pagination); | |
return this.runCall(query); | |
} | |
next() { | |
this.pagination.offset += this.pagination.limit; | |
let query = this.newQuery(this.sort, this.pagination); | |
return this.runCall(query); | |
} | |
reset() { | |
this.pagination.offset = originalPagination.offset; | |
let query = this.newQuery(this.sort, this.pagination); | |
return this.runCall(query); | |
} | |
private async runCall(query) { | |
try { | |
const result = await this._model.HTTP('GET', query, this.requestOptions, true); | |
this.subject.next(result.data); | |
this.data = result.data; | |
return this.data; | |
} catch (err) { | |
// Error handling | |
return err; | |
} | |
} | |
private newQuery(sort, pagination) { | |
let query: any = this.query ? this.query : {}; | |
if (sort) { | |
query.sort = JSON.stringify(sort); | |
} | |
if (pagination) { | |
query.pagination = JSON.stringify(pagination); | |
} | |
return query; | |
} | |
} | |
function MethodCreator(_model, method, endpoint) { | |
const _self = { | |
_model: _model, | |
method: method, | |
endpoint: endpoint | |
}; | |
return (data: any) => { | |
const isGetter = _self.method === 'GET' || _self.method === 'DELETE'; | |
const isSetter = _self.method === 'PATCH' || _self.method === 'POST'; | |
if (isSetter && _self._model.validator) { | |
let validatorResponse = _self._model.validator(data); | |
if (validatorResponse instanceof Error) { | |
throw validatorResponse; | |
} | |
} | |
let requestOptions: any = {}; | |
const endpointId = isGetter ? data : data[_self._model.idName]; | |
requestOptions.setEndpoint = _self.endpoint.includes(':id') | |
? _self.endpoint.replace(':id', endpointId) | |
: _self.endpoint; | |
return _self._model.HTTP(_self.method, data, requestOptions); | |
}; | |
} | |
/** | |
* Atlas - API taming luxurious abstraction service | |
* The Atlas can be instansiated to configurate the model you wanna manipulate via the API | |
*/ | |
export class Atlas { | |
idName: string; | |
endpoint: string; | |
customEndpoints: any = {}; | |
validator: Function; | |
docInstances: any[] = []; | |
HTTPinterceptors: any = {}; | |
constructor() {} | |
/** | |
* Here you instantiate a model that you can manipulate data on going forward | |
* @param options contains a series of settings to initiate the model correctly | |
* @property options.idName referes to the property name of the id of the entries to retrieve by | |
* @property options.endpoint is the endpoint to ask for data on the API | |
* @property [options.endpoints] its an object with custom endpoints for each of the request types (find, createOne, findOne ...) | |
* @property [options.validator] is a function to validate api calls before sending them of to the API, you could use any validator or leave it blank to ignore validation | |
* @property [options.HTTPinterceptors] is a function to manipulate requests before they are sent of its here you can sent http headers and much more | |
* @property [options.customMethods] an array of objects to define custom methods | |
* @property [options.customMethods.name] name of the method | |
* @property [options.customMethods.endpoint] url to hit the endpoint | |
* @property [options.customMethods.method] one of the common 4 methods, GET, PATCH, POST & DELETE | |
* @example | |
* let TodosV1 = new Atlas().model({ | |
* idName: '_id', | |
* endpoint: 'http://localhost:3000/api/todos/:id', | |
* validator: (objToValidate) => { | |
* // Here we're using the mongoose validator | |
* const doc = new mongoose.Document( | |
* objToValidate, | |
* new mongoose.Schema({ | |
* text: { | |
* type: String, | |
* required: true | |
* }, | |
* done: Boolean, | |
* deadline: Date, | |
* reminder: Date | |
* }) | |
* ); | |
* | |
* const validationRes = doc.validateSync(); | |
* | |
* // IMPORTANT: Should return an Error object or true depending on if it valid or invalid | |
* if (validationRes) { | |
* return new Error(validationRes); | |
* } else { | |
* return true; | |
* } | |
* }, | |
* HTTPinterceptors: { | |
* request: (fetchEndpoint, fetchSettings) => { | |
* const myToken = 'JSON_WEB_TOKEN'; // Get token from anywhere | |
* | |
* fetchSettings.headers['Authorization'] = `Bearer ${myToken}`; | |
* | |
* // Important should return both endpoint and settings like an object with correct names | |
* return { | |
* fetchEndpoint, | |
* fetchSettings | |
* } | |
* } | |
* } | |
* }); | |
*/ | |
model(options: ModelInterface): any { | |
this.idName = options.idName; | |
this.endpoint = options.endpoint; | |
this.customEndpoints = options.customEndpoints; | |
// Set validator function | |
this.validator = | |
options && options.validator | |
? options.validator | |
: () => { | |
return true; | |
}; | |
// Set Http interceptor | |
if (options && options.HTTPinterceptors && options.HTTPinterceptors.request) { | |
this.HTTPinterceptors['request'] = options.HTTPinterceptors.request; | |
} | |
const methodsMap = [ | |
{ | |
name: 'findOne', | |
method: 'GET', | |
endpoint: this.endpoint | |
}, | |
{ | |
name: 'createOne', | |
method: 'POST', | |
endpoint: this.endpoint.replace('/:id', '') | |
}, | |
{ | |
name: 'findOneAndUpdate', | |
method: 'PATCH', | |
endpoint: this.endpoint | |
}, | |
{ | |
name: 'findOneAndDelete', | |
method: 'DELETE', | |
endpoint: this.endpoint | |
} | |
]; | |
const customMethods = | |
options.customMethods && options.customMethods.length ? options.customMethods : []; | |
const allMethods = methodsMap.concat(customMethods); | |
allMethods.forEach(methodObj => { | |
const newMethod = MethodCreator(this, methodObj.method, methodObj.endpoint); | |
this[methodObj.name] = newMethod; | |
}); | |
return this; | |
} | |
/** | |
* HTTP is helper method to do API requests | |
* @param method can be GET, DELETE, POST or PATCH | |
* @param data for get and deletes this is the id of the item - for creates and updates its the object you want to update or create | |
* @param options | |
* @property options.asQueryParam boolean to set data as query params instead of using the body | |
*/ | |
HTTP(method: string, data: any, options?: any, asPromise: boolean = false) { | |
if (!method) { | |
throw 'Method is missing'; | |
} | |
if (options && options.asQueryParam) { | |
options.queryParams = data; | |
} | |
// Set default endpoint here | |
let fetchEndpoint = options.setEndpoint ? options.setEndpoint : this.endpoint; | |
let fetchSettings: any = { | |
method: method, | |
mode: 'cors', // no-cors, cors, *same-origin | |
headers: { | |
'Content-Type': 'application/vnd.api+json' | |
} | |
}; | |
// Automaticly set body for post & patch | |
if (method === 'POST' || method === 'PATCH') { | |
fetchSettings['body'] = JSON.stringify({ | |
data: { | |
//type: 'users', | |
attributes: data | |
} | |
}); | |
} | |
// Bind query params to the url before sending it to the interceptor | |
if (options && options.queryParams) { | |
fetchEndpoint += | |
(fetchEndpoint.indexOf('?') === -1 ? '?' : '&') + queryParams(options.queryParams); | |
delete options.queryParams; | |
} | |
// Interceptor is now ran right before doing the actual fetch | |
// So the user have full modifyability of the request being sent | |
if (this.HTTPinterceptors && this.HTTPinterceptors.request) { | |
const afterInterceptor = this.HTTPinterceptors.request(fetchEndpoint, fetchSettings); | |
if (afterInterceptor && afterInterceptor.fetchEndpoint) { | |
fetchEndpoint = afterInterceptor.fetchEndpoint; | |
} | |
if (afterInterceptor && afterInterceptor.fetchSettings) { | |
fetchSettings = { ...fetchSettings, ...afterInterceptor.fetchSettings }; | |
} | |
} | |
if (asPromise) { | |
return new Promise(async (resolve, reject) => { | |
try { | |
const response = await fetch(fetchEndpoint, fetchSettings); | |
const resp = await response.json(); | |
response.ok ? resolve(resp) : reject(resp); | |
} catch (err) { | |
reject(err); | |
} | |
}); | |
} else { | |
// Use fetch to do what have been configured above | |
return fromFetch(fetchEndpoint, fetchSettings).pipe( | |
switchMap(response => { | |
if (response.ok) { | |
// OK return data | |
return response.json(); | |
} else { | |
// Server is returning a status requiring the client to try something else. | |
return of({ error: true, message: `Error ${response.status}` }); | |
} | |
}), | |
catchError(err => { | |
// Network or other error, handle appropriately | |
console.error(err); | |
return of({ error: true, message: err.message }); | |
}) | |
); | |
} | |
function queryParams(params) { | |
return Object.keys(params) | |
.map(k => { | |
if (Array.isArray(params[k])) { | |
return params[k] | |
.map(val => `${encodeURIComponent(k)}[]=${encodeURIComponent(val)}`) | |
.join('&'); | |
} | |
return `${encodeURIComponent(k)}=${encodeURIComponent(params[k])}`; | |
}) | |
.join('&'); | |
} | |
} | |
/** | |
* Used to instantiate the DocInstance or grab it from memory this is to avoid having a memory leak by making an instance many times | |
* @param instanceId the id of the document you want to manipulate | |
* @param getOnInit set to true if you want to populate memory with data when this is instantiated | |
* @example | |
* // Get an instance of a document | |
* const activeTodo = TodosV1.doc(itemId1, true); | |
* | |
* // Subscribe | |
* activeTodo.subject.subscribe(res => { | |
* console.log('subscribed value: ', res); | |
* }); | |
* | |
* // Do a get | |
* activeTodo.get().then(res => { | |
* console.log('after get data: ', res); | |
* }); | |
* | |
* // Do an update | |
* activeTodo.set({ | |
* text: 'new text6' | |
* }); | |
*/ | |
doc(instanceId: string, getOnInit: boolean): any { | |
const _model = this; | |
const instanceIndex = _model.docInstances.map(x => x.instanceId).indexOf(instanceId); | |
if (instanceIndex > -1) { | |
return _model.docInstances[instanceIndex].instance; | |
} else { | |
let newDocInstance = new DocInstance(instanceId, _model, getOnInit); | |
_model.docInstances.push({ | |
instanceId: instanceId, | |
instance: newDocInstance | |
}); | |
return newDocInstance; | |
} | |
} | |
/** | |
* find is used to search the api, with support for query, sort & pagination | |
* @param query is key value pairs in an object that defines the query which is transformed into query params to be send with the get request | |
* @param sort a stringified object that is sent of to the API as param sort - MIGHT BE DEPRECATED OR CHANGED | |
* @param pagination | |
* @property pagination.limit max number of items per request | |
* @property pagination.offset where in the index to start getting items | |
* @example | |
* const findTodos = TodosV1.find( | |
* { | |
* query: 'to' | |
* }, | |
* { | |
* sort: { | |
* text: -1 | |
* }, | |
* pagination: { | |
* offset: 0, | |
* limit: 2 | |
* } | |
* } | |
* ) | |
* | |
* findTodos.subject.subscribe(res => { | |
* console.log('response', res) | |
* }) | |
* | |
* // Initial run | |
* findTodos.run() | |
* | |
* // Next page | |
* findTodos.next() | |
* | |
* // Reset pagination | |
* findTodos.next() | |
*/ | |
find(query: any, settings: FindSettingsInterface) { | |
return new FindInstance(this, query, settings.sort); | |
} | |
// /** | |
// * findOne is used to get one item by id | |
// * @param itemId the id of the item you want to retrieve | |
// * @example | |
// * TodosV1.findOne('ITEM_ID').then(result => { | |
// * console.log('result after: ', result); | |
// * }); | |
// */ | |
// findOne(itemId: string, customEndpoint?: string) { | |
// let requestOptions: any = {}; | |
// if (this.customEndpoints && this.customEndpoints.findOne) { | |
// requestOptions.customEndpoint = this.customEndpoints.findOne; | |
// } | |
// if (customEndpoint) { | |
// requestOptions.customEndpoint = customEndpoint; | |
// } | |
// return this.HTTP('GET', itemId, requestOptions); | |
// } | |
// /** | |
// * createOne is used to create an input here we also call the custom validator if set on the modal instance | |
// * @param createObj can contain any values and is validated by validator if set | |
// * @example | |
// * TodosV1.createOne({ | |
// * text: 'some todo text', | |
// * done: false | |
// * }).then(result => { | |
// * console.log('result after: ', result); | |
// * }); | |
// */ | |
// createOne(createObj: any, customEndpoint?: string) { | |
// let validatorResponse = this.validator(createObj); | |
// if (validatorResponse instanceof Error) { | |
// throw validatorResponse; | |
// } | |
// let requestOptions: any = {}; | |
// if (this.customEndpoints && this.customEndpoints.createOne) { | |
// requestOptions.customEndpoint = this.customEndpoints.createOne; | |
// } | |
// if (customEndpoint) { | |
// requestOptions.customEndpoint = customEndpoint; | |
// } | |
// return this.HTTP('POST', createObj, requestOptions); | |
// } | |
// /** | |
// * findOneAndUpdate is used to update an input here we also call the custom validator if set on the modal instance | |
// * @param updateObj can contain any values and is validated by validator if set | |
// * @example | |
// * TodosV1.findOneAndUpdate({ | |
// * text: 'some other todo text' | |
// * }).then(result => { | |
// * console.log('result after: ', result); | |
// * }); | |
// */ | |
// findOneAndUpdate(updateObj: any, customEndpoint?: string) { | |
// let validatorResponse = this.validator(updateObj); | |
// if (validatorResponse instanceof Error) { | |
// throw validatorResponse; | |
// } | |
// let requestOptions: any = {}; | |
// if (this.customEndpoints && this.customEndpoints.findOneAndUpdate) { | |
// requestOptions.customEndpoint = this.customEndpoints.findOneAndUpdate; | |
// } | |
// if (customEndpoint) { | |
// requestOptions.customEndpoint = customEndpoint; | |
// } | |
// return this.HTTP('PATCH', updateObj, requestOptions); | |
// } | |
// /** | |
// * findOneAndRemove is used to find an item and remove it | |
// * @param itemId the id of the item you want to delete | |
// * @example | |
// * TodosV1.findOneAndRemove('ITEM_ID').then(result => { | |
// * console.log('result after: ', result); | |
// * }); | |
// */ | |
// findOneAndRemove(itemId: string, customEndpoint?: string) { | |
// let requestOptions: any = {}; | |
// if (this.customEndpoints && this.customEndpoints.findOneAndRemove) { | |
// requestOptions.customEndpoint = this.customEndpoints.findOneAndRemove; | |
// } | |
// if (customEndpoint) { | |
// requestOptions.customEndpoint = customEndpoint; | |
// } | |
// return this.HTTP('DELETE', itemId, requestOptions); | |
// } | |
} | |
/** | |
* The DocInstance is an internal class that can be instantiated on a model | |
*/ | |
class DocInstance { | |
_model: any; | |
instanceId: string | number = ''; | |
data = null; | |
subject = new BehaviorSubject(null); | |
/** | |
* The constructor of the doc instance | |
* @param instanceId is the document id | |
* @param _model is the parent model, that has instantiated this doc instance | |
* @param getOnInit this is to retrive the document when you create the document instance | |
*/ | |
constructor(instanceId: string | number, _model: object, getOnInit: boolean) { | |
this.instanceId = instanceId; | |
this._model = _model; | |
if (getOnInit) { | |
this.get(); | |
} | |
} | |
/** | |
* Read - Is to retrive the document from API based on the initial model configuration | |
* @returns successfully - it returns the document recieved from API | |
* @example | |
* // Get an instance of a document | |
* const activeTodo = TodosV1.doc('ITEM_ID'); | |
* | |
* // Do a get | |
* activeTodo.get() | |
* .then((res) => { | |
* console.log(res) | |
* }) | |
*/ | |
async get(): Promise<any> { | |
try { | |
const result = await this._model.findOne(this.instanceId); | |
// Push to subject | |
this.subject.next(result.data); | |
// Set in memory | |
this.data = result.data; | |
return this.data; | |
} catch (err) { | |
// Error handling | |
return err; | |
} | |
} | |
/** | |
* Update - Is to update the data in the API based on the initial model configuration | |
* @returns successfully - it returns the updated document sent to API | |
* @param updateObj | |
* @example | |
* // Get an instance of a document | |
* const activeTodo = TodosV1.doc('ITEM_ID'); | |
* | |
* // Set new data to the item | |
* activeTodo.set({ | |
* text: 'new updated text' | |
* }) | |
* .then((res) => { | |
* console.log(res) | |
* }) | |
*/ | |
async set(updateObj: object): Promise<any> { | |
let finalUpdateObj = updateObj; | |
// Set id | |
finalUpdateObj[this._model.idName] = this.instanceId; | |
try { | |
const result = await this._model.findOneAndUpdate(finalUpdateObj); | |
if (result && result.success) { | |
// Push to subject | |
this.subject.next(result.data); | |
// Set in memory | |
this.data = result.data; | |
// Return value | |
return this.data; | |
} else { | |
throw result && result.message ? result.message : 'An unexpected error happend'; | |
} | |
} catch (err) { | |
// Error handling | |
return err; | |
} | |
} | |
/** | |
* Delete - Is to remove the document from API based on the initial model configuration | |
* @example | |
* // Get an instance of a document | |
* const activeTodo = TodosV1.doc('ITEM_ID'); | |
* | |
* // Delete the todo | |
* activeTodo.remove() | |
*/ | |
async remove(): Promise<any> { | |
try { | |
const result = await this._model.findOneAndRemove(this.instanceId); | |
if (result && result.success) { | |
this.data = null; | |
} else { | |
throw result && result.message ? result.message : 'An unexpected error happend'; | |
} | |
// TODO - remove instance from memory to avoid a memory leak | |
return { | |
message: 'Item has been destroyed' | |
}; | |
} catch (err) { | |
// Error handling | |
return err; | |
} | |
} | |
/** | |
* @returns the data stored in memory, memory gets updated both when using .set() & .get() | |
*/ | |
getMemory(): object { | |
return this.data; | |
} | |
} | |
// export const BasicTokenInterceptor = (myToken: string, type: string = 'Bearer') => { | |
// return { | |
// request: (fetchEndpoint, fetchSettings) => { | |
// if (type === 'Bearer') { | |
// fetchSettings.headers['Authorization'] = `Bearer ${myToken}`; | |
// } | |
// // Important should return both endpoint and settings like an object with correct names | |
// return { | |
// fetchEndpoint, | |
// fetchSettings | |
// }; | |
// } | |
// }; | |
// }; | |
export const LsTokenInterceptor = (target: string, type: string = 'Bearer') => { | |
return { | |
request: (fetchEndpoint, fetchSettings) => { | |
let targetArr = target.split('.'); | |
let lsKeyName = targetArr[0]; | |
let myToken; | |
let myLs = JSON.parse(localStorage.getItem(lsKeyName)); | |
if (myLs) { | |
if (targetArr.length === 1) { | |
myToken = myLs; | |
} else if (targetArr.length === 2) { | |
myToken = myLs[targetArr[1]]; | |
} | |
if (type === 'Bearer') { | |
fetchSettings.headers['Authorization'] = `Bearer ${myToken}`; | |
} | |
} | |
// Important should return both endpoint and settings like an object with correct names | |
return { | |
fetchEndpoint, | |
fetchSettings | |
}; | |
} | |
}; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment