@sasjs/adapter 3.3.1 → 3.4.0
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.
- package/.gitpod.yml +2 -0
- package/README.md +86 -8
- package/index.js +2 -2
- package/node/index.js +2 -2
- package/node/utils/convertToCsv.d.ts +5 -1
- package/package.json +1 -1
- package/src/SASjs.ts +8 -3
- package/src/utils/convertToCsv.ts +108 -52
- package/src/utils/formatDataForRequest.ts +10 -1
- package/test/utils/formatDataForRequest.spec.d.ts +1 -0
- package/utils/convertToCsv.d.ts +5 -1
|
@@ -2,4 +2,8 @@
|
|
|
2
2
|
* Converts the given JSON object array to a CSV string.
|
|
3
3
|
* @param data - the array of JSON objects to convert.
|
|
4
4
|
*/
|
|
5
|
-
export declare const convertToCSV: (data: any
|
|
5
|
+
export declare const convertToCSV: (data: any, sasFormats?: {
|
|
6
|
+
formats: {
|
|
7
|
+
[key: string]: string;
|
|
8
|
+
};
|
|
9
|
+
} | undefined) => string;
|
package/package.json
CHANGED
package/src/SASjs.ts
CHANGED
|
@@ -736,15 +736,19 @@ export default class SASjs {
|
|
|
736
736
|
msg: string
|
|
737
737
|
} {
|
|
738
738
|
if (data === null) return { status: true, msg: '' }
|
|
739
|
+
|
|
740
|
+
const isSasFormatsTable = (key: string) =>
|
|
741
|
+
key.match(/^\$.*/) && Object.keys(data).includes(key.replace(/^\$/, ''))
|
|
742
|
+
|
|
739
743
|
for (const key in data) {
|
|
740
|
-
if (!key.match(/^[a-zA-Z_]/)) {
|
|
744
|
+
if (!key.match(/^[a-zA-Z_]/) && !isSasFormatsTable(key)) {
|
|
741
745
|
return {
|
|
742
746
|
status: false,
|
|
743
747
|
msg: 'First letter of table should be alphabet or underscore.'
|
|
744
748
|
}
|
|
745
749
|
}
|
|
746
750
|
|
|
747
|
-
if (!key.match(/^[a-zA-Z_][a-zA-Z0-9_]*$/)) {
|
|
751
|
+
if (!key.match(/^[a-zA-Z_][a-zA-Z0-9_]*$/) && !isSasFormatsTable(key)) {
|
|
748
752
|
return { status: false, msg: 'Table name should be alphanumeric.' }
|
|
749
753
|
}
|
|
750
754
|
|
|
@@ -755,7 +759,7 @@ export default class SASjs {
|
|
|
755
759
|
}
|
|
756
760
|
}
|
|
757
761
|
|
|
758
|
-
if (this.getType(data[key]) !== 'Array') {
|
|
762
|
+
if (this.getType(data[key]) !== 'Array' && !isSasFormatsTable(key)) {
|
|
759
763
|
return {
|
|
760
764
|
status: false,
|
|
761
765
|
msg: 'Parameter data contains invalid table structure.'
|
|
@@ -771,6 +775,7 @@ export default class SASjs {
|
|
|
771
775
|
}
|
|
772
776
|
}
|
|
773
777
|
}
|
|
778
|
+
|
|
774
779
|
return { status: true, msg: '' }
|
|
775
780
|
}
|
|
776
781
|
|
|
@@ -2,70 +2,115 @@
|
|
|
2
2
|
* Converts the given JSON object array to a CSV string.
|
|
3
3
|
* @param data - the array of JSON objects to convert.
|
|
4
4
|
*/
|
|
5
|
-
export const convertToCSV = (
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
export const convertToCSV = (
|
|
6
|
+
data: any,
|
|
7
|
+
sasFormats?: { formats: { [key: string]: string } }
|
|
8
|
+
) => {
|
|
9
|
+
let formats = sasFormats?.formats
|
|
10
|
+
let headers: string[] = []
|
|
8
11
|
let csvTest
|
|
9
12
|
let invalidString = false
|
|
10
|
-
const
|
|
11
|
-
let firstFoundType: string | null = null
|
|
12
|
-
let hasMixedTypes: boolean = false
|
|
13
|
-
let rowNumError: number = -1
|
|
14
|
-
|
|
15
|
-
const longestValueForField = data
|
|
16
|
-
.map((row: any, index: number) => {
|
|
17
|
-
if (row[field] || row[field] === '') {
|
|
18
|
-
if (firstFoundType) {
|
|
19
|
-
let currentFieldType =
|
|
20
|
-
row[field] === '' || typeof row[field] === 'string'
|
|
21
|
-
? 'chars'
|
|
22
|
-
: 'number'
|
|
23
|
-
|
|
24
|
-
if (!hasMixedTypes) {
|
|
25
|
-
hasMixedTypes = currentFieldType !== firstFoundType
|
|
26
|
-
rowNumError = hasMixedTypes ? index + 1 : -1
|
|
27
|
-
}
|
|
28
|
-
} else {
|
|
29
|
-
if (row[field] === '') {
|
|
30
|
-
firstFoundType = 'chars'
|
|
31
|
-
} else {
|
|
32
|
-
firstFoundType =
|
|
33
|
-
typeof row[field] === 'string' ? 'chars' : 'number'
|
|
34
|
-
}
|
|
35
|
-
}
|
|
13
|
+
const specialMissingValueRegExp = /^[a-z_]{1}$/i
|
|
36
14
|
|
|
37
|
-
|
|
15
|
+
if (formats) {
|
|
16
|
+
headers = Object.keys(formats).map((key) => `${key}:${formats![key]}`)
|
|
17
|
+
}
|
|
38
18
|
|
|
39
|
-
|
|
40
|
-
byteSize = getByteSize(row[field])
|
|
41
|
-
}
|
|
19
|
+
const headerFields = Object.keys(data[0])
|
|
42
20
|
|
|
43
|
-
|
|
21
|
+
headerFields.forEach((field) => {
|
|
22
|
+
if (!formats || !Object.keys(formats).includes(field)) {
|
|
23
|
+
let hasNullOrNumber = false
|
|
24
|
+
let hasSpecialMissingString = false
|
|
25
|
+
|
|
26
|
+
data.forEach((row: { [key: string]: any }) => {
|
|
27
|
+
if (row[field] === null || typeof row[field] === 'number') {
|
|
28
|
+
hasNullOrNumber = true
|
|
29
|
+
} else if (
|
|
30
|
+
typeof row[field] === 'string' &&
|
|
31
|
+
specialMissingValueRegExp.test(row[field])
|
|
32
|
+
) {
|
|
33
|
+
hasSpecialMissingString = true
|
|
44
34
|
}
|
|
45
35
|
})
|
|
46
|
-
.sort((a: number, b: number) => b - a)[0]
|
|
47
|
-
if (longestValueForField && longestValueForField > 32765) {
|
|
48
|
-
invalidString = true
|
|
49
|
-
}
|
|
50
|
-
if (hasMixedTypes) {
|
|
51
|
-
console.error(
|
|
52
|
-
`Row (${rowNumError}), Column (${field}) has mixed types: ERROR`
|
|
53
|
-
)
|
|
54
|
-
}
|
|
55
36
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
37
|
+
if (hasNullOrNumber && hasSpecialMissingString) {
|
|
38
|
+
headers.push(`${field}:best.`)
|
|
39
|
+
|
|
40
|
+
if (!formats) formats = {}
|
|
41
|
+
|
|
42
|
+
formats[field] = 'best.'
|
|
43
|
+
} else {
|
|
44
|
+
let firstFoundType: string | null = null
|
|
45
|
+
let hasMixedTypes: boolean = false
|
|
46
|
+
let rowNumError: number = -1
|
|
47
|
+
|
|
48
|
+
const longestValueForField = data
|
|
49
|
+
.map((row: any, index: number) => {
|
|
50
|
+
if (row[field] || row[field] === '') {
|
|
51
|
+
if (firstFoundType) {
|
|
52
|
+
let currentFieldType =
|
|
53
|
+
row[field] === '' || typeof row[field] === 'string'
|
|
54
|
+
? 'chars'
|
|
55
|
+
: 'number'
|
|
56
|
+
|
|
57
|
+
if (!hasMixedTypes) {
|
|
58
|
+
hasMixedTypes = currentFieldType !== firstFoundType
|
|
59
|
+
rowNumError = hasMixedTypes ? index + 1 : -1
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
if (row[field] === '') {
|
|
63
|
+
firstFoundType = 'chars'
|
|
64
|
+
} else {
|
|
65
|
+
firstFoundType =
|
|
66
|
+
typeof row[field] === 'string' ? 'chars' : 'number'
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let byteSize
|
|
71
|
+
|
|
72
|
+
if (typeof row[field] === 'string') {
|
|
73
|
+
byteSize = getByteSize(row[field])
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return byteSize
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
.sort((a: number, b: number) => b - a)[0]
|
|
80
|
+
|
|
81
|
+
if (longestValueForField && longestValueForField > 32765) {
|
|
82
|
+
invalidString = true
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (hasMixedTypes) {
|
|
86
|
+
console.error(
|
|
87
|
+
`Row (${rowNumError}), Column (${field}) has mixed types: ERROR`
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
headers.push(
|
|
92
|
+
`${field}:${firstFoundType === 'chars' ? '$char' : ''}${
|
|
93
|
+
longestValueForField
|
|
94
|
+
? longestValueForField
|
|
95
|
+
: firstFoundType === 'chars'
|
|
96
|
+
? '1'
|
|
97
|
+
: 'best'
|
|
98
|
+
}.`
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
63
102
|
})
|
|
64
103
|
|
|
65
|
-
if (
|
|
66
|
-
|
|
104
|
+
if (sasFormats) {
|
|
105
|
+
headers = headers.sort(
|
|
106
|
+
(a, b) =>
|
|
107
|
+
headerFields.indexOf(a.replace(/:.*/, '')) -
|
|
108
|
+
headerFields.indexOf(b.replace(/:.*/, ''))
|
|
109
|
+
)
|
|
67
110
|
}
|
|
68
111
|
|
|
112
|
+
if (invalidString) return 'ERROR: LARGE STRING LENGTH'
|
|
113
|
+
|
|
69
114
|
csvTest = data.map((row: any) => {
|
|
70
115
|
const fields = Object.keys(row).map((fieldName, index) => {
|
|
71
116
|
let value
|
|
@@ -76,6 +121,17 @@ export const convertToCSV = (data: any) => {
|
|
|
76
121
|
// stringify with replacer converts null values to empty strings
|
|
77
122
|
value = currentCell === null ? '' : currentCell
|
|
78
123
|
|
|
124
|
+
if (formats && formats[fieldName] === 'best.') {
|
|
125
|
+
if (value && !specialMissingValueRegExp.test(value)) {
|
|
126
|
+
console.log(`🤖[value]🤖`, value)
|
|
127
|
+
throw new Error(
|
|
128
|
+
'Special missing value can only be a single character from A to Z or _'
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return `.${value.toLowerCase()}`
|
|
133
|
+
}
|
|
134
|
+
|
|
79
135
|
// if there any present, it should have preceding (") for escaping
|
|
80
136
|
value = value.replace(/"/g, `""`)
|
|
81
137
|
|
|
@@ -7,9 +7,17 @@ export const formatDataForRequest = (data: any) => {
|
|
|
7
7
|
const result: any = {}
|
|
8
8
|
|
|
9
9
|
for (const tableName in data) {
|
|
10
|
+
if (
|
|
11
|
+
tableName.match(/^\$.*/) &&
|
|
12
|
+
Object.keys(data).includes(tableName.replace(/^\$/, ''))
|
|
13
|
+
) {
|
|
14
|
+
continue
|
|
15
|
+
}
|
|
16
|
+
|
|
10
17
|
tableCounter++
|
|
11
18
|
sasjsTables.push(tableName)
|
|
12
|
-
const csv = convertToCSV(data[tableName])
|
|
19
|
+
const csv = convertToCSV(data[tableName], data[`$${tableName}`])
|
|
20
|
+
|
|
13
21
|
if (csv === 'ERROR: LARGE STRING LENGTH') {
|
|
14
22
|
throw new Error(
|
|
15
23
|
'The max length of a string value in SASjs is 32765 characters.'
|
|
@@ -27,6 +35,7 @@ export const formatDataForRequest = (data: any) => {
|
|
|
27
35
|
result[`sasjs${tableCounter}data`] = csv
|
|
28
36
|
}
|
|
29
37
|
}
|
|
38
|
+
|
|
30
39
|
result['sasjs_tables'] = sasjsTables.join(' ')
|
|
31
40
|
|
|
32
41
|
return result
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/utils/convertToCsv.d.ts
CHANGED
|
@@ -2,4 +2,8 @@
|
|
|
2
2
|
* Converts the given JSON object array to a CSV string.
|
|
3
3
|
* @param data - the array of JSON objects to convert.
|
|
4
4
|
*/
|
|
5
|
-
export declare const convertToCSV: (data: any
|
|
5
|
+
export declare const convertToCSV: (data: any, sasFormats?: {
|
|
6
|
+
formats: {
|
|
7
|
+
[key: string]: string;
|
|
8
|
+
};
|
|
9
|
+
} | undefined) => string;
|