Skip to content
Snippets Groups Projects
Commit 9dce2f78 authored by Mike Lynch's avatar Mike Lynch
Browse files

Merge branch 'random_as_library'

parents 204a6e58 ddacc923
No related merge requests found
node_modules/ node_modules/
output
.idea
\ No newline at end of file
...@@ -20,11 +20,9 @@ a dependency. ...@@ -20,11 +20,9 @@ a dependency.
const catalog = datacrate.datapub2catalog(datapub); const catalog = datacrate.datapub2catalog(datapub);
fs.writeFileSync('CATALOG.json', JSON.stringify(catalog, null, 2)); fs.writeFileSync('CATALOG.json', JSON.stringify(catalog, null, 2));
## random.js ## random.jshis is a utility to generate plausible random DataCrates with no payloads, for testing things like Solr indexers. It generates random text descriptions and a reasonable distribution of keywords and authors.
This is a utility to generate plausible random DataCrates with no payloads, for testing things like Solr indexers. It generates random text descriptions and a reasonable distribution of keywords and authors. node ./random.js -d ./output/ -n 1000
node ./lib/random.js -d ./output/ -n 1000
## To-do ## To-do
......
// Module to generate a bunch of CATALOG.json files which
// have arbitrary but realistic data
//
// TODO: generate some random text files and add them
// as well
const ORGANISATION = {
'id': 'https://examples.edu',
'name': 'Examples University'
};
const EMAIL_DOMAIN = 'examples.edu';
const OWNER = 'owner@examples.edu';
const APPROVER = 'approver@examples.edu';
const NAME_MIN = 3;
const NAME_MAX = 10;
const KEYWORD_MIN = 3;
const KEYWORD_MAX = 12;
const WORD_MIN = 2;
const WORD_MAX = 14;
const SENTENCE_MIN = 3;
const SENTENCE_MAX = 30;
const PARA_MIN = 1;
const PARA_MAX = 10;
const N_KEYWORD_MIN = 2;
const N_KEYWORD_MAX = 10;
const N_PEOPLE_MIN = 1;
const N_PEOPLE_MAX = 5;
const HONORIFICS = [ 'Dr', 'A/Prof', 'Prof', 'Dr', 'Dr', 'Dr', 'Mr', 'Ms' ];
const datacrate = require('./catalog.js');
const _ = require('lodash');
const fs = require('fs-extra');
const randdict = require('random-word');
const path = require('path');
const uuidv4 = require('uuid/v4');
const ArgumentParser = require('argparse').ArgumentParser;
const VOCABULARIES = './vocabularies';
function randrange(min, max) {
return Math.floor(Math.random() * (max - min)) + min;
}
function randoms(n, fn) {
return Array(n).fill(0).map(fn);
}
async function loadsource(file) {
const text = await fs.readFile(file);
return text.toString().split("\n");
}
async function loadsourcedata(dir) {
const sourcedata = {};
sourcedata['surnames'] = await loadsource(path.join(dir, 'surname.txt'));
sourcedata['givennames'] = await loadsource(path.join(dir, 'givenname.txt'));
return sourcedata;
}
function randperson(sourcedata) {
const honorific = _.sample(HONORIFICS);
const surname = _.sample(sourcedata['surnames']);
const givenname = _.sample(sourcedata['givennames']);
const name = givenname + ' ' + surname;
const email = givenname + '.' + surname + '@' + EMAIL_DOMAIN;
const id = uuidv4();
return {
'dc:identifier': id,
'text_full_name': name,
'full_name_honorific': honorific + ' ' + name,
'email': email
}
}
function randkeyword() {
return randdict();
}
function randsentence() {
const nwords = randrange(SENTENCE_MIN, SENTENCE_MAX);
const s = randoms(nwords, randdict).join(' ') + '.';
return _.upperFirst(s);
}
function randtext() {
const nsentences = randrange(PARA_MIN, PARA_MAX);
return randoms(nsentences, randsentence).join(' ') + '\n';
}
function randdatapub(keywords, people) {
const k = _.sampleSize(keywords, randrange(N_KEYWORD_MIN, N_KEYWORD_MAX));
const title = _.startCase(_.camelCase(randsentence()));
const desc = randtext();
const creators = _.clone(_.sampleSize(people, randrange(N_PEOPLE_MIN, N_PEOPLE_MAX)));
const contributors = _.clone(creators);
const collabs = contributors.splice(1);
contributors[0].role = 'Chief Investigator';
return {
finalKeywords: k,
contributor_ci: contributors[0],
contributors: collabs,
creators: creators,
title: title,
description: desc
}
}
function randdatapubs(n, sourcedata) {
const keywords = randoms(Math.floor(n / 2), randkeyword);
const people = randoms(n * 2, () => { return randperson(sourcedata) });
return randoms(n, () => randdatapub(keywords, people))
}
async function makedatacrate(dest, datapub) {
const id = uuidv4();
await fs.ensureDir(path.join(dest, id));
const catalog = await datacrate.datapub2catalog({
id: id,
datapub: datapub,
organisation: ORGANISATION,
owner: OWNER,
approver: APPROVER
});
const catfile = path.join(dest, id, 'CATALOG.json');
await fs.writeFile(catfile, JSON.stringify(catalog, null, 2));
}
async function randomdatacrates(dest, n) {
const sourcedata = await loadsourcedata(VOCABULARIES);
const datapubs = randdatapubs(n, sourcedata);
Promise.all(
datapubs.map(async p => {
await makedatacrate(dest, p);
})
);
console.log("Done");
}
var parser = new ArgumentParser({
version: '1.0.0',
addHelp: true,
description: 'Generates a bunch of plausible random DataCrates'
});
parser.addArgument(
['-d', '--directory'],
{
help: "Directory in which to write DataCrates. Will be created if it doesn't exist",
defaultValue: './output/'
}
);
parser.addArgument(
['-n', '--number'],
{
help: 'Number of DataCrates to generate.',
type: 'int',
defaultValue: 10
}
);
var args = parser.parseArgs();
console.log(`Generating ${args['number']} random DataCrates in ${args['directory']}`);
randomdatacrates(args['directory'], args['number']);
// Module to generate a bunch of CATALOG.json files which
// have arbitrary but realistic data
//
// TODO: generate some random text files and add them
// as well
const ORGANISATION = {
'id': 'https://examples.edu',
'name': 'Examples University'
};
const EMAIL_DOMAIN = 'examples.edu';
const OWNER = 'owner@examples.edu';
const APPROVER = 'approver@examples.edu';
const NAME_MIN = 3;
const NAME_MAX = 10;
const KEYWORD_MIN = 3;
const KEYWORD_MAX = 12;
const WORD_MIN = 2;
const WORD_MAX = 14;
const SENTENCE_MIN = 3;
const SENTENCE_MAX = 30;
const PARA_MIN = 1;
const PARA_MAX = 10;
const N_KEYWORD_MIN = 2;
const N_KEYWORD_MAX = 10;
const N_PEOPLE_MIN = 1;
const N_PEOPLE_MAX = 5;
const HONORIFICS = ['Dr', 'A/Prof', 'Prof', 'Dr', 'Dr', 'Dr', 'Mr', 'Ms'];
const datacrate = require('./catalog.js');
const _ = require('lodash');
const fs = require('fs-extra');
const randdict = require('random-word');
const path = require('path');
const uuidv4 = require('uuid/v4');
const ArgumentParser = require('argparse').ArgumentParser;
const VOCABULARIES = './vocabularies';
function randrange(min, max) {
return Math.floor(Math.random() * (max - min)) + min;
}
function randoms(n, fn) {
return Array(n).fill(0).map(fn);
}
async function loadsource(file) {
const text = await fs.readFile(file);
return text.toString().split("\n");
}
function randperson(sourcedata) {
const honorific = _.sample(HONORIFICS);
const surname = _.sample(sourcedata['surnames']);
const givenname = _.sample(sourcedata['givennames']);
const name = givenname + ' ' + surname;
const email = givenname + '.' + surname + '@' + EMAIL_DOMAIN;
const id = uuidv4();
return {
'dc:identifier': id,
'text_full_name': name,
'full_name_honorific': honorific + ' ' + name,
'email': email
}
}
function randkeyword() {
return randdict();
}
function randsentence() {
const nwords = randrange(SENTENCE_MIN, SENTENCE_MAX);
const s = randoms(nwords, randdict).join(' ') + '.';
return _.upperFirst(s);
}
function randtext() {
const nsentences = randrange(PARA_MIN, PARA_MAX);
return randoms(nsentences, randsentence).join(' ') + '\n';
}
function randdatapub(keywords, people) {
const k = _.sampleSize(keywords, randrange(N_KEYWORD_MIN, N_KEYWORD_MAX));
const title = _.startCase(_.camelCase(randsentence()));
const desc = randtext();
const creators = _.clone(_.sampleSize(people, randrange(N_PEOPLE_MIN, N_PEOPLE_MAX)));
const contributors = _.clone(creators);
const collabs = contributors.splice(1);
contributors[0].role = 'Chief Investigator';
return {
finalKeywords: k,
contributor_ci: contributors[0],
contributors: collabs,
creators: creators,
title: title,
description: desc
}
}
module.exports = {
loadsourcedata: async function (dir) {
const sourcedata = {};
sourcedata['surnames'] = await loadsource(path.join(dir, 'surname.txt'));
sourcedata['givennames'] = await loadsource(path.join(dir, 'givenname.txt'));
return sourcedata;
},
randdatapubs: function (n, sourcedata) {
const keywords = randoms(Math.floor(n / 2), randkeyword);
const people = randoms(n * 2, () => {
return randperson(sourcedata)
});
return randoms(n, () => randdatapub(keywords, people))
},
makedir: async function (dest) {
const id = uuidv4();
const createDir = await fs.ensureDir(path.join(dest, id));
return id;
},
makedatacrate: async function (dest, datapub, id) {
const catalog = await datacrate.datapub2catalog({
id: id,
datapub: datapub,
organisation: ORGANISATION,
owner: OWNER,
approver: APPROVER
});
const catfile = path.join(dest, id, 'CATALOG.json');
await fs.writeFile(catfile, JSON.stringify(catalog, null, 2));
}
};
\ No newline at end of file
...@@ -83,7 +83,7 @@ ...@@ -83,7 +83,7 @@
}, },
"array-equal": { "array-equal": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", "resolved": "http://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz",
"integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=" "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM="
}, },
"array-events": { "array-events": {
...@@ -348,6 +348,32 @@ ...@@ -348,6 +348,32 @@
"string-width": "^3.1.0", "string-width": "^3.1.0",
"strip-ansi": "^5.2.0", "strip-ansi": "^5.2.0",
"wrap-ansi": "^5.1.0" "wrap-ansi": "^5.1.0"
},
"dependencies": {
"ansi-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg="
},
"string-width": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
"integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
"requires": {
"is-fullwidth-code-point": "^2.0.0",
"strip-ansi": "^4.0.0"
},
"dependencies": {
"strip-ansi": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
"integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
"requires": {
"ansi-regex": "^3.0.0"
}
}
}
}
} }
}, },
"co": { "co": {
...@@ -355,6 +381,11 @@ ...@@ -355,6 +381,11 @@
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
"integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ="
}, },
"code-point-at": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c="
},
"codepage": { "codepage": {
"version": "1.12.2", "version": "1.12.2",
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.12.2.tgz", "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.12.2.tgz",
...@@ -366,7 +397,7 @@ ...@@ -366,7 +397,7 @@
"dependencies": { "dependencies": {
"commander": { "commander": {
"version": "2.14.1", "version": "2.14.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.14.1.tgz", "resolved": "http://registry.npmjs.org/commander/-/commander-2.14.1.tgz",
"integrity": "sha512-+YR16o3rK53SmWHU3rEM3tPAh2rwb1yPcQX5irVn7mb0gXbwuCCrnkbV5+PBfETdfg1vui07nM6PCG1zndcjQw==" "integrity": "sha512-+YR16o3rK53SmWHU3rEM3tPAh2rwb1yPcQX5irVn7mb0gXbwuCCrnkbV5+PBfETdfg1vui07nM6PCG1zndcjQw=="
} }
} }
...@@ -435,7 +466,7 @@ ...@@ -435,7 +466,7 @@
}, },
"css-select": { "css-select": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", "resolved": "http://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz",
"integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=",
"requires": { "requires": {
"boolbase": "~1.0.0", "boolbase": "~1.0.0",
...@@ -1163,7 +1194,7 @@ ...@@ -1163,7 +1194,7 @@
}, },
"ncp": { "ncp": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", "resolved": "http://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz",
"integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=" "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M="
}, },
"nice-try": { "nice-try": {
...@@ -1197,6 +1228,11 @@ ...@@ -1197,6 +1228,11 @@
"boolbase": "~1.0.0" "boolbase": "~1.0.0"
} }
}, },
"number-is-nan": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0="
},
"nwsapi": { "nwsapi": {
"version": "2.1.4", "version": "2.1.4",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.1.4.tgz", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.1.4.tgz",
...@@ -1240,7 +1276,7 @@ ...@@ -1240,7 +1276,7 @@
}, },
"os-tmpdir": { "os-tmpdir": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "resolved": "http://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
"integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ="
}, },
"p-defer": { "p-defer": {
...@@ -1334,7 +1370,7 @@ ...@@ -1334,7 +1370,7 @@
}, },
"printj": { "printj": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/printj/-/printj-1.1.2.tgz", "resolved": "http://registry.npmjs.org/printj/-/printj-1.1.2.tgz",
"integrity": "sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==" "integrity": "sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ=="
}, },
"psl": { "psl": {
...@@ -1595,6 +1631,21 @@ ...@@ -1595,6 +1631,21 @@
"emoji-regex": "^7.0.1", "emoji-regex": "^7.0.1",
"is-fullwidth-code-point": "^2.0.0", "is-fullwidth-code-point": "^2.0.0",
"strip-ansi": "^5.1.0" "strip-ansi": "^5.1.0"
},
"dependencies": {
"ansi-regex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg=="
},
"strip-ansi": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
"requires": {
"ansi-regex": "^4.1.0"
}
}
} }
}, },
"string_decoder": { "string_decoder": {
...@@ -1615,7 +1666,7 @@ ...@@ -1615,7 +1666,7 @@
}, },
"strip-eof": { "strip-eof": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", "resolved": "http://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
"integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8="
}, },
"supports-color": { "supports-color": {
...@@ -1816,6 +1867,39 @@ ...@@ -1816,6 +1867,39 @@
"ansi-styles": "^3.2.0", "ansi-styles": "^3.2.0",
"string-width": "^3.0.0", "string-width": "^3.0.0",
"strip-ansi": "^5.0.0" "strip-ansi": "^5.0.0"
},
"dependencies": {
"ansi-regex": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
},
"is-fullwidth-code-point": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
"requires": {
"number-is-nan": "^1.0.0"
}
},
"string-width": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
"strip-ansi": "^3.0.0"
}
},
"strip-ansi": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"requires": {
"ansi-regex": "^2.0.0"
}
}
} }
}, },
"wrappy": { "wrappy": {
...@@ -1859,7 +1943,7 @@ ...@@ -1859,7 +1943,7 @@
}, },
"xmlbuilder": { "xmlbuilder": {
"version": "9.0.7", "version": "9.0.7",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", "resolved": "http://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz",
"integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0="
}, },
"xmldom": { "xmldom": {
......
const randomize = require('./lib/randomize');
const ArgumentParser = require('argparse').ArgumentParser;
const parser = new ArgumentParser({
version: '1.0.0',
addHelp: true,
description: 'Generates a bunch of plausible randomize DataCrates'
});
parser.addArgument(
['-d', '--directory'],
{
help: "Directory in which to write DataCrates. Will be created if it doesn't exist",
defaultValue: './output/'
}
);
parser.addArgument(
['-n', '--number'],
{
help: 'Number of DataCrates to generate.',
type: 'int',
defaultValue: 10
}
);
const args = parser.parseArgs();
console.log(`Generating ${args['number']} randomize DataCrates in ${args['directory']}`);
const dest = args['directory'];
const n = args['number'];
async function createDatacrates(dest, n) {
const sourcedata = await randomize.loadsourcedata('./vocabularies');
const datapubs = randomize.randdatapubs(n, sourcedata);
datapubs.reduce((promise, p, index) => {
return promise.then(async () => {
const id = await randomize.makedir(dest);
return randomize.makedatacrate(dest, p, id)
.then(() => {
if (index >= n) {
console.log("Done");
}
return Promise.resolve();
});
})
}, Promise.resolve());
}
createDatacrates(dest, n);
\ No newline at end of file
...@@ -53,7 +53,7 @@ const ORG = { ...@@ -53,7 +53,7 @@ const ORG = {
'name': 'University of Technology Sydney' 'name': 'University of Technology Sydney'
}; };
const ALLOW_BAD_TYPES = {}; // { 'FeatureCollection': true }; const ALLOW_BAD_TYPES = { 'FeatureCollection': true };
// defining these here so that the tests know what to // defining these here so that the tests know what to
// look up in the results @graph // look up in the results @graph
...@@ -140,7 +140,7 @@ describe("Try to load a janky default DataPub with empty fields", () => { ...@@ -140,7 +140,7 @@ describe("Try to load a janky default DataPub with empty fields", () => {
'approver': APPROVER, 'approver': APPROVER,
'prefixes': IRI_PREFIXES 'prefixes': IRI_PREFIXES
}); });
const roots = cj['@graph'].filter((item) => {return item['path'] === 'data/'}); const roots = cj['@graph'].filter((item) => {return item['path'] === './'});
cjds = roots[0]; cjds = roots[0];
await fs.writeJson('./test_data/janky_CATALOG.json', cj, { 'spaces': 4 }); await fs.writeJson('./test_data/janky_CATALOG.json', cj, { 'spaces': 4 });
}); });
...@@ -214,7 +214,7 @@ describe("Convert a ReDBox 2.0 DataPub to CATALOG.json", () => { ...@@ -214,7 +214,7 @@ describe("Convert a ReDBox 2.0 DataPub to CATALOG.json", () => {
'approver': APPROVER, 'approver': APPROVER,
'prefixes': IRI_PREFIXES 'prefixes': IRI_PREFIXES
}); });
const roots = cj['@graph'].filter((item) => {return item['path'] === 'data/'}); const roots = cj['@graph'].filter((item) => {return item['path'] === './'});
cjds = roots[0]; cjds = roots[0];
await fs.writeJson('./test_data/CATALOG.json', cj, { 'spaces': 4 }); await fs.writeJson('./test_data/CATALOG.json', cj, { 'spaces': 4 });
}); });
......
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment