adapt-migrations 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintignore +1 -1
- package/.eslintrc.json +14 -14
- package/.github/CONTRIBUTING.md +8 -0
- package/.github/ISSUE_TEMPLATE/config.yml +1 -0
- package/.github/ISSUE_TEMPLATE/issue_template.md +23 -0
- package/.github/ISSUE_TEMPLATE.md +17 -0
- package/.github/pull_request_template.md +25 -0
- package/.github/workflows/addtomainproject.yml +19 -0
- package/.github/workflows/releases.yml +25 -0
- package/README.md +106 -106
- package/api/commands.js +33 -33
- package/api/data.js +26 -26
- package/api/describe.js +12 -12
- package/api/errors.js +28 -28
- package/api/helpers.js +17 -17
- package/api/plugins.js +47 -47
- package/api/tests.js +56 -57
- package/api/where.js +46 -46
- package/examples/migrations.js +159 -159
- package/examples/script.js +78 -78
- package/index.js +70 -70
- package/lib/CacheManager.js +95 -95
- package/lib/Journal.js +303 -303
- package/lib/Logger.js +91 -91
- package/lib/Task.js +402 -381
- package/lib/TaskContext.js +27 -27
- package/lib/TaskTest.js +21 -19
- package/lib/lifecycle.js +50 -50
- package/package.json +55 -18
package/lib/Journal.js
CHANGED
|
@@ -1,303 +1,303 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Set a property value
|
|
3
|
-
* @param {Object|Array} object
|
|
4
|
-
* @param {[string]} keys
|
|
5
|
-
* @param {*} value
|
|
6
|
-
* @throws {Cannot locate intermediate key: ${keys}}
|
|
7
|
-
*/
|
|
8
|
-
export function setKeys (object, keys, value) {
|
|
9
|
-
const initialKeys = keys.slice(0, -1)
|
|
10
|
-
const lastKey = keys[keys.length - 1]
|
|
11
|
-
const finalObject = initialKeys.reduce((object, key) => {
|
|
12
|
-
if (!(key in object)) throw new Error(`Cannot locate intermediate key: ${keys}`)
|
|
13
|
-
return object[key]
|
|
14
|
-
}, object)
|
|
15
|
-
finalObject[lastKey] = value
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Get a property value
|
|
20
|
-
* @param {Object|Array} object
|
|
21
|
-
* @param {[string]} keys
|
|
22
|
-
* @returns {*}
|
|
23
|
-
* @throws {Cannot locate intermediate key: ${keys}}
|
|
24
|
-
*/
|
|
25
|
-
export function getKeys (object, keys) {
|
|
26
|
-
return keys.reduce((object, key) => {
|
|
27
|
-
if (!(key in object)) throw new Error(`Cannot locate intermediate key: ${keys}`)
|
|
28
|
-
return object[key]
|
|
29
|
-
}, object)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Delete a property
|
|
34
|
-
* @param {Object|Array} object
|
|
35
|
-
* @param {[string]} keys
|
|
36
|
-
* @throws {Cannot locate intermediate key: ${keys}}
|
|
37
|
-
*/
|
|
38
|
-
export function deleteKeys (object, keys) {
|
|
39
|
-
const initialKeys = keys.slice(0, -1)
|
|
40
|
-
const lastKey = keys[keys.length - 1]
|
|
41
|
-
const finalObject = initialKeys.reduce((object, key) => {
|
|
42
|
-
if (!(key in object)) throw new Error(`Cannot locate intermediate key: ${keys}`)
|
|
43
|
-
return object[key]
|
|
44
|
-
}, object)
|
|
45
|
-
delete finalObject[lastKey]
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Clones some JSON
|
|
50
|
-
* @param {Object|Array|string|number|null} object
|
|
51
|
-
* @returns {Object|Array|string|number|null}
|
|
52
|
-
*/
|
|
53
|
-
export function clone (object) {
|
|
54
|
-
return JSON.parse(JSON.stringify(object))
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Wraps a JSON hierarchy in transparent, dynamically generated
|
|
59
|
-
* Proxy instances to trap JSON change entries, keeping them in
|
|
60
|
-
* a journal to undo, patch or capture.
|
|
61
|
-
*/
|
|
62
|
-
export default class Journal {
|
|
63
|
-
/**
|
|
64
|
-
* @param {Object} options
|
|
65
|
-
* @param {Object|Array} options.data JSON to wrap
|
|
66
|
-
* @param {Function} [options.supplementEntry] Entry supplementation function
|
|
67
|
-
*/
|
|
68
|
-
constructor ({
|
|
69
|
-
logger,
|
|
70
|
-
data
|
|
71
|
-
}) {
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Add supplemental information to the journal
|
|
75
|
-
* for both adapt content and plugins
|
|
76
|
-
*/
|
|
77
|
-
const supplementEntry = (entry, data) => {
|
|
78
|
-
switch (entry.keys[0]) {
|
|
79
|
-
case 'fromPlugins':
|
|
80
|
-
// plugin name
|
|
81
|
-
entry._name = data[entry.keys[0]][entry.keys[1]]?.name ?? '';
|
|
82
|
-
break;
|
|
83
|
-
case 'content':
|
|
84
|
-
// object _id, _type and _component or _extension if available
|
|
85
|
-
entry._id = data[entry.keys[0]][entry.keys[1]]?._id ?? '';
|
|
86
|
-
entry._type = data[entry.keys[0]][entry.keys[1]]?._type ?? '';
|
|
87
|
-
if (entry._type && data[entry.keys[0]][entry.keys[1]]?.[`_${entry._type}`]) {
|
|
88
|
-
entry._name = entry[`_${entry._type}`] = data[entry.keys[0]][entry.keys[1]]?.[`_${entry._type}`] ?? '';
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
return entry;
|
|
92
|
-
}
|
|
93
|
-
/**
|
|
94
|
-
*
|
|
95
|
-
* @param {string} method add/set/delete to signify how the JSON was modified
|
|
96
|
-
* @param {[string]} keys An array of key names to a location in the JSON hierarchy
|
|
97
|
-
* @param {Object|Array|string|number|null} value Value after change
|
|
98
|
-
* @param {Object|Array|string|number|null} previous Value before change
|
|
99
|
-
* @returns
|
|
100
|
-
*/
|
|
101
|
-
const addEntry = (method, keys, value = null, previous = null) => {
|
|
102
|
-
// Prevent entries from being added whilst undoing.
|
|
103
|
-
if (this.isUndoing) return
|
|
104
|
-
// Add entries to the entries list with any necessary supplemental information.
|
|
105
|
-
this.entries.push(supplementEntry({
|
|
106
|
-
method,
|
|
107
|
-
keys,
|
|
108
|
-
// Clone the value and previous to preserve the values of objects by reference.
|
|
109
|
-
value: clone(value),
|
|
110
|
-
previous: clone(previous)
|
|
111
|
-
}, this.data))
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* Wrap an Object or Array in a proxy to trap changes
|
|
115
|
-
* @param {Object|Array} data JSON object or array to wrap in a proxy
|
|
116
|
-
* @param {[string]} [keys] Use the closure to store the key location of the generated proxy instance
|
|
117
|
-
* @returns {Proxy<Object|Array>}
|
|
118
|
-
*/
|
|
119
|
-
const wrap = (data, keys = []) => {
|
|
120
|
-
const journal = this
|
|
121
|
-
return new Proxy(data, {
|
|
122
|
-
/**
|
|
123
|
-
* Getter trap. If the property value is an Array or Object, return wrapped at the next key location
|
|
124
|
-
* @param {Object|Array} target
|
|
125
|
-
* @param {string} prop Array index or object property name
|
|
126
|
-
* @returns {Proxy<Object|Array>|string|number|null}
|
|
127
|
-
*/
|
|
128
|
-
get (target, prop) {
|
|
129
|
-
const value = target[prop]
|
|
130
|
-
if (prop === 'valueOf' && typeof value === 'function') {
|
|
131
|
-
// Special case for turning a proxy into its JSON, which is otherwise not possible
|
|
132
|
-
// This prevents the data side from being infected by proxies.
|
|
133
|
-
return function () {
|
|
134
|
-
return target
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
if (Array.isArray(value) || typeof value === 'object') {
|
|
138
|
-
// Wrap an array or object in a new proxy, passing the appropriate key into the closure.
|
|
139
|
-
return wrap(value, keys.concat([prop]))
|
|
140
|
-
}
|
|
141
|
-
return Reflect.get(...arguments)
|
|
142
|
-
},
|
|
143
|
-
/**
|
|
144
|
-
* Setter trap. Adds entries to the journal at the current key location for sets or adds to the Object or Array.
|
|
145
|
-
* Has special cases for array length modifications.
|
|
146
|
-
* @param {Object|Array} target
|
|
147
|
-
* @param {string} prop
|
|
148
|
-
* @param {Object|Array|string|number|null} value
|
|
149
|
-
* @param {Proxy} proxy
|
|
150
|
-
* @returns {boolean}
|
|
151
|
-
* @throws {Data is frozen, cannot modify}
|
|
152
|
-
*/
|
|
153
|
-
set (target, prop, value, proxy) {
|
|
154
|
-
if (journal.isFrozen) throw new Error('Data is frozen, cannot modify')
|
|
155
|
-
// Special case for turning a proxy into its JSON, which is otherwise not possible.
|
|
156
|
-
// This prevents the data side from being infected by proxies.
|
|
157
|
-
if (value?.valueOf) value = value.valueOf()
|
|
158
|
-
const previous = target[prop]
|
|
159
|
-
const isArrayLength = (Array.isArray(target) && prop === 'length')
|
|
160
|
-
const isObjectPropertyAdd = !(prop in target)
|
|
161
|
-
if (isArrayLength && target.length === value) {
|
|
162
|
-
// Ignore identically set lengths for array (splice can do this)
|
|
163
|
-
// We don't need to add an entry for this.
|
|
164
|
-
return true
|
|
165
|
-
} else if (isObjectPropertyAdd) {
|
|
166
|
-
if (Array.isArray(target) && !isNaN(prop)) {
|
|
167
|
-
// An array index value is being set.
|
|
168
|
-
const intProp = parseInt(prop)
|
|
169
|
-
if (intProp >= target.length) {
|
|
170
|
-
// Make sure to keep the previous length if the array is growing.
|
|
171
|
-
addEntry('set', keys.concat(['length']), intProp + 1, target.length)
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
// Add an entry for an indexed/named property addition, with its new value.
|
|
175
|
-
addEntry('add', keys.concat([prop]), value, undefined)
|
|
176
|
-
} else {
|
|
177
|
-
// Add an entry for a changed indexed/named property value.
|
|
178
|
-
addEntry('set', keys.concat([prop]), value, previous)
|
|
179
|
-
}
|
|
180
|
-
return Reflect.set(target, prop, value, proxy)
|
|
181
|
-
},
|
|
182
|
-
/**
|
|
183
|
-
* Delete trap. Adds a journal entry if an indexed/named property is deleted.
|
|
184
|
-
* Splice on an array or delete on an object.
|
|
185
|
-
* @param {Object|Array} target
|
|
186
|
-
* @param {string} prop
|
|
187
|
-
* @returns {boolean}
|
|
188
|
-
* @throws {Data is frozen, cannot modify}
|
|
189
|
-
*/
|
|
190
|
-
deleteProperty (target, prop) {
|
|
191
|
-
if (journal.isFrozen) throw new Error('Data is frozen, cannot modify')
|
|
192
|
-
const previous = target[prop]
|
|
193
|
-
addEntry('delete', keys.concat([prop]), undefined, previous)
|
|
194
|
-
return Reflect.deleteProperty(...arguments)
|
|
195
|
-
}
|
|
196
|
-
})
|
|
197
|
-
}
|
|
198
|
-
/**
|
|
199
|
-
* Original JSON.
|
|
200
|
-
* @type {Object|Array}
|
|
201
|
-
* */
|
|
202
|
-
this.data = data
|
|
203
|
-
/**
|
|
204
|
-
* Proxied version of data.
|
|
205
|
-
* @type {Proxy<Object|Array>}
|
|
206
|
-
* */
|
|
207
|
-
this.subject = wrap(data)
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Signifies if the proxy is frozen for sets.
|
|
212
|
-
* @type {boolean}
|
|
213
|
-
*/
|
|
214
|
-
get isFrozen () {
|
|
215
|
-
return this._isFrozen ?? false
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Freeze the subject for sets.
|
|
220
|
-
*/
|
|
221
|
-
freeze () {
|
|
222
|
-
this._isFrozen = true
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Open the subject to sets.
|
|
227
|
-
*/
|
|
228
|
-
unfreeze () {
|
|
229
|
-
this._isFrozen = false
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* Undo and return entries.
|
|
234
|
-
* @param {number} [count] Number of entries to undo. Defaults to all.
|
|
235
|
-
* @returns {Array<Object>} Returns undone journal entries.
|
|
236
|
-
* @throws {Cannot locate intermediate key: ${keys}}
|
|
237
|
-
*/
|
|
238
|
-
undo (count = this.entries.length) {
|
|
239
|
-
const entries = []
|
|
240
|
-
this.isUndoing = true
|
|
241
|
-
for (let i = 0, l = count; i < l; i++) {
|
|
242
|
-
const entry = this.entries.pop()
|
|
243
|
-
entries.unshift(entry)
|
|
244
|
-
switch (entry.method) {
|
|
245
|
-
case 'delete':
|
|
246
|
-
case 'set':
|
|
247
|
-
setKeys(this.subject, entry.keys, entry.previous)
|
|
248
|
-
break
|
|
249
|
-
case 'add':
|
|
250
|
-
deleteKeys(this.subject, entry.keys)
|
|
251
|
-
break
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
this.isUndoing = false
|
|
255
|
-
return entries
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Undo entries up until a specified index.
|
|
260
|
-
* @param {Number} [index=-1] Index up to which to undo. Used in conjunction with lastEntryIndex.
|
|
261
|
-
* @returns {Array<Object>} Returns undone journal entries.
|
|
262
|
-
* @throws {Cannot locate intermediate key: ${keys}}
|
|
263
|
-
*/
|
|
264
|
-
undoToIndex (index = -1) {
|
|
265
|
-
const lastIndex = this.entries.length - 1
|
|
266
|
-
const count = lastIndex - index
|
|
267
|
-
return this.undo(count)
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Apply specified entries to the data JSON
|
|
272
|
-
* @param {Array<Object>} entries Entries to apply to the data JSON.
|
|
273
|
-
* @throws Cannot locate intermediate key: ${keys}
|
|
274
|
-
*/
|
|
275
|
-
patch (entries) {
|
|
276
|
-
for (const entry of entries) {
|
|
277
|
-
switch (entry.method) {
|
|
278
|
-
case 'delete':
|
|
279
|
-
deleteKeys(this.subject, entry.keys)
|
|
280
|
-
break
|
|
281
|
-
case 'set':
|
|
282
|
-
case 'add':
|
|
283
|
-
setKeys(this.subject, entry.keys, entry.value)
|
|
284
|
-
break
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/** @type {number} */
|
|
290
|
-
get lastEntryIndex () {
|
|
291
|
-
return this.entries.length - 1
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/** @type {Array<Object>} */
|
|
295
|
-
get entries () {
|
|
296
|
-
return (this._entries = this._entries || [])
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/** @type {Object} */
|
|
300
|
-
get lastEntry () {
|
|
301
|
-
return this.entries[this.entries.length - 1]
|
|
302
|
-
}
|
|
303
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Set a property value
|
|
3
|
+
* @param {Object|Array} object
|
|
4
|
+
* @param {[string]} keys
|
|
5
|
+
* @param {*} value
|
|
6
|
+
* @throws {Cannot locate intermediate key: ${keys}}
|
|
7
|
+
*/
|
|
8
|
+
export function setKeys (object, keys, value) {
|
|
9
|
+
const initialKeys = keys.slice(0, -1)
|
|
10
|
+
const lastKey = keys[keys.length - 1]
|
|
11
|
+
const finalObject = initialKeys.reduce((object, key) => {
|
|
12
|
+
if (!(key in object)) throw new Error(`Cannot locate intermediate key: ${keys}`)
|
|
13
|
+
return object[key]
|
|
14
|
+
}, object)
|
|
15
|
+
finalObject[lastKey] = value
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get a property value
|
|
20
|
+
* @param {Object|Array} object
|
|
21
|
+
* @param {[string]} keys
|
|
22
|
+
* @returns {*}
|
|
23
|
+
* @throws {Cannot locate intermediate key: ${keys}}
|
|
24
|
+
*/
|
|
25
|
+
export function getKeys (object, keys) {
|
|
26
|
+
return keys.reduce((object, key) => {
|
|
27
|
+
if (!(key in object)) throw new Error(`Cannot locate intermediate key: ${keys}`)
|
|
28
|
+
return object[key]
|
|
29
|
+
}, object)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Delete a property
|
|
34
|
+
* @param {Object|Array} object
|
|
35
|
+
* @param {[string]} keys
|
|
36
|
+
* @throws {Cannot locate intermediate key: ${keys}}
|
|
37
|
+
*/
|
|
38
|
+
export function deleteKeys (object, keys) {
|
|
39
|
+
const initialKeys = keys.slice(0, -1)
|
|
40
|
+
const lastKey = keys[keys.length - 1]
|
|
41
|
+
const finalObject = initialKeys.reduce((object, key) => {
|
|
42
|
+
if (!(key in object)) throw new Error(`Cannot locate intermediate key: ${keys}`)
|
|
43
|
+
return object[key]
|
|
44
|
+
}, object)
|
|
45
|
+
delete finalObject[lastKey]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Clones some JSON
|
|
50
|
+
* @param {Object|Array|string|number|null} object
|
|
51
|
+
* @returns {Object|Array|string|number|null}
|
|
52
|
+
*/
|
|
53
|
+
export function clone (object) {
|
|
54
|
+
return JSON.parse(JSON.stringify(object))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Wraps a JSON hierarchy in transparent, dynamically generated
|
|
59
|
+
* Proxy instances to trap JSON change entries, keeping them in
|
|
60
|
+
* a journal to undo, patch or capture.
|
|
61
|
+
*/
|
|
62
|
+
export default class Journal {
|
|
63
|
+
/**
|
|
64
|
+
* @param {Object} options
|
|
65
|
+
* @param {Object|Array} options.data JSON to wrap
|
|
66
|
+
* @param {Function} [options.supplementEntry] Entry supplementation function
|
|
67
|
+
*/
|
|
68
|
+
constructor ({
|
|
69
|
+
logger,
|
|
70
|
+
data
|
|
71
|
+
}) {
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Add supplemental information to the journal
|
|
75
|
+
* for both adapt content and plugins
|
|
76
|
+
*/
|
|
77
|
+
const supplementEntry = (entry, data) => {
|
|
78
|
+
switch (entry.keys[0]) {
|
|
79
|
+
case 'fromPlugins':
|
|
80
|
+
// plugin name
|
|
81
|
+
entry._name = data[entry.keys[0]][entry.keys[1]]?.name ?? '';
|
|
82
|
+
break;
|
|
83
|
+
case 'content':
|
|
84
|
+
// object _id, _type and _component or _extension if available
|
|
85
|
+
entry._id = data[entry.keys[0]][entry.keys[1]]?._id ?? '';
|
|
86
|
+
entry._type = data[entry.keys[0]][entry.keys[1]]?._type ?? '';
|
|
87
|
+
if (entry._type && data[entry.keys[0]][entry.keys[1]]?.[`_${entry._type}`]) {
|
|
88
|
+
entry._name = entry[`_${entry._type}`] = data[entry.keys[0]][entry.keys[1]]?.[`_${entry._type}`] ?? '';
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return entry;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
*
|
|
95
|
+
* @param {string} method add/set/delete to signify how the JSON was modified
|
|
96
|
+
* @param {[string]} keys An array of key names to a location in the JSON hierarchy
|
|
97
|
+
* @param {Object|Array|string|number|null} value Value after change
|
|
98
|
+
* @param {Object|Array|string|number|null} previous Value before change
|
|
99
|
+
* @returns
|
|
100
|
+
*/
|
|
101
|
+
const addEntry = (method, keys, value = null, previous = null) => {
|
|
102
|
+
// Prevent entries from being added whilst undoing.
|
|
103
|
+
if (this.isUndoing) return
|
|
104
|
+
// Add entries to the entries list with any necessary supplemental information.
|
|
105
|
+
this.entries.push(supplementEntry({
|
|
106
|
+
method,
|
|
107
|
+
keys,
|
|
108
|
+
// Clone the value and previous to preserve the values of objects by reference.
|
|
109
|
+
value: clone(value),
|
|
110
|
+
previous: clone(previous)
|
|
111
|
+
}, this.data))
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Wrap an Object or Array in a proxy to trap changes
|
|
115
|
+
* @param {Object|Array} data JSON object or array to wrap in a proxy
|
|
116
|
+
* @param {[string]} [keys] Use the closure to store the key location of the generated proxy instance
|
|
117
|
+
* @returns {Proxy<Object|Array>}
|
|
118
|
+
*/
|
|
119
|
+
const wrap = (data, keys = []) => {
|
|
120
|
+
const journal = this
|
|
121
|
+
return new Proxy(data, {
|
|
122
|
+
/**
|
|
123
|
+
* Getter trap. If the property value is an Array or Object, return wrapped at the next key location
|
|
124
|
+
* @param {Object|Array} target
|
|
125
|
+
* @param {string} prop Array index or object property name
|
|
126
|
+
* @returns {Proxy<Object|Array>|string|number|null}
|
|
127
|
+
*/
|
|
128
|
+
get (target, prop) {
|
|
129
|
+
const value = target[prop]
|
|
130
|
+
if (prop === 'valueOf' && typeof value === 'function') {
|
|
131
|
+
// Special case for turning a proxy into its JSON, which is otherwise not possible
|
|
132
|
+
// This prevents the data side from being infected by proxies.
|
|
133
|
+
return function () {
|
|
134
|
+
return target
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (Array.isArray(value) || (typeof value === 'object' && value !== null)) {
|
|
138
|
+
// Wrap an array or object in a new proxy, passing the appropriate key into the closure.
|
|
139
|
+
return wrap(value, keys.concat([prop]))
|
|
140
|
+
}
|
|
141
|
+
return Reflect.get(...arguments)
|
|
142
|
+
},
|
|
143
|
+
/**
|
|
144
|
+
* Setter trap. Adds entries to the journal at the current key location for sets or adds to the Object or Array.
|
|
145
|
+
* Has special cases for array length modifications.
|
|
146
|
+
* @param {Object|Array} target
|
|
147
|
+
* @param {string} prop
|
|
148
|
+
* @param {Object|Array|string|number|null} value
|
|
149
|
+
* @param {Proxy} proxy
|
|
150
|
+
* @returns {boolean}
|
|
151
|
+
* @throws {Data is frozen, cannot modify}
|
|
152
|
+
*/
|
|
153
|
+
set (target, prop, value, proxy) {
|
|
154
|
+
if (journal.isFrozen) throw new Error('Data is frozen, cannot modify')
|
|
155
|
+
// Special case for turning a proxy into its JSON, which is otherwise not possible.
|
|
156
|
+
// This prevents the data side from being infected by proxies.
|
|
157
|
+
if (value?.valueOf) value = value.valueOf()
|
|
158
|
+
const previous = target[prop]
|
|
159
|
+
const isArrayLength = (Array.isArray(target) && prop === 'length')
|
|
160
|
+
const isObjectPropertyAdd = !(prop in target)
|
|
161
|
+
if (isArrayLength && target.length === value) {
|
|
162
|
+
// Ignore identically set lengths for array (splice can do this)
|
|
163
|
+
// We don't need to add an entry for this.
|
|
164
|
+
return true
|
|
165
|
+
} else if (isObjectPropertyAdd) {
|
|
166
|
+
if (Array.isArray(target) && !isNaN(prop)) {
|
|
167
|
+
// An array index value is being set.
|
|
168
|
+
const intProp = parseInt(prop)
|
|
169
|
+
if (intProp >= target.length) {
|
|
170
|
+
// Make sure to keep the previous length if the array is growing.
|
|
171
|
+
addEntry('set', keys.concat(['length']), intProp + 1, target.length)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// Add an entry for an indexed/named property addition, with its new value.
|
|
175
|
+
addEntry('add', keys.concat([prop]), value, undefined)
|
|
176
|
+
} else {
|
|
177
|
+
// Add an entry for a changed indexed/named property value.
|
|
178
|
+
addEntry('set', keys.concat([prop]), value, previous)
|
|
179
|
+
}
|
|
180
|
+
return Reflect.set(target, prop, value, proxy)
|
|
181
|
+
},
|
|
182
|
+
/**
|
|
183
|
+
* Delete trap. Adds a journal entry if an indexed/named property is deleted.
|
|
184
|
+
* Splice on an array or delete on an object.
|
|
185
|
+
* @param {Object|Array} target
|
|
186
|
+
* @param {string} prop
|
|
187
|
+
* @returns {boolean}
|
|
188
|
+
* @throws {Data is frozen, cannot modify}
|
|
189
|
+
*/
|
|
190
|
+
deleteProperty (target, prop) {
|
|
191
|
+
if (journal.isFrozen) throw new Error('Data is frozen, cannot modify')
|
|
192
|
+
const previous = target[prop]
|
|
193
|
+
addEntry('delete', keys.concat([prop]), undefined, previous)
|
|
194
|
+
return Reflect.deleteProperty(...arguments)
|
|
195
|
+
}
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Original JSON.
|
|
200
|
+
* @type {Object|Array}
|
|
201
|
+
* */
|
|
202
|
+
this.data = data
|
|
203
|
+
/**
|
|
204
|
+
* Proxied version of data.
|
|
205
|
+
* @type {Proxy<Object|Array>}
|
|
206
|
+
* */
|
|
207
|
+
this.subject = wrap(data)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Signifies if the proxy is frozen for sets.
|
|
212
|
+
* @type {boolean}
|
|
213
|
+
*/
|
|
214
|
+
get isFrozen () {
|
|
215
|
+
return this._isFrozen ?? false
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Freeze the subject for sets.
|
|
220
|
+
*/
|
|
221
|
+
freeze () {
|
|
222
|
+
this._isFrozen = true
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Open the subject to sets.
|
|
227
|
+
*/
|
|
228
|
+
unfreeze () {
|
|
229
|
+
this._isFrozen = false
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Undo and return entries.
|
|
234
|
+
* @param {number} [count] Number of entries to undo. Defaults to all.
|
|
235
|
+
* @returns {Array<Object>} Returns undone journal entries.
|
|
236
|
+
* @throws {Cannot locate intermediate key: ${keys}}
|
|
237
|
+
*/
|
|
238
|
+
undo (count = this.entries.length) {
|
|
239
|
+
const entries = []
|
|
240
|
+
this.isUndoing = true
|
|
241
|
+
for (let i = 0, l = count; i < l; i++) {
|
|
242
|
+
const entry = this.entries.pop()
|
|
243
|
+
entries.unshift(entry)
|
|
244
|
+
switch (entry.method) {
|
|
245
|
+
case 'delete':
|
|
246
|
+
case 'set':
|
|
247
|
+
setKeys(this.subject, entry.keys, entry.previous)
|
|
248
|
+
break
|
|
249
|
+
case 'add':
|
|
250
|
+
deleteKeys(this.subject, entry.keys)
|
|
251
|
+
break
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
this.isUndoing = false
|
|
255
|
+
return entries
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Undo entries up until a specified index.
|
|
260
|
+
* @param {Number} [index=-1] Index up to which to undo. Used in conjunction with lastEntryIndex.
|
|
261
|
+
* @returns {Array<Object>} Returns undone journal entries.
|
|
262
|
+
* @throws {Cannot locate intermediate key: ${keys}}
|
|
263
|
+
*/
|
|
264
|
+
undoToIndex (index = -1) {
|
|
265
|
+
const lastIndex = this.entries.length - 1
|
|
266
|
+
const count = lastIndex - index
|
|
267
|
+
return this.undo(count)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Apply specified entries to the data JSON
|
|
272
|
+
* @param {Array<Object>} entries Entries to apply to the data JSON.
|
|
273
|
+
* @throws Cannot locate intermediate key: ${keys}
|
|
274
|
+
*/
|
|
275
|
+
patch (entries) {
|
|
276
|
+
for (const entry of entries) {
|
|
277
|
+
switch (entry.method) {
|
|
278
|
+
case 'delete':
|
|
279
|
+
deleteKeys(this.subject, entry.keys)
|
|
280
|
+
break
|
|
281
|
+
case 'set':
|
|
282
|
+
case 'add':
|
|
283
|
+
setKeys(this.subject, entry.keys, entry.value)
|
|
284
|
+
break
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** @type {number} */
|
|
290
|
+
get lastEntryIndex () {
|
|
291
|
+
return this.entries.length - 1
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** @type {Array<Object>} */
|
|
295
|
+
get entries () {
|
|
296
|
+
return (this._entries = this._entries || [])
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** @type {Object} */
|
|
300
|
+
get lastEntry () {
|
|
301
|
+
return this.entries[this.entries.length - 1]
|
|
302
|
+
}
|
|
303
|
+
}
|