dobo-extra 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 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,23 @@
1
+ # dobo-extra
2
+
3
+ Plugin name: **doboExtra**, alias: **dbxtra**
4
+
5
+ ![GitHub package.json version](https://img.shields.io/github/package-json/v/ardhi/dobo-extra) ![NPM Version](https://img.shields.io/npm/v/dobo-extra)
6
+
7
+ > <br />**Attention**: I do NOT accept any pull request at the moment, thanks!<br /><br />
8
+
9
+ Tools & utility for [Dobo](https://github.com/ardhi/dobo)
10
+
11
+ ## Installation
12
+
13
+ Goto your ```<bajo-base-dir>``` and type:
14
+
15
+ ```bash
16
+ $ npm install dobo-extra
17
+ ```
18
+
19
+ Now open your ```<bajo-data-dir>/config/.plugins``` and put ```dobo-extra``` in it
20
+
21
+ ## License
22
+
23
+ [MIT](LICENSE)
@@ -0,0 +1,20 @@
1
+ {
2
+ "alias": "dbx",
3
+ "export": {
4
+ "maxBatch": 1000,
5
+ "stringify": {
6
+ "open": "[\n",
7
+ "sep": ",\n",
8
+ "close": "\n]\n"
9
+ }
10
+ },
11
+ "import": {
12
+ "maxBatch": 1000
13
+ },
14
+ "archive": {
15
+ "tasks": [],
16
+ "checkInterval": false,
17
+ "runEarly": true
18
+ },
19
+ "dependencies": ["dobo", "bajo-extra"]
20
+ }
package/bajo/init.js ADDED
@@ -0,0 +1,25 @@
1
+ const types = ['datetime', 'date', 'timestamp']
2
+
3
+ async function handler ({ item }) {
4
+ const { join } = this.app.bajo
5
+ const { getSchema } = this.app.dobo
6
+ const { has } = this.app.bajo.lib._
7
+ for (const f of ['source', 'destination']) {
8
+ if (!has(item, f)) throw this.error('Task must have %s model', f)
9
+ const key = `${f}Field`
10
+ item[key] = item[key] ?? 'createdAt'
11
+ const schema = getSchema(item[f])
12
+ const prop = schema.properties.find(p => p.name === item[key])
13
+ if (!prop) throw this.error('Unknown field \'%s@%s\'', item[key], item[f])
14
+ if (!types.includes(prop.type)) throw this.error('\'%s@%s (%s)\' is not supported (must be one of %s)', item[key], item[f], prop.type, join(types))
15
+ }
16
+ if (item.source === item.destination) throw this.error('Source & destination must be different')
17
+ item.maxAge = item.maxAge ?? 1 // in days, less then 1 is ignored
18
+ }
19
+
20
+ async function init () {
21
+ const { buildCollections } = this.app.bajo
22
+ this.archivers = await buildCollections({ ns: this.name, handler, container: 'archive.tasks', dupChecks: ['source'], useDefaultName: false })
23
+ }
24
+
25
+ export default init
@@ -0,0 +1,105 @@
1
+ import path from 'path'
2
+ import format from '../../lib/ndjson-csv-xlsx.js'
3
+ import { createGzip } from 'node:zlib'
4
+ import scramjet from 'scramjet'
5
+ import supportedExt from '../../lib/io-exts.js'
6
+
7
+ const { DataStream } = scramjet
8
+ const { json, ndjson, csv, xlsx } = format
9
+
10
+ async function getFile (dest, ensureDir) {
11
+ const { importPkg, getPluginDataDir } = this.app.bajo
12
+ const { fs } = this.app.bajo.lib
13
+ const increment = await importPkg('add-filename-increment')
14
+ let file
15
+ if (path.isAbsolute(dest)) file = dest
16
+ else {
17
+ file = `${getPluginDataDir(this.name)}/export/${dest}`
18
+ fs.ensureDirSync(path.dirname(file))
19
+ }
20
+ file = increment(file, { fs: true })
21
+ const dir = path.dirname(file)
22
+ if (!fs.existsSync(dir)) {
23
+ if (ensureDir) fs.ensureDirSync(dir)
24
+ else throw this.error('Directory \'%s\' doesn\'t exist', dir)
25
+ }
26
+ let compress = false
27
+ let ext = path.extname(file)
28
+ if (ext === '.gz') {
29
+ compress = true
30
+ ext = path.extname(path.basename(file).replace('.gz', ''))
31
+ // file = file.slice(0, file.length - 3)
32
+ }
33
+ if (!supportedExt.includes(ext)) throw this.error('Unsupported format \'%s\'', ext.slice(1))
34
+ return { file, ext, compress }
35
+ }
36
+
37
+ async function getData ({ source, filter, count, stream, progressFn }) {
38
+ let cnt = count ?? 0
39
+ const { recordFind } = this.app.dobo
40
+ for (;;) {
41
+ const batchStart = new Date()
42
+ const { data, 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, { batchNo: page, data, batchStart, batchEnd: new Date() })
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 } = {}, opts = {}) {
54
+ const { fs } = this.app.bajo.lib
55
+ const { merge } = this.app.bajo.lib._
56
+
57
+ filter.page = 1
58
+ batch = parseInt(batch) ?? 500
59
+ if (batch > this.config.export.maxBatch) batch = this.config.export.maxBatch
60
+ if (batch < 0) batch = 1
61
+ filter.limit = batch
62
+
63
+ return new Promise((resolve, reject) => {
64
+ const { getInfo } = this.app.dobo
65
+ let count = 0
66
+ let file
67
+ let ext
68
+ let stream
69
+ let compress
70
+ let writer
71
+ getInfo(source)
72
+ .then(res => {
73
+ return getFile.call(this, dest, ensureDir)
74
+ })
75
+ .then(res => {
76
+ file = res.file
77
+ ext = res.ext
78
+ compress = res.compress
79
+ writer = fs.createWriteStream(file)
80
+ writer.on('error', err => {
81
+ reject(err)
82
+ })
83
+ writer.on('finish', () => {
84
+ resolve({ file, count })
85
+ })
86
+ stream = new DataStream()
87
+ stream = stream.flatMap(items => (items))
88
+ const pipes = []
89
+ if (ext === '.json') pipes.push(json.stringify(opts))
90
+ else if (['.ndjson', '.jsonl'].includes(ext)) pipes.push(ndjson.stringify(opts))
91
+ else if (ext === '.csv') pipes.push(csv.stringify(merge({}, { headers: useHeader }, opts)))
92
+ else if (ext === '.tsv') pipes.push(csv.stringify(merge({}, { headers: useHeader }, merge({}, opts, { delimiter: '\t' }))))
93
+ else if (ext === '.xlsx') pipes.push(xlsx.stringify(merge({}, { header: useHeader }, opts)))
94
+ if (compress) pipes.push(createGzip())
95
+ DataStream.pipeline(stream, ...pipes).pipe(writer)
96
+ return getData.call(this, { source, filter, count, stream, progressFn })
97
+ })
98
+ .then(cnt => {
99
+ count = cnt
100
+ })
101
+ .catch(reject)
102
+ })
103
+ }
104
+
105
+ export default exportTo
@@ -0,0 +1,67 @@
1
+ import path from 'path'
2
+ import format from '../../lib/ndjson-csv-xlsx.js'
3
+ import { createGunzip } from 'zlib'
4
+ import supportedExt from '../../lib/io-exts.js'
5
+ import scramjet from 'scramjet'
6
+
7
+ const { DataStream } = scramjet
8
+ const { json, ndjson, csv, xlsx } = format
9
+
10
+ async function importFrom (source, dest, { trashOld = true, batch = 1, progressFn, converterFn, useHeader = true, fileType, createOpts = {} } = {}, opts = {}) {
11
+ const { getPluginDataDir } = this.app.bajo
12
+ const { recordCreate } = this.app.dobo
13
+ const { merge } = this.app.bajo.lib._
14
+ const { fs } = this.app.bajo.lib
15
+
16
+ if (dest !== false) this.app.dobo.getInfo(dest) // make sure dest model is valid
17
+ let file
18
+ if (path.isAbsolute(source)) file = source
19
+ else {
20
+ file = `${getPluginDataDir(this.name)}/import/${source}`
21
+ fs.ensureDirSync(path.dirname(file))
22
+ }
23
+ if (!fs.existsSync(file)) throw this.error('Source file \'%s\' doesn\'t exist', file)
24
+ let ext = fileType ? `.${fileType}` : path.extname(file)
25
+ let decompress = false
26
+ if (ext === '.gz') {
27
+ ext = path.extname(path.basename(file, '.gz'))
28
+ decompress = true
29
+ }
30
+ if (!supportedExt.includes(ext)) throw this.error('Unsupported format \'%s\'', ext.slice(1))
31
+ if (trashOld && dest !== false) await this.app.dobo.modelClear(dest)
32
+ const reader = fs.createReadStream(file)
33
+ batch = parseInt(batch) || 100
34
+ if (batch > this.config.import.maxBatch) batch = this.config.import.maxBatch
35
+ if (batch < 0) batch = 1
36
+ let count = 0
37
+ const pipes = [reader]
38
+ if (decompress) pipes.push(createGunzip())
39
+ if (ext === '.json') pipes.push(json.parse(opts))
40
+ else if (['.ndjson', '.jsonl'].includes(ext)) pipes.push(ndjson.parse(opts))
41
+ else if (ext === '.csv') pipes.push(csv.parse(merge({}, { headers: useHeader }, opts)))
42
+ else if (ext === '.tsv') pipes.push(csv.parse(merge({}, { headers: useHeader }, merge({}, opts, { delimiter: '\t' }))))
43
+ else if (ext === '.xlsx') pipes.push(xlsx.parse(merge({}, { header: useHeader }, opts)))
44
+
45
+ const stream = DataStream.pipeline(...pipes)
46
+ let batchNo = 1
47
+ const data = []
48
+ await stream
49
+ .batch(batch)
50
+ .map(async items => {
51
+ if (items.length === 0) return null
52
+ const batchStart = new Date()
53
+ for (let item of items) {
54
+ count++
55
+ item = converterFn ? await converterFn.call(this, item) : item
56
+ if (dest !== false) await recordCreate(dest, item, createOpts)
57
+ else data.push(item)
58
+ }
59
+ if (progressFn) await progressFn.call(this, { batchNo, data: items, batchStart, batchEnd: new Date() })
60
+ batchNo++
61
+ })
62
+ .run()
63
+
64
+ return dest === false ? data : { file, count }
65
+ }
66
+
67
+ export default importFrom
package/bajo/start.js ADDED
@@ -0,0 +1,14 @@
1
+ async function start () {
2
+ if (this.config.archive.checkInterval === false || this.config.archive.checkInterval <= 0) {
3
+ this.log.warn('Automatic archiving is disabled')
4
+ return
5
+ }
6
+ if (this.config.archive.runEarly) await this.archive()
7
+ this.archiveIntv = setInterval(() => {
8
+ this.archive().then().catch(err => {
9
+ this.log.error('Archive error: %s', err.message)
10
+ })
11
+ }, this.config.archive.checkInterval * 60 * 1000)
12
+ }
13
+
14
+ export default start
@@ -0,0 +1,95 @@
1
+ async function move (task) {
2
+ const { importPkg } = this.app.bajo
3
+ const { dayjs } = this.app.bajo.lib
4
+ const { set } = this.app.bajo.lib._
5
+ const { formatInteger } = this.app.bajoExtra
6
+ const { recordFind, recordCreate, recordRemove, statAggregate } = this.app.dobo
7
+ const prompts = await importPkg('bajoCli:@inquirer/prompts')
8
+ const { confirm } = prompts
9
+ // get relevant record
10
+ this.print.info('Copying %s -> %s...', task.source, task.destination)
11
+ const mark = dayjs().subtract(task.maxAge, 'day').toDate()
12
+ const query = set({}, task.sourceField, { $lt: mark })
13
+ let count = await statAggregate(task.source, { query }, { aggregate: 'count' })
14
+ count = count[0].count
15
+ if (count === 0) {
16
+ this.print.warn('No record found, skipped')
17
+ return
18
+ }
19
+ if (this.config.prompt !== false) {
20
+ const answer = await confirm({
21
+ message: this.print.write('%d records will be archived. Continue?', count),
22
+ default: true
23
+ })
24
+ if (!answer) {
25
+ this.print.warn('Task %s -> %s cancelled', task.source, task.destination)
26
+ return
27
+ }
28
+ } else this.print.info('%d records will be archived', count)
29
+ let page = 1
30
+ let total = 0
31
+ let affected = 0
32
+ let error = 0
33
+ let isMax = false
34
+ const ids = []
35
+ const spin = this.print.spinner({ showCounter: true }).start()
36
+ for (;;) {
37
+ const results = await recordFind(task.source, { query, page, limit: 100 }, { noCache: true, noHook: true })
38
+ if (results.length === 0) break
39
+ if (this.bajo.config.tool && total % 1000 === 0 && total !== 0) this.print.succeed('Milestone #%s records copied', formatInteger(total))
40
+ for (const r of results) {
41
+ if (task.maxRecords && task.maxRecords < total) {
42
+ isMax = true
43
+ break
44
+ }
45
+ total++
46
+ try {
47
+ await recordCreate(task.destination, r, { noSanitize: true, noHook: true, noResult: true, noCheckUnique: true })
48
+ ids.push(r.id)
49
+ spin.setText('ID #%s', r.id)
50
+ } catch (err) {
51
+ error++
52
+ }
53
+ }
54
+ if (isMax) break
55
+ affected = total - error
56
+ page++
57
+ }
58
+ if (isMax) this.print.warn('Max of %d records reached', task.maxRecords)
59
+ this.print.info('Removing %s...', task.source)
60
+ for (const idx in ids) {
61
+ const id = ids[idx]
62
+ try {
63
+ await recordRemove(task.source, id, { noHook: true, noResult: true })
64
+ spin.setText('ID #%s', id)
65
+ if (this.bajo.config.tool && idx % 1000 === 0 && idx !== '0') this.print.succeed('Milestone #%s records removed', formatInteger(idx))
66
+ } catch (err) {}
67
+ }
68
+ spin.stop()
69
+ this.print.info('Archiving %s -> %s ended, moved: %s, error: %s', task.source, task.destination, formatInteger(affected), formatInteger(error))
70
+ }
71
+
72
+ async function archive (...args) {
73
+ const { importPkg } = this.app.bajo
74
+ const prompts = await importPkg('bajoCli:@inquirer/prompts')
75
+ const { confirm } = prompts
76
+ if (this.config.prompt !== false) {
77
+ const answer = await confirm({
78
+ message: this.print.write('You\'re about to manually run task archiver. Continue?'),
79
+ default: false
80
+ })
81
+
82
+ if (!answer) this.print.fatal('Aborted!')
83
+ }
84
+ await this.app.dobo.start()
85
+ if (this.archivers.length === 0) this.print.fatal('Nothing to archive')
86
+ for (const t of this.archivers.tasks) {
87
+ if (t.maxAge < 1) {
88
+ this.print.warn('Archive %s -> %s is disabled', t.source, t.destination)
89
+ continue
90
+ }
91
+ await move.call(this, t)
92
+ }
93
+ }
94
+
95
+ export default archive
@@ -0,0 +1,56 @@
1
+ import _path from 'path'
2
+
3
+ const batch = 100
4
+
5
+ function makeProgress (spin) {
6
+ const { secToHms } = this.app.bajo
7
+ return async function ({ batchNo, data, batchStart, batchEnd } = {}) {
8
+ if (data.length === 0) return
9
+ spin.setText('Batch #%d (%s)', batchNo, secToHms(batchEnd.toTime() - batchStart.toTime(), true))
10
+ }
11
+ }
12
+
13
+ async function exportTo (...args) {
14
+ const { importPkg } = this.app.bajo
15
+ const { dayjs } = this.app.bajo.lib
16
+ const { isEmpty, map } = this.app.bajo.lib._
17
+ const { getInfo, start } = this.app.dobo
18
+
19
+ const [input, select] = await importPkg('bajoCli:@inquirer/input',
20
+ 'bajoCli:@inquirer/select')
21
+ const schemas = map(this.app.dobo.schemas, 'name')
22
+ if (isEmpty(schemas)) return this.print.fatal('No schema found!')
23
+ let [model, dest, query] = args
24
+ if (isEmpty(model)) {
25
+ model = await select({
26
+ message: this.print.write('Please choose model:'),
27
+ choices: map(schemas, s => ({ value: s }))
28
+ })
29
+ }
30
+ if (isEmpty(dest)) {
31
+ dest = await input({
32
+ message: this.print.write('Please enter destination file:'),
33
+ default: `${model}-${dayjs().format('YYYYMMDD')}.ndjson`,
34
+ validate: (item) => !isEmpty(item)
35
+ })
36
+ }
37
+ if (isEmpty(query)) {
38
+ query = await input({
39
+ message: this.print.write('Please enter a query (if any):')
40
+ })
41
+ }
42
+ const spin = this.print.spinner().start('Exporting...')
43
+ const progressFn = makeProgress.call(this, spin)
44
+ const { connection } = getInfo(model)
45
+ await start(connection.name)
46
+ try {
47
+ const filter = { query }
48
+ const result = await this.exportTo(model, dest, { filter, batch, progressFn })
49
+ spin.succeed('%d records successfully exported to \'%s\'', result.count, _path.resolve(result.file))
50
+ } catch (err) {
51
+ console.log(err)
52
+ spin.fatal('Error: %s', err.message)
53
+ }
54
+ }
55
+
56
+ export default exportTo
@@ -0,0 +1,51 @@
1
+ import _path from 'path'
2
+
3
+ const batch = 100
4
+
5
+ function makeProgress (spin) {
6
+ const { secToHms } = this.app.bajo
7
+ return async function ({ batchNo, data, batchStart, batchEnd } = {}) {
8
+ spin.setText('Batch #%d (%s)', batchNo, secToHms(batchEnd.toTime() - batchStart.toTime(), true))
9
+ }
10
+ }
11
+
12
+ async function importFrom (...args) {
13
+ const { importPkg } = this.app.bajo
14
+ const { isEmpty, map } = this.app.bajo.lib._
15
+ const { getInfo, start } = this.app.dobo
16
+
17
+ const [input, select, confirm] = await importPkg('bajoCli:@inquirer/input',
18
+ 'bajoCli:@inquirer/select', 'bajoCli:@inquirer/confirm')
19
+ const schemas = map(this.app.dobo.schemas, 'name')
20
+ if (isEmpty(schemas)) return this.print.fatal('No schema found!')
21
+ let [dest, model] = args
22
+ if (isEmpty(dest)) {
23
+ dest = await input({
24
+ message: this.print.write('Please enter source file:'),
25
+ validate: (item) => !isEmpty(item)
26
+ })
27
+ }
28
+ if (isEmpty(model)) {
29
+ model = await select({
30
+ message: this.print.write('Please choose model:'),
31
+ choices: map(schemas, s => ({ value: s }))
32
+ })
33
+ }
34
+ const answer = await confirm({
35
+ message: this.print.write('You\'re about to replace ALL records with the new ones. Are you really sure?'),
36
+ default: false
37
+ })
38
+ if (!answer) return this.print.fatal('Aborted!')
39
+ const spin = this.print.spinner({ showCounter: true }).start('Importing...')
40
+ const progressFn = makeProgress.call(this, spin)
41
+ const { connection } = getInfo(model)
42
+ await start(connection.name)
43
+ try {
44
+ const result = await importFrom(dest, model, { batch, progressFn })
45
+ spin.succeed('%d records successfully imported from \'%s\'', result.count, _path.resolve(result.file))
46
+ } catch (err) {
47
+ spin.fatal('Error: %s', err.message)
48
+ }
49
+ }
50
+
51
+ export default importFrom
@@ -0,0 +1 @@
1
+ export default 'default'
@@ -0,0 +1,37 @@
1
+ {
2
+ "Directory '%s' doesn't exist": "",
3
+ "Unsupported format '%s'": "",
4
+ "Source file '%s' doesn't exist": "",
5
+ "Task must have %s model": "",
6
+ "Unknown field '%s@%s'": "",
7
+ "'%s@%s (%s)' is not supported (must be one of %s)": "",
8
+ "Source & destination must be different": "",
9
+ "Automatic archiving is disabled": "",
10
+ "Archive error: %s": "",
11
+ "Copying %s -> %s...": "",
12
+ "No record found, skipped": "",
13
+ "%d records will be archived. Continue?": "",
14
+ "Task %s -> %s cancelled": "",
15
+ "%d records will be archived": "",
16
+ "Milestone #%s records copied": "",
17
+ "ID #%s": "",
18
+ "Max of %d records reached": "",
19
+ "Removing %s...": "",
20
+ "Milestone #%s records removed": "",
21
+ "Archiving %s -> %s ended, moved: %s, error: %s": "",
22
+ "You're about to manually run task archiver. Continue?": "",
23
+ "Nothing to archive": "",
24
+ "Archive %s -> %s is disabled": "",
25
+ "Batch #%d (%s)": "",
26
+ "No schema found!": "",
27
+ "Please choose model:": "",
28
+ "Please enter destination file:": "",
29
+ "Please enter a query (if any):": "",
30
+ "Exporting...": "",
31
+ "%d records successfully exported to '%s'": "",
32
+ "Error: %s": "",
33
+ "Please enter source file:": "",
34
+ "You're about to replace ALL records with the new ones. Are you really sure?": "",
35
+ "Importing...": "",
36
+ "%d records successfully imported from '%s'": ""
37
+ }
package/lib/io-exts.js ADDED
@@ -0,0 +1 @@
1
+ export default ['.json', '.jsonl', '.ndjson', '.csv', '.xlsx', '.tsv']
@@ -0,0 +1,35 @@
1
+ // Borrowed from: https://github.com/fanlia/ndjson-csv-xlsx/blob/main/index.js
2
+
3
+ import ndjson from 'ndjson'
4
+ import csv from 'fast-csv'
5
+ import xlsxparse from 'xlsx-parse-stream'
6
+ import XLSXWriteStream from '@atomictech/xlsx-write-stream'
7
+ import StreamArray from 'stream-json/streamers/StreamArray.js'
8
+ import stringer from 'stream-json/Stringer.js'
9
+ import disassembler from 'stream-json/Disassembler.js'
10
+ import chain from 'stream-chain'
11
+
12
+ export default {
13
+ ndjson: {
14
+ parse: (...args) => ndjson.parse(...args),
15
+ stringify: (...args) => ndjson.stringify(...args)
16
+ },
17
+ csv: {
18
+ parse: (...args) => csv.parse(...args),
19
+ stringify: (...args) => csv.format(...args)
20
+ },
21
+ xlsx: {
22
+ parse: (...args) => xlsxparse(...args),
23
+ stringify: (...args) => new XLSXWriteStream(...args)
24
+ },
25
+ json: {
26
+ parse: (...args) => chain([
27
+ StreamArray.withParser(...args),
28
+ data => data.value
29
+ ]),
30
+ stringify: (options, ...args) => chain([
31
+ disassembler(),
32
+ stringer({ ...options, makeArray: true })
33
+ ])
34
+ }
35
+ }
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "dobo-extra",
3
+ "version": "1.0.0",
4
+ "description": "Bajo DB Extra Tools/Utility",
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/dobo-extra.git"
13
+ },
14
+ "keywords": [
15
+ "extra",
16
+ "db",
17
+ "driver",
18
+ "bajo",
19
+ "framework",
20
+ "modular"
21
+ ],
22
+ "author": "Ardhi Lukianto <ardhi@lukianto.com>",
23
+ "license": "MIT",
24
+ "bugs": {
25
+ "url": "https://github.com/ardhi/dobo-extra/issues"
26
+ },
27
+ "homepage": "https://github.com/ardhi/dobo-extra#readme",
28
+ "dependencies": {
29
+ "@atomictech/xlsx-write-stream": "^2.0.2",
30
+ "fast-csv": "^5.0.1",
31
+ "fast-jwt": "^4.0.2",
32
+ "ndjson": "^2.0.0",
33
+ "scramjet": "^4.37.0",
34
+ "stream-chain": "^2.2.5",
35
+ "stream-json": "^1.8.0",
36
+ "xlsx-parse-stream": "^1.1.0"
37
+ }
38
+ }