@es-labs/jslib 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +42 -0
  3. package/__test__/services.test.js +32 -0
  4. package/auth/index.js +226 -0
  5. package/auth/keyv.js +23 -0
  6. package/auth/knex.js +29 -0
  7. package/auth/redis.js +23 -0
  8. package/comms/email.js +123 -0
  9. package/comms/nexmo.js +44 -0
  10. package/comms/telegram.js +43 -0
  11. package/comms/telegram2/inbound.js +314 -0
  12. package/comms/telegram2/outbound.js +574 -0
  13. package/comms/webpush.js +60 -0
  14. package/config.js +37 -0
  15. package/express/controller/auth/oauth.js +39 -0
  16. package/express/controller/auth/oidc.js +87 -0
  17. package/express/controller/auth/own.js +100 -0
  18. package/express/controller/auth/saml.js +74 -0
  19. package/express/upload.js +48 -0
  20. package/index.js +1 -0
  21. package/iso/README.md +4 -0
  22. package/iso/__tests__/csv-utils.spec.js +128 -0
  23. package/iso/__tests__/datetime.spec.js +101 -0
  24. package/iso/__tests__/fetch.spec.js +270 -0
  25. package/iso/csv-utils.js +206 -0
  26. package/iso/datetime.js +103 -0
  27. package/iso/fetch.js +129 -0
  28. package/iso/fetch2.js +180 -0
  29. package/iso/log-filter.js +17 -0
  30. package/iso/sleep.js +6 -0
  31. package/iso/ws.js +63 -0
  32. package/node/oss-files/oss-uploader-client-fetch.js +258 -0
  33. package/node/oss-files/oss-uploader-client-fetch.md +31 -0
  34. package/node/oss-files/oss-uploader-client.js +219 -0
  35. package/node/oss-files/oss-uploader-server.js +199 -0
  36. package/node/oss-files/oss-uploader-usage.js +121 -0
  37. package/node/oss-files/oss-uploader-usage.md +34 -0
  38. package/node/oss-files/s3-uploader-client.js +217 -0
  39. package/node/oss-files/s3-uploader-server.js +123 -0
  40. package/node/oss-files/s3-uploader-usage.js +77 -0
  41. package/node/oss-files/s3-uploader-usage.md +34 -0
  42. package/package.json +53 -0
  43. package/packageInfo.js +9 -0
  44. package/services/ali.js +279 -0
  45. package/services/aws.js +194 -0
  46. package/services/db/__tests__/keyv.spec.js +31 -0
  47. package/services/db/keyv.js +14 -0
  48. package/services/db/knex.js +67 -0
  49. package/services/db/redis.js +51 -0
  50. package/services/index.js +57 -0
  51. package/services/mq/README.md +8 -0
  52. package/services/websocket.js +139 -0
  53. package/t4t/README.md +1 -0
  54. package/traps.js +20 -0
  55. package/utils/__tests__/aes.spec.js +52 -0
  56. package/utils/aes.js +23 -0
  57. package/web/UI.md +71 -0
  58. package/web/bwc-autocomplete.js +211 -0
  59. package/web/bwc-combobox.js +343 -0
  60. package/web/bwc-fileupload.js +87 -0
  61. package/web/bwc-loading-overlay.js +54 -0
  62. package/web/bwc-t4t-form.js +511 -0
  63. package/web/bwc-table.js +756 -0
  64. package/web/fetch.js +129 -0
  65. package/web/i18n.js +24 -0
  66. package/web/idle.js +49 -0
  67. package/web/parse-jwt.js +15 -0
  68. package/web/pwa.js +84 -0
  69. package/web/sign-pad.js +164 -0
  70. package/web/t4t-fe.js +164 -0
  71. package/web/util.js +126 -0
  72. package/web/web-cam.js +182 -0
