dbgate-api
Version:
Allows run DbGate data-manipulation scripts.
235 lines (216 loc) • 6.96 kB
JavaScript
const crypto = require('crypto');
const fs = require('fs');
const os = require('os');
const rimraf = require('rimraf');
const path = require('path');
const AsyncLock = require('async-lock');
const lock = new AsyncLock();
const stableStringify = require('json-stable-stringify');
const { evaluateCondition } = require('dbgate-sqltree');
const esort = require('external-sorting');
const { jsldir } = require('./directories');
const LineReader = require('./LineReader');
class JsonLinesDatastore {
constructor(file, formatterFunction) {
this.file = file;
this.formatterFunction = formatterFunction;
this.reader = null;
this.readedDataRowCount = 0;
this.readedSchemaRow = false;
// this.firstRowToBeReturned = null;
this.notifyChangedCallback = null;
this.currentFilter = null;
this.currentSort = null;
if (formatterFunction) {
const requirePluginFunction = require('./requirePluginFunction');
this.rowFormatter = requirePluginFunction(formatterFunction);
}
this.sortedFiles = {};
}
static async sortFile(infile, outfile, sort) {
const tempDir = path.join(os.tmpdir(), crypto.randomUUID());
fs.mkdirSync(tempDir);
await esort
.default({
input: fs.createReadStream(infile),
output: fs.createWriteStream(outfile),
deserializer: JSON.parse,
serializer: JSON.stringify,
tempDir,
maxHeap: 100,
comparer: (a, b) => {
for (const item of sort) {
const { uniqueName, order } = item;
if (a[uniqueName] < b[uniqueName]) {
return order == 'ASC' ? -1 : 1;
}
if (a[uniqueName] > b[uniqueName]) {
return order == 'ASC' ? 1 : -1;
}
}
return 0;
},
})
.asc();
await new Promise(resolve => rimraf(tempDir, resolve));
}
async _closeReader() {
// console.log('CLOSING READER', this.reader);
if (!this.reader) return;
const reader = this.reader;
this.reader = null;
this.readedDataRowCount = 0;
this.readedSchemaRow = false;
// this.firstRowToBeReturned = null;
this.currentFilter = null;
this.currentSort = null;
await reader.close();
}
async notifyChanged(callback) {
this.notifyChangedCallback = callback;
await lock.acquire('reader', async () => {
this._closeReader();
});
const call = this.notifyChangedCallback;
this.notifyChangedCallback = null;
if (call) call();
}
async _openReader(fileName) {
// console.log('OPENING READER', fileName);
// console.log(fs.readFileSync(fileName, 'utf-8'));
const fileStream = fs.createReadStream(fileName);
return new LineReader(fileStream);
}
parseLine(line) {
const res = JSON.parse(line);
return this.rowFormatter ? this.rowFormatter(res) : res;
}
async _readLine(parse) {
// if (this.firstRowToBeReturned) {
// const res = this.firstRowToBeReturned;
// this.firstRowToBeReturned = null;
// return res;
// }
for (;;) {
const line = await this.reader.readLine();
if (!line) {
// EOF
return null;
}
if (!this.readedSchemaRow) {
this.readedSchemaRow = true;
const parsedLine = JSON.parse(line);
if (parsedLine.__isStreamHeader) {
// skip to next line
continue;
}
}
if (this.currentFilter) {
const parsedLine = this.parseLine(line);
if (evaluateCondition(this.currentFilter, parsedLine)) {
this.readedDataRowCount += 1;
return parse ? parsedLine : true;
}
} else {
this.readedDataRowCount += 1;
return parse ? this.parseLine(line) : true;
}
}
// return new Promise((resolve, reject) => {
// const reader = this.reader;
// if (!reader.hasNextLine()) {
// resolve(null);
// return;
// }
// reader.nextLine((err, line) => {
// if (err) {
// reject(err);
// return;
// }
// if (!this.readedSchemaRow) {
// this.readedSchemaRow = true;
// resolve(true);
// return;
// }
// if (this.currentFilter) {
// const parsedLine = JSON.parse(line);
// if (evaluateCondition(this.currentFilter, parsedLine)) {
// console.log('TRUE');
// resolve(parse ? parsedLine : true);
// this.readedDataRowCount += 1;
// return;
// } else {
// console.log('FALSE');
// // skip row
// return;
// }
// }
// this.readedDataRowCount += 1;
// resolve(parse ? JSON.parse(line) : true);
// });
// });
}
async _ensureReader(offset, filter, sort) {
if (
this.readedDataRowCount > offset ||
stableStringify(filter) != stableStringify(this.currentFilter) ||
stableStringify(sort) != stableStringify(this.currentSort)
) {
this._closeReader();
}
if (!this.reader) {
const reader = await this._openReader(sort ? this.sortedFiles[stableStringify(sort)] : this.file);
this.reader = reader;
this.currentFilter = filter;
this.currentSort = sort;
}
// if (!this.readedSchemaRow) {
// const line = await this._readLine(true); // skip structure
// if (!line.__isStreamHeader) {
// // line contains data
// this.firstRowToBeReturned = line;
// }
// }
while (this.readedDataRowCount < offset) {
const line = await this._readLine(false);
if (line == null) break;
// if (this.firstRowToBeReturned) {
// this.firstRowToBeReturned = null;
// } else {
// await this._readLine(false);
// }
}
}
async enumRows(eachRow) {
await lock.acquire('reader', async () => {
await this._ensureReader(0, null);
for (;;) {
const line = await this._readLine(true);
if (line == null) break;
const shouldContinue = eachRow(line);
if (!shouldContinue) break;
}
});
}
async getRows(offset, limit, filter, sort) {
const res = [];
if (sort && !this.sortedFiles[stableStringify(sort)]) {
const jslid = crypto.randomUUID();
const sortedFile = path.join(jsldir(), `${jslid}.jsonl`);
await JsonLinesDatastore.sortFile(this.file, sortedFile, sort);
this.sortedFiles[stableStringify(sort)] = sortedFile;
}
await lock.acquire('reader', async () => {
await this._ensureReader(offset, filter, sort);
// console.log(JSON.stringify(this.currentFilter, undefined, 2));
for (let i = 0; i < limit; i += 1) {
const line = await this._readLine(true);
// console.log('READED LINE', i);
if (line == null) break;
res.push(line);
}
});
return res;
}
}
module.exports = JsonLinesDatastore;