UNPKG

9.23 kBJavaScriptView Raw
1const h54sError = require('./error.js');
2const logs = require('./logs.js');
3const Tables = require('./tables');
4const Files = require('./files');
5const toSasDateTime = require('./tables/utils.js').toSasDateTime;
6
7/**
8 * Checks whether a given table name is a valid SAS macro name
9 * @param {String} macroName The SAS macro name to be given to this table
10 */
11function validateMacro(macroName) {
12 if(macroName.length > 32) {
13 throw new h54sError('argumentError', 'Table name too long. Maximum is 32 characters');
14 }
15
16 const charCodeAt0 = macroName.charCodeAt(0);
17 // validate it starts with A-Z, a-z, or _
18 if((charCodeAt0 < 65 || charCodeAt0 > 90) && (charCodeAt0 < 97 || charCodeAt0 > 122) && macroName[0] !== '_') {
19 throw new h54sError('argumentError', 'Table name starting with number or special characters');
20 }
21
22 for(let i = 0; i < macroName.length; i++) {
23 const charCode = macroName.charCodeAt(i);
24
25 if((charCode < 48 || charCode > 57) &&
26 (charCode < 65 || charCode > 90) &&
27 (charCode < 97 || charCode > 122) &&
28 macroName[i] !== '_')
29 {
30 throw new h54sError('argumentError', 'Table name has unsupported characters');
31 }
32 }
33}
34
35/**
36* h54s SAS data object constructor
37* @constructor
38*
39* @param {array|file} data - Table or file added when object is created
40* @param {String} macroName The SAS macro name to be given to this table
41* @param {number} parameterThreshold - size of data objects sent to SAS (legacy)
42*
43*/
44function SasData(data, macroName, specs) {
45 if(data instanceof Array) {
46 this._files = {};
47 this.addTable(data, macroName, specs);
48 } else if(data instanceof File || data instanceof Blob) {
49 Files.call(this, data, macroName);
50 } else {
51 throw new h54sError('argumentError', 'Data argument wrong type or missing');
52 }
53}
54
55/**
56* Add table to tables object
57* @param {array} table - Array of table objects
58* @param {String} macroName The SAS macro name to be given to this table
59*
60*/
61SasData.prototype.addTable = function(table, macroName, specs) {
62 const isSpecsProvided = !!specs;
63 if(table && macroName) {
64 if(!(table instanceof Array)) {
65 throw new h54sError('argumentError', 'First argument must be array');
66 }
67 if(typeof macroName !== 'string') {
68 throw new h54sError('argumentError', 'Second argument must be string');
69 }
70
71 validateMacro(macroName);
72 } else {
73 throw new h54sError('argumentError', 'Missing arguments');
74 }
75
76 if (typeof table !== 'object' || !(table instanceof Array)) {
77 throw new h54sError('argumentError', 'Table argument is not an array');
78 }
79
80 let key;
81 if(specs) {
82 if(specs.constructor !== Object) {
83 throw new h54sError('argumentError', 'Specs data type wrong. Object expected.');
84 }
85 for(key in table[0]) {
86 if(!specs[key]) {
87 throw new h54sError('argumentError', 'Missing columns in specs data.');
88 }
89 }
90 for(key in specs) {
91 if(specs[key].constructor !== Object) {
92 throw new h54sError('argumentError', 'Wrong column descriptor in specs data.');
93 }
94 if(!specs[key].colType || !specs[key].colLength) {
95 throw new h54sError('argumentError', 'Missing columns in specs descriptor.');
96 }
97 }
98 }
99
100 let i, j, //counters used latter in code
101 row, val, type,
102 specKeys = [];
103 const specialChars = ['"', '\\', '/', '\n', '\t', '\f', '\r', '\b'];
104
105 if(!specs) {
106 specs = {};
107
108 for (i = 0; i < table.length; i++) {
109 row = table[i];
110
111 if(typeof row !== 'object') {
112 throw new h54sError('argumentError', 'Table item is not an object');
113 }
114
115 for(key in row) {
116 if(row.hasOwnProperty(key)) {
117 val = row[key];
118 type = typeof val;
119
120 if(specs[key] === undefined) {
121 specKeys.push(key);
122 specs[key] = {};
123
124 if (type === 'number') {
125 if(val < Number.MIN_SAFE_INTEGER || val > Number.MAX_SAFE_INTEGER) {
126 logs.addApplicationLog('Object[' + i + '].' + key + ' - This value exceeds expected numeric precision.');
127 }
128 specs[key].colType = 'num';
129 specs[key].colLength = 8;
130 } else if (type === 'string' && !(val instanceof Date)) { // straightforward string
131 specs[key].colType = 'string';
132 specs[key].colLength = val.length;
133 } else if(val instanceof Date) {
134 specs[key].colType = 'date';
135 specs[key].colLength = 8;
136 } else if (type === 'object') {
137 specs[key].colType = 'json';
138 specs[key].colLength = JSON.stringify(val).length;
139 }
140 }
141 }
142 }
143 }
144 } else {
145 specKeys = Object.keys(specs);
146 }
147
148 let sasCsv = '';
149
150 // we need two loops - the first one is creating specs and validating
151 for (i = 0; i < table.length; i++) {
152 row = table[i];
153 for(j = 0; j < specKeys.length; j++) {
154 key = specKeys[j];
155 if(row.hasOwnProperty(key)) {
156 val = row[key];
157 type = typeof val;
158
159 if(type === 'number' && isNaN(val)) {
160 throw new h54sError('typeError', 'NaN value in one of the values (columns) is not allowed');
161 }
162 if(val === -Infinity || val === Infinity) {
163 throw new h54sError('typeError', val.toString() + ' value in one of the values (columns) is not allowed');
164 }
165 if(val === true || val === false) {
166 throw new h54sError('typeError', 'Boolean value in one of the values (columns) is not allowed');
167 }
168 if(type === 'string' && val.indexOf('\r\n') !== -1) {
169 throw new h54sError('typeError', 'New line character is not supported');
170 }
171
172 // convert null to '.' for numbers and to '' for strings
173 if(val === null) {
174 if(specs[key].colType === 'string') {
175 val = '';
176 type = 'string';
177 } else if(specs[key].colType === 'num') {
178 val = '.';
179 type = 'number';
180 } else {
181 throw new h54sError('typeError', 'Cannot convert null value');
182 }
183 }
184
185
186 if ((type === 'number' && specs[key].colType !== 'num' && val !== '.') ||
187 ((type === 'string' && !(val instanceof Date) && specs[key].colType !== 'string') &&
188 (type === 'string' && specs[key].colType == 'num' && val !== '.')) ||
189 (val instanceof Date && specs[key].colType !== 'date') ||
190 ((type === 'object' && val.constructor !== Date) && specs[key].colType !== 'json'))
191 {
192 throw new h54sError('typeError', 'There is a specs type mismatch in the array between values (columns) of the same name.' +
193 ' type/colType/val = ' + type +'/' + specs[key].colType + '/' + val );
194 } else if(!isSpecsProvided && type === 'string' && specs[key].colLength < val.length) {
195 specs[key].colLength = val.length;
196 } else if((type === 'string' && specs[key].colLength < val.length) || (type !== 'string' && specs[key].colLength !== 8)) {
197 throw new h54sError('typeError', 'There is a specs length mismatch in the array between values (columns) of the same name.' +
198 ' type/colType/val = ' + type +'/' + specs[key].colType + '/' + val );
199 }
200
201 if (val instanceof Date) {
202 val = toSasDateTime(val);
203 }
204
205 switch(specs[key].colType) {
206 case 'num':
207 case 'date':
208 sasCsv += val;
209 break;
210 case 'string':
211 sasCsv += '"' + val.replace(/"/g, '""') + '"';
212 let colLength = val.length;
213 for(let k = 0; k < val.length; k++) {
214 if(specialChars.indexOf(val[k]) !== -1) {
215 colLength++;
216 } else {
217 let code = val.charCodeAt(k);
218 if(code > 0xffff) {
219 colLength += 3;
220 } else if(code > 0x7ff) {
221 colLength += 2;
222 } else if(code > 0x7f) {
223 colLength += 1;
224 }
225 }
226 }
227 // use maximum value between max previous, current value and 1 (first two can be 0 wich is not supported)
228 specs[key].colLength = Math.max(specs[key].colLength, colLength, 1);
229 break;
230 case 'object':
231 sasCsv += '"' + JSON.stringify(val).replace(/"/g, '""') + '"';
232 break;
233 }
234 }
235 // do not insert if it's the last column
236 if(j < specKeys.length - 1) {
237 sasCsv += ',';
238 }
239 }
240 if(i < table.length - 1) {
241 sasCsv += '\r\n';
242 }
243 }
244
245 //convert specs to csv with pipes
246 const specString = specKeys.map(function(key) {
247 return key + ',' + specs[key].colType + ',' + specs[key].colLength;
248 }).join('|');
249
250 this._files[macroName] = [
251 specString,
252 new Blob([sasCsv], {type: 'text/csv;charset=UTF-8'})
253 ];
254};
255
256/**
257 * Add file as a verbatim blob file uplaod
258 * @param {Blob} file - the blob that will be uploaded as file
259 * @param {String} macroName - the SAS webin name given to this file
260 */
261SasData.prototype.addFile = function(file, macroName) {
262 Files.prototype.add.call(this, file, macroName);
263};
264
265module.exports = SasData;