@n-octo-n/n8n-nodes-json-database
Version:
Use a JSON file as a persistent, hierarchical key-value database.
253 lines • 11.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.JsonDatabase = void 0;
const n8n_workflow_1 = require("n8n-workflow");
const fs_1 = require("fs");
const _ = require('../../lib/lodash.custom.min.js');
;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function handleJsonOperation(execFunctions, itemIndex, options) {
const lockTime = 20000;
let queryPath = options.queryPath.trim();
let filePath = options.filePath.trim();
filePath || (filePath = `${__dirname}/../../../../../../../JsonDatabase.Global.json`);
const directory = filePath.split('/').slice(0, -1).join('/');
const lockPath = ((0, fs_1.existsSync)(filePath) || (!(0, fs_1.existsSync)(filePath) && options.operation === 'opWrite')
? `${directory}/~${filePath.split('/').slice(-1)[0]}.lock`
: false);
function updateLockTime() {
(0, fs_1.writeFileSync)(`${lockPath}/time`, Date.now().toString());
}
let database = {};
let lockTimeUpdater;
if (lockPath) {
while (true) {
try {
(0, fs_1.mkdirSync)(lockPath, { recursive: true });
updateLockTime();
lockTimeUpdater = setInterval(updateLockTime, lockTime / 4);
break;
}
catch (err) {
console.log(err);
if (err.code === 'EEXIST') {
let lockWait = 0;
let knowLockFile = false;
while (lockWait <= lockTime) {
try {
if ((Date.now() - lockTime) >= Number((0, fs_1.readFileSync)(`${lockPath}/time`, 'utf8'))) {
lockWait = -1;
(0, fs_1.rmSync)(lockPath, { recursive: true });
break;
}
else {
await sleep(lockTime / 500);
lockWait += lockTime / 500;
}
}
catch (err) {
if (err.code === 'ENOENT') {
if ((0, fs_1.existsSync)(lockPath) && !knowLockFile) {
knowLockFile = true;
await sleep(lockTime / 250);
lockWait += lockTime / 250;
}
else {
lockWait = -1;
break;
}
}
else {
throw err;
}
}
}
if (lockWait !== -1) {
const errMsg = 'The database is being kept locked for too much time. Try again later.';
throw new n8n_workflow_1.NodeOperationError(execFunctions.getNode(), errMsg, { itemIndex });
return { error: errMsg };
}
}
else {
throw err;
}
}
}
}
if ((0, fs_1.existsSync)(filePath)) {
database = JSON.parse((0, fs_1.readFileSync)(filePath, 'utf8'));
}
let databaseBranch = (queryPath ? _.get(database, queryPath) : database);
if (options.operation === 'opWrite') {
databaseBranch = options.sourceData;
if ((['string', 'number', 'boolean'].includes(typeof (databaseBranch)) || databaseBranch === null) && !queryPath) {
const errMsg = 'Single item values cannot be assigned to the JSON root.';
throw new n8n_workflow_1.NodeOperationError(execFunctions.getNode(), errMsg, { itemIndex });
return { error: errMsg };
}
queryPath ? _.set(database, queryPath, databaseBranch) : database = databaseBranch;
if (!(0, fs_1.existsSync)(directory)) {
(0, fs_1.mkdirSync)(directory, { recursive: true });
}
(0, fs_1.writeFileSync)(filePath, JSON.stringify(database, null, '\t'));
}
if (lockPath) {
clearTimeout(lockTimeUpdater);
(0, fs_1.rmSync)(lockPath, { recursive: true });
}
return { data: databaseBranch };
}
class JsonDatabase {
constructor() {
this.description = {
displayName: 'JSON Database',
name: 'JsonDatabase',
icon: 'file:jsondatabase.svg',
group: ['transform'],
version: 1.0,
description: 'Reads/writes data from/to a JSON file acting as an hierarchical key-value database',
defaults: {
name: 'JsonDatabase',
},
inputs: ['main'],
outputs: ['main'],
properties: [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Read From Database',
value: 'opRead',
description: 'Reads data from a JSON database',
action: 'Read data from a JSON database',
},
{
name: 'Write To Database',
value: 'opWrite',
description: 'Writes data to a JSON database',
action: 'Write data to a JSON database',
},
],
default: 'opRead',
},
{
displayName: 'Query Path',
name: 'queryPath',
type: 'string',
default: '',
placeholder: 'monsters["cookie-monster"].scares',
description: 'Path on which to act on in the JSON tree (leave empty to select root)',
},
{
displayName: 'Data Source',
name: 'sourceType',
type: 'options',
noDataExpression: true,
description: 'The source of the data to write in the chosen database path',
options: [
{
name: 'Object Key',
value: 'sourceTypeObject',
description: 'Sets the data source for writing to a specified object key',
action: 'Set the data source for writing to a specified object key',
},
{
name: 'JSON String',
value: 'sourceTypeJson',
description: 'Sets the data source for writing to an arbitrary JSON string',
action: 'Set the data source for writing to an arbitrary JSON string',
},
],
default: 'sourceTypeObject',
displayOptions: {
show: {
'operation': ['opWrite'],
},
},
},
{
displayName: 'Source Object Key',
name: 'sourceData',
type: 'string',
default: '',
required: true,
placeholder: 'data',
description: 'An object key from the current input context (as set by immediately preceding nodes) to use as data',
displayOptions: {
show: {
'operation': ['opWrite'],
'sourceType': ['sourceTypeObject'],
},
},
},
{
displayName: 'Source JSON String',
name: 'sourceData',
type: 'string',
default: '',
placeholder: '[ "children", "grandma" ]',
description: 'A JSON string to be evaluated as an object (empty field is equal to undefined, which means to delete from database)',
displayOptions: {
show: {
'operation': ['opWrite'],
'sourceType': ['sourceTypeJson'],
},
},
},
{
displayName: 'File Path',
name: 'filePath',
type: 'string',
default: '',
placeholder: '/data/example-database.json',
description: 'Path to the JSON file in which to read/write the data (leave empty to use the default global database)',
},
],
};
}
async execute() {
let inputItems = this.getInputData();
let sourceData;
const returnItems = [];
for (let itemIndex = 0; itemIndex < inputItems.length; itemIndex++) {
try {
const operation = this.getNodeParameter('operation', itemIndex);
const queryPath = this.getNodeParameter('queryPath', itemIndex);
const filePath = this.getNodeParameter('filePath', itemIndex);
if (operation === 'opWrite') {
const sourceType = this.getNodeParameter('sourceType', itemIndex);
sourceData = this.getNodeParameter('sourceData', itemIndex);
switch (sourceType) {
case 'sourceTypeObject':
sourceData = _.get(inputItems[itemIndex].json, sourceData);
break;
case 'sourceTypeJson':
sourceData = (sourceData.trim() ? JSON.parse(sourceData.trim()) : undefined);
break;
}
}
const jsonResult = await handleJsonOperation(this, itemIndex, { operation, queryPath, sourceData, filePath });
returnItems.push({
json: { ...jsonResult },
pairedItem: { item: itemIndex },
});
}
catch (error) {
if (this.continueOnFail()) {
returnItems.push({
json: { error: error.message },
pairedItem: { item: itemIndex },
});
continue;
}
throw error;
}
}
return this.prepareOutputData(returnItems);
}
}
exports.JsonDatabase = JsonDatabase;
//# sourceMappingURL=JsonDatabase.node.js.map