h54s
Version:
HTML5 Data Adapter for SAS
266 lines (237 loc) • 9.23 kB
JavaScript
const h54sError = require('./error.js');
const logs = require('./logs.js');
const Tables = require('./tables');
const Files = require('./files');
const toSasDateTime = require('./tables/utils.js').toSasDateTime;
/**
* Checks whether a given table name is a valid SAS macro name
* @param {String} macroName The SAS macro name to be given to this table
*/
function validateMacro(macroName) {
if(macroName.length > 32) {
throw new h54sError('argumentError', 'Table name too long. Maximum is 32 characters');
}
const charCodeAt0 = macroName.charCodeAt(0);
// validate it starts with A-Z, a-z, or _
if((charCodeAt0 < 65 || charCodeAt0 > 90) && (charCodeAt0 < 97 || charCodeAt0 > 122) && macroName[0] !== '_') {
throw new h54sError('argumentError', 'Table name starting with number or special characters');
}
for(let i = 0; i < macroName.length; i++) {
const charCode = macroName.charCodeAt(i);
if((charCode < 48 || charCode > 57) &&
(charCode < 65 || charCode > 90) &&
(charCode < 97 || charCode > 122) &&
macroName[i] !== '_')
{
throw new h54sError('argumentError', 'Table name has unsupported characters');
}
}
}
/**
* h54s SAS data object constructor
* @constructor
*
* @param {array|file} data - Table or file added when object is created
* @param {String} macroName The SAS macro name to be given to this table
* @param {number} parameterThreshold - size of data objects sent to SAS (legacy)
*
*/
function SasData(data, macroName, specs) {
if(data instanceof Array) {
this._files = {};
this.addTable(data, macroName, specs);
} else if(data instanceof File || data instanceof Blob) {
Files.call(this, data, macroName);
} else {
throw new h54sError('argumentError', 'Data argument wrong type or missing');
}
}
/**
* Add table to tables object
* @param {array} table - Array of table objects
* @param {String} macroName The SAS macro name to be given to this table
*
*/
SasData.prototype.addTable = function(table, macroName, specs) {
const isSpecsProvided = !!specs;
if(table && macroName) {
if(!(table instanceof Array)) {
throw new h54sError('argumentError', 'First argument must be array');
}
if(typeof macroName !== 'string') {
throw new h54sError('argumentError', 'Second argument must be string');
}
validateMacro(macroName);
} else {
throw new h54sError('argumentError', 'Missing arguments');
}
if (typeof table !== 'object' || !(table instanceof Array)) {
throw new h54sError('argumentError', 'Table argument is not an array');
}
let key;
if(specs) {
if(specs.constructor !== Object) {
throw new h54sError('argumentError', 'Specs data type wrong. Object expected.');
}
for(key in table[0]) {
if(!specs[key]) {
throw new h54sError('argumentError', 'Missing columns in specs data.');
}
}
for(key in specs) {
if(specs[key].constructor !== Object) {
throw new h54sError('argumentError', 'Wrong column descriptor in specs data.');
}
if(!specs[key].colType || !specs[key].colLength) {
throw new h54sError('argumentError', 'Missing columns in specs descriptor.');
}
}
}
let i, j, //counters used latter in code
row, val, type,
specKeys = [];
const specialChars = ['"', '\\', '/', '\n', '\t', '\f', '\r', '\b'];
if(!specs) {
specs = {};
for (i = 0; i < table.length; i++) {
row = table[i];
if(typeof row !== 'object') {
throw new h54sError('argumentError', 'Table item is not an object');
}
for(key in row) {
if(row.hasOwnProperty(key)) {
val = row[key];
type = typeof val;
if(specs[key] === undefined) {
specKeys.push(key);
specs[key] = {};
if (type === 'number') {
if(val < Number.MIN_SAFE_INTEGER || val > Number.MAX_SAFE_INTEGER) {
logs.addApplicationLog('Object[' + i + '].' + key + ' - This value exceeds expected numeric precision.');
}
specs[key].colType = 'num';
specs[key].colLength = 8;
} else if (type === 'string' && !(val instanceof Date)) { // straightforward string
specs[key].colType = 'string';
specs[key].colLength = val.length;
} else if(val instanceof Date) {
specs[key].colType = 'date';
specs[key].colLength = 8;
} else if (type === 'object') {
specs[key].colType = 'json';
specs[key].colLength = JSON.stringify(val).length;
}
}
}
}
}
} else {
specKeys = Object.keys(specs);
}
let sasCsv = '';
// we need two loops - the first one is creating specs and validating
for (i = 0; i < table.length; i++) {
row = table[i];
for(j = 0; j < specKeys.length; j++) {
key = specKeys[j];
if(row.hasOwnProperty(key)) {
val = row[key];
type = typeof val;
if(type === 'number' && isNaN(val)) {
throw new h54sError('typeError', 'NaN value in one of the values (columns) is not allowed');
}
if(val === -Infinity || val === Infinity) {
throw new h54sError('typeError', val.toString() + ' value in one of the values (columns) is not allowed');
}
if(val === true || val === false) {
throw new h54sError('typeError', 'Boolean value in one of the values (columns) is not allowed');
}
if(type === 'string' && val.indexOf('\r\n') !== -1) {
throw new h54sError('typeError', 'New line character is not supported');
}
// convert null to '.' for numbers and to '' for strings
if(val === null) {
if(specs[key].colType === 'string') {
val = '';
type = 'string';
} else if(specs[key].colType === 'num') {
val = '.';
type = 'number';
} else {
throw new h54sError('typeError', 'Cannot convert null value');
}
}
if ((type === 'number' && specs[key].colType !== 'num' && val !== '.') ||
((type === 'string' && !(val instanceof Date) && specs[key].colType !== 'string') &&
(type === 'string' && specs[key].colType == 'num' && val !== '.')) ||
(val instanceof Date && specs[key].colType !== 'date') ||
((type === 'object' && val.constructor !== Date) && specs[key].colType !== 'json'))
{
throw new h54sError('typeError', 'There is a specs type mismatch in the array between values (columns) of the same name.' +
' type/colType/val = ' + type +'/' + specs[key].colType + '/' + val );
} else if(!isSpecsProvided && type === 'string' && specs[key].colLength < val.length) {
specs[key].colLength = val.length;
} else if((type === 'string' && specs[key].colLength < val.length) || (type !== 'string' && specs[key].colLength !== 8)) {
throw new h54sError('typeError', 'There is a specs length mismatch in the array between values (columns) of the same name.' +
' type/colType/val = ' + type +'/' + specs[key].colType + '/' + val );
}
if (val instanceof Date) {
val = toSasDateTime(val);
}
switch(specs[key].colType) {
case 'num':
case 'date':
sasCsv += val;
break;
case 'string':
sasCsv += '"' + val.replace(/"/g, '""') + '"';
let colLength = val.length;
for(let k = 0; k < val.length; k++) {
if(specialChars.indexOf(val[k]) !== -1) {
colLength++;
} else {
let code = val.charCodeAt(k);
if(code > 0xffff) {
colLength += 3;
} else if(code > 0x7ff) {
colLength += 2;
} else if(code > 0x7f) {
colLength += 1;
}
}
}
// use maximum value between max previous, current value and 1 (first two can be 0 wich is not supported)
specs[key].colLength = Math.max(specs[key].colLength, colLength, 1);
break;
case 'object':
sasCsv += '"' + JSON.stringify(val).replace(/"/g, '""') + '"';
break;
}
}
// do not insert if it's the last column
if(j < specKeys.length - 1) {
sasCsv += ',';
}
}
if(i < table.length - 1) {
sasCsv += '\r\n';
}
}
//convert specs to csv with pipes
const specString = specKeys.map(function(key) {
return key + ',' + specs[key].colType + ',' + specs[key].colLength;
}).join('|');
this._files[macroName] = [
specString,
new Blob([sasCsv], {type: 'text/csv;charset=UTF-8'})
];
};
/**
* Add file as a verbatim blob file uplaod
* @param {Blob} file - the blob that will be uploaded as file
* @param {String} macroName - the SAS webin name given to this file
*/
SasData.prototype.addFile = function(file, macroName) {
Files.prototype.add.call(this, file, macroName);
};
module.exports = SasData;