UNPKG

@citygro/vdata

Version:

vue-js-data binding

1,623 lines (1,421 loc) 67.7 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } var _r14c_asyncUtils_map = _interopDefault(require('@r14c/async-utils/map')); var includes = _interopDefault(require('lodash/includes')); var isArray = _interopDefault(require('lodash/isArray')); var isBoolean = _interopDefault(require('lodash/isBoolean')); var isNumber = _interopDefault(require('lodash/isNumber')); var isString = _interopDefault(require('lodash/isString')); var uniq = _interopDefault(require('lodash/uniq')); var camelCase = _interopDefault(require('lodash/camelCase')); var concat = _interopDefault(require('lodash/concat')); var join = _interopDefault(require('lodash/join')); var tail = _interopDefault(require('lodash/tail')); var whatwgFetch = require('whatwg-fetch'); var clone = _interopDefault(require('lodash/cloneDeep')); var defaults = _interopDefault(require('lodash/defaultsDeep')); var fork = _interopDefault(require('@r14c/async-utils/fork')); var isFunction = _interopDefault(require('lodash/isFunction')); var pick = _interopDefault(require('lodash/pick')); var map = _interopDefault(require('lodash/map')); var stringify = _interopDefault(require('json-stable-stringify')); var sum = _interopDefault(require('lodash/sum')); var microTask = _interopDefault(require('@r14c/async-utils/microTask')); var filter = _interopDefault(require('lodash/fp/filter')); var flow = _interopDefault(require('lodash/fp/flow')); var isNil = _interopDefault(require('lodash/fp/isNil')); var omitBy = _interopDefault(require('lodash/fp/omitBy')); var sort = _interopDefault(require('lodash/sortBy')); var get$1 = _interopDefault(require('lodash/get')); var to = _interopDefault(require('@r14c/async-utils/to')); var EventEmitter = _interopDefault(require('events')); var toString = _interopDefault(require('lodash/toString')); var immutable = require('immutable'); var isEqual = _interopDefault(require('lodash/isEqual')); var isNil$1 = _interopDefault(require('lodash/isNil')); var isObject = _interopDefault(require('lodash/isObject')); var mergeWith = _interopDefault(require('lodash/mergeWith')); var transform = _interopDefault(require('lodash/transform')); var isEmpty = _interopDefault(require('lodash/isEmpty')); var toNumber = _interopDefault(require('lodash/toNumber')); var Any = _interopDefault(require('p-any')); var fromPairs = _interopDefault(require('lodash/fp/fromPairs')); var keys = _interopDefault(require('lodash/keys')); var zip = _interopDefault(require('lodash/fp/zip')); var Queue = _interopDefault(require('@r14c/async-utils/Queue')); var stores = {}; var register = function register(store) { stores[store.storeId] = store; return store; }; var getStoreById = function getStoreById(storeId) { return stores[storeId] || null; }; var ReplaceStoreFromPropsMixin = { mounted: function mounted() { console.log('[@citygro/vdata] replace store from props', this.$store); }, props: { vdataStoreId: { type: String, required: true } }, computed: { '$store': { get: function get() { return getStoreById(this.vdataStoreId); } } } }; var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; var asyncToGenerator = function (fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }; var defineProperty = function (obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }; var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; var get$2 = function get$2(object, property, receiver) { if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get$2(parent, property, receiver); } } else if ("value" in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; var set = function set(object, property, value, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent !== null) { set(parent, property, value, receiver); } } else if ("value" in desc && desc.writable) { desc.value = value; } else { var setter = desc.set; if (setter !== undefined) { setter.call(receiver, value); } } return value; }; var slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); var toConsumableArray = function (arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; return arr2; } else { return Array.from(arr); } }; var isPrimitive = function isPrimitive(val) { return isNumber(val) || isString(val) || isBoolean(val); }; var cleanRecord = function cleanRecord(_ref) { var store = _ref.store, _ref$record = _ref.record, record = _ref$record === undefined ? {} : _ref$record, _ref$omitKeys = _ref.omitKeys, omitKeys = _ref$omitKeys === undefined ? [] : _ref$omitKeys; if (isPrimitive(record)) { return record; } else if (isArray(record)) { return record.map(function (item) { return cleanRecord({ store: store, record: item, omitKeys: omitKeys }); }); } else { var o = {}; Object.entries(record).filter(function (_ref2) { var _ref3 = slicedToArray(_ref2, 2), key = _ref3[0], value = _ref3[1]; return !includes(omitKeys, key) && value; }).forEach(function (_ref4) { var _ref5 = slicedToArray(_ref4, 2), key = _ref5[0], value = _ref5[1]; if (isArray(value)) { o[key] = value.map(function (item) { return cleanRecord({ store: store, record: item, omitKeys: omitKeys }); }); } else if ((typeof value === 'undefined' ? 'undefined' : _typeof(value)) === 'object') { o[key] = cleanRecord({ store: store, record: value, omitKeys: omitKeys }); } else { o[key] = value; } }); return o; } }; /** * @param {object} options * @param {vdata.Store} options.store * @param {object} options.record * @param {string[]} options.omitKeys * @param {string} options.collectionName */ var cleanRecord$1 = (function (options) { var record = options.record; var store = options.store; var omitKeys = uniq([].concat(toConsumableArray(options.omitKeys || []), ['_id'])); var cleanedRecord = cleanRecord({ store: store, record: record, omitKeys: omitKeys }); return store.createRecord(record._collection || options.collectionName, cleanedRecord); }); /** * @param {object} value * @param {object} diff */ var handleChange = function handleChange(_ref) { var value = _ref.value, diff = _ref.diff; return _extends({}, value, diff); }; /** * @param {object} value * @param {string} key * @param {object} diff */ var handleKeyChange = function handleKeyChange(_ref2) { var value = _ref2.value, key = _ref2.key, diff = _ref2.diff; var updated = handleChange({ value: value[key], diff: diff }); return handleChange({ value: value, diff: defineProperty({}, key, updated) }); }; /** * @param {object} value * @param {number} i * @param {object} diff */ var handleArrayChange = function handleArrayChange(_ref3) { var _ref3$value = _ref3.value, value = _ref3$value === undefined ? [] : _ref3$value, index = _ref3.index, diff = _ref3.diff; var arr = [].concat(toConsumableArray(value)); arr[index] = _extends({}, arr[index] || {}, diff); return arr; }; /** * @param {object} value * @param {number} i * @param {string} key * @param {object} diff */ var handleArrayKeyChange = function handleArrayKeyChange(_ref4) { var _ref4$value = _ref4.value, value = _ref4$value === undefined ? {} : _ref4$value, index = _ref4.index, key = _ref4.key, diff = _ref4.diff; var updated = handleArrayChange({ value: value[key] || [], index: index, diff: diff }); return handleChange({ value: value, diff: defineProperty({}, key, updated) }); }; /** * @param {array} value * @param {object} diff */ var pushToArray = function pushToArray(_ref5) { var _ref5$value = _ref5.value, value = _ref5$value === undefined ? [] : _ref5$value, diff = _ref5.diff; var arr = [].concat(toConsumableArray(value)); arr.push(diff); return arr; }; /** * @param {object} value * @param {string} key * @param {object} diff */ var pushToArrayKey = function pushToArrayKey(_ref6) { var _ref6$value = _ref6.value, value = _ref6$value === undefined ? {} : _ref6$value, key = _ref6.key, diff = _ref6.diff; var arr = [].concat(toConsumableArray(value[key] || [])); arr.push(diff); return handleChange({ value: value, diff: defineProperty({}, key, arr) }); }; /** * @param {array} value * @param {number} i */ var removeFromArray = function removeFromArray(_ref7) { var _ref7$value = _ref7.value, value = _ref7$value === undefined ? [] : _ref7$value, index = _ref7.index; var arr = [].concat(toConsumableArray(value)); arr.splice(index, 1); return arr; }; /** * @param {object} value * @param {number} i * @param {string} key */ var removeFromArrayKey = function removeFromArrayKey(_ref8) { var _ref8$value = _ref8.value, value = _ref8$value === undefined ? {} : _ref8$value, index = _ref8.index, key = _ref8.key; var updated = removeFromArray({ value: value[key], index: index }); return handleChange({ value: value, diff: defineProperty({}, key, updated) }); }; /** * convert snake_case or camelCase strings to CapCase * * @param {String} s */ var capWords = (function (s) { var camel = camelCase(s); var arr = concat([], camel.charAt(0).toUpperCase(), tail(camel)); return join(arr, ''); }); var format = (function (name) { var prefix = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ''; if (prefix === '') { return camelCase(name); } else { return '' + camelCase(prefix) + capWords(name); } }); /** * create a dataflow mixin for a given value prop. * * a 'value' dataflow implements the `v-model` interface. * * custom dataflows follow a pattern: methods are prefixed with the `valueProp` * name and `update:${valueProp}` is emitted. * * @param {string} valueProp - bind dataflow to this prop */ var createDataFlowMixin = function createDataFlowMixin(valueProp) { var _methods; var event = valueProp === 'value' ? 'input' : 'update:' + valueProp; var prefix = valueProp === 'value' ? '' : valueProp; return { methods: (_methods = {}, defineProperty(_methods, format('forwardInput', prefix), function (e) { this.$emit(event, e); }), defineProperty(_methods, format('handleChange', prefix), function (diff) { this.$emit(event, handleChange({ value: this[valueProp], diff: diff })); }), defineProperty(_methods, format('handleKeyChange', prefix), function (key, diff) { this.$emit(event, handleKeyChange({ value: this[valueProp], key: key, diff: diff })); }), defineProperty(_methods, format('handleArrayKeyChange', prefix), function (index, key, diff) { this.$emit(event, handleArrayKeyChange({ value: this[valueProp], index: index, key: key, diff: diff })); }), defineProperty(_methods, format('handleArrayChange', prefix), function (index, diff) { this.$emit(event, handleArrayChange({ value: this[valueProp], index: index, diff: diff })); }), defineProperty(_methods, format('pushToArray', prefix), function (diff) { this.$emit(event, pushToArray({ value: this[valueProp], diff: diff })); }), defineProperty(_methods, format('pushToArrayKey', prefix), function (key, diff) { this.$emit(event, pushToArrayKey({ value: this[valueProp], key: key, diff: diff })); }), defineProperty(_methods, format('removeFromArray', prefix), function (index) { this.$emit(event, removeFromArray({ value: this[valueProp], index: index })); }), defineProperty(_methods, format('removeFromArrayKey', prefix), function (index, key) { this.$emit(event, removeFromArrayKey({ value: this[valueProp], index: index, key: key })); }), _methods) }; }; /* global fetch */ var withDefaults = function withDefaults(options) { return pick(defaults({}, options, { credentials: 'same-origin' }), ['headers', 'body', 'method', 'credentials', 'signal']); }; var interceptors = []; var onError = void 0; var fetchWrapper = function fetchWrapper(url) { var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; return fork(clone(options), interceptors).then(function (request) { return fetch(url, withDefaults(request)).then(function (response) { if (response.status >= 200 && response.status < 400) { return response; } else if (isFunction(onError)) { onError(response, request); } else { throw new Error(response.statusText, { response: response, request: request }); } }); }); }; fetchWrapper.addInterceptor = function (fn) { interceptors.push(fn); }; fetchWrapper.onError = function (fn) { onError = fn; }; var checksum = function checksum() { var s = map(arguments, stringify); var values = map(stringify(s), function (c, i) { return c.codePointAt(0) * i; }); return '' + sum(values); }; var makeRequestKey = function makeRequestKey(url, options) { var headers = Object.entries(options.headers || {}).map(function (_ref) { var _ref2 = slicedToArray(_ref, 2), key = _ref2[0], val = _ref2[1]; return key + ':' + val; }); return options.method + '-' + checksum(headers, url); }; // @flow var filterArray = flow(filter(function (v) { return !isNil(v); })); var filterObject = flow(omitBy(isNil)); /** * removes nil values from arrays and nil `{key: value}` pairs from objects * * @function * @param {Object|Array} value - the source object * @return {Object|Array} */ var omitNil = (function (value) { return isArray(value) ? filterArray(value) : filterObject(value); }); /** * @param {object} o * @param {string} prefix */ var toQueryString = function toQueryString() { var o = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var prefix = arguments[1]; return sort(Object.entries(o), function (e) { return e[0]; }).map(function (_ref) { var _ref2 = slicedToArray(_ref, 2), prop = _ref2[0], value = _ref2[1]; var key = prefix ? prefix + '[' + prop + ']' : prop; return (typeof value === 'undefined' ? 'undefined' : _typeof(value)) === 'object' ? toQueryString(value, key) : encodeURIComponent(key) + '=' + encodeURIComponent(value); }).join('&'); }; var normalizeHeaders = function normalizeHeaders(options) { var headers = _extends({}, options.headers); if (!options.method) { throw new Error('options.method must be defined'); } if (includes(['PUT', 'POST'], options.method.toUpperCase())) { headers['Content-Type'] = 'application/json'; } headers['Accept'] = 'application/json'; return headers; }; var getUrl = function getUrl(options) { var url = options.url; var params = omitNil(options.params || {}); var qs = toQueryString(params); if (qs) { url += '?' + qs; } return url; }; var createHttpAdapter = function createHttpAdapter() { var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var promiseCache = {}; var adapter = options.adapter || fetchWrapper; var deserialize = options.deserialize || function (response, data) { return data; }; var cacheTimeout = options.cacheTimeout || 500; // evict promise cache keys after 500ms by default var createRequest = function createRequest(url, request) { return adapter(url, request).then(function (response) { return response.json().then(function (data) { return microTask(function () { return deserialize(response, data); }); }); }); }; return function () { var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var promise = void 0; var force = options.force || false; var url = getUrl(options); var request = _extends({}, options, { headers: normalizeHeaders(options), body: options.body ? stringify(options.body) : undefined }); if (options.method === 'GET') { var key = makeRequestKey(url, request); promise = promiseCache[key]; if (!promise || force === true) { promise = promiseCache[key] = createRequest(url, request); } setTimeout(function () { delete promiseCache[key]; }, cacheTimeout); } else { promise = createRequest(url, request); } return promise; }; }; /** * @param {object[]} collection * @param {string} key */ var createIndex = (function (collection, key) { var index = {}; collection.forEach(function (item) { index[item[key]] = item; }); return index; }); var pop = (function (o, key, fallback) { var value = o[key]; delete o[key]; return value === undefined ? fallback : value; }); /** * create a mixin that configures a vm to manipulate a single record. you can * use a prop to ask for a record by id or specify a template to create a new * record that is pre-populated with some initial state. * * ```javascript * // @/queries/UserById.js * import {createMixinForItemById} from '@citygro/vdata' * * export default { * mixins: [ * createMixinForItemById({ * idPropertyName: 'userId', * collectionName: 'users', * localPropertyName: 'user', * requestOptions: { * capture: false * } * }) * ] * } * ``` * * a vm which consumes this mixin will have the following props, methods, data, * &c. it will also be configured to react to changes to data in the store and * update itself accordingly. * * ```javascript * { * props: { * userid: String, * userRequestOptionsOverride: Object * }, * data: { * user: Object, * }, * methods: { * userSave: Function, * }, * computed: { * asyncLoading: Boolean, * userLoading: Boolean, * userHasChanges: Boolean * } * } * ``` * * `@/queries/UserById` defines a query that fetches and captures the initial state * for a user record. lets say we have a particular editor that provides read-only * access to a particular resource for some users and read/write access for * others. * * for the case where the editor should be read/write we can default some props * in the vm to change its behavior depending on the permissions of the current * user. * * ```javascript * // UserEditor.js * import UserById from '@/queries/UserById' * * export default { * mixins: [ * UserById * ], * props: { * userRequestOptionsOverride: { * default () { * return { * capture: this.$session.hasPermissionToEditUsers() * } * } * } * } // ... * } * ``` * * @param {object} options * @param {string} options.collectionName * @param {string} options.localPropertyName - the vm data where the result of the query will be stored * @param {string} [options.idPropertyName=id] - the name of the prop you will use to specify the id of the requested record * @param {object} [options.requestOptions] - control some of the behavior of the query * @param {boolean} [options.requestOptions.force=false] - always fetch the latest record * @param {boolean} [options.requestOptions.capture=false] - capture the initial state of the record, implies `force = true` * @param {object} [options.template={}] - the default template for this query * @return {object} item-by-id query mixin */ var createMixinForItemById = function createMixinForItemById(options) { var _props, _methods; var collectionName = options.collectionName; var localPropertyName = options.localPropertyName || camelCase(collectionName).slice(0, -1); var idPropertyName = options.idPropertyName || 'id'; // FIXME `${localPropertyName}Id` var templateName = options.templateName || localPropertyName + 'Template'; var template = options.template || {}; var recordPrimaryKey = options.recordPrimaryKey || '_id'; var getIdMethodName = localPropertyName + 'RecordId'; var hasChangesComputedName = localPropertyName + 'HasChanges'; var saveMethodName = localPropertyName + 'Save'; var asyncLoadingName = localPropertyName + 'Loading'; var idType = options.idType || String; var requestOptions = options.requestOptions || {}; var requestOptionsName = localPropertyName + 'RequestOptions'; var capture = pop(requestOptions, 'capture', false); var requestOptionsOverrideName = localPropertyName + 'RequestOptionsOverride'; var changeCollectionMethodName = localPropertyName + 'ChangeCollection'; if (!collectionName) { throw new Error('[@citygro/vdata#createMixinForItemById] options.collectionName is required'); } if (!options.idPropertyName) { console.warn('[@citygro/vdata#createMixinForItemById]', 'options.idPropertyName will default to `${localPropertyName}Id` in future versions of vdata' // eslint-disable-line no-template-curly-in-string ); } return { props: (_props = {}, defineProperty(_props, idPropertyName, { type: idType, default: null }), defineProperty(_props, templateName, { type: Object, default: function _default() { return clone(template); } }), defineProperty(_props, requestOptionsOverrideName, { type: Object, default: function _default() { return {}; } }), _props), data: function data() { var _data; var data = (_data = {}, defineProperty(_data, localPropertyName, null), defineProperty(_data, requestOptionsName, _extends({}, clone(requestOptions), this[requestOptionsOverrideName])), _data); return data; }, vdata: function vdata(event) { var recordId = this[getIdMethodName](); if (!this[asyncLoadingName] && recordId !== null && event.collectionName === collectionName) { if (capture || this[requestOptionsOverrideName].capture) { this[localPropertyName] = this.$store.rebase(collectionName, this[localPropertyName]); } else { this[localPropertyName] = this.$store.get(collectionName, recordId) || null; } } }, asyncData: defineProperty({}, localPropertyName, function () { var _this = this; return asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee() { var forceOption, captureOption, force, recordId, err, result, _ref, _ref2; return regeneratorRuntime.wrap(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: forceOption = requestOptions.force || _this[requestOptionsOverrideName].force; captureOption = capture || _this[requestOptionsOverrideName].capture; if (forceOption && captureOption) { console.warn('[@citygro/vdata#createMixinForItemById]', '`requestOptions.capture = true` implies `requestOptions.force = true`, setting both options is not necessary'); } force = forceOption || captureOption; recordId = _this[getIdMethodName](); err = void 0, result = void 0; if (!(recordId !== null)) { _context.next = 17; break; } if (!force) { result = _this.$store.get(collectionName, recordId); } if (result) { _context.next = 15; break; } _context.next = 11; return to(_this.$store.find(collectionName, recordId, _extends({}, _this[requestOptionsOverrideName], _this[requestOptionsName]))); case 11: _ref = _context.sent; _ref2 = slicedToArray(_ref, 2); err = _ref2[0]; result = _ref2[1]; case 15: _context.next = 18; break; case 17: result = _this.$store.createRecord(collectionName, _this[templateName]); case 18: if (err) { console.error(err); result = null; } return _context.abrupt('return', result); case 20: case 'end': return _context.stop(); } } }, _callee, _this); }))(); }), watch: defineProperty({}, idPropertyName, function () { var captureOption = capture || this[requestOptionsOverrideName].capture; if (!captureOption) { this.$asyncReload(localPropertyName); } }), computed: defineProperty({}, hasChangesComputedName, function () { return this.$store.hasChanges(collectionName, this[localPropertyName]); }), methods: (_methods = {}, defineProperty(_methods, changeCollectionMethodName, function (name) { collectionName = name; this.$asyncReload(localPropertyName); }), defineProperty(_methods, getIdMethodName, function () { var id = this[idPropertyName] || get$1(this, localPropertyName + '.' + recordPrimaryKey, null); return this.$store.isValidId(id) ? id : null; }), defineProperty(_methods, saveMethodName, function () { var _this2 = this; return asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee2() { var _ref3, _ref4, err, response; return regeneratorRuntime.wrap(function _callee2$(_context2) { while (1) { switch (_context2.prev = _context2.next) { case 0: _context2.next = 2; return to(_this2.$store.save(collectionName, _this2[localPropertyName], _extends({}, _this2[requestOptionsOverrideName], _this2[requestOptionsName]))); case 2: _ref3 = _context2.sent; _ref4 = slicedToArray(_ref3, 2); err = _ref4[0]; response = _ref4[1]; if (!err) { _context2.next = 8; break; } throw err; case 8: if (response) { _this2[localPropertyName] = response; _this2.$emit('update:' + idPropertyName, response[recordPrimaryKey]); } return _context2.abrupt('return', _this2[localPropertyName]); case 10: case 'end': return _context2.stop(); } } }, _callee2, _this2); }))(); }), _methods) }; }; /** * @param {object} options * @param {string} options.collectionName * @param {string} options.localPropertyName * @param {object} options.queryOptions * @param {object} options.requestOptions * @param {string} [options.ttl] - optional query-specific cache timeout * @return {object} */ var createMixinForListByResource = function (options) { var collectionName = options.collectionName; var ttl = options.ttl; var localPropertyName = options.localPropertyName || camelCase(collectionName); var localPropertyForceName = localPropertyName + 'Force'; var queryOptions = options.queryOptions || {}; var requestOptions = options.requestOptions; var requestOptionsName = localPropertyName + 'RequestOptions'; var requestOptionsOverrideName = localPropertyName + 'RequestOptionsOverride'; return { props: defineProperty({}, requestOptionsOverrideName, { type: Object, default: function _default() { return {}; } }), data: function data() { var _ref; return _ref = {}, defineProperty(_ref, localPropertyName, []), defineProperty(_ref, localPropertyForceName, false), defineProperty(_ref, requestOptionsName, _extends({}, clone(requestOptions), this[requestOptionsOverrideName])), _ref; }, vdata: function vdata(event) { if (!this.asyncLoading && event.collectionName === collectionName) { this[localPropertyName] = this.$store.getAll(collectionName) || []; } }, asyncData: defineProperty({}, localPropertyName, function () { var _this = this; return asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee() { var _ref2, _ref3, err, result; return regeneratorRuntime.wrap(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: _context.next = 2; return to(_this.$store.findAll(collectionName, queryOptions, _extends({}, _this[requestOptionsOverrideName], _this[requestOptionsName], { force: _this[localPropertyForceName], ttl: ttl }))); case 2: _ref2 = _context.sent; _ref3 = slicedToArray(_ref2, 2); err = _ref3[0]; result = _ref3[1]; if (err) { console.error(err); result = []; } return _context.abrupt('return', result); case 8: case 'end': return _context.stop(); } } }, _callee, _this); }))(); }) }; }; var KeyMap = { create: function create() { var map$$1 = {}; return { get: function get(collectionName, key) { key = collectionName + '-' + toString(key); return map$$1[key]; }, link: function link(collectionName, a, b) { a = toString(a); b = toString(b); map$$1[collectionName + '-' + a] = b; map$$1[collectionName + '-' + b] = a; }, unlink: function unlink(collectionName, a, b) { a = collectionName + '-' + toString(a); b = collectionName + '-' + toString(b); delete map$$1[a]; delete map$$1[b]; } }; } }; /** * quickly determine if two objects differ * * @param {Object} a * @param {Object} b * @returns {Boolean} */ var fastDiff = (function (a, b) { return stringify(a) !== stringify(b); }); var makeQueryKey = function makeQueryKey(collectionName) { var query = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; return collectionName + '-' + checksum(query, options); }; var mget = (function (value, path) { if (immutable.isImmutable(value)) { return value.getIn(path.split('.')); } else { return get$1(value, path); } }); var merge = function merge() { var object = arguments[0]; var sources = tail(arguments); return mergeWith.apply(undefined, [object].concat(toConsumableArray(sources), [function (objValue, srcValue) { if (isArray(objValue)) { return srcValue; } }])); }; /** * replace all values in an object with `null`. used to generate the ORSet for * diffing operations. * * @param {Object} object * @return {Object} */ var nullify = function nullify(object) { return transform(object, function (result, value, key) { result[key] = isObject(value) && !isArray(value) ? nullify(value) : null; }); }; /** * @param {object} base * @param {object} object * @return {object} */ var difference = function difference(base, object) { var changes = function changes(object, base) { return transform(object, function (result, value, key) { if (!isEqual(value, base[key])) { result[key] = isObject(value) && isObject(base[key]) && !isArray(value) ? changes(value, base[key]) : value; } }); }; return isNil$1(base) ? object : changes(object, base); }; var observedRemoveDiff = function observedRemoveDiff(base, object) { var diff = difference(base, object); var inverseDiff = difference(object, base); var nullDiff = nullify(inverseDiff); var orDiff = merge({}, nullDiff, diff); return orDiff; }; var jsonClone = flow(JSON.stringify, JSON.parse); /** * @param {Object} base * @param {...Object} checkpoints * @returns {Object} */ var rebase = function () { var base = arguments[0]; var diffs = tail(arguments).map(function (checkpoint) { return observedRemoveDiff(base, checkpoint); }); var patch = merge.apply(undefined, [{}].concat(toConsumableArray(diffs))); return merge(jsonClone(base), patch); }; var registerSchemas = function (store) { var modelMap = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; if (isEmpty(modelMap)) { console.error('[@citygro/vdata] you have not defined any models!'); } Object.keys(modelMap).forEach(function (modelName) { store = store.set(modelName, immutable.Map()); }); return store; }; var LCG = function LCG(seed) { var lcg = function lcg(a) { return a * 48271 % 2147483647; }; seed = seed ? lcg(seed) : lcg(Math.random()); return function () { seed = lcg(seed); return seed / 2147483648; }; }; /** * uniqueId * * @param {string} [prefix] - optional prefix * @return {string} */ var uniqueId = (function (prefix) { var ex = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 9e15; var random = LCG(); var id = parseInt((random() * ex).toFixed(0), 10).toString(36); return prefix ? prefix + "-" + id : id; }); var idRegex = /^[0-9a-z]+?-[0-9a-z]+$/i; /** * @param {Object} data * @private */ var convert = function convert(data) { return immutable.fromJS(data, function (key, value) { return immutable.isKeyed(value) ? value.toMap() : value.toList(); }); }; /** * @param {Object} options * @param {Object} options.models * @param {String} [options.basePath=''] - default prefix for http requests * @param {function} [options.adapter] - a custom fetch * @param {function} [options.deserialize] - request post-processing * @return {Store} a vdata store instance */ var createStore = function createStore() { var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var evt = new EventEmitter(); var http = createHttpAdapter(options); var models = clone(options.models); var storeId = uniqueId(null, 1e5); var queryCacheTimeout = options.cacheTimeout || 500; // evict query cache after 500ms by default var keyMap = KeyMap.create(); var queryCache = {}; var store = registerSchemas(immutable.Map(), options.models); var getBasePath = function getBasePath(collectionName) { var model = models[collectionName]; return model.basePath || options.basePath || ''; }; /** * @param {String} id * @private */ var resolveId = function resolveId(collectionName, id) { var isTmp = idRegex.test(id); return isTmp ? id : keyMap.get(collectionName, id); }; /** * @param {String} id * @private */ var resolvePk = function resolvePk(collectionName, id) { var isTmp = idRegex.test(id); return isTmp ? keyMap.get(collectionName, id) : id; }; /** * @param {String} collectionName * @param {Object} data * @private */ var getMeta = function getMeta(collectionName, data) { try { var model = models[collectionName]; var idAttribute = model.idAttribute; return { basePath: getBasePath(collectionName), id: mget(data, '__tmp_id'), idAttribute: idAttribute, pk: mget(data, idAttribute), symId: mget(data, '__sym_id') }; } catch (e) { throw new Error('missing collection: ' + collectionName); } }; /** * queue a micro-task to broadcast the given message object * * @param {String} message * @param {Object} options * @param {Boolean} [options.quiet=false] * @private */ var emit = function emit(message) { var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; var quiet = options.quiet || false; if (quiet === false) { microTask(function () { return evt.emit('all', message); }); } }; /** * @constructor */ var Store = function Store() { evt.setMaxListeners(0); // no limit this.models = options.models; this.storeId = storeId; }; /** * tag a javascript object with metadata that allows it to be tracked by the vdata store. * `__tmp_id` and the `idAttribute` configured for the given collection are both used to * identify the object. editing either of these will cause vdata to see the resulting * object as something new that needs to be tracked separately from the original object. * * @param {String} collection * @param {Object} [data={}] * @return {Object} */ Store.prototype.createRecord = function (collectionName) { var data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; var model = models[collectionName]; var idAttribute = model.idAttribute; var pk = mget(data, idAttribute); var id = mget(data, '__tmp_id'); if (pk && !id) { id = keyMap.get(collectionName, pk) || uniqueId(storeId); // get or gen id keyMap.link(collectionName, pk, id); // 2x link } else if (!pk && id) { // noop } else if (pk && id) { keyMap.link(collectionName, pk, id); // 2x link } else if (!pk && !id) { id = uniqueId(storeId); // gen id } return _extends({}, data, { __tmp_id: id }); }; /** * get a particular object from the store using the primary key provided by * your api server, or the temporary local id that vdata uses internally to * track records. * * @param {String} collectionName * @param {String} pkOrId * @return {Object} */ Store.prototype.get = function (collectionName, pkOrId) { var id = resolveId(collectionName, pkOrId); var versions = store.getIn([collectionName, id], immutable.Stack()); var record = versions.first(); if (record) { var size = versions.size; var index = 0; return this.createRecord(collectionName, _extends({}, record.toJS(), { __sym_id: index + '-' + size })); } else { return null; } }; /** * get all of the records in `collectionName`. if you include a `keys` * parameter, this method returns all of the records that match the ids * listed. * * @param {String} collectionName * @param {string[]} [keys] * @return {object[]} */ Store.prototype.getList = function (collectionName, keys$$1) { var _this = this; var result = void 0; if (isArray(keys$$1)) { result = keys$$1.length ? keys$$1.map(function (key) { return _this.get(collectionName, key); }) : []; } else { result = store.get(collectionName).keySeq().map(function (key) { return _this.get(collectionName, key); }).toJS(); } return result; }; /** * @ignore */ Store.prototype.getAll = function () { return this.getList.apply(this, arguments); }; /** * remove a record from the store, identified by public key or temporary id. * * @emits Store#remove * @param {String} collectionName * @param {String} pkOrId * @param {Object} options * @param {Boolean} options.quiet * @return {Object} */ Store.prototype.remove = function (collectionName, pkOrId) { var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; var id = resolveId(collectionName, pkOrId); var object = this.get(collectionName, id); if (object) { var meta = getMeta(collectionName, object); store = store.removeIn([collectionName, id]); keyMap.unlink(collectionName, meta.pk, meta.id); delete object.__tmp_id; delete object.__sym_id; emit({ collectionName: collectionName, event: 'remove', record: object }, { quiet: options.quiet }); } else if (process.env.NODE_ENV !== 'test') { console.warn('[@citygro/vdata] attempting to remove a record that is not tracked by Store#' + storeId, { collectionName: collectionName, pkOrId: pkOrId, options: options }); } return object; }; /** * remove all of the records in `collectionName` or all of the records that match the ids passed into `keys`. * * @emits Store#remove-list * @param {String} collectionName * @param {string[]} keys * @return {object[]} */ Store.prototype.removeList = function (collectionName, keys$$1) { var _this2 = this; var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; var records = this.getAll(collectionName, keys$$1).map(function (record) { var _getMeta = getMeta(collectionName, record), id = _getMeta.id; return _this2.remove(collectionName, id, { quiet: true }); }); emit({ collectionName: collectionName, event: 'remove-list', records: records }, { quiet: options.quiet }); return records; }; /** * @ignore */ Store.prototype.removeAll = function () { return this.removeList.apply(this, arguments); }; /** * remove all records from all collections * @emits Store#remove-list */ Store.prototype.clear = function () { var _this3 = this; store.keySeq().forEach(function (collectionName) { _this3.removeAll(collectionName); }); }; /** * vdata automatically tracks all of the versions that are created for every * record that it tracks. this version tracking is how `Store#rebase` is able * to implement a simple Observed-Remove Set (ORSet) that enables vdata to * deterministically merge all of the changes to a particular record. * * given `data` with a particular `__sym_id` and the current version of the * same record at `data[idAttribute]`, return a merged record containing all * changes, applied to the base record at `__sym_id` in the following order, * diff'd against `base`: * * 1. current * 2. data * * at CityGro we use the ORSet implementation in vdata to power the real-time * features of our customer portal application. in most cases, the core * diffing algorithm is able to generate merged outputs with intuitive * results. however, it is important to note the rules that we use to * resolve certain edge cases. * * 1. Last-write (from the perspective of the writer) wins. in our * experience, this produces the least surprising results for our users. * 2. Array mutations are all-or-nothing. we currently don't have an * acceptable solution to merging arrays with arbitrary mutations. * following rule #1, we opt to *replace* any previous values with the * latest version of the array. if you have thoughts on this, please open * a ticket on [GitLab](https://gitlab.com/citygro/vdata/issues). * * @param {String} collection * @param {Object} data * @return {Object} */ Store.prototype.rebase = function (collectionName, data) { var record = immutable.isImmutable(data) ? data.toJS() : data; var _getMeta2 = getMeta(collectionName, record), id = _getMeta2.id; var base = null; if (record.__sym_id) { var _record$__sym_id$spli = record.__sym_id.split('-').map(toNumber), _record$__sym_id$spli2 = slicedToArray(_record$__sym_id$spli, 2), index = _record$__sym_id$spli2[0], size = _record$__sym_id$spli2[1]; var offset = index - size; var versions = store.getIn([collectionName, id]); if (versions) { base = versions.get(offset).toJS(); } } var current = this.get(collectionName, id); var object = base || current ? rebase(base, current, record) : record; return object; }; /** * add a record to the store. you *do not* need to pass your data to * `Store#createRecord` before adding it. * * @emits Store#add * @see {Store.rebase} * @param {String} collection * @param {Object} data * @param {Object} options * @param {Boolean} [options.quiet=false] silence store events for this invocation * @return {Object} */ Store.prototype.add = function (collectionName, data) { var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; var record = this.createRecord(collectionName, data); var latest = convert(record); var _getMeta3 = getMeta(collectionName, latest), id = _getMeta3.id; var versions = store.getIn([collectionName, id], immutable.Stack()); store = store.setIn([collectionName, id], versions.unshift(latest)); var object = this.get(collectionName, id); emit({ collectionName: collectionName, event: 'add', record: object }, { quiet: options.quiet }); return object; }; /** * add all of the records in `data` to `colectionName` in a single operation. * * @emits Store#add-list * @param {String} collectionName * @param {Array<Object>} data * @return {Array<Object>} */ Store.prototype.addList = function (collectionName, data) { var _this4 = this; var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; var records = data.map(function (item) { return _this4.add(collectionName, item, { quiet: true }); }); emit({ collectionName: collectionName, event: 'add-list', records: records }, { quiet: options.quiet }); return records; }; /** * check if `data` differs from the current version of the corresponding * record in the store. * * @param {String} collectionName * @param {Object} data * @return {Boolean} */ Store.prototype.hasChanges = function (collectionName, data) { if (!data) { return false; } else { var _getMeta4 = getMeta(collectionName, data), id = _getMeta4.id; var record = this.get(collectionName, id) || {}; return fastDiff(_extends({}, record, { __sym_id: null }), _extends({}, data, { __sym_id: null })); } }; /** * send a `DELETE` request to the endpoint configured for `collectionName` * and remove the corresponding record from the store. * * @async * @emits Store#remove * @param {String} collectionName * @param {Object} data * @param {Object} options * @return {Promise<Object>} */ Store.prototype.destroy = function (collectionName, data) { var _this5 = this; var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; var _getMeta5 = getMeta(collectionName, data), id = _getMeta5.id, pk = _getMeta5.pk, basePath = _getMeta5.basePath; return http(_extends({ url: basePath + '/' + collectionName + '/' + pk, method: 'DELETE' }, options)).then(function () { return microTask(function () { return _this5.remove(collectionName, id); }); }); }; /** * persist `data` using the endpoint configured for `collectonName`. if * `data` is *only* identified by a local temporary id send a `POST` request to * `/:basePath/:collectionName`. if `data` has a primary key send a `PUT` * request to `/:basePath/:collectionName/:primaryKey` * * when updating an existing record, this methods calls Store#rebase. * this gives vdata some important super-powers that you can use to build * real-time applications. check the