/* building a model by postgrest * * a model is simply a cache layer between client * and the database behind an api * * options: { * endpoint: 'resource location', * selects: [ * { * 'label': 'column name', * 'alias': 'displayed name', * 'processor': v => process(v) * } * ] * } */ export default (options={}) => { // TODO // 1. sigular or prural // sanity checks if (typeof options.api == 'undefined' || typeof options.endpoint == 'undefined') throw 'api and endpoint are required options' // private model configs let _configs = { // url part api: options.api, endpoint: options.endpoint, endpoint_type: options.endpoint.startsWith('/rpc') ? 'function' : 'relation', // query part selects: options.selects, order: options.order } // private model cache & meta-info let _cache = { data: null, count: null, upstream_limit: null } // some random variables to make things work let _xhr = null let _promise = null // construct the model return { // reflection methods configs() { return _configs }, cache() { return _cache }, data(offset=0, limit=Infinity) { return _cache.data && _cache.data.slice(offset, offset+limit) || [] }, // main methods select(offset=0, limit=Infinity) { // initialize data (singular or plural) _cache.data = _cache.data || [] // normalize limit if (limit == Infinity) limit = _cache.upstream_limit || _cache.count || Infinity // be lazy 1: if the data is presented, return the value immediately if (this.data(offset, limit).length > 0 && !this.data(offset, limit).includes(undefined)) return Promise.resolve(this.data(offset, limit)) // be lazy 2: if there is a promise, return it if ambient matches or // cancel it if (_promise != null) return _promise // now the hard work _promise = _configs.api.request({ method: 'GET', url: _configs.endpoint, headers: _cache.count == null ? { Prefer: 'count=exact' } : {}, config: xhr => _xhr = xhr, queries: [ // transform model state to postgest queries //...ambient_queries, // selects ...(_configs.selects ? [{ label: 'select', value: _configs.selects.map(select => select.alias ? select.alias + ':' + select.label : select.label).join(',') }] : []), // order ...(_configs.order ? [{ label: 'order', value: _configs.order .map(o => [ o.label, o.direction, o.nulls ? 'nulls'+o.nulls : '' ].filter(a => a).join('.')) .join(',') }] : []), // limit/offset offset == 0 ? undefined : {label: 'offset', value: offset}, limit == Infinity ? undefined : {label: 'limit', value: limit} ] }).then(response => { // gather begin/end/count let [_range, _count] = _xhr.getResponseHeader('content-range').split('/') let [_begin, _end] = _range.split('-').map(v => ~~v) // update count if presented if (_count != '*') _cache.count = _count // see if an upstream limit is exposed if (_end - _begin + 1 < limit) _cache.upstream_limit = _end - _begin + 1 // fill the data cache response.forEach((data, i) => { // process values _configs.selects && _configs.selects .filter(select => select.processor) .forEach(select => data[select.alias || select.label] = select.processor(data[select.alias || select.label])) // save the data _cache.data[_begin + i] = data }) // assert offset/limit and returned range if (offset != _begin || _end - _begin + 1 > limit) throw 'The request and response data range mismatches!' if (_end - _begin + 1 < limit) console.warn('The response range is narrower than requested, probably due to an upstream hard limit.') // clean model state _promise = null // return data return _cache.data.slice(_begin, _end + 1) }) return _promise } } } export const default1 = (args) => { // private model configs let _configs = { queries: args.queries || [], } return { // model & model state fully_loaded: false, // ambient ambient: null, last_ambient: null, ambient_changed() { return this.ambient != this.last_ambient }, reset() { // assemble queries for api.request let ambient_queries = [ ...(_configs.selects ? [{label: 'select', value: Array.from(_configs.selects, ([label, config]) => config.alias ? config.alias + ':' + label : label).join(',')}] : []), ...(_configs.queries ? _configs.queries.map(query => ({label: query.label, op: query.op, value: typeof query.value == 'function' ? query.value() : query.value})) : []), ...(_configs.order ? [{label: 'order', value: _configs.order.map(o => [o.label, o.direction, o.nulls ? 'nulls'+o.nulls : ''].filter(a => a).join('.')).join(',')}] : []) ] this.last_ambient = this.ambient this.ambient =JSON.stringify(ambient_queries) if (this.ambient_changed()) { this.list = [] this.count = null this.fully_loaded = false this.xhr = null } return ambient_queries }, // load data load(args) { let ambient_queries = this.reset() // now the hard work }, // load full model if privileged load_full() { this.reset() if (this.fully_loaded) return Promise.resolve(this.list) return api.request({ method: 'POST', url: '/rpc/query_all', body: { endpoint: _configs.endpoint, selects: Array.from(_configs.selects, ([label, config]) => ({label: label, alias: config.alias})), queries: _configs.queries ? _configs.queries.map(query => ({label: query.label, op: query.op, value: typeof query.value == 'function' ? query.value() : query.value})) : [], order: _configs.order } }).then(response => { this.list = response.map(data => { _value_filters.forEach(([label, config]) => data[config.alias || label] = config.value_filter(data[config.alias || label])) return data }) this.fully_loaded = true return this.list }) } } }