adapt-migrations 1.0.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.
@@ -0,0 +1,95 @@
1
+ import crypto from 'crypto'
2
+ import path from 'path'
3
+ import globs from 'globs'
4
+ import fs from 'fs-extra'
5
+ import os from 'os'
6
+ import Logger from './Logger.js'
7
+
8
+ export const ONE_MINUTE = 60 * 1000
9
+ export const ONE_HOUR = 60 * ONE_MINUTE
10
+ export const ONE_WEEK = 7 * 24 * ONE_HOUR
11
+
12
+ const logger = Logger.getInstance();
13
+
14
+ export default class CacheManager {
15
+ constructor (maxAge = ONE_WEEK) {
16
+ this.maxAge = maxAge
17
+ }
18
+
19
+ static hash (path) {
20
+ return crypto
21
+ .createHash('sha1')
22
+ .update(path, 'utf8')
23
+ .digest('hex')
24
+ }
25
+
26
+ async getTempPath () {
27
+ const osTempPath = await fs.promises.realpath(path.resolve(os.tmpdir()))
28
+ const tempPath = path.posix.join(osTempPath, 'migrations')
29
+ await fs.ensureDir(tempPath)
30
+ return tempPath
31
+ }
32
+
33
+ async getCachePath ({ basePath = process.cwd(), outputPath, tempPath }) {
34
+ const projectHash = CacheManager.hash(path.join(basePath, outputPath))
35
+ if (tempPath) await fs.ensureDir(tempPath)
36
+ const cachePath = path.join(tempPath ?? await this.getTempPath(), `${projectHash}.cache`)
37
+ return cachePath
38
+ }
39
+
40
+ async getCheckFilePath () {
41
+ const checkFilePath = path.join(await this.getTempPath(), 'last.touch')
42
+ return checkFilePath
43
+ }
44
+
45
+ async isCleaningTime () {
46
+ // By default, clean once a day, or with a floor of one hourly intervals
47
+ const checkInterval = Math.max(this.maxAge / 7, ONE_HOUR)
48
+ const checkFilePath = await this.getCheckFilePath()
49
+ // Check if checkFile is older than the cleaning interval
50
+ return (!fs.existsSync(checkFilePath) || Date.now() - (await fs.stat(checkFilePath)).mtime >= checkInterval)
51
+ }
52
+
53
+ async clean () {
54
+ if (!await this.isCleaningTime()) return
55
+ logger.debug('CacheManager -- Clean running')
56
+ const checkFilePath = await this.getCheckFilePath()
57
+ // Touch checkFile
58
+ await fs.writeFile(checkFilePath, String(Date.now()))
59
+ logger.debug('CacheManager -- Clearing compilation caches')
60
+ const tempPath = await this.getTempPath()
61
+ // Fetch all cache files except checkFile
62
+ const files = await new Promise((resolve, reject) => globs([
63
+ `${tempPath}/**`,
64
+ `!${checkFilePath}`
65
+ ], {
66
+ nodir: true
67
+ }, (err, files) => err ? reject(err) : resolve(files)))
68
+ // Fetch file ages
69
+ const fileAges = []
70
+ const now = Date.now()
71
+ for (const index in files) {
72
+ const file = files[index]
73
+ let age = this.maxAge
74
+ try {
75
+ const stat = await fs.stat(file)
76
+ age = (now - stat.mtime)
77
+ } catch (err) {
78
+ logger.error(`CacheManager -- ${err}`)
79
+ }
80
+ fileAges[index] = { file, age }
81
+ }
82
+ // Sort by oldest
83
+ fileAges.sort((a, b) => b.age - a.age)
84
+ // Filter by expired
85
+ const toRemove = fileAges.filter(fileAge => fileAge.age >= this.maxAge)
86
+ // Delete expired cache files
87
+ for (const fileAge of toRemove) {
88
+ try {
89
+ await fs.unlink(fileAge.file)
90
+ } catch (err) {
91
+ logger.error(`CacheManager -- Could not clear cache file ${fileAge.file}`)
92
+ }
93
+ }
94
+ }
95
+ };
package/lib/Journal.js ADDED
@@ -0,0 +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
+ }
package/lib/Logger.js ADDED
@@ -0,0 +1,91 @@
1
+ import chalk from 'chalk'
2
+ import path from 'path'
3
+ import fs from 'fs-extra'
4
+
5
+ function padNum2(value) {
6
+ return String(value).padStart(2, '0');
7
+ }
8
+
9
+ export default class Logger {
10
+ constructor () {
11
+ this.logArr = []
12
+ this.config = {
13
+ levels: {
14
+ error: {
15
+ colour: 'red'
16
+ },
17
+ warn: {
18
+ colour: 'yellow'
19
+ },
20
+ info: {
21
+ colour: 'cyan'
22
+ },
23
+ debug: {
24
+ colour: 'grey'
25
+ }
26
+ }
27
+ }
28
+ }
29
+
30
+ // getInstance used to allow import
31
+ static getInstance () {
32
+ if (Logger.instance === null) {
33
+ Logger.instance = new Logger()
34
+ }
35
+ return Logger.instance
36
+ }
37
+
38
+ // Colour level string, easier to differentiate between info/error/warn/debug
39
+ static colourise (str, colour) {
40
+ const chalkFunc = chalk[colour]
41
+ return chalkFunc ? chalkFunc(str) : str
42
+ }
43
+
44
+ // Use getDateStamp to return readable date/time string.
45
+ static getDateStamp () {
46
+ const d = new Date()
47
+ const date = `${d.getFullYear().toString()}/${padNum2(d.getMonth() + 1)}/${padNum2(d.getDate())}`
48
+ const time = `${padNum2(d.getHours())}:${padNum2(d.getMinutes())}:${padNum2(d.getSeconds())}.${padNum2(d.getMilliseconds())}`
49
+ const str = `${date} ${time}`
50
+ return str
51
+ }
52
+
53
+ // Add Success/Fail ?
54
+ info (...args) {
55
+ this.log('info', args)
56
+ }
57
+
58
+ error (...args) {
59
+ this.log('error', args)
60
+ }
61
+
62
+ warn (...args) {
63
+ this.log('warn', args)
64
+ }
65
+
66
+ debug (...args) {
67
+ this.log('debug', args)
68
+ }
69
+
70
+ // Colour level for easy read, store log in this.logArr as string
71
+ log (level, args) {
72
+ const colour = this?.config?.levels[level]?.colour || 'grey'
73
+ const logFunc = console[level] ?? console.log
74
+ const dateStamp = Logger.getDateStamp()
75
+ const argsStr = args.join(', ')
76
+ logFunc(`(${dateStamp}) -- ${Logger.colourise(level, colour)} -- `, ...args)
77
+ this.logArr.push(`[${dateStamp}] -- ${level} -- ${argsStr}`)
78
+ }
79
+
80
+ // Output this.logArr, 2 log outputs, capture & migrate
81
+ output (dir, type) {
82
+ const logPath = path.join(dir, './logs/')
83
+ if (!fs.existsSync(logPath)) fs.mkdirSync(logPath)
84
+
85
+ const outputName = `${type}_log`
86
+ const outputFile = path.join(logPath, `${outputName}.json`)
87
+ fs.writeJSONSync(outputFile, this.logArr, { replacer: null, spaces: 2 })
88
+ }
89
+ }
90
+
91
+ Logger.instance = null