You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

216 line
5.9 KiB

  1. /* building a model by postgrest
  2. *
  3. * a model is simply a cache layer between client
  4. * and the database behind an api
  5. *
  6. * options: {
  7. * endpoint: 'resource location',
  8. * selects: [
  9. * {
  10. * 'label': 'column name',
  11. * 'alias': 'displayed name',
  12. * 'processor': v => process(v)
  13. * }
  14. * ]
  15. * }
  16. */
  17. export default (options={}) => {
  18. // TODO
  19. // 1. sigular or prural
  20. // sanity checks
  21. if (typeof options.api == 'undefined' || typeof options.endpoint == 'undefined')
  22. throw 'api and endpoint are required options'
  23. // private model configs
  24. let _configs = {
  25. // url part
  26. api: options.api,
  27. endpoint: options.endpoint,
  28. endpoint_type: options.endpoint.startsWith('/rpc') ? 'function' : 'relation',
  29. // query part
  30. selects: options.selects,
  31. order: options.order
  32. }
  33. // private model cache & meta-info
  34. let _cache = {
  35. data: null,
  36. count: null,
  37. upstream_limit: null
  38. }
  39. // some random variables to make things work
  40. let _xhr = null
  41. let _promise = null
  42. // construct the model
  43. return {
  44. // reflection methods
  45. configs() { return _configs },
  46. cache() { return _cache },
  47. data(offset=0, limit=Infinity) { return _cache.data && _cache.data.slice(offset, offset+limit) || [] },
  48. // main methods
  49. select(offset=0, limit=Infinity) {
  50. // initialize data (singular or plural)
  51. _cache.data = _cache.data || []
  52. // normalize limit
  53. if (limit == Infinity)
  54. limit = _cache.upstream_limit || _cache.count || Infinity
  55. // be lazy 1: if the data is presented, return the value immediately
  56. if (this.data(offset, limit).length > 0 && !this.data(offset, limit).includes(undefined))
  57. return Promise.resolve(this.data(offset, limit))
  58. // be lazy 2: if there is a promise, return it if ambient matches or
  59. // cancel it
  60. if (_promise != null)
  61. return _promise
  62. // now the hard work
  63. _promise = _configs.api.request({
  64. method: 'GET',
  65. url: _configs.endpoint,
  66. headers: _cache.count == null ? {
  67. Prefer: 'count=exact'
  68. } : {},
  69. config: xhr => _xhr = xhr,
  70. queries: [
  71. // transform model state to postgest queries
  72. //...ambient_queries,
  73. // selects
  74. ...(_configs.selects ? [{
  75. label: 'select',
  76. value: _configs.selects.map(select => select.alias ? select.alias + ':' + select.label : select.label).join(',')
  77. }] : []),
  78. // order
  79. ...(_configs.order ? [{
  80. label: 'order',
  81. value: _configs.order
  82. .map(o => [
  83. o.label,
  84. o.direction,
  85. o.nulls ? 'nulls'+o.nulls : ''
  86. ].filter(a => a).join('.'))
  87. .join(',')
  88. }] : []),
  89. // limit/offset
  90. offset == 0 ? undefined : {label: 'offset', value: offset},
  91. limit == Infinity ? undefined : {label: 'limit', value: limit}
  92. ]
  93. }).then(response => {
  94. // gather begin/end/count
  95. let [_range, _count] = _xhr.getResponseHeader('content-range').split('/')
  96. let [_begin, _end] = _range.split('-').map(v => ~~v)
  97. // update count if presented
  98. if (_count != '*') _cache.count = _count
  99. // see if an upstream limit is exposed
  100. if (_end - _begin + 1 < limit) _cache.upstream_limit = _end - _begin + 1
  101. // fill the data cache
  102. response.forEach((data, i) => {
  103. // process values
  104. _configs.selects && _configs.selects
  105. .filter(select => select.processor)
  106. .forEach(select => data[select.alias || select.label] = select.processor(data[select.alias || select.label]))
  107. // save the data
  108. _cache.data[_begin + i] = data
  109. })
  110. // assert offset/limit and returned range
  111. if (offset != _begin || _end - _begin + 1 > limit)
  112. throw 'The request and response data range mismatches!'
  113. if (_end - _begin + 1 < limit)
  114. console.warn('The response range is narrower than requested, probably due to an upstream hard limit.')
  115. // clean model state
  116. _promise = null
  117. // return data
  118. return _cache.data.slice(_begin, _end + 1)
  119. })
  120. return _promise
  121. }
  122. }
  123. }
  124. export const default1 = (args) => {
  125. // private model configs
  126. let _configs = {
  127. queries: args.queries || [],
  128. }
  129. return {
  130. // model & model state
  131. fully_loaded: false,
  132. // ambient
  133. ambient: null,
  134. last_ambient: null,
  135. ambient_changed() {
  136. return this.ambient != this.last_ambient
  137. },
  138. reset() {
  139. // assemble queries for api.request
  140. let ambient_queries = [
  141. ...(_configs.selects ? [{label: 'select', value: Array.from(_configs.selects, ([label, config]) => config.alias ? config.alias + ':' + label : label).join(',')}] : []),
  142. ...(_configs.queries ? _configs.queries.map(query => ({label: query.label, op: query.op, value: typeof query.value == 'function' ? query.value() : query.value})) : []),
  143. ...(_configs.order ? [{label: 'order', value: _configs.order.map(o => [o.label, o.direction, o.nulls ? 'nulls'+o.nulls : ''].filter(a => a).join('.')).join(',')}] : [])
  144. ]
  145. this.last_ambient = this.ambient
  146. this.ambient =JSON.stringify(ambient_queries)
  147. if (this.ambient_changed()) {
  148. this.list = []
  149. this.count = null
  150. this.fully_loaded = false
  151. this.xhr = null
  152. }
  153. return ambient_queries
  154. },
  155. // load data
  156. load(args) {
  157. let ambient_queries = this.reset()
  158. // now the hard work
  159. },
  160. // load full model if privileged
  161. load_full() {
  162. this.reset()
  163. if (this.fully_loaded)
  164. return Promise.resolve(this.list)
  165. return api.request({
  166. method: 'POST',
  167. url: '/rpc/query_all',
  168. body: {
  169. endpoint: _configs.endpoint,
  170. selects: Array.from(_configs.selects, ([label, config]) => ({label: label, alias: config.alias})),
  171. queries: _configs.queries ? _configs.queries.map(query => ({label: query.label, op: query.op, value: typeof query.value == 'function' ? query.value() : query.value})) : [],
  172. order: _configs.order
  173. }
  174. }).then(response => {
  175. this.list = response.map(data => {
  176. _value_filters.forEach(([label, config]) => data[config.alias || label] = config.value_filter(data[config.alias || label]))
  177. return data
  178. })
  179. this.fully_loaded = true
  180. return this.list
  181. })
  182. }
  183. }
  184. }