UNPKG

h54s

Version:

HTML5 Data Adapter for SAS

266 lines (237 loc) 9.23 kB
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;