@sasjs/adapter 3.3.1 → 3.5.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.
@@ -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) => string;
5
+ export declare const convertToCSV: (data: any, sasFormats?: {
6
+ formats: {
7
+ [key: string]: string;
8
+ };
9
+ } | undefined) => string;
@@ -1,3 +1,2 @@
1
- /// <reference types="node" />
2
1
  import * as https from 'https';
3
2
  export declare const createAxiosInstance: (baseURL: string, httpsAgent?: https.Agent | undefined) => import("axios").AxiosInstance;
package/package.json CHANGED
@@ -40,45 +40,45 @@
40
40
  },
41
41
  "license": "ISC",
42
42
  "devDependencies": {
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
- "typedoc": "0.19.2",
68
- "typedoc-neo-theme": "^1.1.1",
69
- "typedoc-plugin-external-module-name": "^4.0.6",
70
- "typescript": "4.3.5",
71
- "webpack": "^5.56.0",
72
- "webpack-cli": "^4.7.2"
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.4.7",
55
+ "jest-extended": "2.0.0",
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.3.0",
63
+ "ts-jest": "27.1.3",
64
+ "ts-loader": "9.2.6",
65
+ "tslint": "6.1.3",
66
+ "tslint-config-prettier": "1.18.0",
67
+ "typedoc": "0.22.11",
68
+ "typedoc-neo-theme": "1.1.1",
69
+ "typedoc-plugin-external-module-name": "4.0.6",
70
+ "typescript": "4.5.4",
71
+ "webpack": "5.66.0",
72
+ "webpack-cli": "4.7.2"
73
73
  },
74
74
  "main": "index.js",
75
75
  "dependencies": {
76
- "@sasjs/utils": "^2.32.0",
77
- "axios": "^0.21.4",
78
- "axios-cookiejar-support": "^1.0.1",
79
- "form-data": "^4.0.0",
80
- "https": "^1.0.0",
81
- "tough-cookie": "^4.0.0"
76
+ "@sasjs/utils": "2.35.0",
77
+ "axios": "0.26.0",
78
+ "axios-cookiejar-support": "2.0.3",
79
+ "form-data": "4.0.0",
80
+ "https": "1.0.0",
81
+ "tough-cookie": "4.0.0"
82
82
  },
83
- "version": "3.3.1"
83
+ "version": "3.5.0"
84
84
  }
package/src/SASjs.ts CHANGED
@@ -5,9 +5,8 @@ import {
5
5
  EditContextInput,
6
6
  PollOptions,
7
7
  LoginMechanism,
8
- FolderMember,
9
- ServiceMember,
10
- ExecutionQuery
8
+ ExecutionQuery,
9
+ FileTree
11
10
  } from './types'
12
11
  import { SASViyaApiClient } from './SASViyaApiClient'
13
12
  import { SAS9ApiClient } from './SAS9ApiClient'