@@ -0,0 +1,270 @@
1
+ import assert from 'node:assert/strict';
2
+ import { describe, it, beforeEach, afterEach } from 'node:test';
3
+ import Fetch from '../fetch.js';
4
+
5
+ function createFetchMock() {
6
+ const calls = [];
7
+ const responses = [];
8
+ const fn = async (...args) => {
9
+ calls.push(args);
10
+ const res = responses.shift();
11
+ if (!res) throw new Error('No mock response configured');
12
+ return res;
13
+ };
14
+
15
+ fn.calls = calls;
16
+ fn.mockResolvedValueOnce = (value) => { responses.push(value); return fn; };
17
+ fn.mockResolvedValue = (value) => { responses.length = 0; responses.push(value); return fn; };
18
+ fn.mockClear = () => { calls.length = 0; responses.length = 0; };
19
+ return fn;
20
+ }
21
+
22
+ describe.skip('fetch.js', () => {
23
+ let originalFetch;
24
+ let fetchInstance;
25
+ let fetchMock;
26
+
27
+ beforeEach(() => {
28
+ originalFetch = global.fetch;
29
+ fetchMock = createFetchMock();
30
+ global.fetch = fetchMock;
31
+ fetchInstance = new Fetch();
32
+ });
33
+
34
+ afterEach(() => {
35
+ global.fetch = originalFetch;
36
+ });
37
+
38
+ describe('constructor', () => {
39
+ it('should set default options and tokens', () => {
40
+ assert.strictEqual(fetchInstance.options.baseUrl, '');
41
+ assert.strictEqual(fetchInstance.options.credentials, 'same-origin');
42
+ assert.strictEqual(typeof fetchInstance.options.forceLogoutFn, 'function');
43
+ assert.strictEqual(fetchInstance.options.refreshUrl, '');
44
+ assert.strictEqual(fetchInstance.options.timeoutMs, 0);
45
+ assert.strictEqual(fetchInstance.options.maxRetry, 0);
46
+ assert.deepStrictEqual(fetchInstance.tokens, { access: '', refresh: '' });
47
+ });
48
+
49
+ it('should override options and tokens', () => {
50
+ const options = { baseUrl: 'http://localhost', timeoutMs: 5000 };
51
+ const tokens = { access: 'token123', refresh: 'refresh123' };
52
+ fetchInstance = new Fetch(options, tokens);
53
+ assert.strictEqual(fetchInstance.options.baseUrl, 'http://localhost');
54
+ assert.strictEqual(fetchInstance.options.timeoutMs, 5000);
55
+ assert.strictEqual(fetchInstance.tokens.access, 'token123');
56
+ assert.strictEqual(fetchInstance.tokens.refresh, 'refresh123');
57
+ });
58
+ });
59
+
60
+ describe('parseUrl', () => {
61
+ it('should parse relative URL', () => {
62
+ const result = Fetch.parseUrl('/api/test', 'http://localhost');
63
+ assert.deepStrictEqual(result, {
64
+ urlOrigin: 'http://localhost',
65
+ urlPath: '/api/test',
66
+ urlFull: 'http://localhost/api/test',
67
+ urlSearch: ''
68
+ });
69
+ });
70
+
71
+ it('should parse absolute URL with query', () => {
72
+ const result = Fetch.parseUrl('http://localhost/api/test?key=value');
73
+ assert.deepStrictEqual(result, {
74
+ urlOrigin: 'http://localhost',
75
+ urlPath: '/api/test',
76
+ urlFull: 'http://localhost/api/test',
77
+ urlSearch: '?key=value'
78
+ });
79
+ });
80
+
81
+ it('should handle invalid URL', () => {
82
+ const result = Fetch.parseUrl('invalid-url', 'http://localhost');
83
+ assert.strictEqual(result.urlOrigin, 'http://localhost');
84
+ assert.strictEqual(result.urlPath, 'invalid-url');
85
+ assert.strictEqual(result.urlFull, 'http://localhostinvalid-url');
86
+ assert.strictEqual(result.urlSearch, '');
87
+ });
88
+ });
89
+
90
+ describe('setOptions and getOptions', () => {
91
+ it('should set and get options', () => {
92
+ fetchInstance.setOptions({ baseUrl: 'http://test.com' });
93
+ assert.strictEqual(fetchInstance.getOptions().baseUrl, 'http://test.com');
94
+ });
95
+ });
96
+
97
+ describe('setTokens and getTokens', () => {
98
+ it('should set and get tokens', () => {
99
+ fetchInstance.setTokens({ access: 'newtoken' });
100
+ assert.strictEqual(fetchInstance.getTokens().access, 'newtoken');
101
+ });
102
+ });
103
+
104
+ describe('http', () => {
105
+ it('should make a successful GET request', async () => {
106
+ fetchMock.mockResolvedValueOnce({
107
+ status: 200,
108
+ text: () => Promise.resolve('{"data": "test"}')
109
+ });
110
+
111
+ const result = await fetchInstance.http('GET', 'http://localhost/api/test');
112
+ assert.strictEqual(result.status, 200);
113
+ assert.deepStrictEqual(result.data, { data: 'test' });
114
+ assert.strictEqual(fetchMock.calls.length, 1);
115
+ assert.strictEqual(fetchMock.calls[0][0], 'http://localhost/api/test');
116
+ assert.strictEqual(fetchMock.calls[0][1].method, 'GET');
117
+ });
118
+
119
+ it('should handle query parameters', async () => {
120
+ fetchMock.mockResolvedValueOnce({
121
+ status: 200,
122
+ text: () => Promise.resolve('')
123
+ });
124
+
125
+ await fetchInstance.http('GET', 'http://localhost/api/test', null, { key: 'value' });
126
+ assert.strictEqual(fetchMock.calls[0][0], 'http://localhost/api/test?key=value');
127
+ });
128
+
129
+ it('should handle POST with JSON body', async () => {
130
+ fetchMock.mockResolvedValueOnce({
131
+ status: 201,
132
+ text: () => Promise.resolve('')
133
+ });
134
+
135
+ await fetchInstance.http('POST', 'http://localhost/api/test', { name: 'test' });
136
+ assert.strictEqual(fetchMock.calls[0][0], 'http://localhost/api/test');
137
+ assert.strictEqual(fetchMock.calls[0][1].method, 'POST');
138
+ assert.strictEqual(fetchMock.calls[0][1].body, '{"name":"test"}');
139
+ assert.strictEqual(fetchMock.calls[0][1].headers['Content-Type'], 'application/json');
140
+ });
141
+
142
+ it('should handle POST with URL-encoded body', async () => {
143
+ fetchMock.mockResolvedValueOnce({
144
+ status: 201,
145
+ text: () => Promise.resolve('')
146
+ });
147
+ await fetchInstance.http('POST', 'http://localhost/api/test', { name: 'test' }, null, { 'Content-Type': 'application/x-www-form-urlencoded' });
148
+ assert.strictEqual(fetchMock.calls[0][0], 'http://localhost/api/test');
149
+ assert.strictEqual(fetchMock.calls[0][1].method, 'POST');
150
+ assert.strictEqual(fetchMock.calls[0][1].body.toString(), 'name=test');
151
+ assert.strictEqual(fetchMock.calls[0][1].headers['Content-Type'], 'application/x-www-form-urlencoded');
152
+ });
153
+
154
+ it('should handle POST with octet-stream', async () => {
155
+ fetchMock.mockResolvedValueOnce({ status: 201, text: () => Promise.resolve('') });
156
+
157
+ const binaryData = new Uint8Array(Buffer.from('Hello', 'utf-8'));
158
+ await fetchInstance.http('POST', 'http://localhost/api/test', binaryData, null, { 'Content-Type': 'application/octet-stream' });
159
+ assert.strictEqual(fetchMock.calls[0][0], 'http://localhost/api/test');
160
+ assert.strictEqual(fetchMock.calls[0][1].method, 'POST');
161
+ assert.strictEqual(fetchMock.calls[0][1].body.toString(), binaryData.toString());
162
+ assert.strictEqual(fetchMock.calls[0][1].headers['Content-Type'], 'application/octet-stream');
163
+ });
164
+
165
+ it('should handle FormData body', async () => {
166
+ const formData = new FormData();
167
+ formData.append('file', 'test');
168
+ fetchMock.mockResolvedValueOnce({ status: 200, text: () => Promise.resolve('') });
169
+ await fetchInstance.http('POST', 'http://localhost/api/test', formData);
170
+ assert.strictEqual(fetchMock.calls[0][0], 'http://localhost/api/test');
171
+ assert.strictEqual(fetchMock.calls[0][1].method, 'POST');
172
+ assert.strictEqual(fetchMock.calls[0][1].body, formData);
173
+ });
174
+
175
+ it('should handle 401 with token refresh', async () => {
176
+ fetchMock.mockResolvedValueOnce({
177
+ status: 401,
178
+ text: () => Promise.resolve('{"message": "Token Expired Error"}')
179
+ });
180
+ fetchMock.mockResolvedValueOnce({
181
+ status: 200,
182
+ text: () => Promise.resolve('{"access_token": "new_access", "refresh_token": "new_refresh"}')
183
+ });
184
+ fetchMock.mockResolvedValueOnce({
185
+ status: 200,
186
+ text: () => Promise.resolve('{"data": "success"}')
187
+ });
188
+
189
+ fetchInstance.setOptions({ refreshUrl: '/refresh' });
190
+ fetchInstance.setTokens({ access: 'old_access', refresh: 'old_refresh' });
191
+
192
+ const result = await fetchInstance.http('GET', 'http://127.0.0.1/api/test');
193
+ assert.strictEqual(result.status, 200);
194
+ assert.deepStrictEqual(result.data, { data: 'success' });
195
+ assert.strictEqual(fetchInstance.tokens.access, 'new_access');
196
+ assert.strictEqual(fetchMock.calls.length, 3);
197
+ assert.strictEqual(fetchMock.calls[1][0], 'http://127.0.0.1/refresh');
198
+ });
199
+
200
+ it('should throw when token refresh fails', { only: false }, async () => {
201
+ fetchMock.mockResolvedValueOnce({
202
+ status: 401, text: () => Promise.resolve('{"message": "Token Expired Error"}')
203
+ });
204
+ fetchMock.mockResolvedValueOnce({ status: 404, text: () => Promise.resolve('{"message": "error"}') });
205
+ fetchInstance.setOptions({ refreshUrl: '/refresh' });
206
+ fetchInstance.setTokens({ access: 'old_access', refresh: 'old_refresh' });
207
+
208
+ await assert.rejects(async () => {
209
+ await fetchInstance.http('GET', 'http://localhost/api/test');
210
+ }, (err) => {
211
+ assert.strictEqual(err.status, 404);
212
+ assert.deepStrictEqual(err.data, { message: 'error' });
213
+ return true;
214
+ });
215
+ assert.strictEqual(fetchInstance.tokens.access, 'old_access');
216
+ assert.strictEqual(fetchMock.calls.length, 2);
217
+ });
218
+
219
+ it('should throw on error status', async () => {
220
+ fetchMock.mockResolvedValueOnce({ status: 404, text: () => Promise.resolve('') });
221
+ await assert.rejects(async () => { await fetchInstance.http('GET', 'http://localhost/api/test'); });
222
+ });
223
+
224
+ it('should call forceLogoutFn on 401/403 without token error', async () => {
225
+ let logoutCalled = false;
226
+ fetchInstance.setOptions({ forceLogoutFn: () => { logoutCalled = true; } });
227
+ fetchMock.mockResolvedValueOnce({
228
+ status: 401,
229
+ text: () => Promise.resolve('{"message": "Unauthorized"}')
230
+ });
231
+
232
+ await assert.rejects(async () => { await fetchInstance.http('GET', 'http://localhost/api/test'); });
233
+ assert.strictEqual(logoutCalled, true);
234
+ });
235
+ });
236
+
237
+ describe('convenience methods', () => {
238
+ beforeEach(() => {
239
+ fetchMock.mockResolvedValue({
240
+ status: 200,
241
+ text: () => Promise.resolve('')
242
+ });
243
+ });
244
+
245
+ it('should call http with correct method for get', async () => {
246
+ await fetchInstance.get('/api/test');
247
+ assert.strictEqual(fetchMock.calls[0][1].method, 'GET');
248
+ });
249
+
250
+ it('should call http with correct method for post', async () => {
251
+ await fetchInstance.post('/api/test', { data: 'test' });
252
+ assert.strictEqual(fetchMock.calls[0][1].method, 'POST');
253
+ });
254
+
255
+ it('should call http with correct method for put', async () => {
256
+ await fetchInstance.put('/api/test', { data: 'test' });
257
+ assert.strictEqual(fetchMock.calls[0][1].method, 'PUT');
258
+ });
259
+
260
+ it('should call http with correct method for patch', async () => {
261
+ await fetchInstance.patch('/api/test', { data: 'test' });
262
+ assert.strictEqual(fetchMock.calls[0][1].method, 'PATCH');
263
+ });
264
+
265
+ it('should call http with correct method for del', async () => {
266
+ await fetchInstance.del('/api/test');
267
+ assert.strictEqual(fetchMock.calls[0][1].method, 'DELETE');
268
+ });
269
+ });
270
+ });
@@ -0,0 +1,206 @@
1
+ 'use strict'
2
+ // RFC 4180 CSV parser and generator
3
+ // https://stackoverflow.com/a/41563966
4
+ // https://www.convertcsv.com/json-to-csv.htm
5
+ // - instead of using npm libraries: csv-parse and @json2csv/plainjs
6
+ // - use process.stdout.write(...) if piping output so no extra carraige return is added
7
+ // - double quote only required if field contains newline characters
8
+ // - check if valid json key syntax
9
+ // - PROBLEMS
10
+ // 1. what if columns are not same (use less or use more) ?
11
+ // 2. what if row is missing
12
+
13
+ const DELIM_ROW = "\n" // end of line \r\n for Windows \n for Linux
14
+ const DELIM_COL = ','
15
+ const ESCAPE_CHAR = '""' // this should remain as "" for RFC4180 compliance
16
+ const QUOTE_CHAR = '"'
17
+ const CHAR_CR = '\r'
18
+ const CHAR_LF = '\n'
19
+
20
+ /**
21
+ * RFC 4180-compliant CSV parser (handles quoted fields with embedded commas/newlines)
22
+ * - 1. escaped correctly
23
+ * - 2. same number of columns in each row (TBD?)
24
+ * - 3. trims white space around fields (TBD?)
25
+ * @param {string} str - CSV string to parse
26
+ * @returns {string[][]} - array of rows, each row is an array of fields
27
+ * @throws {Error} - if CSV is invalid (e.g. unclosed quotes)
28
+ */
29
+ const parseCSV = (str, delimCol=DELIM_COL) => {
30
+ const rows = [];
31
+ let row = [];
32
+ let field = '';
33
+ let inQuotes = false;
34
+ let i = 0;
35
+
36
+ while (i < str.length) {
37
+ const ch = str[i];
38
+ const next = str[i + 1];
39
+
40
+ if (inQuotes) {
41
+ if (ch === QUOTE_CHAR && next === QUOTE_CHAR) {
42
+ field += QUOTE_CHAR; // "" → single "
43
+ i += 2;
44
+ } else if (ch === QUOTE_CHAR) {
45
+ inQuotes = false;
46
+ i++;
47
+ } else {
48
+ field += ch;
49
+ i++;
50
+ }
51
+ } else {
52
+ if (ch === QUOTE_CHAR) {
53
+ inQuotes = true;
54
+ i++;
55
+ } else if (ch === delimCol) {
56
+ row.push(field);
57
+ field = '';
58
+ i++;
59
+ } else if (ch === CHAR_LF || ch === CHAR_CR) {
60
+ row.push(field);
61
+ rows.push(row);
62
+ row = [];
63
+ field = '';
64
+ if (ch === CHAR_CR && next === CHAR_LF) i++; // handle \r\n
65
+ i++;
66
+ } else {
67
+ field += ch;
68
+ i++;
69
+ }
70
+ }
71
+ }
72
+
73
+ // Push last field/row
74
+ if (field || row.length) {
75
+ row.push(field);
76
+ rows.push(row);
77
+ }
78
+
79
+ if (inQuotes) throw new Error('Unclosed quoted field — invalid CSV');
80
+ return rows;
81
+ }
82
+
83
+ /**
84
+ * Parse a string as CSV and also validate JSON if a field is a JSON field
85
+ * @param {string[][]} csvString - CSV parsed as array of rows, each row is an array of fields
86
+ * @returns {object} - { valid: boolean, reason?: string, rows?: string[][] }
87
+ * @throws {Error} - if CSV is invalid (e.g. unclosed quotes)
88
+ */
89
+ const parseAndValidateCsv = (csvString) => {
90
+ try {
91
+ const rows = parseCSV(csvString);
92
+ for (const row of rows) {
93
+ for (const field of row) {
94
+ // Catches objects {}, arrays [], strings "...", numbers, booleans, null
95
+ const looksLikeJSON = /^[[{"'\-\d]|^(true|false|null)$/.test(field.trim());
96
+ if (looksLikeJSON) {
97
+ try {
98
+ JSON.parse(field);
99
+ } catch {
100
+ return { valid: false, reason: `Corrupted JSON in field: ${field.slice(0, 50)}` };
101
+ }
102
+ }
103
+ }
104
+ }
105
+ return { valid: true, rows };
106
+ } catch (err) {
107
+ return { valid: false, reason: err.message };
108
+ }
109
+ }
110
+
111
+ /**
112
+ * convert CSV to array of JSON object
113
+ * @param {object} input - conversion inputs
114
+ * @param {string} input._text - CSV text to be converted to array
115
+ * @param {string} input.delimCol - CSV column delimiter, default is comma (,)
116
+ * @param {boolean} input.ignoreColumnMismatch - whether to ignore column count mismatches
117
+ * @returns
118
+ */
119
+ const csvToJson = ({ _text, delimCol = DELIM_COL, ignoreColumnMismatch = false}) => {
120
+ const arr = parseCSV(_text, delimCol);
121
+ const headers = arr.shift() // 1st row is the headers
122
+ return arr.map((row) => {
123
+ const rv = {}
124
+ if (headers.length != row.length && !ignoreColumnMismatch) throw new Error(`Mismatch headers(${headers.length}) != columns (${row.length})`)
125
+ headers.forEach((_, index) => {
126
+ rv[headers[index]] = row[index]
127
+ })
128
+ return rv
129
+ })
130
+ }
131
+
132
+ /**
133
+ * Converts an array of fields to a CSV row, escaping values as needed.
134
+ * escape for Excel, Google Sheets, and RFC 4180-compliant parsers
135
+ *
136
+ * @param {*[]} fields - array of field values to convert to a CSV row
137
+ * @param {string} delimCol - CSV column delimiter, default is comma (,)
138
+ * @returns {string} - CSV datarow string
139
+ */
140
+ const arrayToCSVRow = (fields, delimCol = DELIM_COL) =>{
141
+ return fields.map(field => {
142
+ if (field === null || field === undefined) return '';
143
+ if (typeof field === 'object') {
144
+ const jsonStr = JSON.stringify(field).replace(/"/g, ESCAPE_CHAR);
145
+ return `"${jsonStr}"`;
146
+ }
147
+ // Wrap any plain string containing commas/quotes/newlines too
148
+ if (typeof field === 'string' && /[",\n]/.test(field)) {
149
+ return `"${field.replace(/"/g, ESCAPE_CHAR)}"`;
150
+ }
151
+ return field;
152
+ }).join(delimCol);
153
+ }
154
+
155
+ /**
156
+ * Converts JSON object values to a CSV data row, escaping values as needed.
157
+ * escape for Excel, Google Sheets, and RFC 4180-compliant parsers
158
+ *
159
+ * @param {Object} jsonObj - JSON object to convert to a CSV row
160
+ * @param {string} delimCol - CSV column delimiter, default is comma (,)
161
+ * @returns {string} - CSV data row string
162
+ */
163
+ const jsonToCSVRow = (jsonObj, delimCol = DELIM_COL) => arrayToCSVRow(Object.values(jsonObj), delimCol);
164
+
165
+ /**
166
+ * Converts JSON object keys to a CSV header row, escaping values as needed.
167
+ * escape for Excel, Google Sheets, and RFC 4180-compliant parsers
168
+ *
169
+ * @param {Object} jsonObj - JSON object to convert to a CSV row
170
+ * @param {string} delimCol - CSV column delimiter, default is comma (,)
171
+ * @returns {string} - CSV header row string
172
+ */
173
+ const jsonToCSVHeader = (jsonObj, delimCol = DELIM_COL) => arrayToCSVRow(Object.keys(jsonObj), delimCol);
174
+
175
+ /**
176
+ * convert array of JSON objects to CSV
177
+ * @param {Object[]} _json - array of JS objects to be converted to CSV
178
+ * @param {string} delimRow - CSV row delimiter, default is newline (\n)
179
+ * @param {string} delimCol - CSV column delimiter, default is comma (,)
180
+ * @param {boolean} ignoreColumnMismatch - whether to ignore column count mismatches
181
+ * @returns
182
+ */
183
+ const jsonToCsv = (_json, delimCol=DELIM_COL, delimRow=DELIM_ROW, ignoreColumnMismatch=false) => {
184
+ let csv = ''
185
+ let headers = []
186
+ if (Array.isArray(_json)) _json.forEach((row, index) => {
187
+ if (index === 0) { // create 1st row as header
188
+ headers = Object.keys(row)
189
+ csv += (jsonToCSVHeader(row, delimCol) + delimRow)
190
+ }
191
+ const data = Object.values(row)
192
+ if (headers.length !== data.length && !ignoreColumnMismatch) throw new Error(`Mismatch headers(${headers.length}) != columns (${data.length})`)
193
+ else csv += (arrayToCSVRow(data, delimCol) + delimRow)
194
+ })
195
+ return csv
196
+ }
197
+
198
+ export {
199
+ arrayToCSVRow,
200
+ jsonToCSVHeader,
201
+ jsonToCSVRow,
202
+ jsonToCsv,
203
+ parseAndValidateCsv,
204
+ parseCSV, // to array of arrays, non validating
205
+ csvToJson,
206
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Get array of strings with datetime elements after offseting input date by X days
3
+ * @param {string} isoString - ISO datetime string
4
+ * @param {Number} days - ISO datetime string
5
+ * @returns {string[]}} - return array or YYYY, MM, DD strings - not so useful...
6
+ */
7
+ export const dateStrAddDay = (dateStr, days = 0) => {
8
+ const d = new Date(Date.parse(dateStr) - new Date().getTimezoneOffset());
9
+ d.setDate(d.getDate() + days); // add the days
10
+ return [d.getFullYear().toString(), (d.getMonth() + 1).toString().padStart(2, 0), d.getDate().toString().padStart(2, 0)];
11
+ }
12
+
13
+ /**
14
+ * Get new ISO datetime string date after offseting input date by X days
15
+ * @param {string} isoString - ISO datetime string
16
+ * @param {Number} days - ISO datetime string
17
+ * @returns {string} - return ISO datetime string
18
+ */
19
+ export const dateStrAddDayISO = (isoString, days = 0) => {
20
+ const d = new Date(isoString);
21
+ d.setDate(d.getDate() + days);
22
+ return d.toISOString();
23
+ }
24
+
25
+ /**
26
+ * Get local,date time and TZ in ISO format
27
+ * @param {Date|string} dt - Date object or ISO datetime string
28
+ * @param {string} tz - IANA time zone name, e.g. 'Asia/Singapore'
29
+ * @returns {string} - return string format: 2023-10-24 11:40:15 GMT+8
30
+ */
31
+ export const getLocaleDateTimeTzISO = (dt, tz) => {
32
+ const opts = {
33
+ timeZoneName: 'short',
34
+ hour12: false,
35
+ year: 'numeric',
36
+ month: '2-digit',
37
+ day: '2-digit',
38
+ hour: '2-digit',
39
+ minute: '2-digit',
40
+ second: '2-digit',
41
+ }
42
+ if (tz) opts.timeZone = tz;
43
+ if (!(dt instanceof Date)) dt = new Date(dt);
44
+ return dt.toLocaleString('sv', opts);
45
+ }
46
+
47
+ /**
48
+ * Get local date in ISO format
49
+ * @param {string} isoString - ISO datetime string
50
+ * @param {string} tz - IANA time zone name, e.g. 'Asia/Singapore'
51
+ * @returns {string}
52
+ */
53
+ export const getLocaleDateISO = (isoString, tz) => getLocaleDateTimeTzISO(isoString, tz).substring(0, 10);
54
+
55
+ /**
56
+ * Get local date in ISO format
57
+ * @param {string} isoString - ISO datetime string
58
+ * @param {string} tz - IANA time zone name, e.g. 'Asia/Singapore'
59
+ * @returns {string}
60
+ */
61
+ export const getLocaleTimeISO = (isoString, tz) => getLocaleDateTimeTzISO(isoString, tz).substring(11, 19);
62
+
63
+ /**
64
+ * Get timezone offset in ISO format (+hh:mm or -hh:mm)
65
+ * @param {Date|undefined} date - if undefined, will create a date object and use that timezone
66
+ * @returns {string}
67
+ */
68
+ export const getTzOffsetISO = (date) => {
69
+ const pad = n => `${Math.floor(Math.abs(n))}`.padStart(2, '0'); // Pad a number to 2 digits
70
+ if (!date) date = new Date();
71
+ const tzOffset = -date.getTimezoneOffset();
72
+ return (tzOffset >= 0 ? '+' : '-') + pad(tzOffset / 60) + ':' + pad(tzOffset % 60);
73
+ };
74
+
75
+ /**
76
+ * Get return current UTC timestamp YmdHms
77
+ * @param {Date|undefined} date - if undefined, will create a date object and use that timezone
78
+ * @returns {string} YYYYMMDD_HHmmssZ
79
+ */
80
+ export const getYmdhmsUtc = (date) => {
81
+ if (!date) date = new Date();
82
+ const d = date.toISOString();
83
+ return d.substring(0,4) + d.substring(5,7) + d.substring(8,10) + '_' + d.substring(11,13) + d.substring(14,16) + d.substring(17,19) + 'Z';
84
+ }
85
+
86
+ /**
87
+ * Get day of week index (0-6) for a given date and timezone
88
+ * @param {Date|string} date - Date object or ISO datetime string
89
+ * @param {string} tz - IANA time zone name, e.g. 'Asia/Singapore'
90
+ * @returns {Number}
91
+ */
92
+ export const getDayOfWeek = (date, tz) => {
93
+ if (!(date instanceof Date)) date = new Date(date)
94
+ const opts = { weekday: 'short' }
95
+ if (tz) opts.timeZone = tz
96
+ const shortDayName = new Intl.DateTimeFormat('en-US', opts).format(date) // Get short day name string for specified timezone
97
+ const daysOfWeek = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] // Map the short name back to a numeric index
98
+ return daysOfWeek.indexOf(shortDayName);
99
+ }
100
+
101
+ // Obsolete use getLocaleDateISO and getLocaleTimeISO instead
102
+ // export const dateISO = (date) => new Date(date.getTime() - date.getTimezoneOffset() * 60000).toISOString().substring(0, 10);
103
+ // export const timeISO = (date) => new Date(date.getTime() - date.getTimezoneOffset() * 60000).toISOString().substring(11, 19);