node-red-contrib-kissdb
Version:
A lightweight JSON-based database node for Node-RED
1,395 lines (1,058 loc) • 46.4 kB
JavaScript
module.exports = function(RED) {
function KissDBNode(config) {
RED.nodes.createNode(this, config);
const node = this;
node.on('input', function(msg) {
// Your KISSDB logic here
// Use msg.action, msg.DBFile, etc.
/**************************************************************
* *
* ██ ██ ██ ██████ ██████ ████████ ████████ *
* ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ *
* ██ ██ ██ ██ ██ ██ ██ ██ ██ *
* ██ ██ ██ ██████ ██████ ██ ██ ████████ *
* ████ ██ ██ ██ ██ ██ ██ ██ *
* ██ ████ ██ ██ ██ ██ ██ ██ ██ *
* ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ *
* ██ ██ ██ ██████ ██████ ████████ ████████ *
* *
* *
* Keep It Simple, Structured, Database Base *
* *
**************************************************************/
/**************************************************************
* CRUD Library *
**************************************************************/
const lib = {
parseDB: msg => typeof msg.payload === "string" ? JSON.parse(msg.payload) : msg.payload,
stringifyDB: db => JSON.stringify(db, null, 2),
getNextID: db => (db.lastID || 0) + 1,
/**************************************************************
* CREATE REMOVE UPDATE DELETE RECORDS *
**************************************************************/
/**************************************************************
* CREATE DB *
**************************************************************/
DBCreate: () => ({ lastID: 0, records: [] }),
/**************************************************************
* INSERT NEW RECORD *
**************************************************************/
insertRecord: (db, data) => {
const ID = lib.getNextID(db);
const record = { ID, ...data, createdAt: new Date().toISOString() };
db.records.push(record);
db.lastID = ID;
return db;
},
/**************************************************************
* UPDATE RECORD *
**************************************************************/
updateRecord: (db, ID, changes) => {
db.records = db.records.map(record =>
record.ID === ID ? { ...record, ...changes, updatedAt: new Date().toISOString() } : record
);
return db;
},
/**************************************************************
* DELETE RECORD *
**************************************************************/
deleteRecord: (db, ID) => {
db.records = db.records.filter(record => record.ID !== ID);
return db;
},
/**************************************************************
***************************************************************
* VARIOUS QUERIES *
***************************************************************
**************************************************************/
/**************************************************************
* FIND RECORDS *
**************************************************************/
findRecords: (db, query) => db.records.filter(record =>
Object.entries(query).every(([key, val]) =>
String(record[key] || "").toLowerCase().includes(String(val).toLowerCase())
)
),
/**************************************************************
* FILTER BY DATE RANGE *
**************************************************************/
filterByDateRange: (db, field, from, to) => {
const fromDate = new Date(from);
const toDate = new Date(to);
return db.records.filter(record => {
const value = record[field];
if (!value) return false;
const date = new Date(value);
return date >= fromDate && date <= toDate;
});
},
/**************************************************************
* COUNT BY GROUP *
**************************************************************/
countByGroup: (db, fieldName) => {
if (!fieldName) return {};
const counts = {};
db.records.forEach(record => {
const key = record[fieldName];
if (key !== undefined) {
counts[key] = (counts[key] || 0) + 1;
}
});
return counts;
},
/**************************************************************
* SUM BY DATE RANGE *
**************************************************************/
sumByDateRange: (db, dateField, from, to, valueField) => {
if (!dateField || !from || !to || !valueField) return 0;
const fromDate = new Date(from);
const toDate = new Date(to);
return db.records
.filter(record => {
const dateValue = record[dateField];
if (!dateValue) return false;
const d = new Date(dateValue);
return d >= fromDate && d <= toDate;
})
.reduce((sum, record) => {
const value = parseFloat(record[valueField]);
return !isNaN(value) ? sum + value : sum;
}, 0);
},
/**************************************************************
* AVERAGE BY DATE RANGE *
**************************************************************/
avgByDateRange: (db, dateField, from, to, valueField) => {
if (!dateField || !from || !to || !valueField) return 0;
const fromDate = new Date(from);
const toDate = new Date(to);
const values = db.records
.filter(record => {
const dateValue = record[dateField];
if (!dateValue) return false;
const d = new Date(dateValue);
return d >= fromDate && d <= toDate;
})
.map(record => parseFloat(record[valueField]))
.filter(val => !isNaN(val));
if (values.length === 0) return 0;
const sum = values.reduce((acc, val) => acc + val, 0);
return sum / values.length;
},
/**************************************************************
* MAX BY DATE RANGE *
**************************************************************/
maxByDateRange: (db, dateField, from, to, valueField) => {
if (!dateField || !from || !to || !valueField) return null;
const fromDate = new Date(from);
const toDate = new Date(to);
const values = db.records
.filter(record => {
const dateValue = record[dateField];
if (!dateValue) return false;
const d = new Date(dateValue);
return d >= fromDate && d <= toDate;
})
.map(record => parseFloat(record[valueField]))
.filter(val => !isNaN(val));
if (values.length === 0) return null;
return Math.max(...values);
},
/**************************************************************
* MIN BY DATE RANGE *
**************************************************************/
minByDateRange: (db, dateField, from, to, valueField) => {
if (!dateField || !from || !to || !valueField) return null;
const fromDate = new Date(from);
const toDate = new Date(to);
const values = db.records
.filter(record => {
const dateValue = record[dateField];
if (!dateValue) return false;
const d = new Date(dateValue);
return d >= fromDate && d <= toDate;
})
.map(record => parseFloat(record[valueField]))
.filter(val => !isNaN(val));
if (values.length === 0) return null;
return Math.min(...values);
},
/**************************************************************
* TIME SERIES BY DATE RANGE *
**************************************************************/
timeseriesByDateRange: (db, dateField, from, to, valueField, groupBy, method) => {
if (!dateField || !from || !to || !valueField || !groupBy || !method) return [];
const fromDate = new Date(from);
const toDate = new Date(to);
// Funzione per formattare la data in base al tipo di raggruppamento
const formatPeriod = (date) => {
const d = new Date(date);
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
switch (groupBy) {
case "day": return `${y}-${m}-${day}`;
case "month": return `${y}-${m}`;
case "year": return `${y}`;
case "week": {
const tempDate = new Date(date);
tempDate.setHours(0, 0, 0, 0);
tempDate.setDate(tempDate.getDate() + 3 - ((tempDate.getDay() + 6) % 7));
const week1 = new Date(tempDate.getFullYear(), 0, 4);
const diff = tempDate.getTime() - week1.getTime();
const weekNumber = Math.round((diff / 86400000 - 3 + ((week1.getDay() + 6) % 7)) / 7) + 1;
return `${tempDate.getFullYear()}-W${String(weekNumber).padStart(2, "0")}`;
}
default: return "";
}
};
// Funzione per generare tutti i periodi intermedi
const generatePeriods = () => {
const periods = [];
const current = new Date(fromDate);
while (current <= toDate) {
periods.push(formatPeriod(current));
switch (groupBy) {
case "day":
current.setDate(current.getDate() + 1);
break;
case "week":
current.setDate(current.getDate() + 7);
break;
case "month":
current.setMonth(current.getMonth() + 1);
break;
case "year":
current.setFullYear(current.getFullYear() + 1);
break;
}
}
return [...new Set(periods)];
};
// Raggruppa i valori per periodo
const grouped = {};
db.records.forEach(record => {
const dateValue = record[dateField];
const value = record[valueField];
if (!dateValue || value === undefined) return;
const d = new Date(dateValue);
if (d < fromDate || d > toDate) return;
const period = formatPeriod(d);
const num = parseFloat(value);
if (isNaN(num)) return;
if (!grouped[period]) grouped[period] = [];
grouped[period].push(num);
});
// Calcola il valore aggregato per ogni periodo
const allPeriods = generatePeriods();
const result = allPeriods.map(period => {
const values = grouped[period] || [];
let value = 0;
if (values.length > 0) {
switch (method) {
case "avg":
value = values.reduce((a, b) => a + b, 0) / values.length;
break;
case "max":
value = Math.max(...values);
break;
case "min":
value = Math.min(...values);
break;
case "sum":
value = values.reduce((a, b) => a + b, 0);
break;
case "count":
value = values.length;
break;
}
}
return { period, value: Math.round(value * 100) / 100 };
});
return result;
},
/**************************************************************
* GET UNIQUE VALUES *
**************************************************************/
getUniqueValues: (db, field) => {
if (!field || !db.records || !Array.isArray(db.records)) return [];
const values = db.records
.map(record => record[field])
.filter(v => v !== undefined && v !== null);
const unique = [...new Set(values)];
return unique.sort();
},
/**************************************************************
* COUNT BY DATE RANGE *
**************************************************************/
countByDateRange: (db, dateField, from, to) => {
if (!dateField || !from || !to) return 0;
const fromDate = new Date(from);
const toDate = new Date(to);
return db.records.filter(record => {
const value = record[dateField];
if (!value) return false;
const date = new Date(value);
return date >= fromDate && date <= toDate;
}).length;
},
/**************************************************************
* CHART BY DATE RANGE *
**************************************************************/
chartDataByDateRange: (db, dateField, from, to, valueField) => {
if (!dateField || !from || !to || !valueField) return [];
const fromDate = new Date(from);
const toDate = new Date(to);
const points = db.records
.filter(record => {
const dateValue = record[dateField];
if (!dateValue) return false;
const d = new Date(dateValue);
return d >= fromDate && d <= toDate;
})
.filter(record => record.hasOwnProperty(valueField))
.map(record => ({
x: record[dateField],
y: parseFloat(record[valueField])
}))
.filter(point => !isNaN(point.y));
return points.sort((a, b) => new Date(a.x).getTime() - new Date(b.x).getTime());
},
/**************************************************************
***************************************************************
* MANIPULATE FIELDS MASSIVELY *
***************************************************************
**************************************************************/
/**************************************************************
* RENAME FIELD *
**************************************************************/
renameField: (db, fromKey, toKey) => {
if (fromKey === "ID" || toKey === "ID") return "ID_PROTECTED";
let count = 0;
db.records = db.records.map(record => {
if (record.hasOwnProperty(fromKey)) {
record[toKey] = record[fromKey];
delete record[fromKey];
count++;
}
return record;
});
db._renameCount = count; // valore temporaneo, utile per msg.info
return db;
},
/**************************************************************
* REMOVE FIELD *
**************************************************************/
removeField: (db, fieldName) => {
if (fieldName === "ID") return "ID_PROTECTED";
db.records = db.records.map(record => {
delete record[fieldName];
return record;
});
return db;
},
/**************************************************************
* ADD FIELD *
**************************************************************/
addField: (db, fieldName, value) => {
if (fieldName === "ID") return "ID_PROTECTED";
db.records = db.records.map(record => {
if (!record.hasOwnProperty(fieldName)) {
record[fieldName] = value;
}
return record;
});
return db;
},
/**************************************************************
* VALIDATE RECORDS *
**************************************************************/
validateRecords: (db, requiredFields = ["ID", "titolo", "stato"]) => {
return db.records
.filter(record => {
const missing = requiredFields.filter(f => !record.hasOwnProperty(f));
return missing.length > 0;
})
.map(record => {
const missing = requiredFields.filter(f => !record.hasOwnProperty(f));
return { ID: record.ID, missing };
});
},
/**************************************************************
* SORT RECORDS *
**************************************************************/
sortRecords: (db, field, order = "asc") => {
if (!field || !Array.isArray(db.records)) return db;
db.records.sort((a, b) => {
const va = a[field];
const vb = b[field];
if (va === undefined || vb === undefined) return 0;
if (typeof va === "number" && typeof vb === "number") {
return order === "desc" ? vb - va : va - vb;
}
const sa = String(va).toLowerCase();
const sb = String(vb).toLowerCase();
return order === "desc" ? sb.localeCompare(sa) : sa.localeCompare(sb);
});
return db;
},
/**************************************************************
* MERGE FIELDS *
**************************************************************/
mergeFields: (db, fields, newField, separator = " ") => {
if (!Array.isArray(fields) || fields.length === 0 || !newField) return db;
db.records.forEach(record => {
const parts = fields.map(f => record[f] !== undefined ? String(record[f]) : "").filter(p => p !== "");
record[newField] = parts.join(separator);
});
return db;
},
/**************************************************************
* SPLIT FIELD *
**************************************************************/
splitField: (db, field, format, into) => {
if (!field || !format || !Array.isArray(into) || into.length === 0) return db;
db.records.forEach(record => {
const value = record[field];
if (typeof value !== "string") return;
let parts = [];
if (format === "YYYY-MM-DD") {
parts = value.split("-");
} else if (format === "DD/MM/YYYY") {
parts = value.split("/");
} else if (format === "custom" && msg.split?.separator) {
parts = value.split(msg.split.separator);
}
into.forEach((key, i) => {
record[key] = parts[i] || "";
});
});
return db;
},
/**************************************************************
* CONVERT TO DATE WITH SUFFIX *
**************************************************************/
convertToDateWithSuffix: (db, field) => {
if (!field || !Array.isArray(db.records)) return db;
const newField = `${field}_Date`;
db.records.forEach(record => {
const value = record[field];
if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/.test(value)) {
const d = new Date(value);
if (!isNaN(d.getTime())) {
record[newField] = d;
}
}
});
return db;
},
/**************************************************************
* REORDER FIELDS *
**************************************************************/
reorderFields: (db, order) => {
if (!Array.isArray(order) || order.length === 0 || !Array.isArray(db.records)) return db;
db.records = db.records.map(record => {
const reordered = {};
// Inserisce prima i campi indicati
order.forEach(key => {
if (record.hasOwnProperty(key)) {
reordered[key] = record[key];
}
});
// Aggiunge tutti gli altri campi non indicati, nell'ordine originale
Object.keys(record).forEach(key => {
if (!reordered.hasOwnProperty(key)) {
reordered[key] = record[key];
}
});
return reordered;
});
return db;
},
/**************************************************************
* KEEP ONLY FIELDS *
**************************************************************/
keepOnlyFields: (db, fields) => {
if (!Array.isArray(fields) || fields.length === 0 || !Array.isArray(db.records)) return db;
// Ensure "ID" is preserved
if (!fields.includes("ID")) {
fields = ["ID", ...fields];
}
db.records = db.records.map(record => {
const filtered = {};
fields.forEach(key => {
if (record.hasOwnProperty(key)) {
filtered[key] = record[key];
}
});
return filtered;
});
return db;
},
/**************************************************************
* BEAUTIFY FIELDS *
**************************************************************/
beautifyField: (db, field, operation, options = {}) => {
if (!field || !operation || !Array.isArray(db.records)) return db;
db.records.forEach(record => {
let value = record[field];
if (value === undefined || value === null) return;
switch (operation) {
case "round":
value = parseFloat(value);
if (!isNaN(value)) record[field] = Math.round(value);
break;
case "abs":
value = parseFloat(value);
if (!isNaN(value)) record[field] = Math.abs(value);
break;
case "lowercase":
record[field] = String(value).toLowerCase();
break;
case "uppercase":
record[field] = String(value).toUpperCase();
break;
case "trim":
record[field] = String(value).trim();
break;
case "multiply":
const factor = parseFloat(options.factor);
value = parseFloat(value);
if (!isNaN(value) && !isNaN(factor)) {
record[field] = Math.round(value * factor * 100) / 100;
}
break;
case "replace":
const from = options.from || "";
const to = options.to || "";
record[field] = String(value).replaceAll(from, to);
break;
case "dateformat":
const format = options.format || "DD/MM/YYYY";
const str = String(value);
if (format === "DD/MM/YYYY" && /^\d{4}-\d{2}-\d{2}$/.test(str)) {
const [y, m, d] = str.split("-");
record[field] = `${d}/${m}/${y}`;
} else if (format === "YYYY-MM-DD" && /^\d{2}\/\d{2}\/\d{4}$/.test(str)) {
const [d, m, y] = str.split("/");
record[field] = `${y}-${m}-${d}`;
}
break;
case "capitalize":
record[field] = String(value)
.split(" ")
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(" ");
break;
}
});
return db;
},
/**************************************************************
***************************************************************
* EXPORT VARIOUS FORMATS *
***************************************************************
**************************************************************/
/**************************************************************
* EXPORT FIELDS *
**************************************************************/
exportFields: (db, fields = []) => {
if (!Array.isArray(fields) || fields.length !== 2) return [];
const [f1, f2] = fields;
return db.records
.filter(record => record.hasOwnProperty(f1) && record.hasOwnProperty(f2))
.map(record => ({
[f1]: record[f1],
[f2]: record[f2]
}));
},
/**************************************************************
* EXPORT JSON BY DATE RANGE *
**************************************************************/
exportJSONByDateRange: (db, fields = [], dateField = "", from = "", to = "") => {
if (!Array.isArray(fields) || fields.length !== 2) return [];
if (!dateField || !from || !to) return [];
const fromDate = new Date(from);
const toDate = new Date(to);
const [f1, f2] = fields;
return db.records
.filter(record => {
const value = record[dateField];
if (!value) return false;
const date = new Date(value);
return date >= fromDate && date <= toDate;
})
.filter(record => record.hasOwnProperty(f1) && record.hasOwnProperty(f2))
.map(record => ({
[f1]: record[f1],
[f2]: record[f2]
}));
},
/**************************************************************
* TIME SERIES CSV BY DATE RANGE *
**************************************************************/
timeseriesCSVByDateRange: (db, dateField, from, to, valueField, groupBy, method) => {
if (!dateField || !from || !to || !valueField || !groupBy || !method) return "";
const fromDate = new Date(from);
const toDate = new Date(to);
const formatPeriod = (date) => {
const d = new Date(date);
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
switch (groupBy) {
case "day": return `${y}-${m}-${day}`;
case "month": return `${y}-${m}`;
case "year": return `${y}`;
case "week": {
const tempDate = new Date(date);
tempDate.setHours(0, 0, 0, 0);
tempDate.setDate(tempDate.getDate() + 3 - ((tempDate.getDay() + 6) % 7));
const week1 = new Date(tempDate.getFullYear(), 0, 4);
const diff = tempDate.getTime() - week1.getTime();
const weekNumber = Math.round((diff / 86400000 - 3 + ((week1.getDay() + 6) % 7)) / 7) + 1;
return `${tempDate.getFullYear()}-W${String(weekNumber).padStart(2, "0")}`;
}
default: return "";
}
};
const generatePeriods = () => {
const periods = [];
const current = new Date(fromDate);
while (current <= toDate) {
periods.push(formatPeriod(current));
switch (groupBy) {
case "day": current.setDate(current.getDate() + 1); break;
case "week": current.setDate(current.getDate() + 7); break;
case "month": current.setMonth(current.getMonth() + 1); break;
case "year": current.setFullYear(current.getFullYear() + 1); break;
}
}
return [...new Set(periods)];
};
const grouped = {};
db.records.forEach(record => {
const dateValue = record[dateField];
const value = record[valueField];
if (!dateValue || value === undefined) return;
const d = new Date(dateValue);
if (d < fromDate || d > toDate) return;
const period = formatPeriod(d);
const num = parseFloat(value);
if (isNaN(num)) return;
if (!grouped[period]) grouped[period] = [];
grouped[period].push(num);
});
const allPeriods = generatePeriods();
const lines = ["period,value"];
allPeriods.forEach(period => {
const values = grouped[period] || [];
let value = 0;
if (values.length > 0) {
switch (method) {
case "avg": value = values.reduce((a, b) => a + b, 0) / values.length; break;
case "max": value = Math.max(...values); break;
case "min": value = Math.min(...values); break;
case "sum": value = values.reduce((a, b) => a + b, 0); break;
case "count": value = values.length; break;
}
}
lines.push(`${period},${Math.round(value * 100) / 100}`);
});
return lines.join("\n");
},
/**************************************************************
* EXPORT CSV *
**************************************************************/
exportCSV: (db, fields = [], separator = ",") => {
if (!Array.isArray(fields) || fields.length !== 2) {
return "Errore: servono esattamente 2 campi";
}
const [f1, f2] = fields;
const header = `${f1}${separator}${f2}`;
const rows = db.records
.filter(record => record.hasOwnProperty(f1) && record.hasOwnProperty(f2))
.map(record => `${record[f1]}${separator}${record[f2]}`);
return [header, ...rows].join("\n");
},
exportCSVByDateRange: (db, fields = [], dateField = "", from = "", to = "", separator = ",") => {
if (!Array.isArray(fields) || fields.length !== 2) return "Errore: servono esattamente 2 campi";
if (!dateField || !from || !to) return "Errore: intervallo data non valido";
const fromDate = new Date(from);
const toDate = new Date(to);
const [f1, f2] = fields;
const header = `${f1}${separator}${f2}`;
const rows = db.records
.filter(record => {
const dateValue = record[dateField];
if (!dateValue) return false;
const d = new Date(dateValue);
return d >= fromDate && d <= toDate;
})
.filter(record => record.hasOwnProperty(f1) && record.hasOwnProperty(f2))
.map(record => `${record[f1]}${separator}${record[f2]}`);
return [header, ...rows].join("\n");
},
/**************************************************************
* HELP *
**************************************************************/
getHelpCommands: () => {
return [
{ action: "CREATE", description: "Initialize a new empty database", params: [] },
{ action: "INSERT", description: "Add a new record to the database", params: ["data"] },
{ action: "UPDATE", description: "Update an existing record by ID", params: ["ID", "update"] },
{ action: "DELETE", description: "Delete a record by ID", params: ["ID"] },
{ action: "FIND", description: "Search records matching a query", params: ["query"] },
{ action: "COUNT", description: "Return total number of records", params: [] },
{ action: "COUNTBYGROUP", description: "Count records grouped by a field", params: ["group.field"] },
{ action: "RENAME", description: "Rename a field in all records", params: ["rename.from", "rename.to"] },
{ action: "REMOVE", description: "Remove a field from all records", params: ["remove.field"] },
{ action: "ADD", description: "Add a new field with a fixed value", params: ["add.field", "add.value"] },
{ action: "REORDERFIELDS", description: "Prioritize selected fields in record order", params: ["order"] },
{ action: "KEEPONLYFIELDS", description: "Keep only selected fields, remove others", params: ["fields"] },
{ action: "MERGEFIELDS", description: "Merge multiple fields into one", params: ["merge.fields", "merge.newField", "merge.separator"] },
{ action: "SPLITFIELD", description: "Split a field into multiple parts", params: ["split.field", "split.format", "split.into"] },
{ action: "BEAUTIFYFIELD", description: "Transform field values (e.g. round, lowercase, capitalize)", params: ["beautify.field", "beautify.operation", "beautify.options"] },
{ action: "CONVERTTODATE", description: "Create a new field with Date object from ISO string", params: ["field"] },
{ action: "GETUNIQUEVALUES", description: "Return all unique values of a field", params: ["field"] },
{ action: "SORT", description: "Sort records by a field", params: ["sort.field", "sort.order"] },
{ action: "FILTERBYDATERANGE", description: "Filter records within a date range", params: ["dateRange.field", "dateRange.from", "dateRange.to"] },
{ action: "SUMBYDATERANGE", description: "Sum values within a date range", params: ["sum.dateField", "sum.from", "sum.to", "sum.valueField"] },
{ action: "AVGBYDATERANGE", description: "Average values within a date range", params: ["avg.dateField", "avg.from", "avg.to", "avg.valueField"] },
{ action: "MAXBYDATERANGE", description: "Maximum value within a date range", params: ["max.dateField", "max.from", "max.to", "max.valueField"] },
{ action: "MINBYDATERANGE", description: "Minimum value within a date range", params: ["min.dateField", "min.from", "min.to", "min.valueField"] },
{ action: "COUNTBYDATERANGE", description: "Count records within a date range", params: ["count.dateField", "count.from", "count.to"] },
{ action: "TIMESERIESBYDATERANGE", description: "Generate time series aggregation", params: ["timeseries.dateField", "from", "to", "valueField", "groupBy", "method"] },
{ action: "TIMESERIESCSVBYDATERANGE", description: "Generate time series CSV output", params: ["timeseries.dateField", "from", "to", "valueField", "groupBy", "method"] },
{ action: "EXPORTCSV", description: "Export two fields to CSV", params: ["fields"] },
{ action: "EXPORTCSVBYDATERANGE", description: "Export two fields to CSV within date range", params: ["fields", "dateField", "from", "to"] },
{ action: "EXPORTFIELDS", description: "Export two fields from all records", params: ["fields"] },
{ action: "EXPORTJSONBYDATERANGE", description: "Export two fields in JSON within date range", params: ["fields", "dateField", "from", "to"] },
{ action: "HELP", description: "List all available commands", params: [] }
];
}
};
/**************************************************************
***************************************************************
* Initial Parsing *
***************************************************************
**************************************************************/
let db = lib.parseDB(msg);
let result = null;
let info = "";
let write = false;
/**************************************************************
***************************************************************
* Command Logic *
***************************************************************
**************************************************************/
switch ((msg.action || "").toUpperCase()) {
case "HELP":
result = lib.getHelpCommands();
info = `HELP command executed: ${result.length} available actions`;
write = false;
break;
case "INSERT":
db = lib.insertRecord(db, msg.data);
result = lib.stringifyDB(db);
info = "Command INSERT: added record";
write = true;
break;
case "UPDATE":
db = lib.updateRecord(db, msg.ID, msg.update);
result = lib.stringifyDB(db);
info = `Command UPDATE: record ${msg.ID} updated`;
write = true;
break;
case "DELETE":
db = lib.deleteRecord(db, msg.ID);
result = lib.stringifyDB(db);
info = `Command DELETE: record ${msg.ID} removed`;
write = true;
break;
case "FIND":
const found = lib.findRecords(db, msg.query);
result = found;
info = `Command FIND: ${found.length} records found`;
write = false;
break;
case "CREATE":
result = lib.stringifyDB(lib.DBCreate());
info = "Command CREATE: empty database created";
write = true;
break;
case "RENAME":
const renamed = lib.renameField(db, msg.rename.from, msg.rename.to);
if (renamed === "ID_PROTECTED") {
result = null;
info = "Error: field 'ID' is reserved";
write = false;
} else {
db = renamed;
result = lib.stringifyDB(db);
info = `Command RENAME: '${msg.rename.from}' renamed as '${msg.rename.to}' on ${db._renameCount} records`;
delete db._renameCount;
write = true;
}
break;
case "REORDERFIELDS":
const reorder = msg.order || [];
db = lib.reorderFields(db, reorder);
result = db;
info = `Command REORDERFIELDS: fields ordered with ${reorder.join(", ")}`;
write = true;
break;
case "KEEPONLYFIELDS":
const keep = msg.fields || [];
db = lib.keepOnlyFields(db, keep);
result = db;
info = `Command KEEPONLYFIELDS: kept only fields ${keep.join(", ")}`;
write = true;
break;
case "CONVERTTODATE":
const fieldToConvert = msg.field || "";
db = lib.convertToDateWithSuffix(db, fieldToConvert);
result = db;
info = `Command CONVERTTODATE: created field '${fieldToConvert}_Date' with object Date`;
write = true;
break;
case "MERGEFIELDS":
const merge = msg.merge || {};
db = lib.mergeFields(db, merge.fields || [], merge.newField || "", merge.separator || " ");
result = db;
info = `Command MERGEFIELDS: field '${merge.newField}' creatd from ${merge.fields?.join(", ")}`;
write = true;
break;
case "SPLITFIELD":
const split = msg.split || {};
db = lib.splitField(db, split.field || "", split.format || "", split.into || []);
result = db;
info = `Command SPLITFIELD: field '${split.field}' split in ${split.into?.join(", ")}`;
write = true;
break;
case "BEAUTIFYFIELD":
const bf = msg.beautify || {};
db = lib.beautifyField(db, bf.field || "", bf.operation || "", bf.options || {});
result = db;
info = `Command BEAUTIFYFIELD: field '${bf.field}' transformed with '${bf.operation}'`;
write = true;
break;
case "GETUNIQUEVALUES":
const field = msg.field || "";
const uniqueValues = lib.getUniqueValues(db, field);
result = uniqueValues;
info = `Command GETUNIQUEVALUES: ${uniqueValues.length} distinct entries found for '${field}'`;
write = false;
break;
case "REMOVE":
const removed = lib.removeField(db, msg.remove.field);
if (removed === "ID_PROTECTED") {
result = null;
info = "Error: field 'ID' is reserved";
write = false;
} else {
db = removed;
result = lib.stringifyDB(db);
info = `Command REMOVE: field '${msg.remove.field}' removed`;
write = true;
}
break;
case "ADD":
const added = lib.addField(db, msg.add.field, msg.add.value);
if (added === "ID_PROTECTED") {
result = null;
info = "Error: field 'ID' is reserved";
write = false;
} else {
db = added;
result = lib.stringifyDB(db);
info = `Command ADD: field '${msg.add.field}' added`;
write = true;
}
break;
case "SORT":
db = lib.sortRecords(db, msg.sort.field, msg.sort.order);
result = lib.stringifyDB(db);
info = `Command SORT: order by '${msg.sort.field}' (${msg.sort.order})`;
write = true;
break;
case "FILTERBYDATERANGE":
const filtered = lib.filterByDateRange(db, msg.dateRange.field, msg.dateRange.from, msg.dateRange.to);
result = filtered;
info = `Command FILTERBYDATERANGE: ${filtered.length} records found`;
write = false;
break;
case "TIMESERIESBYDATERANGE":
const ts = msg.timeseries || {};
const tsResult = lib.timeseriesByDateRange(
db,
ts.dateField || "",
ts.from || "",
ts.to || "",
ts.valueField || "",
ts.groupBy || "day",
ts.method || "avg"
);
result = tsResult;
info = `Command TIMESERIESBYDATERANGE: ${tsResult.length} periods`;
write = false;
break;
case "TIMESERIESCSVBYDATERANGE":
const tscsv = msg.timeseries || {};
const csvResult = lib.timeseriesCSVByDateRange(
db,
tscsv.dateField || "",
tscsv.from || "",
tscsv.to || "",
tscsv.valueField || "",
tscsv.groupBy || "day",
tscsv.method || "avg"
);
result = csvResult;
info = `Command TIMESERIESCSVBYDATERANGE: CSV generated with ${csvResult.split("\\n").length - 1} lines`;
write = false;
break;
case "MAXBYDATERANGE":
const maxDateField = msg.max?.dateField || "";
const maxFrom = msg.max?.from || "";
const maxTo = msg.max?.to || "";
const maxValueField = msg.max?.valueField || "";
const maxResult = lib.maxByDateRange(db, maxDateField, maxFrom, maxTo, maxValueField);
result = maxResult;
info = `Command MAXBYDATERANGE: max of '${maxValueField}' from ${maxFrom} to ${maxTo} = ${maxResult}`;
write = false;
break;
case "MINBYDATERANGE":
const minDateField = msg.min?.dateField || "";
const minFrom = msg.min?.from || "";
const minTo = msg.min?.to || "";
const minValueField = msg.min?.valueField || "";
const minResult = lib.minByDateRange(db, minDateField, minFrom, minTo, minValueField);
result = minResult;
info = `Command MINBYDATERANGE: min of '${minValueField}' from ${minFrom} to ${minTo} = ${minResult}`;
write = false;
break;
case "SUMBYDATERANGE":
const sumDateField = msg.sum?.dateField || "";
const sumFrom = msg.sum?.from || "";
const sumTo = msg.sum?.to || "";
const sumValueField = msg.sum?.valueField || "";
const totalSum = lib.sumByDateRange(db, sumDateField, sumFrom, sumTo, sumValueField);
result = totalSum;
info = `Command SUMBYDATERANGE: sum of '${sumValueField}' from ${sumFrom} to ${sumTo} = ${totalSum}`;
write = false;
break;
case "AVGBYDATERANGE":
const avgDateField = msg.avg?.dateField || "";
const avgFrom = msg.avg?.from || "";
const avgTo = msg.avg?.to || "";
const avgValueField = msg.avg?.valueField || "";
const avgResult = lib.avgByDateRange(db, avgDateField, avgFrom, avgTo, avgValueField);
result = avgResult;
info = `Command AVGBYDATERANGE: average of '${avgValueField}' from ${avgFrom} to ${avgTo} = ${avgResult.toFixed(2)}`;
write = false;
break;
case "COUNTBYDATERANGE":
const countDateField = msg.count?.dateField || "";
const countFrom = msg.count?.from || "";
const countTo = msg.count?.to || "";
const countResult = lib.countByDateRange(db, countDateField, countFrom, countTo);
result = countResult;
info = `Command COUNTBYDATERANGE: ${countResult} records from ${countFrom} to ${countTo}`;
write = false;
break;
case "COUNT":
const total = db.records.length;
result = total;
info = `Command COUNT: ${total} records found`;
write = false;
break;
case "COUNTBYGROUP":
const groupField = msg.group?.field || "";
const grouped = lib.countByGroup(db, groupField);
result = grouped;
info = `Command COUNTBYGROUP: ${Object.keys(grouped).length} groups found for field '${groupField}'`;
write = false;
break;
case "VALIDATE":
const invalids = lib.validateRecords(db, msg.validate.required);
result = invalids;
info = `Command VALIDATE: ${invalids.length} incomplete records`;
write = false;
break;
case "EXPORTCSV":
const sep = msg.export?.separator || ",";
const csv = lib.exportCSV(db, msg.export?.fields, sep);
result = csv;
info = `Command EXPORTCSV: ${csv.split("\n").length - 1} lines generated with separator '${sep}'`;
write = false;
break;
case "EXPORTJSONBYDATERANGE":
const jsonFields = msg.export?.fields || [];
const jsonDateField = msg.export?.dateField || "";
const jsonFrom = msg.export?.from || "";
const jsonTo = msg.export?.to || "";
const jsonFiltered = lib.exportJSONByDateRange(db, jsonFields, jsonDateField, jsonFrom, jsonTo);
result = jsonFiltered;
info = `Command EXPORTJSONBYDATERANGE: ${jsonFiltered.length} record from ${jsonFrom} to ${jsonTo}`;
write = false;
break;
case "CHARTDATABYDATERANGE":
const chartDateField = msg.chart?.dateField || "";
const chartFrom = msg.chart?.from || "";
const chartTo = msg.chart?.to || "";
const chartValueField = msg.chart?.valueField || "";
const chartData = lib.chartDataByDateRange(db, chartDateField, chartFrom, chartTo, chartValueField);
result = chartData;
info = `Command CHARTDATABYDATERANGE: ${chartData.length} points from ${chartFrom} to ${chartTo}`;
write = false;
break;
case "EXPORTCSVBYDATERANGE":
const fields = msg.export?.fields || [];
const dateField = msg.export?.dateField || "";
const fromDate = msg.export?.from || "";
const toDate = msg.export?.to || "";
const separator = msg.export?.separator || ",";
const csvFiltered = lib.exportCSVByDateRange(db, fields, dateField, fromDate, toDate, separator);
result = csvFiltered;
info = `Command EXPORTCSVBYDATERANGE: found ${csvFiltered.split("\n").length - 1} record from ${fromDate} to ${toDate}`;
write = false;
break;
case "EXPORTJSON":
const selected = lib.exportFields(db, msg.export?.fields);
result = selected;
info = `Command EXPORTJSON : ${selected.length} record with field '${msg.export.fields.join(", ")}'`;
write = false;
break;
default:
result = null;
info = "Error - unknown command";
write = false;
}
/**************************************************************
* NODE STATUS *
**************************************************************/
node.status({
fill: write ? "green" : "blue",
shape: write ? "dot" : "ring",
text: info.slice(0, 40)
});
/**************************************************************
* OUTPUT *
**************************************************************/
node.send([
write ? { payload: result, DBFile: msg.DBFile } : null,
{ payload: result, info }
]);
});
}
RED.nodes.registerType("kissdb", KissDBNode, {
category: "storage",
defaults: {
name: { value: "" }
},
inputs: 1,
outputs: 2,
icon: "font-awesome/fa-database"
});
};