mirror of
https://github.com/sasjs/adapter.git
synced 2026-01-08 21:10:05 +00:00
chore(*): refactor, move all auth-related code to auth folder
This commit is contained in:
@@ -12,7 +12,7 @@ export const computeTests = (adapter: SASjs): TestSuite => ({
|
|||||||
return adapter.startComputeJob("/Public/app/common/sendArr", data);
|
return adapter.startComputeJob("/Public/app/common/sendArr", data);
|
||||||
},
|
},
|
||||||
assertion: (res: any) => {
|
assertion: (res: any) => {
|
||||||
const expectedProperties = ["id", "applicationName", "attributes"]
|
const expectedProperties = ["id", "applicationName", "attributes"];
|
||||||
return validate(expectedProperties, res);
|
return validate(expectedProperties, res);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -21,11 +21,22 @@ export const computeTests = (adapter: SASjs): TestSuite => ({
|
|||||||
description: "Should start a compute job and return the job",
|
description: "Should start a compute job and return the job",
|
||||||
test: () => {
|
test: () => {
|
||||||
const data: any = { table1: [{ col1: "first col value" }] };
|
const data: any = { table1: [{ col1: "first col value" }] };
|
||||||
return adapter.startComputeJob("/Public/app/common/sendArr", data, {}, "", true);
|
return adapter.startComputeJob(
|
||||||
|
"/Public/app/common/sendArr",
|
||||||
|
data,
|
||||||
|
{},
|
||||||
|
"",
|
||||||
|
true
|
||||||
|
);
|
||||||
},
|
},
|
||||||
assertion: (res: any) => {
|
assertion: (res: any) => {
|
||||||
const expectedProperties = ["id", "state", "creationTimeStamp", "jobConditionCode"]
|
const expectedProperties = [
|
||||||
return validate(expectedProperties, res.result);
|
"id",
|
||||||
|
"state",
|
||||||
|
"creationTimeStamp",
|
||||||
|
"jobConditionCode"
|
||||||
|
];
|
||||||
|
return validate(expectedProperties, res.job);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -38,19 +49,19 @@ export const computeTests = (adapter: SASjs): TestSuite => ({
|
|||||||
`output;`,
|
`output;`,
|
||||||
`end;`,
|
`end;`,
|
||||||
`run;`
|
`run;`
|
||||||
]
|
];
|
||||||
|
|
||||||
return adapter.executeScriptSASViya(
|
return adapter.executeScriptSASViya(
|
||||||
'sasCode.sas',
|
"sasCode.sas",
|
||||||
fileLines,
|
fileLines,
|
||||||
'SAS Studio compute context',
|
"SAS Studio compute context",
|
||||||
undefined,
|
undefined,
|
||||||
true
|
true
|
||||||
)
|
);
|
||||||
},
|
},
|
||||||
assertion: (res: any) => {
|
assertion: (res: any) => {
|
||||||
const expectedLogContent = `1 data;\\n2 do x=1 to 100;\\n3 output;\\n4 end;\\n5 run;\\n\\n`
|
const expectedLogContent = `1 data;\\n2 do x=1 to 100;\\n3 output;\\n4 end;\\n5 run;\\n\\n`;
|
||||||
|
|
||||||
return validateLog(expectedLogContent, res.log);
|
return validateLog(expectedLogContent, res.log);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -58,21 +69,21 @@ export const computeTests = (adapter: SASjs): TestSuite => ({
|
|||||||
title: "Execute Script Viya - failed job",
|
title: "Execute Script Viya - failed job",
|
||||||
description: "Should execute sas file and return log",
|
description: "Should execute sas file and return log",
|
||||||
test: () => {
|
test: () => {
|
||||||
const fileLines = [
|
const fileLines = [`%abort;`];
|
||||||
`%abort;`
|
|
||||||
]
|
return adapter
|
||||||
|
.executeScriptSASViya(
|
||||||
return adapter.executeScriptSASViya(
|
"sasCode.sas",
|
||||||
'sasCode.sas',
|
fileLines,
|
||||||
fileLines,
|
"SAS Studio compute context",
|
||||||
'SAS Studio compute context',
|
undefined,
|
||||||
undefined,
|
true
|
||||||
true
|
)
|
||||||
).catch((err: any) => err )
|
.catch((err: any) => err);
|
||||||
},
|
},
|
||||||
assertion: (res: any) => {
|
assertion: (res: any) => {
|
||||||
const expectedLogContent = `1 %abort;\\nERROR: The %ABORT statement is not valid in open code.\\n`
|
const expectedLogContent = `1 %abort;\\nERROR: The %ABORT statement is not valid in open code.\\n`;
|
||||||
|
|
||||||
return validateLog(expectedLogContent, res.log);
|
return validateLog(expectedLogContent, res.log);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,16 +91,16 @@ export const computeTests = (adapter: SASjs): TestSuite => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const validateLog = (text: string, log: string): boolean => {
|
const validateLog = (text: string, log: string): boolean => {
|
||||||
const isValid = JSON.stringify(log).includes(text)
|
const isValid = JSON.stringify(log).includes(text);
|
||||||
|
|
||||||
return isValid
|
return isValid;
|
||||||
}
|
};
|
||||||
|
|
||||||
const validate = (expectedProperties: string[], data: any): boolean => {
|
const validate = (expectedProperties: string[], data: any): boolean => {
|
||||||
const actualProperties = Object.keys(data);
|
const actualProperties = Object.keys(data);
|
||||||
|
|
||||||
const isValid = expectedProperties.every(
|
const isValid = expectedProperties.every((property) =>
|
||||||
(property) => actualProperties.includes(property)
|
actualProperties.includes(property)
|
||||||
);
|
);
|
||||||
return isValid
|
return isValid;
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -23,11 +23,12 @@ export const sasjsRequestTests = (adapter: SASjs): TestSuite => ({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Make error and capture log",
|
title: "Make error and capture log",
|
||||||
description: "Should make an error and capture log, in the same time it is testing if debug override is working",
|
description:
|
||||||
|
"Should make an error and capture log, in the same time it is testing if debug override is working",
|
||||||
test: async () => {
|
test: async () => {
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
adapter
|
adapter
|
||||||
.request("common/makeErr", data, {debug: true})
|
.request("common/makeErr", data, { debug: true })
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
//no action here, this request must throw error
|
//no action here, this request must throw error
|
||||||
})
|
})
|
||||||
@@ -38,9 +39,11 @@ export const sasjsRequestTests = (adapter: SASjs): TestSuite => ({
|
|||||||
req.serviceLink.includes("makeErr")
|
req.serviceLink.includes("makeErr")
|
||||||
) || null;
|
) || null;
|
||||||
|
|
||||||
if (!makeErrRequest) resolve(false)
|
if (!makeErrRequest) return resolve(false);
|
||||||
|
|
||||||
resolve(!!(makeErrRequest.logFile && makeErrRequest.logFile.length > 0));
|
return resolve(
|
||||||
|
!!(makeErrRequest.logFile && makeErrRequest.logFile.length > 0)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { isLogInRequired, needsRetry, isUrl } from './utils'
|
import { needsRetry, isUrl } from './utils'
|
||||||
import { CsrfToken } from './types/CsrfToken'
|
import { CsrfToken } from './types/CsrfToken'
|
||||||
import { UploadFile } from './types/UploadFile'
|
import { UploadFile } from './types/UploadFile'
|
||||||
import { ErrorResponse } from './types'
|
import { ErrorResponse } from './types'
|
||||||
import axios, { AxiosInstance } from 'axios'
|
import axios, { AxiosInstance } from 'axios'
|
||||||
|
import { isLogInRequired } from './auth'
|
||||||
|
|
||||||
const requestRetryLimit = 5
|
const requestRetryLimit = 5
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
isAuthorizeFormRequired,
|
|
||||||
parseAndSubmitAuthorizeForm,
|
parseAndSubmitAuthorizeForm,
|
||||||
convertToCSV,
|
convertToCSV,
|
||||||
makeRequest,
|
|
||||||
isRelativePath,
|
isRelativePath,
|
||||||
isUri,
|
isUri,
|
||||||
isUrl
|
isUrl
|
||||||
@@ -24,12 +22,15 @@ import { SessionManager } from './SessionManager'
|
|||||||
import { ContextManager } from './ContextManager'
|
import { ContextManager } from './ContextManager'
|
||||||
import { timestampToYYYYMMDDHHMMSS } from '@sasjs/utils/time'
|
import { timestampToYYYYMMDDHHMMSS } from '@sasjs/utils/time'
|
||||||
import { Logger, LogLevel } from '@sasjs/utils/logger'
|
import { Logger, LogLevel } from '@sasjs/utils/logger'
|
||||||
|
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
||||||
|
import { isAuthorizeFormRequired } from './auth/isAuthorizeFormRequired'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A client for interfacing with the SAS Viya REST API.
|
* A client for interfacing with the SAS Viya REST API.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export class SASViyaApiClient {
|
export class SASViyaApiClient {
|
||||||
|
private httpClient: AxiosInstance
|
||||||
constructor(
|
constructor(
|
||||||
private serverUrl: string,
|
private serverUrl: string,
|
||||||
private rootFolderName: string,
|
private rootFolderName: string,
|
||||||
@@ -37,6 +38,7 @@ export class SASViyaApiClient {
|
|||||||
private setCsrfToken: (csrfToken: CsrfToken) => void
|
private setCsrfToken: (csrfToken: CsrfToken) => void
|
||||||
) {
|
) {
|
||||||
if (serverUrl) isUrl(serverUrl)
|
if (serverUrl) isUrl(serverUrl)
|
||||||
|
this.httpClient = axios.create({ baseURL: serverUrl })
|
||||||
}
|
}
|
||||||
|
|
||||||
private csrfToken: CsrfToken | null = null
|
private csrfToken: CsrfToken | null = null
|
||||||
@@ -146,10 +148,11 @@ export class SASViyaApiClient {
|
|||||||
headers.Authorization = `Bearer ${accessToken}`
|
headers.Authorization = `Bearer ${accessToken}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const { result: contexts } = await this.request<{ items: Context[] }>(
|
const { result: contexts } = await this.get<{ items: Context[] }>(
|
||||||
`${this.serverUrl}/compute/contexts?limit=10000`,
|
`/compute/contexts?limit=10000`,
|
||||||
{ headers }
|
accessToken
|
||||||
)
|
)
|
||||||
|
|
||||||
const executionContext =
|
const executionContext =
|
||||||
contexts.items && contexts.items.length
|
contexts.items && contexts.items.length
|
||||||
? contexts.items.find((c: any) => c.name === contextName)
|
? contexts.items.find((c: any) => c.name === contextName)
|
||||||
@@ -165,9 +168,10 @@ export class SASViyaApiClient {
|
|||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const { result: createdSession } = await this.request<Session>(
|
const { result: createdSession } = await this.post<Session>(
|
||||||
`${this.serverUrl}/compute/contexts/${executionContext.id}/sessions`,
|
`/compute/contexts/${executionContext.id}/sessions`,
|
||||||
createSessionRequest
|
{},
|
||||||
|
accessToken
|
||||||
)
|
)
|
||||||
|
|
||||||
return createdSession
|
return createdSession
|
||||||
@@ -370,22 +374,18 @@ export class SASViyaApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Execute job in session
|
// Execute job in session
|
||||||
const postJobRequest = {
|
const jobRequestBody = JSON.stringify({
|
||||||
method: 'POST',
|
name: fileName,
|
||||||
headers,
|
description: 'Powered by SASjs',
|
||||||
body: JSON.stringify({
|
code: linesOfCode,
|
||||||
name: fileName,
|
variables: jobVariables,
|
||||||
description: 'Powered by SASjs',
|
arguments: jobArguments
|
||||||
code: linesOfCode,
|
})
|
||||||
variables: jobVariables,
|
const { result: postedJob, etag } = await this.post<Job>(
|
||||||
arguments: jobArguments
|
`/compute/sessions/${executionSessionId}/jobs`,
|
||||||
})
|
jobRequestBody,
|
||||||
}
|
accessToken
|
||||||
|
).catch((err: any) => {
|
||||||
const { result: postedJob, etag } = await this.request<Job>(
|
|
||||||
`${this.serverUrl}/compute/sessions/${executionSessionId}/jobs`,
|
|
||||||
postJobRequest
|
|
||||||
).catch((err) => {
|
|
||||||
throw err
|
throw err
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -409,9 +409,9 @@ export class SASViyaApiClient {
|
|||||||
pollOptions
|
pollOptions
|
||||||
)
|
)
|
||||||
|
|
||||||
const { result: currentJob } = await this.request<Job>(
|
const { result: currentJob } = await this.get<Job>(
|
||||||
`${this.serverUrl}/compute/sessions/${executionSessionId}/jobs/${postedJob.id}`,
|
`/compute/sessions/${executionSessionId}/jobs/${postedJob.id}`,
|
||||||
{ headers }
|
accessToken
|
||||||
).catch((err) => {
|
).catch((err) => {
|
||||||
throw err
|
throw err
|
||||||
})
|
})
|
||||||
@@ -422,11 +422,9 @@ export class SASViyaApiClient {
|
|||||||
const logLink = currentJob.links.find((l) => l.rel === 'log')
|
const logLink = currentJob.links.find((l) => l.rel === 'log')
|
||||||
|
|
||||||
if (debug && logLink) {
|
if (debug && logLink) {
|
||||||
log = await this.request<any>(
|
log = await this.get<any>(
|
||||||
`${this.serverUrl}${logLink.href}/content?limit=10000`,
|
`${logLink.href}/content?limit=10000`,
|
||||||
{
|
accessToken
|
||||||
headers
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
.then((res: any) =>
|
.then((res: any) =>
|
||||||
res.result.items.map((i: any) => i.line).join('\n')
|
res.result.items.map((i: any) => i.line).join('\n')
|
||||||
@@ -449,18 +447,16 @@ export class SASViyaApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (resultLink) {
|
if (resultLink) {
|
||||||
jobResult = await this.request<any>(
|
jobResult = await this.get<any>(
|
||||||
`${this.serverUrl}${resultLink}`,
|
resultLink,
|
||||||
{ headers },
|
accessToken,
|
||||||
'text'
|
'text/plain'
|
||||||
).catch(async (e) => {
|
).catch(async (e) => {
|
||||||
if (e && e.status === 404) {
|
if (e && e.status === 404) {
|
||||||
if (logLink) {
|
if (logLink) {
|
||||||
log = await this.request<any>(
|
log = await this.get<any>(
|
||||||
`${this.serverUrl}${logLink.href}/content?limit=10000`,
|
`${logLink.href}/content?limit=10000`,
|
||||||
{
|
accessToken
|
||||||
headers
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
.then((res: any) =>
|
.then((res: any) =>
|
||||||
res.result.items.map((i: any) => i.line).join('\n')
|
res.result.items.map((i: any) => i.line).join('\n')
|
||||||
@@ -471,7 +467,7 @@ export class SASViyaApiClient {
|
|||||||
|
|
||||||
return Promise.reject({
|
return Promise.reject({
|
||||||
status: 500,
|
status: 500,
|
||||||
log: log
|
log
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -568,22 +564,13 @@ export class SASViyaApiClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const createFolderRequest: RequestInit = {
|
const { result: createFolderResponse } = await this.post<Folder>(
|
||||||
method: 'POST',
|
`/folders/folders?parentFolderUri=${parentFolderUri}`,
|
||||||
body: JSON.stringify({
|
JSON.stringify({
|
||||||
name: folderName,
|
name: folderName,
|
||||||
type: 'folder'
|
type: 'folder'
|
||||||
})
|
}),
|
||||||
}
|
accessToken
|
||||||
|
|
||||||
createFolderRequest.headers = { 'Content-Type': 'application/json' }
|
|
||||||
if (accessToken) {
|
|
||||||
createFolderRequest.headers.Authorization = `Bearer ${accessToken}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const { result: createFolderResponse } = await this.request<Folder>(
|
|
||||||
`${this.serverUrl}/folders/folders?parentFolderUri=${parentFolderUri}`,
|
|
||||||
createFolderRequest
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// update folder map with newly created folder.
|
// update folder map with newly created folder.
|
||||||
@@ -617,13 +604,9 @@ export class SASViyaApiClient {
|
|||||||
parentFolderUri = await this.getFolderUri(parentFolderPath, accessToken)
|
parentFolderUri = await this.getFolderUri(parentFolderPath, accessToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
const createJobDefinitionRequest: RequestInit = {
|
return await this.post<Job>(
|
||||||
method: 'POST',
|
`${this.serverUrl}/jobDefinitions/definitions?parentFolderUri=${parentFolderUri}`,
|
||||||
headers: {
|
JSON.stringify({
|
||||||
'Content-Type': 'application/vnd.sas.job.definition+json',
|
|
||||||
Accept: 'application/vnd.sas.job.definition+json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
name: jobName,
|
name: jobName,
|
||||||
parameters: [
|
parameters: [
|
||||||
{
|
{
|
||||||
@@ -634,19 +617,8 @@ export class SASViyaApiClient {
|
|||||||
],
|
],
|
||||||
type: 'Compute',
|
type: 'Compute',
|
||||||
code
|
code
|
||||||
})
|
}),
|
||||||
}
|
accessToken
|
||||||
|
|
||||||
if (accessToken) {
|
|
||||||
createJobDefinitionRequest!.headers = {
|
|
||||||
...createJobDefinitionRequest.headers,
|
|
||||||
Authorization: `Bearer ${accessToken}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return await this.request<Job>(
|
|
||||||
`${this.serverUrl}/jobDefinitions/definitions?parentFolderUri=${parentFolderUri}`,
|
|
||||||
createJobDefinitionRequest
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -795,13 +767,10 @@ export class SASViyaApiClient {
|
|||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
headers.Authorization = `Bearer ${accessToken}`
|
headers.Authorization = `Bearer ${accessToken}`
|
||||||
}
|
}
|
||||||
const deleteResponse = await this.request(url, {
|
|
||||||
method: 'DELETE',
|
|
||||||
credentials: 'include',
|
|
||||||
headers
|
|
||||||
})
|
|
||||||
|
|
||||||
return deleteResponse
|
const deleteResponse = await this.delete(url, accessToken)
|
||||||
|
|
||||||
|
return deleteResponse.result
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -871,9 +840,9 @@ export class SASViyaApiClient {
|
|||||||
throw new Error(`URI of job definition was not found.`)
|
throw new Error(`URI of job definition was not found.`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { result: jobDefinition } = await this.request<JobDefinition>(
|
const { result: jobDefinition } = await this.get<JobDefinition>(
|
||||||
`${this.serverUrl}${jobDefinitionLink.href}`,
|
`${this.serverUrl}${jobDefinitionLink.href}`,
|
||||||
{ headers }
|
accessToken
|
||||||
)
|
)
|
||||||
|
|
||||||
code = jobDefinition.code
|
code = jobDefinition.code
|
||||||
@@ -948,20 +917,10 @@ export class SASViyaApiClient {
|
|||||||
const jobDefinitionLink = jobToExecute?.links.find(
|
const jobDefinitionLink = jobToExecute?.links.find(
|
||||||
(l) => l.rel === 'getResource'
|
(l) => l.rel === 'getResource'
|
||||||
)?.href
|
)?.href
|
||||||
const requestInfo: any = {
|
|
||||||
method: 'GET'
|
|
||||||
}
|
|
||||||
const headers: any = { 'Content-Type': 'application/json' }
|
|
||||||
|
|
||||||
if (!!accessToken) {
|
const { result: jobDefinition } = await this.get<Job>(
|
||||||
headers.Authorization = `Bearer ${accessToken}`
|
|
||||||
}
|
|
||||||
|
|
||||||
requestInfo.headers = headers
|
|
||||||
|
|
||||||
const { result: jobDefinition } = await this.request<Job>(
|
|
||||||
`${this.serverUrl}${jobDefinitionLink}`,
|
`${this.serverUrl}${jobDefinitionLink}`,
|
||||||
requestInfo
|
accessToken
|
||||||
)
|
)
|
||||||
|
|
||||||
const jobArguments: { [key: string]: any } = {
|
const jobArguments: { [key: string]: any } = {
|
||||||
@@ -988,48 +947,43 @@ export class SASViyaApiClient {
|
|||||||
jobArguments[`_webin_name${index + 1}`] = fileInfo.tableName
|
jobArguments[`_webin_name${index + 1}`] = fileInfo.tableName
|
||||||
})
|
})
|
||||||
|
|
||||||
const postJobRequest = {
|
const postJobRequestBody = JSON.stringify({
|
||||||
method: 'POST',
|
name: `exec-${jobName}`,
|
||||||
headers,
|
description: 'Powered by SASjs',
|
||||||
body: JSON.stringify({
|
jobDefinition,
|
||||||
name: `exec-${jobName}`,
|
arguments: jobArguments
|
||||||
description: 'Powered by SASjs',
|
})
|
||||||
jobDefinition,
|
const { result: postedJob, etag } = await this.post<Job>(
|
||||||
arguments: jobArguments
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const { result: postedJob, etag } = await this.request<Job>(
|
|
||||||
`${this.serverUrl}/jobExecution/jobs?_action=wait`,
|
`${this.serverUrl}/jobExecution/jobs?_action=wait`,
|
||||||
postJobRequest
|
postJobRequestBody
|
||||||
)
|
)
|
||||||
const jobStatus = await this.pollJobState(postedJob, etag, accessToken)
|
const jobStatus = await this.pollJobState(postedJob, etag, accessToken)
|
||||||
const { result: currentJob } = await this.request<Job>(
|
const { result: currentJob } = await this.get<Job>(
|
||||||
`${this.serverUrl}/jobExecution/jobs/${postedJob.id}`,
|
`${this.serverUrl}/jobExecution/jobs/${postedJob.id}`,
|
||||||
{ headers }
|
accessToken
|
||||||
)
|
)
|
||||||
|
|
||||||
let jobResult
|
let jobResult
|
||||||
let log
|
let log
|
||||||
if (jobStatus === 'failed') {
|
|
||||||
return Promise.reject(currentJob.error)
|
|
||||||
}
|
|
||||||
const resultLink = currentJob.results['_webout.json']
|
const resultLink = currentJob.results['_webout.json']
|
||||||
const logLink = currentJob.links.find((l) => l.rel === 'log')
|
const logLink = currentJob.links.find((l) => l.rel === 'log')
|
||||||
if (resultLink) {
|
if (resultLink) {
|
||||||
jobResult = await this.request<any>(
|
jobResult = await this.get<any>(
|
||||||
`${this.serverUrl}${resultLink}/content`,
|
`${this.serverUrl}${resultLink}/content`,
|
||||||
{ headers },
|
accessToken,
|
||||||
'text'
|
'text/plain'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (debug && logLink) {
|
if (debug && logLink) {
|
||||||
log = await this.request<any>(
|
log = await this.get<any>(
|
||||||
`${this.serverUrl}${logLink.href}/content`,
|
`${this.serverUrl}${logLink.href}/content`,
|
||||||
{
|
accessToken
|
||||||
headers
|
|
||||||
}
|
|
||||||
).then((res: any) => res.result.items.map((i: any) => i.line).join('\n'))
|
).then((res: any) => res.result.items.map((i: any) => i.line).join('\n'))
|
||||||
}
|
}
|
||||||
|
if (jobStatus === 'failed') {
|
||||||
|
return Promise.reject({ error: currentJob.error, log })
|
||||||
|
}
|
||||||
return { result: jobResult?.result, log }
|
return { result: jobResult?.result, log }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1042,22 +996,13 @@ export class SASViyaApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const url = '/folders/folders/@item?path=' + path
|
const url = '/folders/folders/@item?path=' + path
|
||||||
const requestInfo: any = {
|
const { result: folder } = await this.get<Folder>(`${url}`, accessToken)
|
||||||
method: 'GET'
|
|
||||||
}
|
|
||||||
if (accessToken) {
|
|
||||||
requestInfo.headers = { Authorization: `Bearer ${accessToken}` }
|
|
||||||
}
|
|
||||||
const { result: folder } = await this.request<Folder>(
|
|
||||||
`${this.serverUrl}${url}`,
|
|
||||||
requestInfo
|
|
||||||
)
|
|
||||||
if (!folder) {
|
if (!folder) {
|
||||||
throw new Error(`The path ${path} does not exist on ${this.serverUrl}`)
|
throw new Error(`The path ${path} does not exist on ${this.serverUrl}`)
|
||||||
}
|
}
|
||||||
const { result: members } = await this.request<{ items: any[] }>(
|
const { result: members } = await this.get<{ items: any[] }>(
|
||||||
`${this.serverUrl}/folders/folders/${folder.id}/members?limit=${folder.memberCount}`,
|
`/folders/folders/${folder.id}/members?limit=${folder.memberCount}`,
|
||||||
requestInfo
|
accessToken
|
||||||
)
|
)
|
||||||
|
|
||||||
const itemsAtRoot = members.items
|
const itemsAtRoot = members.items
|
||||||
@@ -1093,12 +1038,10 @@ export class SASViyaApiClient {
|
|||||||
Promise.reject(`Job state link was not found.`)
|
Promise.reject(`Job state link was not found.`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { result: state } = await this.request<string>(
|
const { result: state } = await this.get<string>(
|
||||||
`${this.serverUrl}${stateLink.href}?_action=wait&wait=30`,
|
`${this.serverUrl}${stateLink.href}?_action=wait&wait=30`,
|
||||||
{
|
accessToken,
|
||||||
headers
|
'text/plain'
|
||||||
},
|
|
||||||
'text'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const currentState = state.trim()
|
const currentState = state.trim()
|
||||||
@@ -1116,12 +1059,10 @@ export class SASViyaApiClient {
|
|||||||
postedJobState === 'pending'
|
postedJobState === 'pending'
|
||||||
) {
|
) {
|
||||||
if (stateLink) {
|
if (stateLink) {
|
||||||
const { result: jobState } = await this.request<string>(
|
const { result: jobState } = await this.get<string>(
|
||||||
`${this.serverUrl}${stateLink.href}?_action=wait&wait=30`,
|
`${this.serverUrl}${stateLink.href}?_action=wait&wait=30`,
|
||||||
{
|
accessToken,
|
||||||
headers
|
'text/plain'
|
||||||
},
|
|
||||||
'text'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
postedJobState = jobState.trim()
|
postedJobState = jobState.trim()
|
||||||
@@ -1164,17 +1105,10 @@ export class SASViyaApiClient {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const createFileRequest = {
|
const uploadResponse = await this.uploadFile(
|
||||||
method: 'POST',
|
|
||||||
body: csv,
|
|
||||||
headers
|
|
||||||
}
|
|
||||||
|
|
||||||
const uploadResponse = await this.request<any>(
|
|
||||||
`${this.serverUrl}/files/files#rawUpload`,
|
`${this.serverUrl}/files/files#rawUpload`,
|
||||||
createFileRequest,
|
csv,
|
||||||
'json',
|
accessToken
|
||||||
'fileUpload'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
uploadedFiles.push({ tableName, file: uploadResponse.result })
|
uploadedFiles.push({ tableName, file: uploadResponse.result })
|
||||||
@@ -1184,16 +1118,10 @@ export class SASViyaApiClient {
|
|||||||
|
|
||||||
private async getFolderUri(folderPath: string, accessToken?: string) {
|
private async getFolderUri(folderPath: string, accessToken?: string) {
|
||||||
const url = '/folders/folders/@item?path=' + folderPath
|
const url = '/folders/folders/@item?path=' + folderPath
|
||||||
const requestInfo: any = {
|
const { result: folder } = await this.get<Folder>(
|
||||||
method: 'GET'
|
|
||||||
}
|
|
||||||
if (accessToken) {
|
|
||||||
requestInfo.headers = { Authorization: `Bearer ${accessToken}` }
|
|
||||||
}
|
|
||||||
const { result: folder } = await this.request<Folder>(
|
|
||||||
`${this.serverUrl}${url}`,
|
`${this.serverUrl}${url}`,
|
||||||
requestInfo
|
accessToken
|
||||||
).catch((err) => {
|
).catch(() => {
|
||||||
return { result: null }
|
return { result: null }
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1203,18 +1131,11 @@ export class SASViyaApiClient {
|
|||||||
|
|
||||||
private async getRecycleBinUri(accessToken: string) {
|
private async getRecycleBinUri(accessToken: string) {
|
||||||
const url = '/folders/folders/@myRecycleBin'
|
const url = '/folders/folders/@myRecycleBin'
|
||||||
const requestInfo = {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Authorization: 'Bearer ' + accessToken
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { result: folder } = await this.request<Folder>(
|
const { result: folder } = await this.get<Folder>(
|
||||||
`${this.serverUrl}${url}`,
|
`${this.serverUrl}${url}`,
|
||||||
requestInfo
|
accessToken
|
||||||
).catch((err) => {
|
).catch(() => {
|
||||||
return { result: null }
|
return { result: null }
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1280,22 +1201,14 @@ export class SASViyaApiClient {
|
|||||||
const sourceFolderId = sourceFolderUri?.split('/').pop()
|
const sourceFolderId = sourceFolderUri?.split('/').pop()
|
||||||
const url = sourceFolderUri
|
const url = sourceFolderUri
|
||||||
|
|
||||||
const requestInfo = {
|
const { result: folder } = await this.patch<Folder>(
|
||||||
method: 'PATCH',
|
`${this.serverUrl}${url}`,
|
||||||
headers: {
|
JSON.stringify({
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Authorization: 'Bearer ' + accessToken
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
id: sourceFolderId,
|
id: sourceFolderId,
|
||||||
name: targetFolderName,
|
name: targetFolderName,
|
||||||
parentFolderUri: targetParentFolderUri
|
parentFolderUri: targetParentFolderUri
|
||||||
})
|
}),
|
||||||
}
|
accessToken
|
||||||
|
|
||||||
const { result: folder } = await this.request<Folder>(
|
|
||||||
`${this.serverUrl}${url}`,
|
|
||||||
requestInfo
|
|
||||||
).catch((err) => {
|
).catch((err) => {
|
||||||
if (err.code && err.code === 'ENOTFOUND') {
|
if (err.code && err.code === 'ENOTFOUND') {
|
||||||
const notFoundError = {
|
const notFoundError = {
|
||||||
@@ -1346,32 +1259,165 @@ export class SASViyaApiClient {
|
|||||||
this.fileUploadCsrfToken = csrfToken
|
this.fileUploadCsrfToken = csrfToken
|
||||||
}
|
}
|
||||||
|
|
||||||
private async request<T>(
|
private get<T>(
|
||||||
url: string,
|
url: string,
|
||||||
options: RequestInit,
|
accessToken?: string,
|
||||||
contentType: 'text' | 'json' = 'json',
|
contentType = 'application/json'
|
||||||
type: 'fileUpload' | 'other' = 'other'
|
|
||||||
) {
|
) {
|
||||||
const callback =
|
const headers: any = {
|
||||||
type === 'fileUpload'
|
'Content-Type': contentType
|
||||||
? this.setFileUploadCsrfToken
|
|
||||||
: this.setCsrfTokenLocal
|
|
||||||
|
|
||||||
if (type === 'other') {
|
|
||||||
if (this.csrfToken) {
|
|
||||||
options.headers = {
|
|
||||||
...options.headers,
|
|
||||||
[this.csrfToken.headerName]: this.csrfToken.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (this.fileUploadCsrfToken) {
|
|
||||||
options.headers = {
|
|
||||||
...options.headers,
|
|
||||||
[this.fileUploadCsrfToken.headerName]: this.fileUploadCsrfToken.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return await makeRequest<T>(url, options, callback, contentType)
|
if (accessToken) {
|
||||||
|
headers.Authorization = `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestConfig: AxiosRequestConfig = {
|
||||||
|
headers,
|
||||||
|
responseType: contentType === 'text/plain' ? 'text' : 'json',
|
||||||
|
withCredentials: true
|
||||||
|
}
|
||||||
|
if (contentType === 'text/plain') {
|
||||||
|
requestConfig.headers.Accept = '*/*'
|
||||||
|
requestConfig.transformResponse = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.httpClient.get<T>(url, requestConfig).then((response) => ({
|
||||||
|
result: response.data,
|
||||||
|
etag: response.headers['etag']
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
private post<T>(
|
||||||
|
url: string,
|
||||||
|
data: any = {},
|
||||||
|
accessToken?: string
|
||||||
|
): Promise<{ result: T; etag: string }> {
|
||||||
|
const headers: any = {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
if (accessToken) {
|
||||||
|
headers.Authorization = `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.csrfToken?.value) {
|
||||||
|
headers[this.csrfToken.headerName] = this.csrfToken.value
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.httpClient
|
||||||
|
.post<T>(url, data, { headers, withCredentials: true })
|
||||||
|
.then((response) => {
|
||||||
|
return {
|
||||||
|
result: response.data as T,
|
||||||
|
etag: response.headers['etag'] as string
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
const response = e.response as AxiosResponse
|
||||||
|
if (response.status === 403 || response.status === 449) {
|
||||||
|
const tokenHeader = (response.headers[
|
||||||
|
'x-csrf-header'
|
||||||
|
] as string)?.toLowerCase()
|
||||||
|
|
||||||
|
if (tokenHeader) {
|
||||||
|
const token = response.headers[tokenHeader]
|
||||||
|
this.setCsrfTokenLocal({
|
||||||
|
headerName: tokenHeader,
|
||||||
|
value: token || ''
|
||||||
|
})
|
||||||
|
return this.post<T>(url, data, accessToken)
|
||||||
|
}
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
throw e
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private delete<T>(url: string, accessToken?: string) {
|
||||||
|
const headers: any = {}
|
||||||
|
if (accessToken) {
|
||||||
|
headers.Authorization = `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.csrfToken?.value) {
|
||||||
|
headers[this.csrfToken.headerName] = this.csrfToken.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestConfig: AxiosRequestConfig = {
|
||||||
|
headers
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.httpClient.delete<T>(url, requestConfig).then((response) => ({
|
||||||
|
result: response.data,
|
||||||
|
etag: response.headers['etag']
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
private patch<T>(url: string, data: any = {}, accessToken?: string) {
|
||||||
|
const headers: any = {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
if (accessToken) {
|
||||||
|
headers.Authorization = `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.csrfToken?.value) {
|
||||||
|
headers[this.csrfToken.headerName] = this.csrfToken.value
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.httpClient
|
||||||
|
.patch<T>(url, data, { headers, withCredentials: true })
|
||||||
|
.then((response) => {
|
||||||
|
return {
|
||||||
|
result: response.data as T,
|
||||||
|
etag: response.headers['etag'] as string
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private uploadFile(
|
||||||
|
url: string,
|
||||||
|
content: string,
|
||||||
|
accessToken?: string
|
||||||
|
): Promise<any> {
|
||||||
|
const headers: any = {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
if (accessToken) {
|
||||||
|
headers.Authorization = `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.fileUploadCsrfToken?.value) {
|
||||||
|
headers[
|
||||||
|
this.fileUploadCsrfToken.headerName
|
||||||
|
] = this.fileUploadCsrfToken.value
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.httpClient
|
||||||
|
.post(url, content, { headers, withCredentials: true })
|
||||||
|
.then((response) => {
|
||||||
|
return {
|
||||||
|
result: response.data,
|
||||||
|
etag: response.headers['etag'] as string
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
const response = e.response as AxiosResponse
|
||||||
|
if (response.status === 403 || response.status === 449) {
|
||||||
|
const tokenHeader = (response.headers[
|
||||||
|
'x-csrf-header'
|
||||||
|
] as string)?.toLowerCase()
|
||||||
|
|
||||||
|
if (tokenHeader) {
|
||||||
|
const token = response.headers[tokenHeader]
|
||||||
|
this.setFileUploadCsrfToken({
|
||||||
|
headerName: tokenHeader,
|
||||||
|
value: token || ''
|
||||||
|
})
|
||||||
|
return this.uploadFile(url, content, accessToken)
|
||||||
|
}
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
throw e
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
15
src/SASjs.ts
15
src/SASjs.ts
@@ -2,7 +2,6 @@ import {
|
|||||||
convertToCSV,
|
convertToCSV,
|
||||||
compareTimestamps,
|
compareTimestamps,
|
||||||
splitChunks,
|
splitChunks,
|
||||||
isLogInRequired,
|
|
||||||
parseSourceCode,
|
parseSourceCode,
|
||||||
parseGeneratedCode,
|
parseGeneratedCode,
|
||||||
parseWeboutResponse,
|
parseWeboutResponse,
|
||||||
@@ -24,7 +23,7 @@ import {
|
|||||||
import { SASViyaApiClient } from './SASViyaApiClient'
|
import { SASViyaApiClient } from './SASViyaApiClient'
|
||||||
import { SAS9ApiClient } from './SAS9ApiClient'
|
import { SAS9ApiClient } from './SAS9ApiClient'
|
||||||
import { FileUploader } from './FileUploader'
|
import { FileUploader } from './FileUploader'
|
||||||
import { AuthManager } from './auth/auth'
|
import { isLogInRequired, AuthManager } from './auth'
|
||||||
|
|
||||||
const defaultConfig: SASjsConfig = {
|
const defaultConfig: SASjsConfig = {
|
||||||
serverUrl: '',
|
serverUrl: '',
|
||||||
@@ -732,7 +731,11 @@ export default class SASjs {
|
|||||||
let responseJson
|
let responseJson
|
||||||
|
|
||||||
try {
|
try {
|
||||||
responseJson = JSON.parse(response!.result)
|
if (typeof response!.result === 'string') {
|
||||||
|
responseJson = JSON.parse(response!.result)
|
||||||
|
} else {
|
||||||
|
responseJson = response!.result
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
responseJson = JSON.parse(parseWeboutResponse(response!.result))
|
responseJson = JSON.parse(parseWeboutResponse(response!.result))
|
||||||
}
|
}
|
||||||
@@ -837,7 +840,11 @@ export default class SASjs {
|
|||||||
let responseJson
|
let responseJson
|
||||||
|
|
||||||
try {
|
try {
|
||||||
responseJson = JSON.parse(response!.result)
|
if (typeof response!.result === 'string') {
|
||||||
|
responseJson = JSON.parse(response!.result)
|
||||||
|
} else {
|
||||||
|
responseJson = response!.result
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
responseJson = JSON.parse(
|
responseJson = JSON.parse(
|
||||||
parseWeboutResponse(response!.result)
|
parseWeboutResponse(response!.result)
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
import axios, { AxiosInstance } from 'axios'
|
import axios, { AxiosInstance } from 'axios'
|
||||||
|
import { isAuthorizeFormRequired, parseAndSubmitAuthorizeForm } from '.'
|
||||||
import { ServerType } from '../types'
|
import { ServerType } from '../types'
|
||||||
import {
|
import { serialize } from '../utils'
|
||||||
serialize,
|
|
||||||
isAuthorizeFormRequired,
|
|
||||||
parseAndSubmitAuthorizeForm,
|
|
||||||
isLogInSuccess
|
|
||||||
} from '../utils'
|
|
||||||
|
|
||||||
export class AuthManager {
|
export class AuthManager {
|
||||||
public userName = ''
|
public userName = ''
|
||||||
@@ -15,7 +11,7 @@ export class AuthManager {
|
|||||||
constructor(
|
constructor(
|
||||||
private serverUrl: string,
|
private serverUrl: string,
|
||||||
private serverType: ServerType,
|
private serverType: ServerType,
|
||||||
private loginCallback: Function
|
private loginCallback: () => Promise<void>
|
||||||
) {
|
) {
|
||||||
this.httpClient = axios.create({ baseURL: this.serverUrl })
|
this.httpClient = axios.create({ baseURL: this.serverUrl })
|
||||||
this.loginUrl = `/SASLogon/login`
|
this.loginUrl = `/SASLogon/login`
|
||||||
@@ -41,7 +37,7 @@ export class AuthManager {
|
|||||||
|
|
||||||
const { isLoggedIn, loginForm } = await this.checkSession()
|
const { isLoggedIn, loginForm } = await this.checkSession()
|
||||||
if (isLoggedIn) {
|
if (isLoggedIn) {
|
||||||
this.loginCallback()
|
await this.loginCallback()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isLoggedIn,
|
isLoggedIn,
|
||||||
@@ -58,7 +54,10 @@ export class AuthManager {
|
|||||||
.post<string>(this.loginUrl, loginParamsStr, {
|
.post<string>(this.loginUrl, loginParamsStr, {
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
responseType: 'text',
|
responseType: 'text',
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
Accept: '*/*'
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.then((response) => response.data)
|
.then((response) => response.data)
|
||||||
|
|
||||||
@@ -159,3 +158,6 @@ export class AuthManager {
|
|||||||
return this.httpClient.get(this.logoutUrl).then(() => true)
|
return this.httpClient.get(this.logoutUrl).then(() => true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isLogInSuccess = (response: string): boolean =>
|
||||||
|
/You have signed in/gm.test(response)
|
||||||
6
src/auth/index.ts
Normal file
6
src/auth/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { AuthManager } from './AuthManager'
|
||||||
|
|
||||||
|
export * from './AuthManager'
|
||||||
|
export * from './isAuthorizeFormRequired'
|
||||||
|
export * from './isLoginRequired'
|
||||||
|
export * from './parseAndSubmitAuthorizeForm'
|
||||||
@@ -37,6 +37,12 @@ export const parseAndSubmitAuthorizeForm = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
return await axios
|
return await axios
|
||||||
.post(authUrl, formData, { withCredentials: true, responseType: 'text' })
|
.post(authUrl, formData, {
|
||||||
.then((response) => response.data)
|
withCredentials: true,
|
||||||
|
responseType: 'text',
|
||||||
|
headers: {
|
||||||
|
Accept: '*/*'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((res) => res.data)
|
||||||
}
|
}
|
||||||
@@ -1,15 +1,11 @@
|
|||||||
export * from './asyncForEach'
|
export * from './asyncForEach'
|
||||||
export * from './compareTimestamps'
|
export * from './compareTimestamps'
|
||||||
export * from './convertToCsv'
|
export * from './convertToCsv'
|
||||||
export * from './isAuthorizeFormRequired'
|
|
||||||
export * from './isLoginRequired'
|
|
||||||
export * from './isLoginSuccess'
|
|
||||||
export * from './isRelativePath'
|
export * from './isRelativePath'
|
||||||
export * from './isUri'
|
export * from './isUri'
|
||||||
export * from './isUrl'
|
export * from './isUrl'
|
||||||
export * from './makeRequest'
|
export * from './makeRequest'
|
||||||
export * from './needsRetry'
|
export * from './needsRetry'
|
||||||
export * from './parseAndSubmitAuthorizeForm'
|
|
||||||
export * from './parseGeneratedCode'
|
export * from './parseGeneratedCode'
|
||||||
export * from './parseSourceCode'
|
export * from './parseSourceCode'
|
||||||
export * from './parseSasViyaLog'
|
export * from './parseSasViyaLog'
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
export const isLogInSuccess = (response: string): boolean =>
|
|
||||||
/You have signed in/gm.test(response)
|
|
||||||
Reference in New Issue
Block a user