1 | const h54sError = require('./error.js');
|
2 | const logs = require('./logs.js');
|
3 | const Tables = require('./tables');
|
4 | const Files = require('./files');
|
5 | const toSasDateTime = require('./tables/utils.js').toSasDateTime;
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 | function 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 |
|
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 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 |
|
44 | function 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 |
|
57 |
|
58 |
|
59 |
|
60 |
|
61 | SasData.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,
|
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)) {
|
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 |
|
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 |
|
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 |
|
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 |
|
236 | if(j < specKeys.length - 1) {
|
237 | sasCsv += ',';
|
238 | }
|
239 | }
|
240 | if(i < table.length - 1) {
|
241 | sasCsv += '\r\n';
|
242 | }
|
243 | }
|
244 |
|
245 |
|
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 |
|
258 |
|
259 |
|
260 |
|
261 | SasData.prototype.addFile = function(file, macroName) {
|
262 | Files.prototype.add.call(this, file, macroName);
|
263 | };
|
264 |
|
265 | module.exports = SasData;
|