@sasjs/adapter 3.2.1 → 3.4.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.
- package/.gitpod.yml +2 -0
- package/README.md +86 -8
- package/SASjs.d.ts +10 -2
- package/index.js +2 -2
- package/node/SASjs.d.ts +10 -2
- package/node/index.js +2 -2
- package/node/utils/convertToCsv.d.ts +5 -1
- package/package.json +35 -35
- package/src/SASjs.ts +8 -3
- package/src/SessionManager.ts +1 -1
- package/src/api/viya/pollJobState.ts +4 -3
- package/src/request/RequestClient.ts +9 -6
- package/src/request/SasjsRequestClient.ts +1 -1
- package/src/test/SAS_server_app.ts +1 -0
- 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
|
@@ -40,45 +40,45 @@
|
|
|
40
40
|
},
|
|
41
41
|
"license": "ISC",
|
|
42
42
|
"devDependencies": {
|
|
43
|
-
"@types/axios": "
|
|
44
|
-
"@types/express": "
|
|
45
|
-
"@types/form-data": "
|
|
46
|
-
"@types/jest": "
|
|
47
|
-
"@types/mime": "
|
|
48
|
-
"@types/pem": "
|
|
49
|
-
"@types/tough-cookie": "
|
|
50
|
-
"copyfiles": "
|
|
51
|
-
"cp": "
|
|
52
|
-
"dotenv": "
|
|
53
|
-
"express": "
|
|
54
|
-
"jest": "
|
|
55
|
-
"jest-extended": "
|
|
56
|
-
"node-polyfill-webpack-plugin": "
|
|
57
|
-
"path": "
|
|
58
|
-
"pem": "
|
|
59
|
-
"process": "
|
|
60
|
-
"rimraf": "
|
|
61
|
-
"semantic-release": "
|
|
62
|
-
"terser-webpack-plugin": "
|
|
63
|
-
"ts-jest": "
|
|
64
|
-
"ts-loader": "
|
|
65
|
-
"tslint": "
|
|
66
|
-
"tslint-config-prettier": "
|
|
43
|
+
"@types/axios": "0.14.0",
|
|
44
|
+
"@types/express": "4.17.13",
|
|
45
|
+
"@types/form-data": "2.5.0",
|
|
46
|
+
"@types/jest": "27.0.2",
|
|
47
|
+
"@types/mime": "2.0.3",
|
|
48
|
+
"@types/pem": "1.9.6",
|
|
49
|
+
"@types/tough-cookie": "4.0.1",
|
|
50
|
+
"copyfiles": "2.4.1",
|
|
51
|
+
"cp": "0.2.0",
|
|
52
|
+
"dotenv": "10.0.0",
|
|
53
|
+
"express": "4.17.1",
|
|
54
|
+
"jest": "27.2.0",
|
|
55
|
+
"jest-extended": "0.11.5",
|
|
56
|
+
"node-polyfill-webpack-plugin": "1.1.4",
|
|
57
|
+
"path": "0.12.7",
|
|
58
|
+
"pem": "1.14.4",
|
|
59
|
+
"process": "0.11.10",
|
|
60
|
+
"rimraf": "3.0.2",
|
|
61
|
+
"semantic-release": "18.0.0",
|
|
62
|
+
"terser-webpack-plugin": "5.2.4",
|
|
63
|
+
"ts-jest": "27.0.3",
|
|
64
|
+
"ts-loader": "9.2.6",
|
|
65
|
+
"tslint": "6.1.3",
|
|
66
|
+
"tslint-config-prettier": "1.18.0",
|
|
67
67
|
"typedoc": "0.19.2",
|
|
68
|
-
"typedoc-neo-theme": "
|
|
69
|
-
"typedoc-plugin-external-module-name": "
|
|
68
|
+
"typedoc-neo-theme": "1.1.1",
|
|
69
|
+
"typedoc-plugin-external-module-name": "4.0.6",
|
|
70
70
|
"typescript": "4.3.5",
|
|
71
|
-
"webpack": "
|
|
72
|
-
"webpack-cli": "
|
|
71
|
+
"webpack": "5.56.0",
|
|
72
|
+
"webpack-cli": "4.7.2"
|
|
73
73
|
},
|
|
74
74
|
"main": "index.js",
|
|
75
75
|
"dependencies": {
|
|
76
|
-
"@sasjs/utils": "
|
|
77
|
-
"axios": "
|
|
78
|
-
"axios-cookiejar-support": "
|
|
79
|
-
"form-data": "
|
|
80
|
-
"https": "
|
|
81
|
-
"tough-cookie": "
|
|
76
|
+
"@sasjs/utils": "2.32.0",
|
|
77
|
+
"axios": "0.25.0",
|
|
78
|
+
"axios-cookiejar-support": "1.0.1",
|
|
79
|
+
"form-data": "4.0.0",
|
|
80
|
+
"https": "1.0.0",
|
|
81
|
+
"tough-cookie": "4.0.0"
|
|
82
82
|
},
|
|
83
|
-
"version": "3.
|
|
83
|
+
"version": "3.4.1"
|
|
84
84
|
}
|
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
|
|
package/src/SessionManager.ts
CHANGED
|
@@ -168,7 +168,7 @@ export class SessionManager {
|
|
|
168
168
|
) {
|
|
169
169
|
if (stateLink) {
|
|
170
170
|
if (this.debug && !this.printedSessionState.printed) {
|
|
171
|
-
logger.info(
|
|
171
|
+
logger.info(`Polling: ${this.serverUrl + stateLink.href}`)
|
|
172
172
|
|
|
173
173
|
this.printedSessionState.printed = true
|
|
174
174
|
}
|
|
@@ -206,10 +206,11 @@ const doPoll = async (
|
|
|
206
206
|
|
|
207
207
|
pollCount++
|
|
208
208
|
|
|
209
|
+
const jobHref = postedJob.links.find((l: Link) => l.rel === 'self')!.href
|
|
210
|
+
|
|
209
211
|
if (pollOptions?.streamLog) {
|
|
210
|
-
const jobUrl = postedJob.links.find((l: Link) => l.rel === 'self')
|
|
211
212
|
const { result: job } = await requestClient.get<Job>(
|
|
212
|
-
|
|
213
|
+
jobHref,
|
|
213
214
|
authConfig?.access_token
|
|
214
215
|
)
|
|
215
216
|
|
|
@@ -231,7 +232,7 @@ const doPoll = async (
|
|
|
231
232
|
}
|
|
232
233
|
|
|
233
234
|
if (debug && printedState !== state) {
|
|
234
|
-
logger.info(
|
|
235
|
+
logger.info(`Polling: ${requestClient.getBaseUrl() + jobHref}/state`)
|
|
235
236
|
logger.info(`Current job state: ${state}`)
|
|
236
237
|
|
|
237
238
|
printedState = state
|
|
@@ -195,9 +195,7 @@ export class RequestClient implements HttpClient {
|
|
|
195
195
|
}
|
|
196
196
|
),
|
|
197
197
|
debug
|
|
198
|
-
)
|
|
199
|
-
throw prefixMessage(err, 'Error while handling error. ')
|
|
200
|
-
})
|
|
198
|
+
)
|
|
201
199
|
})
|
|
202
200
|
}
|
|
203
201
|
|
|
@@ -217,6 +215,7 @@ export class RequestClient implements HttpClient {
|
|
|
217
215
|
.post<T>(url, data, { headers, withCredentials: true })
|
|
218
216
|
.then((response) => {
|
|
219
217
|
throwIfError(response)
|
|
218
|
+
|
|
220
219
|
return this.parseResponse<T>(response)
|
|
221
220
|
})
|
|
222
221
|
.catch(async (e) => {
|
|
@@ -466,6 +465,8 @@ export class RequestClient implements HttpClient {
|
|
|
466
465
|
|
|
467
466
|
if (e instanceof LoginRequiredError) {
|
|
468
467
|
this.clearCsrfTokens()
|
|
468
|
+
|
|
469
|
+
throw e
|
|
469
470
|
}
|
|
470
471
|
|
|
471
472
|
if (response?.status === 403 || response?.status === 449) {
|
|
@@ -488,7 +489,8 @@ export class RequestClient implements HttpClient {
|
|
|
488
489
|
else return
|
|
489
490
|
}
|
|
490
491
|
|
|
491
|
-
throw e
|
|
492
|
+
if (e.message) throw e
|
|
493
|
+
else throw prefixMessage(e, 'Error while handling error. ')
|
|
492
494
|
}
|
|
493
495
|
|
|
494
496
|
protected parseResponse<T>(response: AxiosResponse<any>) {
|
|
@@ -540,8 +542,9 @@ export class RequestClient implements HttpClient {
|
|
|
540
542
|
|
|
541
543
|
this.httpClient = createAxiosInstance(baseUrl, httpsAgent)
|
|
542
544
|
|
|
543
|
-
this.httpClient.defaults.validateStatus = (status) =>
|
|
544
|
-
status >= 200 && status
|
|
545
|
+
this.httpClient.defaults.validateStatus = (status) => {
|
|
546
|
+
return status >= 200 && status <= 401
|
|
547
|
+
}
|
|
545
548
|
}
|
|
546
549
|
}
|
|
547
550
|
|
|
@@ -13,7 +13,7 @@ export class SasjsRequestClient extends RequestClient {
|
|
|
13
13
|
|
|
14
14
|
headers.Accept = contentType === 'application/json' ? contentType : '*/*'
|
|
15
15
|
|
|
16
|
-
if (!accessToken)
|
|
16
|
+
if (!accessToken && typeof window !== 'undefined')
|
|
17
17
|
accessToken = localStorage.getItem('accessToken') ?? undefined
|
|
18
18
|
|
|
19
19
|
if (accessToken) headers.Authorization = `Bearer ${accessToken}`
|
|
@@ -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;
|