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/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
+ }