mirror of
https://github.com/sasjs/adapter.git
synced 2026-01-04 19:20:05 +00:00
chore(merge): Merge branch 'master' into removed-url-package
This commit is contained in:
@@ -194,6 +194,8 @@ Note - to use the web approach, the `useComputeApi` property must be `undefined`
|
||||
### Using the JES API
|
||||
Here we are running Jobs using the Job Execution Service except this time we are making the requests directly using the REST API instead of through the JES Web App. This is helpful when we need to call web services outside of a browser (eg with the SASjs CLI or other commandline tools). To save one network request, the adapter prefetches the JOB URIs and passes them in the `__job` parameter. Depending on your network bandwidth, it may or may not be faster than the JES Web approach.
|
||||
|
||||
This approach (`useComputeApi: false`) also ensures that jobs are displayed in Environment Manager.
|
||||
|
||||
```
|
||||
{
|
||||
appLoc:"/Your/Path",
|
||||
@@ -206,6 +208,8 @@ Here we are running Jobs using the Job Execution Service except this time we are
|
||||
### Using the Compute API
|
||||
This approach is by far the fastest, as a result of the optimisations we have built into the adapter. With this configuration, in the first sasjs request, we take a URI map of the services in the target folder, and create a session manager. This manager will spawn a additional session every time a request is made. Subsequent requests will use the existing 'hot' session, if it exists. Sessions are always deleted after every use, which actually makes this _less_ resource intensive than a typical JES web app, in which all sessions are kept alive by default for 15 minutes.
|
||||
|
||||
With this approach (`useComputeApi: true`), the requests/logs will _not_ appear in the list in Environment manager.
|
||||
|
||||
```
|
||||
{
|
||||
appLoc:"/Your/Path",
|
||||
|
||||
14770
package-lock.json
generated
14770
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -41,6 +41,19 @@ So you can run the script like so:
|
||||
SSH_ACCOUNT=me@my-sas-server.com DEPLOY_PATH=/var/www/html/my-folder/sasjs-tests npm run deploy
|
||||
```
|
||||
|
||||
If you are on `WINDOWS`, you will first need to install one dependency:
|
||||
```bash
|
||||
npm i -g copyfiles
|
||||
```
|
||||
and then run to build:
|
||||
```bash
|
||||
npm run update:adapter && npm run build
|
||||
```
|
||||
when it finishes run to deploy:
|
||||
```bash
|
||||
scp -rp ./build/* me@my-sas-server.com:/var/www/html/my-folder/sasjs-tests
|
||||
```
|
||||
|
||||
If you'd like to deploy just `sasjs-tests` without changing the adapter version, you can use the `deploy:tests` script, while also setting the same environment variables as above.
|
||||
|
||||
## 3. Creating the required SAS services
|
||||
|
||||
@@ -23,7 +23,8 @@
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"update:adapter": "cd .. && npm run package:lib && cd sasjs-tests && npm i ../build/sasjs-adapter-5.0.0.tgz",
|
||||
"deploy:tests": "rsync -avhe ssh ./build/* --delete $SSH_ACCOUNT:$DEPLOY_PATH",
|
||||
"deploy:tests": "rsync -avhe ssh ./build/* --delete $SSH_ACCOUNT:$DEPLOY_PATH || npm run deploy:tests-win",
|
||||
"deploy:tests-win": "scp %DEPLOY_PATH% ./build/*",
|
||||
"deploy": "npm run update:adapter && npm run build && npm run deploy:tests"
|
||||
},
|
||||
"eslintConfig": {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Session, Context, CsrfToken, SessionVariable } from './types'
|
||||
import { Session, Context, SessionVariable } from './types'
|
||||
import { NoSessionStateError } from './types/errors'
|
||||
import { asyncForEach, isUrl } from './utils'
|
||||
import { prefixMessage } from '@sasjs/utils/error'
|
||||
import { RequestClient } from './request/RequestClient'
|
||||
@@ -173,7 +174,8 @@ export class SessionManager {
|
||||
this.printedSessionState.printed = true
|
||||
}
|
||||
|
||||
const state = await this.getSessionState(
|
||||
const { result: state, responseStatus: responseStatus } =
|
||||
await this.getSessionState(
|
||||
`${this.serverUrl}${stateLink.href}?wait=30`,
|
||||
etag!,
|
||||
accessToken
|
||||
@@ -198,7 +200,14 @@ export class SessionManager {
|
||||
|
||||
resolve(this.waitForSession(session, etag, accessToken))
|
||||
} else {
|
||||
reject('Could not get session state.')
|
||||
reject(
|
||||
new NoSessionStateError(
|
||||
responseStatus,
|
||||
this.serverUrl + stateLink.href,
|
||||
session.links.find((l: any) => l.rel === 'log')
|
||||
?.href as string
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,7 +226,10 @@ export class SessionManager {
|
||||
) {
|
||||
return await this.requestClient
|
||||
.get(url, accessToken, 'text/plain', { 'If-None-Match': etag })
|
||||
.then((res) => res.result as string)
|
||||
.then((res) => ({
|
||||
result: res.result as string,
|
||||
responseStatus: res.status
|
||||
}))
|
||||
.catch((err) => {
|
||||
throw err
|
||||
})
|
||||
|
||||
@@ -84,7 +84,7 @@ export class RequestClient implements HttpClient {
|
||||
contentType: string = 'application/json',
|
||||
overrideHeaders: { [key: string]: string | number } = {},
|
||||
debug: boolean = false
|
||||
): Promise<{ result: T; etag: string }> {
|
||||
): Promise<{ result: T; etag: string; status: number }> {
|
||||
const headers = {
|
||||
...this.getHeaders(accessToken, contentType),
|
||||
...overrideHeaders
|
||||
@@ -429,8 +429,8 @@ export class RequestClient implements HttpClient {
|
||||
throw new Error('Valid JSON could not be extracted from response.')
|
||||
}
|
||||
|
||||
isValidJson(weboutResponse)
|
||||
parsedResponse = JSON.parse(weboutResponse)
|
||||
const jsonResponse = isValidJson(weboutResponse)
|
||||
parsedResponse = jsonResponse
|
||||
} catch {
|
||||
parsedResponse = response.data
|
||||
}
|
||||
@@ -438,9 +438,15 @@ export class RequestClient implements HttpClient {
|
||||
includeSAS9Log = true
|
||||
}
|
||||
|
||||
let responseToReturn: { result: T; etag: any; log?: string } = {
|
||||
let responseToReturn: {
|
||||
result: T
|
||||
etag: any
|
||||
log?: string
|
||||
status: number
|
||||
} = {
|
||||
result: parsedResponse as T,
|
||||
etag
|
||||
etag,
|
||||
status: response.status
|
||||
}
|
||||
|
||||
if (includeSAS9Log) {
|
||||
|
||||
@@ -39,7 +39,7 @@ export class Sas9RequestClient extends RequestClient {
|
||||
contentType: string = 'application/json',
|
||||
overrideHeaders: { [key: string]: string | number } = {},
|
||||
debug: boolean = false
|
||||
): Promise<{ result: T; etag: string }> {
|
||||
): Promise<{ result: T; etag: string; status: number }> {
|
||||
const headers = {
|
||||
...this.getHeaders(accessToken, contentType),
|
||||
...overrideHeaders
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { SessionManager } from '../SessionManager'
|
||||
import * as dotenv from 'dotenv'
|
||||
import { RequestClient } from '../request/RequestClient'
|
||||
import { NoSessionStateError } from '../types/errors'
|
||||
import * as dotenv from 'dotenv'
|
||||
import axios from 'axios'
|
||||
|
||||
jest.mock('axios')
|
||||
const mockedAxios = axios as jest.Mocked<typeof axios>
|
||||
|
||||
@@ -43,4 +45,38 @@ describe('SessionManager', () => {
|
||||
).resolves.toEqual(expectedResponse)
|
||||
})
|
||||
})
|
||||
|
||||
describe('waitForSession', () => {
|
||||
it('should reject with NoSessionStateError if SAS server did not provide session state', async () => {
|
||||
const responseStatus = 304
|
||||
|
||||
mockedAxios.get.mockImplementation(() =>
|
||||
Promise.resolve({ data: '', status: responseStatus })
|
||||
)
|
||||
|
||||
await expect(
|
||||
sessionManager['waitForSession'](
|
||||
{
|
||||
id: 'id',
|
||||
state: '',
|
||||
links: [
|
||||
{ rel: 'state', href: '', uri: '', type: '', method: 'GET' }
|
||||
],
|
||||
attributes: {
|
||||
sessionInactiveTimeout: 0
|
||||
},
|
||||
creationTimeStamp: ''
|
||||
},
|
||||
null,
|
||||
'access_token'
|
||||
)
|
||||
).rejects.toEqual(
|
||||
new NoSessionStateError(
|
||||
responseStatus,
|
||||
process.env.SERVER_URL as string,
|
||||
'logUrl'
|
||||
)
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
31
src/test/utils/isValidJson.spec.ts
Normal file
31
src/test/utils/isValidJson.spec.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { isValidJson } from '../../utils'
|
||||
|
||||
describe('jsonValidator', () => {
|
||||
it('should not throw an error with an valid json', () => {
|
||||
const json = {
|
||||
test: 'test'
|
||||
}
|
||||
|
||||
expect(isValidJson(json)).toBe(json)
|
||||
})
|
||||
|
||||
it('should not throw an error with an valid json string', () => {
|
||||
const json = {
|
||||
test: 'test'
|
||||
}
|
||||
|
||||
expect(isValidJson(JSON.stringify(json))).toStrictEqual(json)
|
||||
})
|
||||
|
||||
it('should throw an error with an invalid json', () => {
|
||||
const json = `{\"test\":\"test\"\"test2\":\"test\"}`
|
||||
|
||||
expect(() => {
|
||||
try {
|
||||
isValidJson(json)
|
||||
} catch (err) {
|
||||
throw new Error()
|
||||
}
|
||||
}).toThrowError
|
||||
})
|
||||
})
|
||||
15
src/types/errors/NoSessionStateError.ts
Normal file
15
src/types/errors/NoSessionStateError.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export class NoSessionStateError extends Error {
|
||||
constructor(
|
||||
public serverResponseStatus: number,
|
||||
public sessionStateUrl: string,
|
||||
public logUrl: string
|
||||
) {
|
||||
super(
|
||||
`Could not get session state. Server responded with ${serverResponseStatus} whilst checking state: ${sessionStateUrl}`
|
||||
)
|
||||
|
||||
this.name = 'NoSessionStatus'
|
||||
|
||||
Object.setPrototypeOf(this, NoSessionStateError.prototype)
|
||||
}
|
||||
}
|
||||
@@ -5,3 +5,4 @@ export * from './JobExecutionError'
|
||||
export * from './LoginRequiredError'
|
||||
export * from './NotFoundError'
|
||||
export * from './ErrorResponse'
|
||||
export * from './NoSessionStateError'
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
* Checks if string is in valid JSON format else throw error.
|
||||
* @param str - string to check.
|
||||
*/
|
||||
export const isValidJson = (str: string) => {
|
||||
export const isValidJson = (str: string | object) => {
|
||||
try {
|
||||
JSON.parse(str)
|
||||
if (typeof str === 'object') return str
|
||||
|
||||
return JSON.parse(str)
|
||||
} catch (e) {
|
||||
throw new Error('Invalid JSON response.')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user