UNPKG

@n-octo-n/n8n-nodes-json-database

Version:

Use a JSON file as a persistent, hierarchical key-value database.

253 lines 11.1 kB
"use strict"; 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