312 lines
12 KiB
JavaScript
312 lines
12 KiB
JavaScript
/**
|
|
* Copyright (c) 2018 Julian Knight (Totally Information)
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
**/
|
|
|
|
// Node for Node-Red that outputs a nicely formatted string from a date/time
|
|
// object or string using the moment.js library.
|
|
|
|
// require moment.js (must be installed from package.js as a dependency)
|
|
var moment = require('moment-timezone');
|
|
var parseFormat = require('moment-parseformat');
|
|
var osLocale = require('os-locale');
|
|
var hostTz = moment.tz.guess();
|
|
var hostLocale = osLocale.sync();
|
|
|
|
|
|
// Module name must match this nodes html file
|
|
var moduleName = 'moment';
|
|
|
|
module.exports = function(RED) {
|
|
'use strict';
|
|
|
|
// The main node definition - most things happen in here
|
|
function nodeGo(config) {
|
|
// Create a RED node
|
|
RED.nodes.createNode(this,config);
|
|
|
|
// Store local copies of the node configuration (as defined in the .html)
|
|
this.topic = config.topic;
|
|
this.input = config.input || 'payload'; // where to take the input from
|
|
this.inputType = config.inputType || 'msg'; // msg, flow, global, timestamp or string
|
|
this.fakeUTC = config.fakeUTC || false; // is the input UTC rather than local date/time?
|
|
this.adjAmount = config.adjAmount || 0; // number
|
|
this.adjType = config.adjType || 'days'; // days, hours, etc.
|
|
this.adjDir = config.adjDir || 'add'; // add or subtract
|
|
this.format = config.format || ''; // valid moment.js format string
|
|
this.locale = config.locale || false // valid moment.js locale string
|
|
this.output = config.output || 'payload'; // where to put the output
|
|
this.outputType = config.outputType || 'msg'; // msg, flow or global
|
|
this.inTz = config.inTz || false; // timezone, '' or zone name, e.g. Europe/London
|
|
this.outTz = config.outTz || this.inTz; // timezone, '' or zone name, e.g. Europe/London
|
|
|
|
// copy "this" object in case we need it in context of callbacks of other functions.
|
|
var node = this;
|
|
|
|
// respond to inputs....
|
|
node.on('input', function (msg) {
|
|
'use strict'; // We will be using eval() so lets get a bit of safety using strict
|
|
|
|
// If the node's topic is set, copy to output msg
|
|
if ( node.topic !== '' ) {
|
|
msg.topic = node.topic;
|
|
} // If nodes topic is blank, the input msg.topic is already there
|
|
|
|
// make sure output property is set, if not, assume msg.payload
|
|
if ( node.output === '' ) {
|
|
node.output = 'payload';
|
|
//node.warn('Output field is REQUIRED, currently blank, set to payload');
|
|
}
|
|
if ( node.outputType === '' ) {
|
|
node.outputType = 'msg';
|
|
node.warn('Output Type field is REQUIRED, currently blank, set to msg');
|
|
}
|
|
|
|
// If the input property is blank, assume NOW as the required timestamp
|
|
// or make sure that the node's input property actually exists on the input msg
|
|
var inp = '';
|
|
// If input is a blank string, use a Date object with Now DT
|
|
if ( node.input === '' ) {
|
|
inp = new Date();
|
|
} else {
|
|
// Otherwise, check which input type & get the input
|
|
try {
|
|
switch ( node.inputType ) {
|
|
case 'msg':
|
|
inp = RED.util.getMessageProperty(msg, node.input);
|
|
break;
|
|
case 'flow':
|
|
inp = node.context().flow.get(node.input);
|
|
break;
|
|
case 'global':
|
|
inp = node.context().global.get(node.input);
|
|
break;
|
|
case 'date':
|
|
inp = new Date();
|
|
break;
|
|
case 'str':
|
|
inp = node.input.trim();
|
|
break;
|
|
default:
|
|
inp = new Date();
|
|
node.warn('Unrecognised Input Type, ' + node.inputType + '. Output has been set to NOW.');
|
|
}
|
|
} catch (err) {
|
|
inp = new Date();
|
|
node.warn('Input property, ' + node.inputType + '.' + node.input + ', does NOT exist. Output has been set to NOW.');
|
|
}
|
|
}
|
|
// We are going to overwrite the output property without warning or permission!
|
|
|
|
// Final check for input being a string (which moment doesn't really want to handle)
|
|
// NB: moment.js v3 will stop accepting strings. v2.7+ throws a warning.
|
|
var dtHack = '', inpFmt = '';
|
|
// @from v3 2018-09-23: If input is `null`, change to empty string
|
|
if ( inp === null ) inp = '';
|
|
if ( (typeof inp) === 'string' ) {
|
|
inp = inp.trim();
|
|
// Some string input hacks
|
|
switch (inp.toLowerCase()) {
|
|
case 'today':
|
|
inp = new Date();
|
|
break;
|
|
case 'yesterday':
|
|
inp = new Date();
|
|
dtHack = {days:-1};
|
|
break;
|
|
case 'tomorrow':
|
|
inp = new Date();
|
|
dtHack = {days:+1};
|
|
break;
|
|
default:
|
|
var prefOrder = {preferredOrder: {'/': 'DMY', '.': 'DMY', '-': 'YMD'} };
|
|
if ( (node.locale.toLowerCase().replace('-','_') === 'en_US') || (node.inTz.split('/')[0] === 'America') ) {
|
|
prefOrder = {preferredOrder: {'/': 'MDY', '.': 'DMY', '-': 'YMD'} };
|
|
}
|
|
inpFmt = parseFormat(inp, prefOrder);
|
|
}
|
|
} else if ( (typeof inp) === 'number' ) {
|
|
// @from 2017-06-15 IF inp is a number at this point, it needs turning into a date object
|
|
inp = new Date(inp)
|
|
}
|
|
|
|
// At this point, `inp` SHOULD be a Date object and safe to pass to moment.js
|
|
|
|
if ( !node.inTz ) { node.inTz = hostTz; }
|
|
if ( !node.outTz ) { node.outTz = node.inTz; }
|
|
// Validate input and output timezones - @since 2.0.4
|
|
if ( moment.tz.zone(node.inTz) === null ) {
|
|
// tz invalid, warn and set to UTC
|
|
node.warn('Moment: Input Timezone Invalid, reset to UTC - see http://momentjs.com/timezone/docs/#/data-loading/')
|
|
node.inTz = 'UTC'
|
|
}
|
|
if ( moment.tz.zone(node.outTz) === null ) {
|
|
// tz invalid, warn and set to UTC
|
|
node.warn('Moment: Output Timezone Invalid, reset to UTC - see http://momentjs.com/timezone/docs/#/data-loading/')
|
|
node.outTz = 'UTC'
|
|
}
|
|
|
|
// Get a Moment.JS date/time - NB: the result might not be
|
|
// valid since the input might not parse as a date/time
|
|
var mDT;
|
|
if ( inpFmt !== '' ) {
|
|
mDT = moment.tz(inp, inpFmt, true, node.inTz);
|
|
} else {
|
|
// @from 2017-06-15 change to momentjs meant having to add null parameter
|
|
mDT = moment.tz(inp, null, true, node.inTz)
|
|
}
|
|
|
|
// Fallback to JS built-in Date parsing, if not recognized by moment
|
|
if (!mDT.isValid()) {
|
|
var dtm = new Date(inp);
|
|
if (dtm === 'Invalid Date') {
|
|
node.warn('Unrecognized date string format => ' + inp);
|
|
}
|
|
else {
|
|
mDT = moment(dtm);
|
|
}
|
|
}
|
|
|
|
// Adjust the date for input hacks if needed (e.g. input was "yesterday" or "tommorow")
|
|
if ( dtHack !== '' ) { mDT.add(dtHack); }
|
|
|
|
// JK: Added OS locale lookup
|
|
if ( !node.locale ) { node.locale = hostLocale; }
|
|
// JK: Add a trap to Jaques44's locale code in case the output locale string is invalid
|
|
try {
|
|
// Jacques44 - set locale for localised output formats
|
|
mDT.locale(node.locale);
|
|
} catch (err) {
|
|
node.warn('Locale string invalid - check moment.js for valid strings');
|
|
}
|
|
|
|
// Adjust the input date if required
|
|
if ( node.adjAmount !== 0 ) {
|
|
// check if measure is valid
|
|
if ( isMeasureValid(node.adjType) ) {
|
|
// NB: moments are mutable so don't need to reassign
|
|
if ( node.adjDir === 'subtract') {
|
|
mDT.subtract(node.adjAmount, node.adjType);
|
|
} else {
|
|
mDT.add(node.adjAmount, node.adjType);
|
|
}
|
|
} else {
|
|
// it isn't valid so warn and don't adjust
|
|
node.warn('Adjustment measure type not valid, no adjustment made - check moment.js docs for valid measures (days, hours, etc)');
|
|
}
|
|
}
|
|
|
|
// ==== NO MORE DATE/TIME CALCULATIONS AFTER HERE ==== //
|
|
|
|
// If required, change to the output Timezone
|
|
if ( node.outTz !== '' ) mDT.tz(node.outTz);
|
|
|
|
// Check if the input is a date?
|
|
if ( ! mDT.isValid() ) {
|
|
// THIS SHOULD NEVER BE CALLED - it left to catch the occasional error
|
|
node.warn('The input property was NOT a recognisable date. Output will be a blank string');
|
|
setOutput(msg, node.outputType, node.output, '');
|
|
} else {
|
|
// Handle different format strings. We allow any fmt str that
|
|
// Moment.JS supports but also some special formats
|
|
|
|
// If format not set, assume ISO8601 string if input is a Date otherwise assume Date
|
|
switch ( node.format.toLowerCase() ) {
|
|
case '':
|
|
case 'iso8601':
|
|
case 'iso':
|
|
// Default to ISO8601 string
|
|
setOutput(msg, node.outputType, node.output, mDT.toISOString());
|
|
break;
|
|
case 'fromnow':
|
|
case 'timeago':
|
|
// We are also going to handle time-from-now (AKA time ago) format
|
|
setOutput(msg, node.outputType, node.output, mDT.fromNow());
|
|
break;
|
|
case 'calendar':
|
|
case 'aroundnow':
|
|
// We are also going to handle calendar format (AKA around now)
|
|
// Force dates >1 week from now to be in ISO instead of US format
|
|
setOutput(msg, node.outputType, node.output, mDT.calendar(null,{sameElse:'YYYY-MM-DD'}));
|
|
break;
|
|
case 'date':
|
|
case 'jsdate':
|
|
// we also allow output as a Javascript Date object
|
|
setOutput(msg, node.outputType, node.output, mDT.toDate());
|
|
break;
|
|
case 'object':
|
|
// we also allow output as a Javascript Date object
|
|
setOutput(msg, node.outputType, node.output, mDT.toObject());
|
|
break;
|
|
default:
|
|
// or we assume it is a valid format definition ...
|
|
setOutput(msg, node.outputType, node.output, mDT.format(node.format));
|
|
}
|
|
}
|
|
|
|
// Send the output message
|
|
node.send(msg);
|
|
});
|
|
|
|
// Tidy up if we need to
|
|
//node.on('close', function() {
|
|
// Called when the node is shutdown - eg on redeploy.
|
|
// Allows ports to be closed, connections dropped etc.
|
|
// eg: node.client.disconnect();
|
|
//});
|
|
|
|
// Set the appropriate output variable
|
|
function setOutput(msg, outputType, output, value) {
|
|
try {
|
|
switch ( outputType ) {
|
|
case 'msg':
|
|
RED.util.setMessageProperty(msg, output, value);
|
|
break;
|
|
case 'flow':
|
|
node.context().flow.set(output, value);
|
|
break;
|
|
case 'global':
|
|
node.context().global.set(output, value);
|
|
break;
|
|
default:
|
|
node.warn('Unrecognised Output Type, ' + outputType + '. No output.');
|
|
}
|
|
} catch (err) {
|
|
node.warn('Output property, ' + outputType + '.' + output + ', cannot be set. No output.', err);
|
|
}
|
|
} // --- end of setOutput function --- //
|
|
|
|
// Is the date/time adjustment type (measure) valid? See moment.js docs
|
|
function isMeasureValid(adjType) {
|
|
var validTypes = ['years','y','quarters','Q','months','M','weeks','w','days','d','hours','h','minutes','m','seconds','s','milliseconds','ms'];
|
|
//return validTypes.includes(adjType);
|
|
return validTypes.indexOf(adjType) > -1;
|
|
} // --- end of isMeasureValid function --- //
|
|
|
|
} // ---- end of nodeGo function ---- //
|
|
|
|
// Register the node by name. This must be called before overriding any of the
|
|
// Node functions.
|
|
RED.nodes.registerType(moduleName,nodeGo);
|
|
|
|
// Create API listener: sends the host locale & timezone to the admin ui
|
|
// NB: uses Express middleware on the admin server
|
|
RED.httpAdmin.get('/contribapi/moment', RED.auth.needsPermission('moment.read'), function(req,res) {
|
|
res.json({
|
|
'tz': hostTz,
|
|
'locale': hostLocale
|
|
});
|
|
});
|
|
};
|