@@ -736,15 +735,19 @@ export default class SASjs {
736
735
  msg: string
737
736
  } {
738
737
  if (data === null) return { status: true, msg: '' }
738
+
739
+ const isSasFormatsTable = (key: string) =>
740
+ key.match(/^\$.*/) && Object.keys(data).includes(key.replace(/^\$/, ''))
741
+
739
742
  for (const key in data) {
740
- if (!key.match(/^[a-zA-Z_]/)) {
743
+ if (!key.match(/^[a-zA-Z_]/) && !isSasFormatsTable(key)) {
741
744
  return {
742
745
  status: false,
743
746
  msg: 'First letter of table should be alphabet or underscore.'
744
747
  }
745
748
  }
746
749
 
747
- if (!key.match(/^[a-zA-Z_][a-zA-Z0-9_]*$/)) {
750
+ if (!key.match(/^[a-zA-Z_][a-zA-Z0-9_]*$/) && !isSasFormatsTable(key)) {
748
751
  return { status: false, msg: 'Table name should be alphanumeric.' }
749
752
  }
750
753
 
@@ -755,7 +758,7 @@ export default class SASjs {
755
758
  }
756
759
  }
757
760
 
758
- if (this.getType(data[key]) !== 'Array') {
761
+ if (this.getType(data[key]) !== 'Array' && !isSasFormatsTable(key)) {
759
762
  return {
760
763
  status: false,
761
764
  msg: 'Parameter data contains invalid table structure.'
@@ -771,6 +774,7 @@ export default class SASjs {
771
774
  }
772
775
  }
773
776
  }
777
+
774
778
  return { status: true, msg: '' }
775
779
  }
776
780
 
@@ -860,8 +864,22 @@ export default class SASjs {
860
864
  )
861
865
  }
862
866
 
863
- public async deployToSASjs(members: [FolderMember, ServiceMember]) {
864
- return await this.sasJSApiClient?.deploy(members, this.sasjsConfig.appLoc)
867
+ /**
868
+ * Creates the folders and services at the given location `appLoc` on the given server `serverUrl`.
869
+ * @param members - the JSON specifying the folders and services to be created.
870
+ * @param appLoc - the base folder in which to create the new folders and
871
+ * services. If not provided, is taken from SASjsConfig.
872
+ * @param authConfig - a valid client, secret, refresh and access tokens that are authorised to execute compute jobs.
873
+ */
874
+ public async deployToSASjs(
875
+ members: FileTree,
876
+ appLoc?: string,
877
+ authConfig?: AuthConfig
878
+ ) {
879
+ if (!appLoc) {
880
+ appLoc = this.sasjsConfig.appLoc
881
+ }
882
+ return await this.sasJSApiClient?.deploy(members, appLoc, authConfig)
865
883
  }
866
884
 
867
885
  public async executeJobSASjs(query: ExecutionQuery) {
@@ -1,8 +1,11 @@
1
- import { FolderMember, ServiceMember, ExecutionQuery } from './types'
1
+ import { AuthConfig, ServerType } from '@sasjs/utils/types'
2
+ import { FileTree, ExecutionQuery } from './types'
2
3
  import { RequestClient } from './request/RequestClient'
3
4
  import { getAccessTokenForSasjs } from './auth/getAccessTokenForSasjs'
4
5
  import { refreshTokensForSasjs } from './auth/refreshTokensForSasjs'
5
6
  import { getAuthCodeForSasjs } from './auth/getAuthCodeForSasjs'
7
+ import { parseWeboutResponse } from './utils'
8
+ import { getTokens } from './auth/getTokens'
6
9
 
7
10
  export class SASjsApiClient {
8
11
  constructor(
@@ -14,7 +17,19 @@ export class SASjsApiClient {
14
17
  if (serverUrl) this.serverUrl = serverUrl
15
18
  }
16
19
 
17
- public async deploy(members: [FolderMember, ServiceMember], appLoc: string) {
20
+ public async deploy(
21
+ members: FileTree,
22
+ appLoc: string,
23
+ authConfig?: AuthConfig
24
+ ) {
25
+ let access_token = (authConfig || {}).access_token
26
+ if (authConfig) {
27
+ ;({ access_token } = await getTokens(
28
+ this.requestClient,
29
+ authConfig,
30
+ ServerType.Sasjs
31
+ ))
32
+ }
18
33
  const { result } = await this.requestClient.post<{
19
34
  status: string
20
35
  message: string
@@ -22,7 +37,7 @@ export class SASjsApiClient {
22
37
  }>(
23
38
  'SASjsApi/drive/deploy',
24
39
  { fileTree: members, appLoc: appLoc },
25
- undefined
40
+ access_token
26
41
  )
27
42
 
28
43
  return Promise.resolve(result)
@@ -35,8 +50,13 @@ export class SASjsApiClient {
35
50
  log?: string
36
51
  logPath?: string
37
52
  error?: {}
53
+ _webout?: string
38
54
  }>('SASjsApi/stp/execute', query, undefined)
39
55
 
56
+ if (Object.keys(result).includes('_webout')) {
57
+ result._webout = parseWeboutResponse(result._webout!)
58
+ }
59
+
40
60
  return Promise.resolve(result)
41
61
  }
42
62
 
@@ -272,7 +272,13 @@ export async function executeScript(
272
272
 
273
273
  return { result: jobResult?.result, log }
274
274
  } catch (e) {
275
- if (e && e.status === 404) {
275
+ interface HttpError {
276
+ status: number
277
+ }
278
+
279
+ const error = e as HttpError
280
+
281
+ if (error.status === 404) {
276
282
  return executeScript(
277
283
  requestClient,
278
284
  sessionManager,
@@ -287,7 +293,7 @@ export async function executeScript(
287
293
  true
288
294
  )
289
295
  } else {
290
- throw prefixMessage(e, 'Error while executing script. ')
296
+ throw prefixMessage(e as Error, 'Error while executing script. ')
291
297
  }
292
298
  }
293
299
  }
@@ -3,13 +3,9 @@ import { WriteStream } from '../../types'
3
3
  export const writeStream = async (
4
4
  stream: WriteStream,
5
5
  content: string
6
- ): Promise<void> => {
7
- return new Promise((resolve, reject) => {
8
- stream.write(content + '\n', (e) => {
9
- if (e) {
10
- return reject(e)
11
- }
12
- return resolve()
13
- })
6
+ ): Promise<void> =>
7
+ stream.write(content + '\n', (e) => {
8
+ if (e) return Promise.reject(e)
9
+
10
+ return Promise.resolve()
14
11
  })
15
- }
@@ -1,6 +1,6 @@
1
1
  import * as https from 'https'
2
2
  import { AxiosRequestConfig } from 'axios'
3
- import axiosCookieJarSupport from 'axios-cookiejar-support'
3
+ import * as axiosCookieJarSupport from 'axios-cookiejar-support'
4
4
  import * as tough from 'tough-cookie'
5
5
  import { prefixMessage } from '@sasjs/utils/error'
6
6
  import { RequestClient, throwIfError } from './RequestClient'
@@ -17,7 +17,7 @@ export class Sas9RequestClient extends RequestClient {
17
17
  status >= 200 && status < 303
18
18
 
19
19
  if (axiosCookieJarSupport) {
20
- axiosCookieJarSupport(this.httpClient)
20
+ axiosCookieJarSupport.wrapper(this.httpClient)
21
21
  this.httpClient.defaults.jar = new tough.CookieJar()
22
22
  }
23
23
  }
@@ -0,0 +1 @@
1
+ import 'jest-extended'
@@ -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 = (data: any) => {
6
- const replacer = (key: any, value: any) => (value === null ? '' : value)
7
- const headerFields = Object.keys(data[0])
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 headers = headerFields.map((field) => {
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
- let byteSize
15
+ if (formats) {
16
+ headers = Object.keys(formats).map((key) => `${key}:${formats![key]}`)
17
+ }
38
18
 
39
- if (typeof row[field] === 'string') {
40
- byteSize = getByteSize(row[field])
41
- }
19
+ const headerFields = Object.keys(data[0])
42
20
 
43
- return byteSize
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
- return `${field}:${firstFoundType === 'chars' ? '$char' : ''}${
57
- longestValueForField
58
- ? longestValueForField
59
- : firstFoundType === 'chars'
60
- ? '1'
61
- : 'best'
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 (invalidString) {
66
- return 'ERROR: LARGE STRING LENGTH'
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
@@ -10,6 +10,7 @@ export const parseWeboutResponse = (response: string, url?: string): string => {
10
10
  .split('>>weboutEND<<')[0]
11
11
  } catch (e) {
12
12
  if (url) throw new WeboutResponseError(url)
13
+
13
14
  sasResponse = ''
14
15
  console.error(e)
15
16
  }
@@ -0,0 +1 @@
1
+ export {};
package/tsconfig.json CHANGED
@@ -6,7 +6,8 @@
6
6
  "declaration": true,
7
7
  "outDir": "./build",
8
8
  "strict": true,
9
- "sourceMap": true
9
+ "sourceMap": true,
10
+ "typeRoots": ["./node_modules/@types", "./src/types/system"]
10
11
  },
11
12
  "include": ["src"],
12
13
  "exclude": ["node_modules"]
@@ -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) => string;
5
+ export declare const convertToCSV: (data: any, sasFormats?: {
6
+ formats: {
7
+ [key: string]: string;
8
+ };
9
+ } | undefined) => string;
@@ -1,3 +1,2 @@
1
- /// <reference types="node" />
2
1
  import * as https from 'https';
3
2
  export declare const createAxiosInstance: (baseURL: string, httpsAgent?: https.Agent | undefined) => import("axios").AxiosInstance;