| 'use strict'; |
| |
| var IDB_NULL = Number.MIN_SAFE_INTEGER; |
| var IDB_FALSE = Number.MIN_SAFE_INTEGER + 1; |
| var IDB_TRUE = Number.MIN_SAFE_INTEGER + 2; |
| |
| // |
| // IndexedDB only allows valid JS names in its index paths, whereas JSON allows |
| // for any string at all. This converts invalid JS names to valid ones, to allow |
| // for them to be indexed. |
| // |
| // For example, "foo-bar" is a valid JSON key, but cannot be a valid JS name |
| // (because that would be read as foo minus bar). |
| // |
| // Very high level rules for valid JS names are: |
| // - First character cannot start with a number |
| // - Otherwise all characters must be be a-z, A-Z, 0-9, $ or _. |
| // - We allow . unless the name represents a single field, as that represents |
| // a deep index path. |
| // |
| // This is more aggressive than it needs to be, but also simpler. |
| // |
| var KEY_INVALID = /[^a-zA-Z0-9_$]+|(^[^a-zA-Z_$])/g; |
| var PATH_INVALID = /(\\.)|[^a-zA-Z0-9_$.]+|(^[^a-zA-Z_$])/g; |
| var SLASH = '\\'.charCodeAt(0); |
| |
| // These are the same as above but without the global flag |
| // we want to use RegExp.test because it's really fast, but the global flag |
| // makes the regex const stateful (seriously) as it walked through all instances |
| var TEST_KEY_INVALID = /[^a-zA-Z0-9_$]+|(^[^a-zA-Z_$])/; |
| var TEST_PATH_INVALID = /(\\.)|[^a-zA-Z0-9_$.]+|(^[^a-zA-Z_$])/; |
| function needsSanitise(name, isPath) { |
| if (isPath) { |
| return TEST_PATH_INVALID.test(name); |
| } else { |
| return TEST_KEY_INVALID.test(name); |
| } |
| } |
| |
| function sanitise(name, isPath) { |
| var correctCharacters = function (match) { |
| var good = ''; |
| for (var i = 0; i < match.length; i++) { |
| var code = match.charCodeAt(i); |
| // If you're sanitising a path, a slash character is there to be interpreted |
| // by whatever parses the path later as "escape the next thing". |
| // |
| // e.g., if you want to index THIS string: |
| // {"foo": {"bar.baz": "THIS"}} |
| // Your index path would be "foo.bar\.baz". |
| if (code === SLASH && isPath) { |
| continue; |
| } |
| |
| good += '_c' + code + '_'; |
| } |
| return good; |
| }; |
| |
| if (isPath) { |
| return name.replace(PATH_INVALID, correctCharacters); |
| } else { |
| return name.replace(KEY_INVALID, correctCharacters); |
| } |
| } |
| |
| function needsRewrite(data) { |
| for (var key of Object.keys(data)) { |
| if (needsSanitise(key)) { |
| return true; |
| } else if (data[key] === null || typeof data[key] === 'boolean') { |
| return true; |
| } else if (typeof data[key] === 'object') { |
| return needsRewrite(data[key]); |
| } |
| } |
| } |
| |
| function rewrite(data) { |
| if (!needsRewrite(data)) { |
| return false; |
| } |
| |
| var isArray = Array.isArray(data); |
| var clone = isArray |
| ? [] |
| : {}; |
| |
| Object.keys(data).forEach(function (key) { |
| var safeKey = isArray ? key : sanitise(key); |
| |
| if (data[key] === null) { |
| clone[safeKey] = IDB_NULL; |
| } else if (typeof data[key] === 'boolean') { |
| clone[safeKey] = data[key] ? IDB_TRUE : IDB_FALSE; |
| } else if (typeof data[key] === 'object') { |
| clone[safeKey] = rewrite(data[key]); |
| } else { |
| clone[safeKey] = data[key]; |
| } |
| }); |
| |
| return clone; |
| } |
| |
| export { |
| IDB_NULL, |
| IDB_TRUE, |
| IDB_FALSE, |
| rewrite, |
| sanitise |
| }; |