1
0
mirror of https://github.com/sasjs/adapter.git synced 2025-12-11 09:24:35 +00:00

Compare commits

...

25 Commits

Author SHA1 Message Date
Allan Bowe
467493dcd1 fix: bump sasjs/lint with optional type 2022-02-23 08:47:13 +00:00
Allan Bowe
0170e67b13 fix: utilise utils types for HttpsAgentOptions 2022-02-22 21:17:06 +00:00
Muhammad Saad
92434e48ad Merge pull request #655 from sasjs/node-form-data-fix
fix: not to use NodeFormData function on web
2022-02-22 17:20:00 +05:00
Saad Jutt
d3c91e143a fix: parse empty string in res to empty {} 2022-02-22 17:09:47 +05:00
Saad Jutt
872e73b5f0 fix: not to use NodeFormData function on web 2022-02-22 17:06:27 +05:00
Allan Bowe
af4ad3a7af Merge pull request #654 from sasjs/axios-setting-for-larger-deploy-to-sasjs-server
fix: added additional params to axios POST while deploy to SASJS server
2022-02-22 13:21:12 +02:00
Saad Jutt
1ff67ed93c fix: added additional params to axios POST while deploy to SASJS server 2022-02-22 16:09:15 +05:00
Allan Bowe
d2739d1791 Merge pull request #653 from sasjs/form-data-fix
fix: reverted sasjsJobExecutor no need for that
2022-02-22 11:15:16 +02:00
Allan Bowe
487cb489f3 fix: comments and logic tidy up 2022-02-22 08:38:25 +00:00
Saad Jutt
d9cb2db61f fix: reverted sasjsJobExecutor no need for that 2022-02-22 06:53:29 +05:00
Muhammad Saad
35f37ac796 Merge pull request #650 from sasjs/sasjs-server-webout
fix: no need to parse if webout is an object
2022-02-21 15:54:12 +04:00
Saad Jutt
d7ad0288b9 fix: no need to parse if webout is an object 2022-02-21 16:45:41 +05:00
Allan Bowe
9c98cabe6c Merge pull request #649 from sasjs/sasjs-server-log-parsing
fix: adopted new log structure of SASJS server
2022-02-20 23:27:36 +02:00
Saad Jutt
a6f6897543 fix: adopted new log structure of SASJS server 2022-02-20 20:50:38 +05:00
Allan Bowe
2ea3925977 Merge pull request #647 from sasjs/issue-623
feat(sasjs-config): add optional attribute requestHistoryLimit
2022-02-18 16:25:49 +02:00
489df78391 chore: lint fix 2022-02-18 15:30:24 +05:00
Allan Bowe
842c7f9b23 chore: adding docs for requestHistoryLimit 2022-02-17 20:46:09 +00:00
fe3f6d6287 feat(sasjs-config): add optional attribute requestHistoryLimit 2022-02-17 23:39:55 +05:00
munja
9728ebd98d chore: reducing dependabot limit to 2 per month 2022-02-17 11:07:03 +00:00
Allan Bowe
8e116d81d7 Merge pull request #641 from sasjs/issue-624
fix(special-missing): fix convertToCSV format object
2022-02-17 13:05:24 +02:00
Yury Shkoda
fbca91144d test(generateFileUploadForm): add test data 2022-02-17 13:49:21 +03:00
Allan Bowe
c392390147 Merge pull request #642 from sasjs/sasjs-server-job-executor
feat: execute job on SASJS server
2022-02-17 12:48:41 +02:00
Yury Shkoda
9b239abda0 test(generateFileUploadForm): add unit test 2022-02-17 13:34:13 +03:00
Saad Jutt
bfc534da15 feat: execute job on SASJS server 2022-02-17 02:01:19 +05:00
Yury Shkoda
d3dff44918 fix(special-missings): fix convertToCSV format object 2022-02-16 17:13:00 +03:00
16 changed files with 304 additions and 110 deletions

View File

@@ -4,4 +4,4 @@ updates:
directory: '/'
schedule:
interval: monthly
open-pull-requests-limit: 10
open-pull-requests-limit: 2

View File

