papir
Version:
Rest API Modelling library
1,348 lines (1,319 loc) • 66.6 kB
JavaScript
import Prop from './prop'
import Query from './query'
import axios from 'axios'
import {clone} from '../services/util'
/**
* Endpoint
*/
export default class Endpoint {
constructor(endpoint, controller, apiSlug = null, predefined = {}, config = {}) {
/**
* Public Scope
*/
let accessor = this
/**
* Public / Reserved Variables
*/
/**
* Get last fetched raw value
*/
accessor.raw = null
/**
* If endpoint is list related, children is saved here
*/
accessor.children = []
/**
* Default arguments added to requests
*/
accessor.args = {
fetch: [],
save: [],
custom: [],
create: [],
batch: [],
upload: [],
remove: [],
get: [],
post: [],
put: [],
patch: [],
delete: [],
head: [],
trace: [],
connect: [],
options: []
}
/**
* Shared Variables
*/
accessor.shared = {
storage: null, // Storage is free to be used for anything on app level
// Default Config (config level 0 - greater is stronger)
config: {
multiple: false,
batchIdentifier: 'batch',
post: {
keepNull: false
}
},
api: null,
defaultApi: apiSlug,
map: null,
endpoint: endpoint,
controller: controller,
requester: controller,
predefined: predefined,
accessor: accessor,
reserved: [
'loading',
'loaders',
// Property Requesters
'fetch',
'custom',
'save',
'create',
'batch',
'clear',
'upload',
'remove',
// Custom Requesters
'get',
'post',
'put',
'patch',
'delete',
'head',
'trace',
'connect',
'options',
// Property Methods
'args',
'query',
'set',
'clone',
'changes',
'props',
'shared',
'identifier',
'identifiers',
'removeIdentifiers',
'reverseMapping',
// Accessor Variables
'children',
'raw',
'headers',
'invalids',
'exchange',
'sort',
'reserved'
]
}
/**
* Private Variables
*/
let cancelers = {
fetch: null,
save: null,
create: null,
remove: null,
upload: null,
batch: null
}
/**
* Public / Reserved Variable Names
* @warning Can not be used as a property name in models
*/
accessor.loading = false
accessor.loaders = []
accessor.invalids = {} // Reserved properties from Server is stored here
accessor.headers = {
mapped: {},
unmapped: {}
} // @note - Related to Properties
/**
* Private Methods
* ---------------
* Initialization
*/
let init = (accessor = this) => {
/**
* If an Endpoint object was passed instead of string, copy values to this endpoint before resolving constructors
*/
if (typeof accessor.shared.endpoint !== 'string' && typeof accessor.shared.endpoint !== 'undefined') {
accessor.shared.controller = accessor.shared.endpoint.shared.controller // Replace controller
accessor.shared.requester = accessor.shared.endpoint.shared.requester // Replace requester
// Replace defaultApi only if no apiSlug was given
if (accessor.shared.defaultApi === null) {
accessor.shared.defaultApi = accessor.shared.endpoint.shared.defaultApi
}
accessor.args = clone({}, accessor.shared.endpoint.args)
accessor.set(accessor.shared.endpoint, false) // Replace props
accessor.shared.config = accessor.shared.endpoint.shared.config // Replace config
accessor.shared.endpoint = accessor.shared.endpoint.shared.endpoint // Replace endpoint string
}
/**
* Map Resolver
*/
let resolveMap = () => {
let map = null
try {
map = accessor.shared.api.mappings[accessor.shared.endpoint]
if (typeof map !== 'undefined' && typeof map.config !== 'undefined' && map.config.constructor === Object) {
// Mapped Config (config level 1 - greater is stronger)
accessor.shared.config = Object.assign(clone({}, accessor.shared.config), map.config)
}
} catch (e) {
console.error(e)
}
return map
}
/**
* Resolve Requester
*/
if (typeof accessor.shared.controller.apis !== 'undefined') {
accessor.shared.defaultApi = accessor.shared.defaultApi === null ? accessor.shared.controller.default : accessor.shared.defaultApi
accessor.shared.api = accessor.shared.controller.apis[accessor.shared.defaultApi]
accessor.shared.requester = accessor.shared.api.requester
accessor.shared.map = resolveMap()
// Custom Config (config level 2 - greater is stronger)
accessor.shared.config = Object.assign(clone({}, accessor.shared.config), config)
accessor.shared.buildProps(accessor.shared.map, accessor.shared.predefined)
} else {
console.error('No apis is hooked to Controller', accessor.shared.controller)
accessor.shared.controller = null
}
}
/**
* Shared Methods
*/
/**
* Build mapped / predefined properties
*/
accessor.shared.buildProps = (map = accessor.shared.map, predefined = accessor.shared.predefined) => {
if (map !== null && typeof map !== 'undefined' && typeof map.props !== 'undefined') {
try {
Object.keys(map.props).reduce((prev, key) => {
if (!accessor.reserved(key) && typeof accessor[key] === 'undefined') {
accessor[map.props[key]] = new Prop(accessor, map.props[key], null)
} else if (key === 'invalids' && typeof accessor.invalids[key] === 'undefined') {
accessor.invalids[map.props[key]] = new Prop(accessor, map.props[key], null)
}
}, {})
} catch (error) {
console.error('Error in property mapping for api ' + accessor.shared.defaultApi)
console.error(map.props)
}
}
try {
Object.keys(predefined).reduce((prev, key) => {
if (!accessor.reserved(key) && typeof accessor[key] === 'undefined') {
accessor[key] = new Prop(accessor, key, predefined[key])
} else if (!accessor.reserved(key) && typeof accessor[key] !== 'undefined') {
accessor[key].value = predefined[key]
accessor[key].changed(false)
} else if (accessor.reserved(key) && typeof accessor.invalids[key] === 'undefined') {
accessor.invalids[key] = new Prop(accessor, key, predefined[key])
} else {
accessor.invalids[key].value = predefined[key]
accessor.invalids[key].changed(false)
}
}, {})
} catch (error) {
console.error('Error in predefined properties')
console.error(predefined)
}
if (map !== null && typeof map !== 'undefined' && typeof map.identifier !== 'undefined' && map.identifier !== null && map.identifier !== '') {
let mappedIdentifier = map.identifier
if (typeof map.props !== 'undefined' && typeof map.props[map.identifier] !== 'undefined') {
mappedIdentifier = map.props[map.identifier]
}
if (!accessor.reserved(mappedIdentifier) && typeof accessor[mappedIdentifier] !== 'undefined') {
accessor.identifier = accessor[mappedIdentifier]
} else if (accessor.reserved(mappedIdentifier) && typeof accessor.invalids[mappedIdentifier] !== 'undefined') {
accessor.identifier = accessor.invalids[mappedIdentifier]
} else if (!accessor.reserved(mappedIdentifier)) {
accessor.identifier = accessor[mappedIdentifier] = new Prop(accessor, mappedIdentifier)
} else {
accessor.identifier = accessor.invalids[mappedIdentifier] = new Prop(accessor, mappedIdentifier)
}
} else {
accessor.identifier = null
}
}
/**
* Url Resolver
*/
accessor.shared.resolveUrl = (endpoint = accessor.shared.endpoint, map = accessor.shared.map, api = accessor.shared.api, args = null, batch = false) => {
let base = (api !== null && typeof api.base !== 'undefined') ? api.base : ''
// Remove last slash if any from base
if (base.length > 0 && base[(base.length - 1)] === '/') {
base = base.slice(0, -1)
}
let path = endpoint
// If mapping is set
if (map !== null && typeof map !== 'undefined') {
path = map.endpoint
// Add slash to path if missing
if (path.length > 0 && path[0] !== '/') {
path = '/' + path
}
}
// Resolve Identifiers. Ex.: {id} or {/parentId} etc...
let identifiers = accessor.identifiers(path)
Object.keys(identifiers).reduce((prev, key) => {
let slash = identifiers[key].slash
let hook = identifiers[key].hook
// Resolve mapping
if (map !== null && typeof map !== 'undefined' && typeof map.props !== 'undefined') {
key = typeof map.props[key] !== 'undefined' ? map.props[key] : key
}
// Replace hook with value from mapped prop
if (!accessor.reserved(key) && typeof accessor[key] !== 'undefined' && accessor[key].value !== null && (batch || key !== accessor.shared.config.batchIdentifier)) {
path = path.replace(hook, (slash ? '/' : '') + accessor[key].value)
} else if (accessor.reserved(key) && typeof accessor.invalids[key] !== 'undefined' && accessor[key].value !== null && (batch || key !== accessor.shared.config.batchIdentifier)) {
path = path.replace(hook, (slash ? '/' : '') + accessor.invalids[key].value)
} else {
path = path.replace(hook, '')
}
}, {})
while (path.indexOf('//') !== -1) {
path = path.replace('//', '/')
}
let url = base + path
if (map !== null && typeof map !== 'undefined' && typeof map.params !== 'undefined' && map.params.constructor === Array) {
if (args !== null) {
args = args.concat(map.params)
} else {
args = map.params
}
}
// Add Query Arguments
if (args !== null) {
if (url.indexOf('?') === -1) {
url += '?'
} else if (url[(url.length - 1)] !== '?' && url[(url.length - 1)] !== '&') {
url += '&'
}
for (let i = 0, l = args.length; i < l; i++ ) {
let arg = args[i]
url += arg.key + '=' + arg.value + '&'
}
if (url[(url.length - 1)] === '&' || url[(url.length - 1)] === '?') {
url = url.slice(0, -1)
}
}
return url
}
/**
* Start Loader
*/
let startLoader = (loadSlug) => {
accessor.loading = true
return accessor.loaders.push(loadSlug)
}
/**
* Stop Loader
*/
let stopLoader = (loadSlug) => {
let index = accessor.loaders.indexOf(loadSlug)
if (index !== -1) {
accessor.loaders.splice(index, 1)
accessor.loading = accessor.loaders.length > 0
}
return accessor.loaders
}
/**
* Handle Cancelation of Running Requests
*/
accessor.shared.handleCancellation = (cancellation) => {
if (cancellation !== null) {
cancellation()
}
return {
promise: new Promise(resolve => {
cancellation = resolve
}),
cancellation: cancellation
}
}
/**
* Handle Mapping
*/
accessor.shared.handleMapping = (response, key = null, batch, multiple, map = accessor.shared.map) => {
let conf = clone({}, accessor.shared.config)
return new Promise((resolve, reject) => {
let resolved = false
let data = response.data // Raw from server
let headers = response.headers // In lowercase
try {
let parsed = data
let isObjOrArray = parsed.constructor === Object || parsed.constructor === Array
if (!isObjOrArray) {
parsed = JSON.parse(parsed)
}
if (typeof parsed !== 'undefined' && parsed !== null && isObjOrArray) {
if (!batch && !multiple) {
// Parse Data
response = accessor.set(parsed, false, true, key)
} else if (batch && map !== null && typeof map !== 'undefined') {
if (parsed.constructor === Object) {
let match = 0
let hasBatch = map.batch !== null && typeof map.batch !== 'undefined'
if (hasBatch) {
Object.keys(map.batch).reduce((prev, key) => {
if (typeof parsed[map.batch[key]] !== 'undefined') {
match++
}
}, {})
}
// If response has batch mapping keys, resolve by keys
if (match > 0) {
let deleteKey = (typeof map.batch.delete !== 'undefined' && map.batch.delete !== null) ? map.batch.delete : 'delete'
// Exchange all without delete
Object.keys(parsed).reduce((prev, method) => {
// Exchange updated
if (method !== deleteKey) {
for (let i = 0, l = parsed[method].length; i < l; i++ ) {
let child = parsed[method][i]
let endpoint = new Endpoint(
accessor,
accessor.shared.controller,
accessor.shared.defaultApi,
Object.assign(child, accessor.shared.predefined),
Object.assign(conf, {multiple: false})
)
accessor.exchange(endpoint)
}
} else {
// Remove deleted
for (let i = 0, l = parsed[method].length; i < l; i++ ) {
let child = parsed[method][i]
let endpoint = new Endpoint(
accessor,
accessor.shared.controller,
accessor.shared.defaultApi,
Object.assign(child, accessor.shared.predefined),
Object.assign(conf, {multiple: false})
)
accessor.exchange(endpoint, true, false, true)
}
}
}, {})
} else {
// If response has no keys mapped in batch, expect one instance
let endpoint = new Endpoint(
accessor,
accessor.shared.controller,
accessor.shared.defaultApi,
Object.assign(parsed, accessor.shared.predefined),
Object.assign(conf, {multiple: false})
)
accessor.exchange(endpoint)
}
} else {
// If response is array expect multiple instances
for (let i = 0, l = parsed.length; i < l; i++ ) {
let obj = parsed[i]
let endpoint = new Endpoint(
accessor,
accessor.shared.controller,
accessor.shared.defaultApi,
Object.assign(obj, accessor.shared.predefined),
Object.assign(conf, {multiple: false})
)
accessor.exchange(endpoint)
}
}
} else if (multiple) {
if (response.config.method.toLowerCase() === 'get') {
accessor.children = []
}
for (let i = 0, l = parsed.length; i < l; i++ ) {
let child = parsed[i]
let endpoint = new Endpoint(
accessor,
accessor.shared.controller,
accessor.shared.defaultApi,
Object.assign(child, accessor.shared.predefined),
Object.assign(conf, {multiple: false})
)
if (response.config.method.toLowerCase() === 'get') {
accessor.children.push(endpoint)
} else {
if (!accessor.exchange(endpoint)) {
accessor.children.push(endpoint)
}
}
}
}
// Parse Headers
if (key === null) {
Object.keys(headers).reduce((prev, key) => {
if (
map !== null &&
typeof map !== 'undefined' &&
typeof map.headers !== 'undefined' &&
typeof map.headers[key] !== 'undefined'
) {
accessor.headers.mapped[map.headers[key]] = headers[key]
} else {
accessor.headers.unmapped[key] = headers[key]
}
}, {})
}
}
resolved = true
resolve(response)
} catch (error) {
// Not valid JSON, go to next parser
}
// @todo - Add additional parsers. Ex. xml
if (!resolved) {
reject(new Error({
error: 'Invalid Data',
message: 'Could not parse data from response',
data: data,
response: response
}))
}
})
}
/**
* Handle Request Error Catching
*/
accessor.shared.handleError = (error) => {
if (axios.isCancel(error)) {
// Manually cancelled
} else if (error.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
// error.response.data
// error.response.status
// error.response.headers
} else if (error.request) {
// The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js
// error.request
} else {
// Something happened in setting up the request that triggered an Error
// error.message
}
// error.config
return error
}
/**
* Handle Request Success Response
*/
accessor.shared.handleSuccess = (response, replace = true, key = null, batch = false, map = accessor.shared.map) => {
let multiple = ((map !== null && typeof map !== 'undefined' && typeof map.multiple !== 'undefined' && map.multiple) || accessor.shared.config.multiple)
return new Promise((resolve, reject) => {
if (replace) {
accessor.shared.handleMapping(response, key, batch, multiple).then(results => {
resolve(results)
}).catch(error => {
reject(error)
})
} else {
resolve(response)
}
})
}
/**
* Exchange endpoint in accessor.children with match from input
* @returns Endpoint (exchanged) | Endpoint.children (On Remove) | false (If no match found)
*/
accessor.exchange = (endpoint, add = true, reliable = false, remove = false /* , map = accessor.shared.map */) => {
// @note - This could be more heavy and alot slower
let smartFind = (endpoint) => {
// Reliable.
// Check for Creation Identifier match.
let exchange = resolveCreationIdentifier(endpoint)
// Reliable.
// Incoming needs all existing props (No differ).
// Existing needs all incoming props (No differ).
if (typeof exchange === 'undefined') {
exchange = findExactMatch(endpoint)
}
// Not reliable, but could be usable anyways
if (!reliable && typeof exchange === 'undefined') {
// @todo - Add resolveByIndex (find response index by request index) method and make it optional in config
// Unreliable.
// Incoming needs all existing props (No differ).
// Existing could have more props.
if (typeof exchange === 'undefined') {
exchange = findExactExistingMatch(endpoint)
}
// Unreliable.
// Existing needs all incoming props (No differ).
// Incoming could have more props.
// Extra incoming props are added to existing props.
if (typeof exchange === 'undefined') {
exchange = findExactIncomingMatch(endpoint)
}
// Unreliable.
// Incoming needs all unchanged props (No differ).
// Existing could have more props.
// Incoming could have more props.
// Existing changes is replaced by incoming changes (Also differed).
// Extra incoming props are added to existing props.
if (typeof exchange === 'undefined') {
exchange = findExactUnchangedMatch(endpoint)
}
// Unreliable.
// Incoming needs all changed props (No differ).
// Existing could have more props.
// Incoming could have more props.
// Existing props is replaced by incoming props (Also differed) if not changed.
// Extra incoming props are added to existing props.
if (typeof exchange === 'undefined') {
exchange = findExactChangedMatch(endpoint)
}
// Unreliable.
// Incoming props which matches existing (No differ).
// Existing could have more props.
// Incoming could have more props.
// Extra incoming props are added to existing props.
if (typeof exchange === 'undefined') {
exchange = findIncomingMatch(endpoint)
}
// Unreliable.
// Incoming props which matches unchanged existing (No differ).
// Existing could have more props.
// Incoming could have more props.
// Existing changes is replaced by incoming changes (Also differed).
// Extra incoming props are added to existing props.
if (typeof exchange === 'undefined') {
exchange = findUnchangedMatch(endpoint)
}
// Unreliable.
// Incoming props which matches changed existing (No differ).
// Existing could have more props.
// Incoming could have more props.
// Existing props is replaced by incoming props (Also differed) if not changed.
// Extra incoming props are added to existing props.
if (typeof exchange === 'undefined') {
exchange = findChangedMatch(endpoint)
}
}
return exchange
}
// Resolve Creation Identifier
let resolveCreationIdentifier = (endpoint) => {
let match
for (let i = 0, l = accessor.children.length; i < l; i++ ) {
let child = accessor.children[i]
if (
child.shared.map !== null &&
typeof child.shared.map !== 'undefined' &&
typeof child.shared.map.creationIdentifier !== 'undefined' &&
typeof child.shared.creationIdentifier !== 'undefined' &&
child.shared.creationIdentifier !== ''
) {
let identifier = child.shared.map.creationIdentifier
let prop = identifier.split('=')[0]
if (!endpoint.reserved(prop)) {
if (
typeof endpoint[prop] !== 'undefined' &&
typeof endpoint[prop].value !== 'undefined' &&
JSON.stringify(endpoint[prop].value).indexOf(child.shared.creationIdentifier) !== -1
) {
if (!endpoint.reserved(child.identifier.key)) {
child.identifier.value = endpoint[child.identifier.key].value
} else {
child.identifier.value = endpoint.invalids[child.identifier.key].value
}
match = child
}
} else {
if (
typeof endpoint.invalids[prop] !== 'undefined' &&
typeof endpoint.invalids[prop].value !== 'undefined' &&
JSON.stringify(endpoint.invalids[prop].value).indexOf(child.shared.creationIdentifier) !== -1
) {
if (!endpoint.reserved(child.identifier.key)) {
child.identifier.value = endpoint[child.identifier.key].value
} else {
child.identifier.value = endpoint.invalids[child.identifier.key].value
}
match = child
}
}
}
}
return match
}
// Find exact match by all props (Reliable)
let findExactMatch = (endpoint) => {
let exchange = accessor.children.find(child => {
if (child.identifier === null || child.identifier.value === null) {
let props = child.props()
let endpointProps = endpoint.props()
let match = true // Expect match
Object.keys(props).reduce((prev, key) => {
if (typeof endpointProps[key] !== 'undefined') {
if (JSON.stringify(props[key]) !== JSON.stringify(endpointProps[key])) {
// If unchanged props doesnt match, set match to false
match = false
}
} else {
match = false
}
}, {})
Object.keys(endpointProps).reduce((prev, key) => {
if (typeof props[key] === 'undefined') {
match = false
}
}, {})
return match
} else {
return false
}
})
return exchange
}
// Find exact match by all exisiting props (Reliable)
let findExactExistingMatch = (endpoint) => {
let exchange = accessor.children.find(child => {
if (child.identifier === null || child.identifier.value === null) {
let props = child.props()
let endpointProps = endpoint.props()
let match = true // Expect match
Object.keys(props).reduce((prev, key) => {
if (typeof endpointProps[key] !== 'undefined') {
if (JSON.stringify(props[key]) !== JSON.stringify(endpointProps[key])) {
// If unchanged props doesnt match, set match to false
match = false
}
} else {
match = false
}
}, {})
return match
} else {
return false
}
})
return exchange
}
// Find exact match by unchanged props (Less Reliable - Requires incoming props to exist)
let findExactUnchangedMatch = (endpoint) => {
let exchange = accessor.children.find(child => {
if (child.identifier === null || child.identifier.value === null) {
let changes = child.changes()
let props = child.props()
let endpointProps = endpoint.props()
let match = true // Expect match
Object.keys(props).reduce((prev, key) => {
// If not changed prop
if (typeof changes[key] === 'undefined') {
// If new child has same prop
if (typeof endpointProps[key] !== 'undefined') {
// If they do not match
if (JSON.stringify(props[key]) !== JSON.stringify(endpointProps[key])) {
// Skip this
match = false
}
// If incoming prop doesnt exist
} else {
// Skip this
match = false
}
}
}, {})
return match
} else {
return false
}
})
return exchange
}
// Find exact match by changed props (Less Reliable - Requires incoming props to exist)
let findExactChangedMatch = (endpoint) => {
let exchange = accessor.children.find(child => {
if (child.identifier === null || child.identifier.value === null) {
let changes = child.changes()
let props = child.props()
let endpointProps = endpoint.props()
let match = true // Expect match
Object.keys(props).reduce((prev, key) => {
// If changed prop
if (typeof changes[key] !== 'undefined') {
// If new child has same prop
if (typeof endpointProps[key] !== 'undefined') {
// If they do not match
if (JSON.stringify(props[key]) !== JSON.stringify(endpointProps[key])) {
// Skip this
match = false
}
// If incoming prop doesnt exist
} else {
// Skip this
match = false
}
}
}, {})
return match
} else {
return false
}
})
return exchange
}
// Find match by unchanged props (Less Reliable - Doesnt require incoming props to exist)
let findUnchangedMatch = (endpoint) => {
let exchange = accessor.children.find(child => {
if (child.identifier === null || child.identifier.value === null) {
let changes = child.changes()
let props = child.props()
let endpointProps = endpoint.props()
let match = true // Expect match
Object.keys(props).reduce((prev, key) => {
// If not changed prop
if (typeof changes[key] === 'undefined') {
// If new child has same prop
if (typeof endpointProps[key] !== 'undefined') {
// If they do not match
if (JSON.stringify(props[key]) !== JSON.stringify(endpointProps[key])) {
// Skip this
match = false
}
}
}
}, {})
return match
} else {
return false
}
})
return exchange
}
// Find match by changed props (Less Reliable - Doesnt require incoming props to exist)
let findChangedMatch = (endpoint) => {
let exchange = accessor.children.find(child => {
if (child.identifier === null || child.identifier.value === null) {
let changes = child.changes()
let props = child.props()
let endpointProps = endpoint.props()
let match = true // Expect match
Object.keys(props).reduce((prev, key) => {
// If changed prop
if (typeof changes[key] !== 'undefined') {
// If new child has same prop
if (typeof endpointProps[key] !== 'undefined') {
// If they do not match
if (JSON.stringify(props[key]) !== JSON.stringify(endpointProps[key])) {
// Skip this
match = false
}
}
}
}, {})
return match
} else {
return false
}
})
return exchange
}
// Find exact match by incoming props (Less Reliable - Requires existing props to have all incoming props)
let findExactIncomingMatch = (endpoint) => {
let exchange = accessor.children.find(child => {
if (child.identifier === null || child.identifier.value === null) {
let props = child.props()
let endpointProps = endpoint.props()
let match = true // Expect match
Object.keys(endpointProps).reduce((prev, key) => {
if (typeof props[key] !== 'undefined') {
if (JSON.stringify(props[key]) !== JSON.stringify(endpointProps[key])) {
// If props are unique
match = false
}
} else {
// If existing doesnt have all incoming props
match = false
}
}, {})
return match
} else {
return false
}
})
return exchange
}
// Find match by incoming props (Less Reliable - Doesnt require existing props to have all incoming props)
let findIncomingMatch = (endpoint) => {
let exchange = accessor.children.find(child => {
if (child.identifier === null || child.identifier.value === null) {
let props = child.props()
let endpointProps = endpoint.props()
let match = true // Expect match
Object.keys(endpointProps).reduce((prev, key) => {
if (typeof props[key] !== 'undefined') {
if (JSON.stringify(props[key]) !== JSON.stringify(endpointProps[key])) {
// If props are unique
match = false
}
}
}, {})
return match
} else {
return false
}
})
return exchange
}
let exchange
if (endpoint.identifier !== null) {
exchange = accessor.children.find(child => {
return (typeof child.identifier !== 'undefined' && typeof endpoint.identifier !== 'undefined' && child.identifier !== null && child.identifier.value === endpoint.identifier.value)
})
if (typeof exchange === 'undefined' || exchange === false) {
exchange = smartFind(endpoint)
}
} else {
exchange = smartFind(endpoint)
}
if (typeof exchange !== 'undefined' && !remove) {
// Handle Exchange
return exchange.set(endpoint, false)
} else if (!remove) {
// If no match found but add by force, push to children
if (add) {
accessor.children.push(endpoint)
}
return false
} else if (typeof exchange !== 'undefined') {
// Handle Remove
let index = accessor.children.indexOf(exchange)
if (index !== -1) {
accessor.children.splice(index, 1)
}
return accessor.children
} else {
return false
}
}
/**
* Make Any Request
*/
accessor.shared.makeRequest = (
canceler,
method,
apiSlug = accessor.shared.defaultApi,
args = null,
data = null,
upload = false,
conf = {},
promise = new Promise(resolve => resolve()),
batch = false
) => {
// Custom Request Config (config level 3 - greater is stronger)
conf = Object.assign(clone({}, accessor.shared.config), conf)
if (canceler !== false) {
let cancelHandler = accessor.shared.handleCancellation(cancelers[canceler])
cancelers[canceler] = cancelHandler.cancellation
promise = cancelHandler.promise
}
return new Promise((resolve, reject) => {
// startLoader(method)
let api = (accessor.shared.controller !== null && apiSlug !== null) ? accessor.shared.controller.apis[apiSlug] : accessor.shared.api
accessor.shared.requester[method.toLowerCase()](accessor.shared.resolveUrl(accessor.shared.endpoint, accessor.shared.map, api, args, batch), promise, data, upload, conf).then(response => {
accessor.raw = response
// stopLoader(method)
resolve(response)
}).catch(error => {
// stopLoader(method)
reject(accessor.shared.handleError(error))
})
})
}
accessor.shared.identifier = () => {
let identifier = null
if (accessor.identifier !== null && typeof accessor.identifier !== 'undefined' && accessor.identifier.key !== null) {
if (!accessor.reserved(accessor.identifier.key)) {
identifier = accessor[accessor.identifier.key]
} else {
identifier = accessor.invalids[accessor.identifier.key]
}
}
return identifier
}
/**
* Public / Reserved Method Names
* @warning Can not be used as a property name in models
* ---------------
* Query builder (Create arguments, and make endpoints default fetch method available afterwards)
*/
accessor.query = () => {
return new Query(accessor)
}
/**
* Request Fetch @note - Related to Properties
*/
accessor.fetch = (
apiSlug = accessor.shared.defaultApi,
args = accessor.args.fetch,
replace = true,
perform = true
) => {
return new Promise((resolve, reject) => {
let loadSlug = 'fetch'
startLoader(loadSlug)
accessor.shared.makeRequest(
loadSlug,
'GET',
apiSlug,
args,
null,
false,
{
perform: perform
}
).then(response => {
accessor.shared.handleSuccess(response, replace).then(() => {
stopLoader(loadSlug)
resolve(accessor)
}).catch(error => {
stopLoader(loadSlug)
reject(error)
})
}).catch(error => {
stopLoader(loadSlug)
reject(error)
})
})
}
/**
* Request Save @note - Saves all changed Properties
* @apiSlug Use custom api by slug
* @args Custom arguments as object (key: value)
* @replace replace all properties in endpoint from response
* @create Attempt to create if save fails (Ex.: if no id provided to endpoint)
*/
accessor.save = (
apiSlug = accessor.shared.defaultApi,
args = accessor.args.save,
replace = true,
create = true,
perform = true,
map = accessor.shared.map
) => {
if ((map !== null && typeof map !== 'undefined' && typeof map.multiple !== 'undefined' && map.multiple) || accessor.shared.config.multiple) {
return accessor.batch({create: create}, apiSlug, args, replace, map)
} else {
return new Promise((resolve, reject) => {
let loadSlug = 'save'
startLoader(loadSlug)
let identifier = accessor.shared.identifier()
if (identifier !== null && identifier.value === null) {
accessor.create(apiSlug, args, replace, true, perform).then(() => {
stopLoader(loadSlug)
resolve(accessor)
}).catch(error => {
stopLoader(loadSlug)
reject(error)
})
} else {
accessor.shared.makeRequest(
loadSlug,
'PUT',
apiSlug,
args,
accessor.removeIdentifiers(
accessor.reverseMapping(
accessor.changes(false, false, true)
)
),
false,
{
perform: perform
}
).then(response => {
accessor.shared.handleSuccess(response, replace).then(() => {
stopLoader(loadSlug)
resolve(accessor)
}).catch(error => {
stopLoader(loadSlug)
reject(error)
})
}).catch(error => {
// If could not save, try create
if (create) {
accessor.create(apiSlug, args, replace, true, perform).then(() => {
stopLoader(loadSlug)
resolve(accessor)
}).catch(error => {
stopLoader(loadSlug)
reject(error)
})
} else {
stopLoader(loadSlug)
reject(error)
}
})
}
})
}
}
/**
* Request Create @note - Saves all Properties
*/
accessor.create = (
apiSlug = accessor.shared.defaultApi,
args = accessor.args.create,
replace = true,
save = true,
perform = true,
map = accessor.shared.map
) => {
if ((map !== null && typeof map !== 'undefined' && typeof map.multiple !== 'undefined' && map.multiple) || accessor.shared.config.multiple) {
return accessor.batch({save: save}, apiSlug, args, replace, perform, map)
} else {
return new Promise((resolve, reject) => {
let withEmpty = accessor.removeIdentifiers(accessor.reverseMapping())
let data = {}
if (!accessor.shared.config.post.keepNull) {
Object.keys(withEmpty).reduce((prev, key) => {
if (withEmpty[key] !== null) {
data[key] = withEmpty[key]
}
}, {})
} else {
data = withEmpty
}
let loadSlug = 'create'
startLoader(loadSlug)
accessor.shared.makeRequest(
loadSlug,
'POST',
apiSlug,
args,
data,
false,
{
perform: perform
}
).then(response => {
accessor.shared.handleSuccess(response, replace).then(() => {
stopLoader(loadSlug)
resolve(accessor)
}).catch(error => {
stopLoader(loadSlug)
reject(error)
})
}).catch(error => {
stopLoader(loadSlug)
reject(error)
})
})
}
}
/**
* Request Remove @note - Related to Properties
*/
accessor.remove = (
apiSlug = accessor.shared.defaultApi,
args = accessor.args.remove,
replace = true,
perform = true,
map = accessor.shared.map
) => {
if ((map !== null && typeof map !== 'undefined' && typeof map.multiple !== 'undefined' && map.multiple) || accessor.shared.config.multiple) {
return accessor.batch({save: false, create: false, delete: true}, apiSlug, args, replace, map)
} else {
return new Promise((resolve, reject) => {
let loadSlug = 'remove'
startLoader(loadSlug)
accessor.shared.makeRequest(
loadSlug,
'DELETE',
apiSlug,
args,
null,
false,
{
perform: perform
}
).then(response => {
if (typeof response !== 'undefined') {
accessor.shared.handleSuccess(response, replace).then(() => {
stopLoader(loadSlug)
resolve(accessor)
}).catch(error => {
stopLoader(loadSlug)
reject(error)
})
} else {
resolve(accessor)
}
}).catch(error => {
stopLoader(loadSlug)
if (error.response && error.response.status === 410) {
// Already deleted (Gone)
resolve(accessor)
} else {
reject(error)
}
})
})
}
}
/**
* Request Upload @note - Related to Properties
* @note: batch upload not yet supported
*/
accessor.upload = (
file,
apiSlug = accessor.shared.defaultApi,
args = accessor.args.upload,
replace = true,
perform = true,
method = 'POST',
map = false
) => {
return new Promise((resolve, reject) => {
let loadSlug = 'upload'
startLoader(loadSlug)
accessor.shared.makeRequest(
loadSlug,
method,
apiSlug,
args,
file,
true,
{
perform: perform
}
).then(response => {
if (map) {
accessor.shared.handleSuccess(response, replace).then(() => {
stopLoader(loadSlug)
resolve(accessor)
}).catch(error => {
stopLoader(loadSlug)
reject(error)
})
} else {
stopLoader(loadSlug)
resolve(response)
}
}).catch(error => {
stopLoader(loadSlug)
reject(error)
})
})
}
/**
* Request Batch @note - Updates all children
*/
accessor.batch = (
options = {},
apiSlug = accessor.shared.defaultApi,
args = accessor.args.batch,
replace = true,
perform = true,
map = accessor.shared.map
) => {
options = Object.assign({
create: true,
save: true,
delete: false,
merge: false, // Merge with parent props if any (usually there is none)
limit: 100, // Split requests into limited amount of children
from: 0 // Exclude children before index from request
}, options)
let data = {}
// Handle create
if (options.create) {
let hook = (map !== null && typeof map !== 'undefined' && typeof map.batch !== 'undefined' && typeof map.batch.create !== 'undefined') ? map.batch.create : 'create'
data[hook] = []
for (let i = 0, l = accessor.children.length; i < l; i++ ) {
let child = accessor.children[i]
if (i >= options.from && i < (options.from + options.limit)) {
// Create Creation Identifier
if (child.identifier === null || child.identifier.value === null) {
if (child.shared.map !== null && typeof child.shared.map !== 'undefined' && typeof child.shared.map.creationIdentifier !== 'undefined' && child.shared.map.creationIdentifier !== '') {
let identifier = child.shared.map.creationIdentifier
let prop = identifier.split('=')[0]
let val = identifier.substring((prop.length + 1))
// Resolve mapping
if (typeof child.shared.map.props !== 'undefined' && typeof child.shared.map.props[prop] !== 'undefined') {
prop = child.shared.map.props[prop]
}
// Check if property exist and make reference to prop
if (!child.reserved(prop) && typeof child[prop] !== 'undefined') {
prop = child[prop]
} else if (child.reserved(prop) && typeof child.invalids[prop] !== 'undefined') {
prop = child.invalids[prop]
} else {
// Create prop if not exist
if (!child.reserved(prop)) {
prop = child[prop] = new Prop(child, prop)
} else {
prop = child.invalids[prop] = new Prop(child, prop)
}
}
// Generate creation identifier
let id = Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 15)
child.shared.creationIdentifier = id
if (val.length > 0) {
val = JSON.parse(val.replace('identifier', id))
if (prop.value === null) {
prop.value = val
} else {
if (prop.value.constructor === Array && val.constructor === Array) {
prop.value = prop.value.concat(val)
} else if (prop.value.constructor !== Array && val.constructor !== Array) {
prop.value = Object.assign(prop.value, val)
}
}
} else {
prop.value = id
}
}
let withEmpty = child.removeIdentifiers(child.reverseMapping(child.props(false, true)))
let results = {}
if (!accessor.shared.config.post.keepNull) {
Object.keys(withEmpty).reduce((prev, key) => {
if (withEmpty[key] !== null) {
results[key] = withEmpty[key]
}
}, {})
} else {
results = withEmpty
}
data[hook].push(results)
}
}
}
}
// Handle save
if (options.save) {
let hook = (map !== null && typeof map !== 'undefined' && typeof map.batch !== 'undefined' && typeof map.batch.save !== 'undefined') ? map.batch.save : 'save'
data[hook] = []
for (let i = 0, l = accessor.children.length; i < l; i++ ) {
let child = accessor.children[i]
if (i >= options.from && i < (options.from + options.limit)) {
if (child.identifier !== null && child.identifier.value !== null) {
// If endpoint has identifier, secure that identifier is added for update and only post changes
let obj = child.changes(false, false, true)
obj[child.identifier.key] = child.identifier.value
data[hook].push(accessor.reverseMapping(obj))
} else if (child.identifier === null) {
// If endpoint has no identifier, add the whole child, and not only props
data[hook].push(accessor.reverseMapping(child.props(false, true)))
}
}
}
}
// Handle delete
if (options.delete) {
let hook = (map !== null && typeof map !== 'undefined' && typeof map.batch !== 'undefined' && typeof map.batch.delete !== 'undefined') ? map.batch.delete : 'delete'
data[hook] = []
for (let i = 0, l = accessor.children.length; i < l; i++ ) {
let child = accessor.children[i]
if (i >= options.from && i < (options.from + options.limit)) {
if (child.identifier !== null && child.identifier.value !== null) {
// If endpoint has identifier only add id to array
data[