bajo-extra 0.1.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/LICENSE +21 -0
- package/README.md +1 -0
- package/bajo/config.json +16 -0
- package/bajo/helper/export-to.js +109 -0
- package/bajo/helper/fetch.js +18 -0
- package/bajo/helper/gunzip.js +7 -0
- package/bajo/helper/gzip.js +23 -0
- package/bajo/helper/import-from.js +65 -0
- package/bajoCli/tool/export-to.js +54 -0
- package/bajoCli/tool/import-from.js +50 -0
- package/bajoCli/tool.js +8 -0
- package/package.json +35 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Ardhi Lukianto
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# bajo-extra
|
package/bajo/config.json
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import scramjet from 'scramjet'
|
|
3
|
+
import format from 'ndjson-csv-xlsx'
|
|
4
|
+
import { createGzip } from 'zlib'
|
|
5
|
+
|
|
6
|
+
const { json, ndjson, csv, xlsx } = format
|
|
7
|
+
const { DataStream } = scramjet
|
|
8
|
+
|
|
9
|
+
const supportedExt = ['.json', '.jsonl', '.ndjson', '.csv', '.xlsx']
|
|
10
|
+
|
|
11
|
+
async function getFile (dest, ensureDir) {
|
|
12
|
+
const { importPkg, getConfig, error } = this.bajo.helper
|
|
13
|
+
const [fs, increment] = await importPkg('fs-extra', 'add-filename-increment')
|
|
14
|
+
const config = getConfig()
|
|
15
|
+
let file
|
|
16
|
+
if (path.isAbsolute(dest)) file = dest
|
|
17
|
+
else {
|
|
18
|
+
file = `${config.dir.data}/plugins/bajoDb/export/${dest}`
|
|
19
|
+
fs.ensureDirSync(path.dirname(file))
|
|
20
|
+
}
|
|
21
|
+
file = increment(file, { fs: true })
|
|
22
|
+
const dir = path.dirname(file)
|
|
23
|
+
if (!fs.existsSync(dir)) {
|
|
24
|
+
if (ensureDir) fs.ensureDirSync(dir)
|
|
25
|
+
else throw error('Directory \'%s\' doesn\'t exist', dir)
|
|
26
|
+
}
|
|
27
|
+
let compress = false
|
|
28
|
+
let ext = path.extname(file)
|
|
29
|
+
if (ext === '.gz') {
|
|
30
|
+
compress = true
|
|
31
|
+
ext = path.extname(path.basename(file).replace('.gz', ''))
|
|
32
|
+
// file = file.slice(0, file.length - 3)
|
|
33
|
+
}
|
|
34
|
+
if (!supportedExt.includes(ext)) throw error('Unsupported format \'%s\'', ext.slice(1))
|
|
35
|
+
return { file, ext, compress }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function getData ({ source, filter, count, stream, progressFn }) {
|
|
39
|
+
let cnt = count ?? 0
|
|
40
|
+
const { recordFind } = this.bajoDb.helper
|
|
41
|
+
for (;;) {
|
|
42
|
+
const { data, pages, page } = await recordFind(source, filter, { dataOnly: false })
|
|
43
|
+
if (data.length === 0) break
|
|
44
|
+
cnt += data.length
|
|
45
|
+
await stream.pull(data)
|
|
46
|
+
if (progressFn) await progressFn.call(this, { batchTotal: pages, batchNo: page, data })
|
|
47
|
+
filter.page++
|
|
48
|
+
}
|
|
49
|
+
await stream.end()
|
|
50
|
+
return cnt
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function exportTo (source, dest, { filter = {}, ensureDir, useHeader = true, batch = 500, progressFn } = {}) {
|
|
54
|
+
const { error, importPkg, getConfig } = this.bajo.helper
|
|
55
|
+
const cfg = getConfig('bajoExtra')
|
|
56
|
+
if (!this.bajoDb) throw error('Bajo DB isn\'t loaded')
|
|
57
|
+
filter.page = 1
|
|
58
|
+
batch = parseInt(batch) ?? 500
|
|
59
|
+
if (batch > cfg.stream.export.maxBatch) batch = cfg.stream.export.maxBatch
|
|
60
|
+
if (batch < 0) batch = 1
|
|
61
|
+
filter.limit = batch
|
|
62
|
+
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
const { getInfo } = this.bajoDb.helper
|
|
65
|
+
let count = 0
|
|
66
|
+
let fs
|
|
67
|
+
let file
|
|
68
|
+
let ext
|
|
69
|
+
let stream
|
|
70
|
+
let compress
|
|
71
|
+
let writer
|
|
72
|
+
getInfo(source)
|
|
73
|
+
.then(() => {
|
|
74
|
+
return importPkg('fs-extra')
|
|
75
|
+
})
|
|
76
|
+
.then(res => {
|
|
77
|
+
fs = res
|
|
78
|
+
return getFile.call(this, dest, ensureDir)
|
|
79
|
+
})
|
|
80
|
+
.then(res => {
|
|
81
|
+
file = res.file
|
|
82
|
+
ext = res.ext
|
|
83
|
+
compress = res.compress
|
|
84
|
+
writer = fs.createWriteStream(file)
|
|
85
|
+
writer.on('error', err => {
|
|
86
|
+
reject(err)
|
|
87
|
+
})
|
|
88
|
+
writer.on('finish', () => {
|
|
89
|
+
resolve({ file, count })
|
|
90
|
+
})
|
|
91
|
+
stream = new DataStream()
|
|
92
|
+
stream = stream.flatMap(items => (items))
|
|
93
|
+
const pipes = []
|
|
94
|
+
if (ext === '.json') pipes.push(json.stringify())
|
|
95
|
+
else if (['.ndjson', '.jsonl'].includes(ext)) pipes.push(ndjson.stringify())
|
|
96
|
+
else if (ext === '.csv') pipes.push(csv.stringify({ headers: useHeader }))
|
|
97
|
+
else if (ext === '.xlsx') pipes.push(xlsx.stringify({ header: useHeader }))
|
|
98
|
+
if (compress) pipes.push(createGzip())
|
|
99
|
+
DataStream.pipeline(stream, ...pipes).pipe(writer)
|
|
100
|
+
return getData.call(this, { source, filter, count, stream, progressFn })
|
|
101
|
+
})
|
|
102
|
+
.then(cnt => {
|
|
103
|
+
count = cnt
|
|
104
|
+
})
|
|
105
|
+
.catch(reject)
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export default exportTo
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import axios from 'axios'
|
|
2
|
+
|
|
3
|
+
async function fetch (url, opts = {}, ext = {}) {
|
|
4
|
+
const { importPkg } = this.bajo.helper
|
|
5
|
+
const { has, isPlainObject, cloneDeep } = await importPkg('lodash-es')
|
|
6
|
+
if (isPlainObject(url)) {
|
|
7
|
+
ext = cloneDeep(opts)
|
|
8
|
+
opts = cloneDeep(url)
|
|
9
|
+
} else opts.url = url
|
|
10
|
+
opts.params = opts.params ?? {}
|
|
11
|
+
if (!has(ext, 'cacheBuster')) ext.cacheBuster = true
|
|
12
|
+
if (ext.cacheBuster) opts.params[ext.cacheBusterKey ?? '_'] = Date.now()
|
|
13
|
+
const resp = await axios(opts)
|
|
14
|
+
if (ext.rawResponse) return resp
|
|
15
|
+
return resp.data
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default fetch
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createGzip, createGunzip } from 'zlib'
|
|
2
|
+
|
|
3
|
+
function gzip (file, deleteOld, unzip) {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
const { importPkg } = this.bajo.helper
|
|
6
|
+
importPkg('fs-extra')
|
|
7
|
+
.then(fs => {
|
|
8
|
+
const newFile = unzip ? file.slice(0, file.length - 3) : (file + '.gz')
|
|
9
|
+
const reader = fs.createReadStream(file)
|
|
10
|
+
const writer = fs.createWriteStream(newFile)
|
|
11
|
+
const method = unzip ? createGunzip() : createGzip()
|
|
12
|
+
reader.pipe(method).pipe(writer)
|
|
13
|
+
writer.on('error', reject)
|
|
14
|
+
writer.on('finish', err => {
|
|
15
|
+
if (err) return reject(err)
|
|
16
|
+
if (deleteOld) fs.unlinkSync(file)
|
|
17
|
+
resolve()
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default gzip
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import scramjet from 'scramjet'
|
|
3
|
+
import format from 'ndjson-csv-xlsx'
|
|
4
|
+
import { createGunzip } from 'zlib'
|
|
5
|
+
|
|
6
|
+
const { json, ndjson, csv, xlsx } = format
|
|
7
|
+
const { DataStream } = scramjet
|
|
8
|
+
const supportedExt = ['.json', '.jsonl', '.ndjson', '.csv', '.xlsx']
|
|
9
|
+
|
|
10
|
+
async function importFrom (source, dest, { trashOld = true, batch, progressFn, useHeader = true } = {}) {
|
|
11
|
+
const { error, importPkg, getConfig } = this.bajo.helper
|
|
12
|
+
if (!this.bajoDb) throw error('Bajo DB isn\'t loaded')
|
|
13
|
+
const { getInfo, recordClear, recordCreate } = this.bajoDb.helper
|
|
14
|
+
await getInfo(dest)
|
|
15
|
+
const fs = await importPkg('fs-extra')
|
|
16
|
+
const config = getConfig()
|
|
17
|
+
const cfg = getConfig('bajoExtra')
|
|
18
|
+
|
|
19
|
+
let file
|
|
20
|
+
if (path.isAbsolute(source)) file = source
|
|
21
|
+
else {
|
|
22
|
+
file = `${config.dir.data}/plugins/bajoDb/import/${source}`
|
|
23
|
+
fs.ensureDirSync(path.dirname(file))
|
|
24
|
+
}
|
|
25
|
+
if (!fs.existsSync(file)) throw error('Source file \'%s\' doesn\'t exist', file)
|
|
26
|
+
let ext = path.extname(file)
|
|
27
|
+
let decompress = false
|
|
28
|
+
if (ext === '.gz') {
|
|
29
|
+
ext = path.extname(path.basename(file, '.gz'))
|
|
30
|
+
decompress = true
|
|
31
|
+
}
|
|
32
|
+
if (!supportedExt.includes(ext)) throw error('Unsupported format \'%s\'', ext.slice(1))
|
|
33
|
+
if (trashOld) await recordClear(dest)
|
|
34
|
+
const reader = fs.createReadStream(file)
|
|
35
|
+
batch = parseInt(batch) ?? 100
|
|
36
|
+
if (batch > cfg.stream.import.maxBatch) batch = cfg.stream.import.maxBatch
|
|
37
|
+
if (batch < 0) batch = 1
|
|
38
|
+
let count = 0
|
|
39
|
+
const pipes = [reader]
|
|
40
|
+
if (decompress) pipes.push(createGunzip())
|
|
41
|
+
if (ext === '.json') pipes.push(json.parse())
|
|
42
|
+
else if (['.ndjson', '.jsonl'].includes(ext)) pipes.push(ndjson.parse())
|
|
43
|
+
else if (ext === '.csv') pipes.push(csv.parse({ headers: useHeader }))
|
|
44
|
+
else if (ext === '.xlsx') pipes.push(xlsx.parse({ header: useHeader }))
|
|
45
|
+
|
|
46
|
+
const stream = DataStream.pipeline(...pipes)
|
|
47
|
+
let batchNo = 1
|
|
48
|
+
await stream
|
|
49
|
+
.batch(batch)
|
|
50
|
+
.map(async items => {
|
|
51
|
+
if (items.length === 0) return null
|
|
52
|
+
if (progressFn) await progressFn.call(this, { batchNo, data: items })
|
|
53
|
+
for (let i = 0; i < items.length; i++) {
|
|
54
|
+
count++
|
|
55
|
+
await recordCreate(dest, items[i])
|
|
56
|
+
}
|
|
57
|
+
batchNo++
|
|
58
|
+
return null
|
|
59
|
+
})
|
|
60
|
+
.run()
|
|
61
|
+
|
|
62
|
+
return { file, count }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export default importFrom
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import Path from 'path'
|
|
2
|
+
|
|
3
|
+
function makeProgress (spinner) {
|
|
4
|
+
return async function ({ batchNo, batchTotal, data } = {}) {
|
|
5
|
+
if (batchTotal === 0) return
|
|
6
|
+
spinner.setText('Batch %d of %d (%d records)', batchNo, batchTotal, data.length)
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function exportTo (path, args) {
|
|
11
|
+
const { importPkg, print, dayjs, getConfig, importModule } = this.bajo.helper
|
|
12
|
+
const { isEmpty, map } = await importPkg('lodash-es')
|
|
13
|
+
const [input, select] = await importPkg('bajo-cli:@inquirer/input',
|
|
14
|
+
'bajo-cli:@inquirer/select')
|
|
15
|
+
if (!this.bajoDb) print.fatal('Bajo DB isn\'t loaded')
|
|
16
|
+
const schemas = map(this.bajoDb.schemas, 'name')
|
|
17
|
+
if (isEmpty(schemas)) print.fatal('No schema found!')
|
|
18
|
+
let [repo, dest, query] = args
|
|
19
|
+
if (isEmpty(repo)) {
|
|
20
|
+
repo = await select({
|
|
21
|
+
message: print.__('Please choose repository:'),
|
|
22
|
+
choices: map(schemas, s => ({ value: s }))
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
if (isEmpty(dest)) {
|
|
26
|
+
dest = await input({
|
|
27
|
+
message: print.__('Please enter destination file:'),
|
|
28
|
+
default: `${repo}-${dayjs().format('YYYYMMDD')}.ndjson`,
|
|
29
|
+
validate: (item) => !isEmpty(item)
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
if (isEmpty(query)) {
|
|
33
|
+
query = await input({
|
|
34
|
+
message: print.__('Please enter a query (if any):')
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
const spinner = print.bora('Exporting...').start()
|
|
38
|
+
const progressFn = makeProgress.call(this, spinner)
|
|
39
|
+
const cfg = getConfig('bajoDb', { full: true })
|
|
40
|
+
const { batch } = getConfig()
|
|
41
|
+
const start = await importModule(`${cfg.dir}/bajo/start.js`)
|
|
42
|
+
const { connection } = await this.bajoDb.helper.getInfo(repo)
|
|
43
|
+
await start.call(this, connection.name)
|
|
44
|
+
try {
|
|
45
|
+
const filter = { query }
|
|
46
|
+
const result = await this.bajoExtra.helper.exportTo(repo, dest, { filter, batch, progressFn })
|
|
47
|
+
spinner.succeed('%d records successfully exported to \'%s\'', result.count, Path.resolve(result.file))
|
|
48
|
+
} catch (err) {
|
|
49
|
+
console.log(err)
|
|
50
|
+
spinner.fatal('Error: %s', err.message)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default exportTo
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import Path from 'path'
|
|
2
|
+
|
|
3
|
+
function makeProgress (spinner) {
|
|
4
|
+
return async function ({ batchNo, data } = {}) {
|
|
5
|
+
spinner.setText('Batch %d (%d records)', batchNo, data.length)
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async function importFrom (path, args) {
|
|
10
|
+
const { importPkg, print, importModule, getConfig } = this.bajo.helper
|
|
11
|
+
const { isEmpty, map } = await importPkg('lodash-es')
|
|
12
|
+
const [input, select, confirm] = await importPkg('bajo-cli:@inquirer/input',
|
|
13
|
+
'bajo-cli:@inquirer/select', 'bajo-cli:@inquirer/confirm')
|
|
14
|
+
if (!this.bajoDb) print.fatal('Bajo DB isn\'t loaded')
|
|
15
|
+
const schemas = map(this.bajoDb.schemas, 'name')
|
|
16
|
+
if (isEmpty(schemas)) print.fatal('No schema found!')
|
|
17
|
+
let [dest, repo] = args
|
|
18
|
+
if (isEmpty(dest)) {
|
|
19
|
+
dest = await input({
|
|
20
|
+
message: print.__('Please enter source file:'),
|
|
21
|
+
validate: (item) => !isEmpty(item)
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
if (isEmpty(repo)) {
|
|
25
|
+
repo = await select({
|
|
26
|
+
message: print.__('Please choose repository:'),
|
|
27
|
+
choices: map(schemas, s => ({ value: s }))
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
const answer = await confirm({
|
|
31
|
+
message: print.__('You\'re about to replace ALL records with the new ones. Are you really sure?'),
|
|
32
|
+
default: false
|
|
33
|
+
})
|
|
34
|
+
if (!answer) print.fatal('Aborted!')
|
|
35
|
+
const spinner = print.bora('Importing...').start()
|
|
36
|
+
const progressFn = makeProgress.call(this, spinner)
|
|
37
|
+
const cfg = getConfig('bajoDb', { full: true })
|
|
38
|
+
const { batch } = getConfig()
|
|
39
|
+
const start = await importModule(`${cfg.dir}/bajo/start.js`)
|
|
40
|
+
const { connection } = await this.bajoDb.helper.getInfo(repo)
|
|
41
|
+
await start.call(this, connection.name)
|
|
42
|
+
try {
|
|
43
|
+
const result = await this.bajoExtra.helper.importFrom(dest, repo, { batch, progressFn })
|
|
44
|
+
spinner.succeed('%d records successfully imported from \'%s\'', result.count, Path.resolve(result.file))
|
|
45
|
+
} catch (err) {
|
|
46
|
+
spinner.fatal('Error: %s', err.message)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export default importFrom
|
package/bajoCli/tool.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
async function tool ({ path, args = [] }) {
|
|
2
|
+
const { currentLoc } = this.bajo.helper
|
|
3
|
+
const { runToolMethod } = this.bajoCli.helper
|
|
4
|
+
const options = { demonize: ['shell'] }
|
|
5
|
+
await runToolMethod({ path, args, dir: `${currentLoc(import.meta).dir}/tool`, options })
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default tool
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bajo-extra",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Extra package for Bajo Framework",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/ardhi/bajo-extra.git"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"bajo",
|
|
16
|
+
"extra",
|
|
17
|
+
"nodejs",
|
|
18
|
+
"modular",
|
|
19
|
+
"module",
|
|
20
|
+
"package"
|
|
21
|
+
],
|
|
22
|
+
"author": "Ardhi Lukianto <ardhi@lukianto.com>",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/ardhi/bajo-extra/issues"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://github.com/ardhi/bajo-extra#readme",
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"async": "^3.2.4",
|
|
30
|
+
"axios": "^1.4.0",
|
|
31
|
+
"ndjson-csv-xlsx": "^1.1.1",
|
|
32
|
+
"query-string": "^8.1.0",
|
|
33
|
+
"scramjet": "^4.36.9"
|
|
34
|
+
}
|
|
35
|
+
}
|