1
0
mirror of https://github.com/sasjs/adapter.git synced 2026-01-02 18:20:06 +00:00

Compare commits

...

16 Commits

Author SHA1 Message Date
Allan Bowe
cc28dfe7f0 fix: tidying up logic and adding descriptive comments 2022-02-21 19:40:24 +00:00
2ec2147245 chore: add detailed comment 2022-02-21 23:22:57 +05:00
9ca4732f76 fix: call sasJsJobExecutor only from cli 2022-02-21 23:08:55 +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
Yury Shkoda
9b239abda0 test(generateFileUploadForm): add unit test 2022-02-17 13:34:13 +03:00
Yury Shkoda
d3dff44918 fix(special-missings): fix convertToCSV format object 2022-02-16 17:13:00 +03:00
11 changed files with 139 additions and 27 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).

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

@@ -79,7 +79,8 @@ export default class SASjs {
}
/**
* Executes the sas code against SAS9 server
* 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.
@@ -99,7 +100,7 @@ export default class SASjs {
}
/**
* Executes the sas code against SASViya server
* 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.
@@ -643,7 +644,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.
@@ -652,7 +654,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.
@@ -682,18 +685,40 @@ 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.Sasjs) {
return await this.sasJsJobExecutor!.execute(
sasJob,
data,
config,
loginRequiredCallback,
authConfig,
extraResponseAttributes
)
/**
* When sending the JSON data object to SAS, it is first converted to
* a set of specially formatted CSVs. These are passed as multi-part
* form data (converted to input files in the backend SAS session by the
* API). When running outside of a browser, the FormData object is not
* available. So in this case, a slightly different executor is invoked,
* which is similar to the sas9JobExecutor.
*/
if (typeof FormData === 'undefined') {
// cli invocation
return await this.sasJsJobExecutor!.execute(
sasJob,
data,
config,
loginRequiredCallback,
authConfig,
extraResponseAttributes
)
} else {
// web invocation
return await this.webJobExecutor!.execute(
sasJob,
data,
config,
loginRequiredCallback,
authConfig,
extraResponseAttributes
)
}
} else if (
config.serverType !== ServerType.Sas9 &&
config.serverType === ServerType.SasViya &&
config.useComputeApi !== undefined &&
config.useComputeApi !== null
) {
@@ -1034,7 +1059,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

@@ -5,8 +5,11 @@ export const generateFileUploadForm = (
data: any
): FormData => {
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

@@ -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

@@ -52,17 +52,25 @@ export class SasJsJobExecutor extends BaseJobExecutor {
authConfig?.access_token
)
.then(async (res: any) => {
const parsedSasjsServerLog = res.result.log
.map((logLine: any) => logLine.line)
.join('\n')
const resObj = {
result: res.result._webout,
log: res.result.log
log: parsedSasjsServerLog
}
this.requestClient!.appendRequest(resObj, sasJob, config.debug)
let jsonResponse = res.result
if (config.debug) {
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)
}
} else {
jsonResponse = getValidJson(res.result._webout)
}

View File

@@ -108,6 +108,8 @@ export class WebJobExecutor extends BaseJobExecutor {
...this.getRequestParams(config)
}
// FormData is only valid in browser
// FormData is a part of JS web API (not included in native NodeJS).
let formData = new FormData()
if (data) {
@@ -146,11 +148,16 @@ export class WebJobExecutor extends BaseJobExecutor {
const requestPromise = new Promise((resolve, reject) => {
this.requestClient!.post(apiUrl, formData, authConfig?.access_token)
.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 +180,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

@@ -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)
}
}

View File

@@ -60,7 +60,7 @@ 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`
*/
@@ -69,6 +69,11 @@ export class SASjsConfig {
* 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