|
1 | | -/* global self */ |
2 | | -import { sorter, select, getLimit, AdapterBase } from '@feathersjs/adapter-commons' |
3 | | -import { _ } from '@feathersjs/commons' |
4 | | -import errors from '@feathersjs/errors' |
5 | | -import LocalForage from 'localforage' |
6 | | -import sift from 'sift' |
7 | | -import makeDebug from 'debug' |
8 | | -import { stringsToDates } from './strings-to-dates.js' |
9 | | - |
10 | | -export { default as LocalForage } from 'localforage' |
11 | | - |
12 | | -const debug = makeDebug('@feathersjs-offline:feathers-localforage') |
13 | | -const usedKeys = [] |
14 | | - |
15 | | -const _select = (data, params, ...args) => { |
16 | | - const base = select(params, ...args) |
17 | | - |
18 | | - return base(JSON.parse(JSON.stringify(data))) |
19 | | -} |
20 | | - |
21 | | -const validDrivers = { |
22 | | - INDEXEDDB: LocalForage.INDEXEDDB, |
23 | | - WEBSQL: LocalForage.WEBSQL, |
24 | | - LOCALSTORAGE: LocalForage.LOCALSTORAGE |
25 | | -} |
26 | | - |
27 | | -// Create the adapter |
28 | | -class Adapter extends AdapterBase { |
29 | | - constructor (options = {}) { |
30 | | - super(_.extend({ |
31 | | - id: 'id', |
32 | | - matcher: sift, |
33 | | - sorter |
34 | | - }, options)) |
35 | | - |
36 | | - this.store = options.store || {} |
37 | | - this._dates = options.dates || false |
38 | | - |
39 | | - this.sanitizeParameters(options) |
40 | | - |
41 | | - debug(`Constructor started: |
42 | | -\t_storageType = ${JSON.stringify(this._storageType)} |
43 | | -\t_version = ${JSON.stringify(this._version)} |
44 | | -\t_name = ${JSON.stringify(this._name)} |
45 | | -\t_storageKey = ${JSON.stringify(this._storageKey)} |
46 | | -\t_storageSize = ${JSON.stringify(this._storageSize)} |
47 | | -\t_reuseKeys = ${JSON.stringify(this._reuseKeys)}\n`) |
48 | | - |
49 | | - this._storage = LocalForage.createInstance({ |
50 | | - driver: this._storageType, |
51 | | - name: this._name, |
52 | | - size: this._storageSize, |
53 | | - version: this._version, |
54 | | - storeName: this._storageKey, |
55 | | - description: 'Created by @feathersjs-offline/localforage' |
56 | | - }) |
57 | | - |
58 | | - this.checkStoreName() |
59 | | - |
60 | | - // Make a handy suffix primarily for debugging owndata/ownnet |
61 | | - const self = this |
62 | | - this._debugSuffix = self._name.includes('_local') |
63 | | - ? ' LOCAL' |
64 | | - : (self._name.includes('_queue') ? ' QUEUE' : '') |
65 | | - |
66 | | - this.ready() |
67 | | - } |
68 | | - |
69 | | - sanitizeParameters (options) { |
70 | | - this._name = options.name || 'feathersjs-offline' |
71 | | - this._storageKey = options.storeName || options.name || 'feathers' |
72 | | - |
73 | | - let storage = this.options.storage || 'LOCALSTORAGE' |
74 | | - storage = Array.isArray(storage) ? storage : [storage] |
75 | | - const ok = storage.reduce((value, s) => value && (s.toUpperCase() in validDrivers), true) |
76 | | - if (!ok) { throw new errors.NotAcceptable(`Unknown storage type specified '${this.options.storage}\nPlease use one (or more) of 'websql', 'indexeddb', or 'localstorage'.`) } |
77 | | - |
78 | | - this._storageType = storage.map(s => validDrivers[s.toUpperCase()]) |
79 | | - |
80 | | - this._version = options.version || 1.0 |
81 | | - |
82 | | - // Default DB size is _JUST UNDER_ 5MB, as it's the highest size we can use without a prompt. |
83 | | - this._storageSize = options.storageSize || 4980736 |
84 | | - this._reuseKeys = options.reuseKeys || false |
85 | | - |
86 | | - this._id = options.startId || 0 |
87 | | - } |
88 | | - |
89 | | - checkStoreName () { |
90 | | - if (usedKeys.indexOf(this._storageKey) === -1) { |
91 | | - usedKeys.push(this._storageKey) |
92 | | - } else { |
93 | | - if (!this._reuseKeys) { // Allow reuse if options.reuseKeys set to true |
94 | | - throw new errors.Forbidden(`The storage name '${this._storageKey}' is already in use by another instance.`) |
95 | | - } |
96 | | - } |
97 | | - } |
98 | | - |
99 | | - async ready () { |
100 | | - const self = this |
101 | | - // Now pre-load data (if any) |
102 | | - const keys = Object.keys(this.store) |
103 | | - await Promise.all([ |
104 | | - keys.forEach(key => { |
105 | | - let id = self.store[key][self.id] |
106 | | - id = self.setMax(id) |
107 | | - return self.getModel().setItem(String(id), self.store[key]) |
108 | | - } |
109 | | - )]) |
110 | | - } |
111 | | - |
112 | | - getModel () { |
113 | | - return this._storage |
114 | | - } |
115 | | - |
116 | | - async getEntries (params = {}) { |
117 | | - debug(`getEntries(${JSON.stringify(params)})` + this._debugSuffix) |
118 | | - |
119 | | - return this._find({ |
120 | | - ...params, |
121 | | - paginate: false |
122 | | - }) |
123 | | - .then(select(params, this.id)) |
124 | | - .then(stringsToDates(this._dates)) |
125 | | - } |
126 | | - |
127 | | - getQuery (params) { |
128 | | - const options = this.getOptions(params) |
129 | | - const { $skip, $sort, $limit, $select, ...query } = params.query || {} |
130 | | - |
131 | | - return { |
132 | | - query, |
133 | | - filters: { $skip, $sort, $limit: getLimit($limit, options.paginate), $select } |
134 | | - } |
135 | | - } |
136 | | - |
137 | | - setMax (id) { |
138 | | - if (Number.isInteger(id)) { |
139 | | - this._id = Math.max(Number.parseInt(id), this._id) |
140 | | - } |
141 | | - return id |
142 | | - } |
143 | | - |
144 | | - async _find (params = {}) { |
145 | | - debug(`_find(${JSON.stringify(params)})` + this._debugSuffix) |
146 | | - const self = this |
147 | | - const { paginate } = this.getOptions(params) |
148 | | - const { query, filters } = self.getQuery(params) |
149 | | - let keys = await self.getModel().keys() |
150 | | - |
151 | | - // An async equivalent of Array.filter() |
152 | | - const asyncFilter = async (arr, predicate) => { |
153 | | - const results = await Promise.all(arr.map(predicate)) |
154 | | - |
155 | | - return arr.filter((_v, index) => results[index]) |
156 | | - } |
157 | | - |
158 | | - // Determine relevant keys |
159 | | - keys = await asyncFilter(keys, async key => { |
160 | | - const item = await self.getModel().getItem(key) |
161 | | - const match = self.options.matcher(query)(item) |
162 | | - return match |
163 | | - }) |
164 | | - |
165 | | - // Now retrieve all values |
166 | | - let values = await Promise.all(keys.map(key => self.getModel().getItem(key))) |
167 | | - const total = values.length |
168 | | - |
169 | | - // Now we sort (if requested) |
170 | | - if (filters.$sort !== undefined) { |
171 | | - values.sort(this.options.sorter(filters.$sort)) |
172 | | - } |
173 | | - |
174 | | - // Skip requested items |
175 | | - if (filters.$skip !== undefined) { |
176 | | - values = values.slice(filters.$skip) |
177 | | - } |
178 | | - |
179 | | - // Limit result to specified (or default) length |
180 | | - if (filters.$limit !== undefined) { |
181 | | - values = values.slice(0, filters.$limit) |
182 | | - } |
183 | | - |
184 | | - // If wanted we convert all ISO string dates to Date objects |
185 | | - values = stringsToDates(this._dates)(values) |
186 | | - |
187 | | - const result = { |
188 | | - total, |
189 | | - limit: filters.$limit, |
190 | | - skip: filters.$skip || 0, |
191 | | - data: values.map(value => _select(value, params, this.id)) |
192 | | - } |
193 | | - |
194 | | - if (!(paginate && paginate.default)) { |
195 | | - debug(`_find res = ${JSON.stringify(result.data)}`) |
196 | | - return result.data |
197 | | - } |
198 | | - |
199 | | - debug(`_find res = ${JSON.stringify(result)}`) |
200 | | - return result |
201 | | - } |
202 | | - |
203 | | - async _get (id, params = {}) { |
204 | | - debug(`_get(${id}, ${JSON.stringify(params)})` + this._debugSuffix) |
205 | | - const self = this |
206 | | - const { query } = this.getQuery(params) |
207 | | - |
208 | | - return this.getModel().getItem(String(id), null) |
209 | | - .catch(err => { throw new errors.NotFound(`No record found for ${this.id} '${id}', err=${err.name} ${err.message}` + this._debugSuffix) }) |
210 | | - .then(item => { |
211 | | - if (item === null) throw new errors.NotFound(`No match for ${this.id} = '${id}', query=${JSON.stringify(query)}` + this._debugSuffix) |
212 | | - |
213 | | - const match = self.options.matcher(query)(item) |
214 | | - if (match) { |
215 | | - return item |
216 | | - } else { |
217 | | - throw new errors.NotFound(`No match for item = ${JSON.stringify(item)}, query=${JSON.stringify(query)}` + this._debugSuffix) |
218 | | - } |
219 | | - }) |
220 | | - .then(select(params, this.id)) |
221 | | - .then(stringsToDates(this._dates)) |
222 | | - } |
223 | | - |
224 | | - async _findOrGet (id, params = {}) { |
225 | | - debug(`_findOrGet(${id}, ${JSON.stringify(params)})` + this._debugSuffix) |
226 | | - if (id === null) { |
227 | | - return this._find(_.extend({}, params, { |
228 | | - paginate: false |
229 | | - })) |
230 | | - } |
231 | | - |
232 | | - return this._get(id, params) |
233 | | - } |
234 | | - |
235 | | - async _create (raw, params = {}) { |
236 | | - if (Array.isArray(raw) && !this.allowsMulti('create', params)) { |
237 | | - throw new errors.MethodNotAllowed('Can not create multiple entries') |
238 | | - } |
239 | | - debug(`_create(${JSON.stringify(raw)}, ${JSON.stringify(params)})` + this._debugSuffix) |
240 | | - |
241 | | - const addId = item => { |
242 | | - const thisId = item[this.id] |
243 | | - |
244 | | - item[this.id] = thisId !== undefined ? this.setMax(thisId) : ++this._id |
245 | | - |
246 | | - return item |
247 | | - } |
248 | | - // Duplicate with generated IDs so that we can track which item had previously one or not |
249 | | - const data = Array.isArray(raw) ? raw.map(item => addId(Object.assign({}, item))) : addId(Object.assign({}, raw)) |
250 | | - // We default to automatically add ID to items without but this can be skipped |
251 | | - const addItemId = (!Object.prototype.hasOwnProperty.call(params, 'addId') || params.addId) |
252 | | - const doOne = (item, indexOrData) => { |
253 | | - // Check if initial data had an ID or not |
254 | | - const originalItem = (typeof indexOrData === 'object' ? indexOrData : raw[indexOrData]) |
255 | | - const hadId = (originalItem[this.id] !== undefined) |
256 | | - return this.getModel().setItem(String(item[this.id]), addItemId || hadId ? item : _.omit(item, [this.id]), null) |
257 | | - .then(() => addItemId || hadId ? item : _.omit(item, [this.id])) |
258 | | - .then(select(params, this.id)) |
259 | | - .then(stringsToDates(this._dates)) |
260 | | - .then(item => { |
261 | | - return item |
262 | | - }) |
263 | | - .catch(err => { |
264 | | - throw new errors.GeneralError(`_create doOne: ERROR: err=${err.name}, ${err.message}`) |
265 | | - }) |
266 | | - } |
267 | | - |
268 | | - return Array.isArray(data) ? Promise.all(data.map(doOne)) : doOne(data, raw) |
269 | | - } |
270 | | - |
271 | | - async _patch (id, data, params = {}) { |
272 | | - if (id === null && !this.allowsMulti('patch', params)) { |
273 | | - throw new errors.MethodNotAllowed('Can not patch multiple entries') |
274 | | - } |
275 | | - debug(`_patch(${id}, ${JSON.stringify(data)}, ${JSON.stringify(params)})` + this._debugSuffix) |
276 | | - const self = this |
277 | | - const items = await this._findOrGet(id, params) |
278 | | - |
279 | | - if (params.upsert) { |
280 | | - if (Array.isArray(items) && (items.length === 0)) { |
281 | | - return self._create(data) |
282 | | - } else if (!items) { |
283 | | - return self._create(data) |
284 | | - } |
285 | | - } |
286 | | - |
287 | | - const patchEntry = async entry => { |
288 | | - const currentId = entry[this.id] |
289 | | - |
290 | | - const item = _.extend(entry, _.omit(data, this.id)) |
291 | | - await self.getModel().setItem(String(currentId), item, null) |
292 | | - |
293 | | - return stringsToDates(this._dates)(_select(item, params, this.id)) |
294 | | - } |
295 | | - |
296 | | - if (Array.isArray(items)) { |
297 | | - return Promise.all(items.map(patchEntry)) |
298 | | - } else { |
299 | | - return patchEntry(items) |
300 | | - } |
301 | | - } |
302 | | - |
303 | | - async _update (id, data, params = {}) { |
304 | | - if (id === null || Array.isArray(data)) { |
305 | | - throw new errors.BadRequest("You can not replace multiple instances. Did you mean 'patch'?") |
306 | | - } |
307 | | - debug(`_update(${id}, ${JSON.stringify(data)}, ${JSON.stringify(params)})` + this._debugSuffix) |
308 | | - const item = await this._findOrGet(id, params) |
309 | | - |
310 | | - if (params.upsert) { |
311 | | - if (Array.isArray(item) && (item.length === 0)) { |
312 | | - return self._create(data) |
313 | | - } else if (!item) { |
314 | | - return self._create(data) |
315 | | - } |
316 | | - } |
317 | | - |
318 | | - id = item[this.id] |
319 | | - |
320 | | - const entry = _.omit(data, this.id) |
321 | | - entry[this.id] = id |
322 | | - |
323 | | - return this.getModel().setItem(String(id), entry, null) |
324 | | - .then(() => entry) |
325 | | - .then(select(params, this.id)) |
326 | | - .then(stringsToDates(this._dates)) |
327 | | - } |
328 | | - |
329 | | - async __removeItem (item) { |
330 | | - await this.getModel(null).removeItem(String(item[this.id]), null) |
331 | | - |
332 | | - return item |
333 | | - } |
334 | | - |
335 | | - async _remove (id, params = {}) { |
336 | | - if (id === null && !this.allowsMulti('remove', params)) { |
337 | | - throw new errors.MethodNotAllowed('Can not remove multiple entries') |
338 | | - } |
339 | | - debug(`_remove(${id}, ${JSON.stringify(params)})` + this._debugSuffix) |
340 | | - const items = await this._findOrGet(id, params) |
341 | | - if (Array.isArray(items)) { |
342 | | - return Promise.all(items.map(item => this.__removeItem(item), null)) |
343 | | - .then(select(params, this.id)) |
344 | | - } else { |
345 | | - return this.__removeItem(items) |
346 | | - .then(select(params, this.id)) |
347 | | - } |
348 | | - } |
349 | | -} |
350 | | - |
351 | | -// Create the service. |
352 | | -export class Service extends Adapter { |
353 | | - constructor (options = {}) { |
354 | | - super(options) |
355 | | - } |
356 | | - |
357 | | - // Perform requests to adapter |
358 | | - async find (params) { |
359 | | - return this._find(params) |
360 | | - } |
361 | | - |
362 | | - async get (id, params) { |
363 | | - return this._get(id, params) |
364 | | - } |
365 | | - |
366 | | - async create (data, params) { |
367 | | - return this._create(data, params) |
368 | | - } |
369 | | - |
370 | | - async update (id, data, params) { |
371 | | - return this._update(id, data, params) |
372 | | - } |
373 | | - |
374 | | - async patch (id, data, params) { |
375 | | - return this._patch(id, data, params) |
376 | | - } |
377 | | - |
378 | | - async remove (id, params) { |
379 | | - return this._remove(id, params) |
380 | | - } |
381 | | -} |
382 | | -export function init (options) { |
383 | | - return new Service(options) |
384 | | -} |
| 1 | +export * from './service.js' |
0 commit comments