@@ -254,6 +254,7 @@ Configuration on the client side involves passing an object on startup, which ca
* `LoginMechanism` - either `Default` or `Redirected`. If `Redirected` then authentication occurs through the injection of an additional screen, which contains the SASLogon prompt. This allows for more complex authentication flows (such as 2FA) and avoids the need to handle passwords in the application itself. The styling of the redirect flow can also be modified. If left at "Default" then the developer must capture the username and password and use these with the `.login()` method.
* `useComputeApi` - Only relevant when the serverType is `SASVIYA`. If `true` the [Compute API](#using-the-compute-api) is used. If `false` the [JES API](#using-the-jes-api) is used. If `null` or `undefined` the [Web](#using-jes-web-app) approach is used.
* `contextName` - Compute context on which the requests will be called. If missing or not provided, defaults to `Job Execution Compute context`.
* `requestHistoryLimit` - Request history limit. Increasing this limit may affect browser performance, especially with debug (logs) enabled. Default is 10.
The adapter supports a number of approaches for interfacing with Viya (`serverType` is `SASVIYA`). For maximum performance, be sure to [configure your compute context](https://sasjs.io/guide-viya/#shared-account-and-server-re-use) with `reuseServerProcesses` as `true` and a system account in `runServerAs`. This functionality is available since Viya 3.5. This configuration is supported when [creating contexts using the CLI](https://sasjs.io/sasjs-cli-context/#sasjs-context-create).

160
package-lock.json generated
View File

@@ -8,7 +8,7 @@
"hasInstallScript": true,
"license": "ISC",
"dependencies": {
"@sasjs/utils": "2.35.0",
"@sasjs/utils": "^2.36.1",
"axios": "0.26.0",
"axios-cookiejar-support": "1.0.1",
"form-data": "4.0.0",
@@ -1143,23 +1143,50 @@
}
},
"node_modules/@sasjs/utils": {
"version": "2.35.0",
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.35.0.tgz",
"integrity": "sha512-q9ZKV+TXqwiaj+0z5U7/00eBpp2QpjKfC9BKx7A6rQjBl10WtoWd5C9Em+RQULWVEdRbVS2XcnNsWelbKq/Zsw==",
"version": "2.36.1",
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.36.1.tgz",
"integrity": "sha512-JkGUpLOODsvkeU+S25jb9k2WnvzyD2w6cEk7YyQ/byuqKL8xawH91PPWegrVcJlDY8WmqKE4CPcA3d1mM3B3LA==",
"hasInstallScript": true,
"dependencies": {
"@types/fs-extra": "^9.0.13",
"@types/prompts": "^2.0.13",
"chalk": "^4.1.1",
"cli-table": "^0.3.6",
"consola": "^2.15.0",
"csv-stringify": "^5.6.5",
"@types/fs-extra": "9.0.13",
"@types/prompts": "2.0.13",
"chalk": "4.1.1",
"cli-table": "0.3.6",
"consola": "2.15.0",
"csv-stringify": "5.6.5",
"find": "0.3.0",
"fs-extra": "^10.0.0",
"jwt-decode": "^3.1.2",
"prompts": "^2.4.1",
"rimraf": "^3.0.2",
"valid-url": "^1.0.9"
"fs-extra": "10.0.0",
"jwt-decode": "3.1.2",
"prompts": "2.4.1",
"rimraf": "3.0.2",
"valid-url": "1.0.9"
}
},
"node_modules/@sasjs/utils/node_modules/chalk": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
"integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/@sasjs/utils/node_modules/prompts": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.1.tgz",
"integrity": "sha512-EQyfIuO2hPDsX1L/blblV+H7I0knhgAd82cVneCwcdND9B8AuCDuRcBH6yIcG4dFzlOUqbazQqwGjx5xmsNLuQ==",
"dependencies": {
"kleur": "^3.0.3",
"sisteransi": "^1.0.5"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/@semantic-release/commit-analyzer": {
@@ -1644,9 +1671,9 @@
"dev": true
},
"node_modules/@types/prompts": {
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/@types/prompts/-/prompts-2.0.14.tgz",
"integrity": "sha512-HZBd99fKxRWpYCErtm2/yxUZv6/PBI9J7N4TNFffl5JbrYMHBwF25DjQGTW3b3jmXq+9P6/8fCIb2ee57BFfYA==",
"version": "2.0.13",
"resolved": "https://registry.npmjs.org/@types/prompts/-/prompts-2.0.13.tgz",
"integrity": "sha512-jwMOIGy49VruR/gYehhJYgpVzB+EVpEE7t7j9m1oTo4HMpOe7KmsyqdBuoxAzA5B4caUgx0cKrWr7wUEqMXJ7Q==",
"dependencies": {
"@types/node": "*"
}
@@ -2068,6 +2095,7 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"engines": {
"node": ">=8"
}
@@ -2692,6 +2720,7 @@
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
@@ -2762,12 +2791,14 @@
}
},
"node_modules/cli-table": {
"version": "0.3.9",
"resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.9.tgz",
"integrity": "sha512-7eA6hFtAZwVx3dWAGoaBqTrzWko5jRUFKpHT64ZHkJpaA3y5wf5NlLjguqTRmqycatJZiwftODYYyGNLbQ7MuA==",
"version": "0.3.6",
"resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.6.tgz",
"integrity": "sha512-ZkNZbnZjKERTY5NwC2SeMeLeifSPq/pubeRoTpdr3WchLlnZg6hEgvHkK5zL7KNFdd9PmHN8lxrENUwI3cE8vQ==",
"dependencies": {
"colors": "1.0.3",
"strip-ansi": "^6.0.1"
"colors": "1.0.3"
},
"engines": {
"node": ">= 0.2.0"
}
},
"node_modules/cli-table3": {
@@ -2912,9 +2943,9 @@
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"node_modules/consola": {
"version": "2.15.3",
"resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz",
"integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw=="
"version": "2.15.0",
"resolved": "https://registry.npmjs.org/consola/-/consola-2.15.0.tgz",
"integrity": "sha512-vlcSGgdYS26mPf7qNi+dCisbhiyDnrN1zaRbw3CSuc2wGOMEGGPsp46PdRG5gqXwgtJfjxDkxRNAgRPr1B77vQ=="
},
"node_modules/console-browserify": {
"version": "1.2.0",
@@ -10539,6 +10570,7 @@
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
"integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==",
"dev": true,
"dependencies": {
"kleur": "^3.0.3",
"sisteransi": "^1.0.5"
@@ -11685,6 +11717,7 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
@@ -14018,22 +14051,42 @@
}
},
"@sasjs/utils": {
"version": "2.35.0",
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.35.0.tgz",
"integrity": "sha512-q9ZKV+TXqwiaj+0z5U7/00eBpp2QpjKfC9BKx7A6rQjBl10WtoWd5C9Em+RQULWVEdRbVS2XcnNsWelbKq/Zsw==",
"version": "2.36.1",
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.36.1.tgz",
"integrity": "sha512-JkGUpLOODsvkeU+S25jb9k2WnvzyD2w6cEk7YyQ/byuqKL8xawH91PPWegrVcJlDY8WmqKE4CPcA3d1mM3B3LA==",
"requires": {
"@types/fs-extra": "^9.0.13",
"@types/prompts": "^2.0.13",
"chalk": "^4.1.1",
"cli-table": "^0.3.6",
"consola": "^2.15.0",
"csv-stringify": "^5.6.5",
"@types/fs-extra": "9.0.13",
"@types/prompts": "2.0.13",
"chalk": "4.1.1",
"cli-table": "0.3.6",
"consola": "2.15.0",
"csv-stringify": "5.6.5",
"find": "0.3.0",
"fs-extra": "^10.0.0",
"jwt-decode": "^3.1.2",
"prompts": "^2.4.1",
"rimraf": "^3.0.2",
"valid-url": "^1.0.9"
"fs-extra": "10.0.0",
"jwt-decode": "3.1.2",
"prompts": "2.4.1",
"rimraf": "3.0.2",
"valid-url": "1.0.9"
},
"dependencies": {
"chalk": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
"integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"prompts": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.1.tgz",
"integrity": "sha512-EQyfIuO2hPDsX1L/blblV+H7I0knhgAd82cVneCwcdND9B8AuCDuRcBH6yIcG4dFzlOUqbazQqwGjx5xmsNLuQ==",
"requires": {
"kleur": "^3.0.3",
"sisteransi": "^1.0.5"
}
}
}
},
"@semantic-release/commit-analyzer": {
@@ -14458,9 +14511,9 @@
"dev": true
},
"@types/prompts": {
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/@types/prompts/-/prompts-2.0.14.tgz",
"integrity": "sha512-HZBd99fKxRWpYCErtm2/yxUZv6/PBI9J7N4TNFffl5JbrYMHBwF25DjQGTW3b3jmXq+9P6/8fCIb2ee57BFfYA==",
"version": "2.0.13",
"resolved": "https://registry.npmjs.org/@types/prompts/-/prompts-2.0.13.tgz",
"integrity": "sha512-jwMOIGy49VruR/gYehhJYgpVzB+EVpEE7t7j9m1oTo4HMpOe7KmsyqdBuoxAzA5B4caUgx0cKrWr7wUEqMXJ7Q==",
"requires": {
"@types/node": "*"
}
@@ -14828,7 +14881,8 @@
"ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true
},
"ansi-styles": {
"version": "4.3.0",
@@ -15312,6 +15366,7 @@
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
@@ -15364,12 +15419,11 @@
"dev": true
},
"cli-table": {
"version": "0.3.9",
"resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.9.tgz",
"integrity": "sha512-7eA6hFtAZwVx3dWAGoaBqTrzWko5jRUFKpHT64ZHkJpaA3y5wf5NlLjguqTRmqycatJZiwftODYYyGNLbQ7MuA==",
"version": "0.3.6",
"resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.6.tgz",
"integrity": "sha512-ZkNZbnZjKERTY5NwC2SeMeLeifSPq/pubeRoTpdr3WchLlnZg6hEgvHkK5zL7KNFdd9PmHN8lxrENUwI3cE8vQ==",
"requires": {
"colors": "1.0.3",
"strip-ansi": "^6.0.1"
"colors": "1.0.3"
}
},
"cli-table3": {
@@ -15491,9 +15545,9 @@
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"consola": {
"version": "2.15.3",
"resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz",
"integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw=="
"version": "2.15.0",
"resolved": "https://registry.npmjs.org/consola/-/consola-2.15.0.tgz",
"integrity": "sha512-vlcSGgdYS26mPf7qNi+dCisbhiyDnrN1zaRbw3CSuc2wGOMEGGPsp46PdRG5gqXwgtJfjxDkxRNAgRPr1B77vQ=="
},
"console-browserify": {
"version": "1.2.0",
@@ -21233,6 +21287,7 @@
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
"integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==",
"dev": true,
"requires": {
"kleur": "^3.0.3",
"sisteransi": "^1.0.5"
@@ -22135,6 +22190,7 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"requires": {
"ansi-regex": "^5.0.1"
}

View File

@@ -73,7 +73,7 @@
},
"main": "index.js",
"dependencies": {
"@sasjs/utils": "2.35.0",
"@sasjs/utils": "2.36.1",
"axios": "0.26.0",
"axios-cookiejar-support": "1.0.1",
"form-data": "4.0.0",

View File

@@ -80,7 +80,7 @@ const errorAndCsrfData: any = {
}
const testTable = 'sometable'
const testTableWithNullVars: { [key: string]: any } = {
export const testTableWithNullVars: { [key: string]: any } = {
[testTable]: [
{ var1: 'string', var2: 232, nullvar: 'A' },
{ var1: 'string', var2: 232, nullvar: 'B' },

View File

@@ -76,6 +76,13 @@ export default class SASjs {
return this.requestClient?.getCsrfToken(type)
}
/**
* Executes code against a SAS 9 server. Requires a runner to be present in
* the users home directory in metadata.
* @param linesOfCode - lines of sas code from the file to run.
* @param username - a string representing the username.
* @param password - a string representing the password.
*/
public async executeScriptSAS9(
linesOfCode: string[],
userName: string,
@@ -90,6 +97,38 @@ export default class SASjs {
)
}
/**
* Executes sas code in a SAS Viya compute session.
* @param fileName - name of the file to run. It will be converted to path to the file being submitted for execution.
* @param linesOfCode - lines of sas code from the file to run.
* @param contextName - context name on which code will be run on the server.
* @param authConfig - (optional) the access token, refresh token, client and secret for authorizing the request.
* @param debug - (optional) if true, global debug config will be overriden
*/
public async executeScriptSASViya(
fileName: string,
linesOfCode: string[],
contextName: string,
authConfig?: AuthConfig,
debug?: boolean
) {
this.isMethodSupported('executeScriptSASViya', [ServerType.SasViya])
if (!contextName) {
throw new Error(
'Context name is undefined. Please set a `contextName` in your SASjs or override config.'
)
}
return await this.sasViyaApiClient!.executeScript(
fileName,
linesOfCode,
contextName,
authConfig,
null,
debug ? debug : this.sasjsConfig.debug
)
}
/**
* Gets compute contexts.
* @param accessToken - an access token for an authorized user.
@@ -253,38 +292,6 @@ export default class SASjs {
return await this.sasViyaApiClient!.createSession(contextName, accessToken)
}
/**
* Executes the sas code against given sas server
* @param fileName - name of the file to run. It will be converted to path to the file being submitted for execution.
* @param linesOfCode - lines of sas code from the file to run.
* @param contextName - context name on which code will be run on the server.
* @param authConfig - (optional) the access token, refresh token, client and secret for authorizing the request.
* @param debug - (optional) if true, global debug config will be overriden
*/
public async executeScriptSASViya(
fileName: string,
linesOfCode: string[],
contextName: string,
authConfig?: AuthConfig,
debug?: boolean
) {
this.isMethodSupported('executeScriptSASViya', [ServerType.SasViya])
if (!contextName) {
throw new Error(
'Context name is undefined. Please set a `contextName` in your SASjs or override config.'
)
}
return await this.sasViyaApiClient!.executeScript(
fileName,
linesOfCode,
contextName,
authConfig,
null,
debug ? debug : this.sasjsConfig.debug
)
}
/**
* Creates a folder in the logical SAS folder tree
* @param folderName - name of the folder to be created.
@@ -635,7 +642,8 @@ export default class SASjs {
}
/**
* Makes a request to the SAS Service specified in `SASjob`. The response
* Makes a request to program specified in `SASjob` (could be a Viya Job, a
* SAS 9 Stored Process, or a SASjs Server Stored Program). The response
* object will always contain table names in lowercase, and column names in
* uppercase. Values are returned formatted by default, unformatted
* values can be configured as an option in the `%webout` macro.
@@ -644,7 +652,8 @@ export default class SASjs {
* the SAS `_program` parameter to run a Job Definition or SAS 9 Stored
* Process). Is prepended at runtime with the value of `appLoc`.
* @param data - a JSON object containing one or more tables to be sent to
* SAS. Can be `null` if no inputs required.
* SAS. For an example of the table structure, see the project README. This
* value can be `null` if no inputs are required.
* @param config - provide any changes to the config here, for instance to
* enable/disable `debug`. Any change provided will override the global config,
* for that particular function call.
@@ -674,9 +683,19 @@ export default class SASjs {
const validationResult = this.validateInput(data)
// status is true if the data passes validation checks above
if (validationResult.status) {
if (
config.serverType !== ServerType.Sas9 &&
if (config.serverType === ServerType.Sasjs) {
return await this.webJobExecutor!.execute(
sasJob,
data,
config,
loginRequiredCallback,
authConfig,
extraResponseAttributes
)
} else if (
config.serverType === ServerType.SasViya &&
config.useComputeApi !== undefined &&
config.useComputeApi !== null
) {
@@ -1017,7 +1036,8 @@ export default class SASjs {
: RequestClient
this.requestClient = new RequestClientClass(
this.sasjsConfig.serverUrl,
this.sasjsConfig.httpsAgentOptions
this.sasjsConfig.httpsAgentOptions,
this.sasjsConfig.requestHistoryLimit
)
} else {
this.requestClient.setConfig(

View File

@@ -37,7 +37,10 @@ export class SASjsApiClient {
}>(
'SASjsApi/drive/deploy',
{ fileTree: members, appLoc: appLoc },
access_token
access_token,
undefined,
{},
{ maxContentLength: Infinity, maxBodyLength: Infinity }
)
return Promise.resolve(result)

View File

@@ -1,12 +1,25 @@
import * as NodeFormData from 'form-data'
import { convertToCSV } from '../utils/convertToCsv'
/**
* One of the approaches SASjs takes to send tables-formatted JSON (see README)
* to SAS is as multipart form data, where each table is provided as a specially
* formatted CSV file.
* @param formData Different objects are used depending on whether the adapter is
* running in the browser, or in the CLI
* @param data Special, tables-formatted JSON (see README)
* @returns Populated formData
*/
export const generateFileUploadForm = (
formData: FormData,
formData: FormData | NodeFormData,
data: any
): FormData => {
): FormData | NodeFormData => {
for (const tableName in data) {
if (!Array.isArray(data[tableName])) continue
const name = tableName
const csv = convertToCSV(data[tableName])
if (csv === 'ERROR: LARGE STRING LENGTH') {
throw new Error(
'The max length of a string value in SASjs is 32765 characters.'

View File

@@ -1,7 +1,11 @@
import * as NodeFormData from 'form-data'
import { convertToCSV } from '../utils/convertToCsv'
import { splitChunks } from '../utils/splitChunks'
export const generateTableUploadForm = (formData: FormData, data: any) => {
export const generateTableUploadForm = (
formData: FormData | NodeFormData,
data: any
) => {
const sasjsTables = []
const requestParams: any = {}
let tableCounter = 0

View File

@@ -0,0 +1,55 @@
import { generateFileUploadForm } from '../generateFileUploadForm'
describe('generateFileUploadForm', () => {
beforeAll(() => {
function FormDataMock(this: any) {
this.append = () => {}
}
const BlobMock = jest.fn()
;(global as any).FormData = FormDataMock
;(global as any).Blob = BlobMock
})
it('should generate file upload form from data', () => {
const formData = new FormData()
const testTable = 'sometable'
const testTableWithNullVars: { [key: string]: any } = {
[testTable]: [
{ var1: 'string', var2: 232, nullvar: 'A' },
{ var1: 'string', var2: 232, nullvar: 'B' },
{ var1: 'string', var2: 232, nullvar: '_' },
{ var1: 'string', var2: 232, nullvar: 0 },
{ var1: 'string', var2: 232, nullvar: 'z' },
{ var1: 'string', var2: 232, nullvar: null }
],
[`$${testTable}`]: { formats: { var1: '$char12.', nullvar: 'best.' } }
}
const tableName = Object.keys(testTableWithNullVars).filter((key: string) =>
Array.isArray(testTableWithNullVars[key])
)[0]
jest.spyOn(formData, 'append').mockImplementation(() => {})
generateFileUploadForm(formData, testTableWithNullVars)
expect(formData.append).toHaveBeenCalledOnce()
expect(formData.append).toHaveBeenCalledWith(
tableName,
{},
`${tableName}.csv`
)
})
it('should throw an error if too large string was provided', () => {
const formData = new FormData()
const data = { testTable: [{ var1: 'z'.repeat(32765 + 1) }] }
expect(() => generateFileUploadForm(formData, data)).toThrow(
new Error(
'The max length of a string value in SASjs is 32765 characters.'
)
)
})
})

View File

@@ -1,3 +1,4 @@
import * as NodeFormData from 'form-data'
import {
AuthConfig,
ExtraResponseAttributes,
@@ -108,7 +109,12 @@ export class WebJobExecutor extends BaseJobExecutor {
...this.getRequestParams(config)
}
let formData = new FormData()
/**
* Use the available form data object (FormData in Browser, NodeFormData in
* Node)
*/
let formData =
typeof FormData === 'undefined' ? new NodeFormData() : new FormData()
if (data) {
const stringifiedData = JSON.stringify(data)
@@ -143,14 +149,30 @@ export class WebJobExecutor extends BaseJobExecutor {
}
}
/* The NodeFormData object does not set the request header - so, set it */
const contentType =
formData instanceof NodeFormData && typeof FormData === 'undefined'
? `multipart/form-data; boundary=${formData.getBoundary()}`
: undefined
const requestPromise = new Promise((resolve, reject) => {
this.requestClient!.post(apiUrl, formData, authConfig?.access_token)
this.requestClient!.post(
apiUrl,
formData,
authConfig?.access_token,
contentType
)
.then(async (res: any) => {
const parsedSasjsServerLog =
this.serverType === ServerType.Sasjs
? res.result.log.map((logLine: any) => logLine.line).join('\n')
: res.result.log
const resObj =
this.serverType === ServerType.Sasjs
? {
result: res.result._webout,
log: res.result.log
log: parsedSasjsServerLog
}
: res
this.requestClient!.appendRequest(resObj, sasJob, config.debug)
@@ -173,8 +195,12 @@ export class WebJobExecutor extends BaseJobExecutor {
: res.result
break
case ServerType.Sasjs:
const webout = parseWeboutResponse(res.result._webout, apiUrl)
jsonResponse = getValidJson(webout)
if (typeof res.result._webout === 'object') {
jsonResponse = res.result._webout
} else {
const webout = parseWeboutResponse(res.result._webout, apiUrl)
jsonResponse = getValidJson(webout)
}
break
}
} else if (this.serverType === ServerType.Sasjs) {

View File

@@ -1,6 +1,6 @@
export * from './ComputeJobExecutor'
export * from './FileUploader'
export * from './JesJobExecutor'
export * from './JobExecutor'
export * from './Sas9JobExecutor'
export * from './WebJobExecutor'
export * from './FileUploader'

View File

@@ -56,6 +56,7 @@ export interface HttpClient {
export class RequestClient implements HttpClient {
private requests: SASjsRequest[] = []
private requestsLimit: number = 10
protected csrfToken: CsrfToken = { headerName: '', value: '' }
protected fileUploadCsrfToken: CsrfToken | undefined
@@ -63,9 +64,11 @@ export class RequestClient implements HttpClient {
constructor(
protected baseUrl: string,
httpsAgentOptions?: https.AgentOptions
httpsAgentOptions?: https.AgentOptions,
requestsLimit?: number
) {
this.createHttpClient(baseUrl, httpsAgentOptions)
if (requestsLimit) this.requestsLimit = requestsLimit
}
public setConfig(baseUrl: string, httpsAgentOptions?: https.AgentOptions) {
@@ -149,7 +152,7 @@ export class RequestClient implements HttpClient {
SASWORK: sasWork
})
if (this.requests.length > 20) {
if (this.requests.length > this.requestsLimit) {
this.requests.splice(0, 1)
}
}
@@ -204,7 +207,8 @@ export class RequestClient implements HttpClient {
data: any,
accessToken: string | undefined,
contentType = 'application/json',
overrideHeaders: { [key: string]: string | number } = {}
overrideHeaders: { [key: string]: string | number } = {},
additionalSettings: { [key: string]: string | number } = {}
): Promise<{ result: T; etag: string }> {
const headers = {
...this.getHeaders(accessToken, contentType),
@@ -212,7 +216,11 @@ export class RequestClient implements HttpClient {
}
return this.httpClient
.post<T>(url, data, { headers, withCredentials: true })
.post<T>(url, data, {
headers,
withCredentials: true,
...additionalSettings
})
.then((response) => {
throwIfError(response)

View File

@@ -1,5 +1,6 @@
import * as https from 'https'
import { ServerType } from '@sasjs/utils/types'
import { HttpsAgentOptions } from '@sasjs/utils/types'
/**
* Specifies the configuration for the SASjs instance - eg where and how to
@@ -60,15 +61,20 @@ export class SASjsConfig {
*/
useComputeApi: boolean | null = null
/**
* Optional settings to configure HTTPS Agent.
* Optional setting to configure HTTPS Agent.
* By providing `key`, `cert`, `ca` to connect with server
* Other options can be set `rejectUnauthorized` and `requestCert`
*/
httpsAgentOptions?: https.AgentOptions
httpsAgentOptions?: HttpsAgentOptions
/**
* Supported login mechanisms are - Redirected and Default
*/
loginMechanism: LoginMechanism = LoginMechanism.Default
/**
* Optional setting to configure request history limit. Increasing this limit
* may affect browser performance, especially with debug (logs) enabled.
*/
requestHistoryLimit?: number = 10
}
export enum LoginMechanism {

View File

@@ -3,7 +3,7 @@
* @param data - the array of JSON objects to convert.
*/
export const convertToCSV = (
data: any,
data: any[],
sasFormats?: { formats: { [key: string]: string } }
) => {
let formats = sasFormats?.formats
@@ -76,7 +76,7 @@ export const convertToCSV = (
return byteSize
}
})
.sort((a: number, b: number) => b - a)[0]
.sort((a: any, b: any) => b - a)[0]
if (longestValueForField && longestValueForField > 32765) {
invalidString = true

View File

@@ -12,6 +12,8 @@ export const getValidJson = (str: string | object): object => {
if (typeof str === 'object') return str
if (str === '') return {}
return JSON.parse(str)
} catch (e) {
if (e instanceof JsonParseArrayError) throw e