|
- /* 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
- })
- }
- }
- }
|