mirror of
https://github.com/sasjs/adapter.git
synced 2026-01-17 17:10:05 +00:00
feat(compute-api): implement job execution via compute API
This commit is contained in:
@@ -6,6 +6,7 @@
|
|||||||
"appLoc": "/Public/app",
|
"appLoc": "/Public/app",
|
||||||
"serverType": "SASVIYA",
|
"serverType": "SASVIYA",
|
||||||
"debug": false,
|
"debug": false,
|
||||||
"contextName": "SAS Job Execution compute context"
|
"contextName": "SharedCompute",
|
||||||
|
"useComputeApi": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const Login = (): ReactElement<{}> => {
|
|||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
(e) => {
|
(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
appContext.adapter.logIn(username, password).then(() => {
|
appContext.adapter.logIn(username, password).then((res) => {
|
||||||
appContext.setIsLoggedIn(true);
|
appContext.setIsLoggedIn(true);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const defaultConfig: SASjsConfig = {
|
|||||||
serverType: ServerType.SASViya,
|
serverType: ServerType.SASViya,
|
||||||
debug: true,
|
debug: true,
|
||||||
contextName: "SAS Job Execution compute context",
|
contextName: "SAS Job Execution compute context",
|
||||||
|
useComputeApi: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const customConfig = {
|
const customConfig = {
|
||||||
|
|||||||
@@ -74,9 +74,8 @@ export const sendArrTests = (adapter: SASjs): TestSuite => ({
|
|||||||
description:
|
description:
|
||||||
"Should error out with long string values over 32765 characters",
|
"Should error out with long string values over 32765 characters",
|
||||||
test: () => {
|
test: () => {
|
||||||
return adapter
|
const data = getLongStringData(32767);
|
||||||
.request("common/sendArr", getLongStringData(32767))
|
return adapter.request("common/sendArr", data).catch((e) => e);
|
||||||
.catch((e) => e);
|
|
||||||
},
|
},
|
||||||
assertion: (error: any) => {
|
assertion: (error: any) => {
|
||||||
return !!error && !!error.MESSAGE;
|
return !!error && !!error.MESSAGE;
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import {
|
|||||||
import * as NodeFormData from "form-data";
|
import * as NodeFormData from "form-data";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import { Job, Session, Context, Folder, CsrfToken } from "./types";
|
import { Job, Session, Context, Folder, CsrfToken } from "./types";
|
||||||
|
import { JobDefinition } from "./types/JobDefinition";
|
||||||
|
import { formatDataForRequest } from "./utils/formatDataForRequest";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A client for interfacing with the SAS Viya REST API
|
* A client for interfacing with the SAS Viya REST API
|
||||||
@@ -68,7 +70,7 @@ export class SASViyaApiClient {
|
|||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
headers.Authorization = `Bearer ${accessToken}`;
|
headers.Authorization = `Bearer ${accessToken}`;
|
||||||
}
|
}
|
||||||
const contexts = await this.request<{ items: Context[] }>(
|
const { result: contexts } = await this.request<{ items: Context[] }>(
|
||||||
`${this.serverUrl}/compute/contexts`,
|
`${this.serverUrl}/compute/contexts`,
|
||||||
{ headers }
|
{ headers }
|
||||||
);
|
);
|
||||||
@@ -93,7 +95,7 @@ export class SASViyaApiClient {
|
|||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
headers.Authorization = `Bearer ${accessToken}`;
|
headers.Authorization = `Bearer ${accessToken}`;
|
||||||
}
|
}
|
||||||
const contexts = await this.request<{ items: Context[] }>(
|
const { result: contexts } = await this.request<{ items: Context[] }>(
|
||||||
`${this.serverUrl}/compute/contexts`,
|
`${this.serverUrl}/compute/contexts`,
|
||||||
{ headers }
|
{ headers }
|
||||||
);
|
);
|
||||||
@@ -153,7 +155,7 @@ export class SASViyaApiClient {
|
|||||||
headers.Authorization = `Bearer ${accessToken}`;
|
headers.Authorization = `Bearer ${accessToken}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const contexts = await this.request<{ items: Context[] }>(
|
const { result: contexts } = await this.request<{ items: Context[] }>(
|
||||||
`${this.serverUrl}/compute/contexts`,
|
`${this.serverUrl}/compute/contexts`,
|
||||||
{ headers }
|
{ headers }
|
||||||
);
|
);
|
||||||
@@ -172,7 +174,7 @@ export class SASViyaApiClient {
|
|||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const createdSession = this.request<Session>(
|
const { result: createdSession } = await this.request<Session>(
|
||||||
`${this.serverUrl}/compute/contexts/${executionContext.id}/sessions`,
|
`${this.serverUrl}/compute/contexts/${executionContext.id}/sessions`,
|
||||||
createSessionRequest
|
createSessionRequest
|
||||||
);
|
);
|
||||||
@@ -190,12 +192,14 @@ export class SASViyaApiClient {
|
|||||||
* @param silent - optional flag to turn of logging.
|
* @param silent - optional flag to turn of logging.
|
||||||
*/
|
*/
|
||||||
public async executeScript(
|
public async executeScript(
|
||||||
fileName: string,
|
jobName: string,
|
||||||
linesOfCode: string[],
|
linesOfCode: string[],
|
||||||
contextName: string,
|
contextName: string,
|
||||||
accessToken?: string,
|
accessToken?: string,
|
||||||
sessionId = "",
|
sessionId = "",
|
||||||
silent = false
|
silent = false,
|
||||||
|
data = null,
|
||||||
|
debug = false
|
||||||
) {
|
) {
|
||||||
const headers: any = {
|
const headers: any = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -206,7 +210,7 @@ export class SASViyaApiClient {
|
|||||||
if (this.csrfToken) {
|
if (this.csrfToken) {
|
||||||
headers[this.csrfToken.headerName] = this.csrfToken.value;
|
headers[this.csrfToken.headerName] = this.csrfToken.value;
|
||||||
}
|
}
|
||||||
const contexts = await this.request<{ items: Context[] }>(
|
const { result: contexts } = await this.request<{ items: Context[] }>(
|
||||||
`${this.serverUrl}/compute/contexts`,
|
`${this.serverUrl}/compute/contexts`,
|
||||||
{ headers }
|
{ headers }
|
||||||
);
|
);
|
||||||
@@ -225,13 +229,52 @@ export class SASViyaApiClient {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
headers,
|
headers,
|
||||||
};
|
};
|
||||||
const createdSession = await this.request<Session>(
|
const { result: createdSession, etag } = await this.request<Session>(
|
||||||
`${this.serverUrl}/compute/contexts/${executionContext.id}/sessions`,
|
`${this.serverUrl}/compute/contexts/${executionContext.id}/sessions`,
|
||||||
createSessionRequest
|
createSessionRequest
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await this.waitForSession(createdSession, etag);
|
||||||
|
|
||||||
executionSessionId = createdSession.id;
|
executionSessionId = createdSession.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let jobArguments: { [key: string]: any } = {
|
||||||
|
_contextName: contextName,
|
||||||
|
_OMITJSONLISTING: true,
|
||||||
|
_OMITJSONLOG: true,
|
||||||
|
_OMITSESSIONRESULTS: true,
|
||||||
|
_OMITTEXTLISTING: true,
|
||||||
|
_OMITTEXTLOG: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (debug) {
|
||||||
|
jobArguments["_OMITTEXTLOG"] = false;
|
||||||
|
jobArguments["_OMITSESSIONRESULTS"] = false;
|
||||||
|
jobArguments["_DEBUG"] = 131;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileName = `exec-${
|
||||||
|
jobName.includes("/") ? jobName.split("/")[1] : jobName
|
||||||
|
}`;
|
||||||
|
|
||||||
|
let jobVariables: any = { SYS_JES_JOB_URI: "", _program: jobName };
|
||||||
|
let files: any[] = [];
|
||||||
|
if (data) {
|
||||||
|
if (JSON.stringify(data).includes(";")) {
|
||||||
|
files = await this.uploadTables(data, accessToken);
|
||||||
|
jobVariables["_webin_file_count"] = files.length;
|
||||||
|
files.forEach((fileInfo, index) => {
|
||||||
|
jobVariables[
|
||||||
|
`_webin_fileuri${index + 1}`
|
||||||
|
] = `/files/files/${fileInfo.file.id}`;
|
||||||
|
jobVariables[`_webin_name${index + 1}`] = fileInfo.tableName;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
jobVariables = { ...jobVariables, ...formatDataForRequest(data) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Execute job in session
|
// Execute job in session
|
||||||
const postJobRequest = {
|
const postJobRequest = {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -240,9 +283,11 @@ export class SASViyaApiClient {
|
|||||||
name: fileName,
|
name: fileName,
|
||||||
description: "Powered by SASjs",
|
description: "Powered by SASjs",
|
||||||
code: linesOfCode,
|
code: linesOfCode,
|
||||||
|
variables: jobVariables,
|
||||||
|
arguments: jobArguments,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
const postedJob = await this.request<Job>(
|
const { result: postedJob, etag } = await this.request<Job>(
|
||||||
`${this.serverUrl}/compute/sessions/${executionSessionId}/jobs`,
|
`${this.serverUrl}/compute/sessions/${executionSessionId}/jobs`,
|
||||||
postJobRequest
|
postJobRequest
|
||||||
);
|
);
|
||||||
@@ -255,18 +300,42 @@ export class SASViyaApiClient {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const jobStatus = await this.pollJobState(postedJob, accessToken, silent);
|
const jobStatus = await this.pollJobState(
|
||||||
const logLink = postedJob.links.find((l: any) => l.rel === "log");
|
postedJob,
|
||||||
if (logLink) {
|
etag,
|
||||||
const log = await this.request(
|
accessToken,
|
||||||
`${this.serverUrl}${logLink.href}?limit=100000`,
|
silent
|
||||||
|
);
|
||||||
|
const { result: currentJob } = await this.request<Job>(
|
||||||
|
`${this.serverUrl}/compute/sessions/${executionSessionId}/jobs/${postedJob.id}`,
|
||||||
|
{ headers }
|
||||||
|
);
|
||||||
|
|
||||||
|
let jobResult, log;
|
||||||
|
if (jobStatus === "failed" || jobStatus === "error") {
|
||||||
|
return Promise.reject(currentJob.error);
|
||||||
|
}
|
||||||
|
const resultLink = `/compute/sessions/${executionSessionId}/filerefs/_webout/content`;
|
||||||
|
const logLink = currentJob.links.find((l) => l.rel === "log");
|
||||||
|
if (resultLink) {
|
||||||
|
jobResult = await this.request<any>(
|
||||||
|
`${this.serverUrl}${resultLink}`,
|
||||||
|
{ headers },
|
||||||
|
"text"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (true && logLink) {
|
||||||
|
log = await this.request<any>(
|
||||||
|
`${this.serverUrl}${logLink.href}/content`,
|
||||||
{
|
{
|
||||||
headers,
|
headers,
|
||||||
}
|
}
|
||||||
|
).then((res: any) =>
|
||||||
|
res.result.items.map((i: any) => i.line).join("\n")
|
||||||
);
|
);
|
||||||
|
|
||||||
return { jobStatus, log };
|
|
||||||
}
|
}
|
||||||
|
return { result: jobResult?.result, log };
|
||||||
} else {
|
} else {
|
||||||
console.error(
|
console.error(
|
||||||
`Unable to find execution context ${contextName}.\nPlease check the contextName in the tgtDeployVars and try again.`
|
`Unable to find execution context ${contextName}.\nPlease check the contextName in the tgtDeployVars and try again.`
|
||||||
@@ -334,7 +403,7 @@ export class SASViyaApiClient {
|
|||||||
createFolderRequest.headers.Authorization = `Bearer ${accessToken}`;
|
createFolderRequest.headers.Authorization = `Bearer ${accessToken}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const createFolderResponse = await this.request<Folder>(
|
const { result: createFolderResponse } = await this.request<Folder>(
|
||||||
`${this.serverUrl}/folders/folders?parentFolderUri=${parentFolderUri}`,
|
`${this.serverUrl}/folders/folders?parentFolderUri=${parentFolderUri}`,
|
||||||
createFolderRequest
|
createFolderRequest
|
||||||
);
|
);
|
||||||
@@ -556,6 +625,71 @@ export class SASViyaApiClient {
|
|||||||
return deleteResponse;
|
return deleteResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a job via the SAS Viya Compute API
|
||||||
|
* @param sasJob - the relative path to the job.
|
||||||
|
* @param contextName - the name of the context where the job is to be executed.
|
||||||
|
* @param debug - sets the _debug flag in the job arguments.
|
||||||
|
* @param data - any data to be passed in as input to the job.
|
||||||
|
* @param accessToken - an optional access token for an authorized user.
|
||||||
|
*/
|
||||||
|
public async executeComputeJob(
|
||||||
|
sasJob: string,
|
||||||
|
contextName: string,
|
||||||
|
debug: boolean,
|
||||||
|
data?: any,
|
||||||
|
accessToken?: string
|
||||||
|
) {
|
||||||
|
if (!this.rootFolder) {
|
||||||
|
await this.populateRootFolder(accessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.rootFolder) {
|
||||||
|
throw new Error("Root folder was not found");
|
||||||
|
}
|
||||||
|
if (!this.rootFolderMap.size) {
|
||||||
|
await this.populateRootFolderMap(accessToken);
|
||||||
|
}
|
||||||
|
if (!this.rootFolderMap.size) {
|
||||||
|
throw new Error(
|
||||||
|
`The job ${sasJob} was not found in ${this.rootFolderName}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers: any = { "Content-Type": "application/json" };
|
||||||
|
if (!!accessToken) {
|
||||||
|
headers.Authorization = `Bearer ${accessToken}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const folderName = sasJob.split("/")[0];
|
||||||
|
const jobName = sasJob.split("/")[1];
|
||||||
|
const jobFolder = this.rootFolderMap.get(folderName);
|
||||||
|
const jobToExecute = jobFolder?.find((item) => item.name === jobName);
|
||||||
|
const jobDefinitionLink = jobToExecute?.links.find(
|
||||||
|
(l) => l.rel === "getResource"
|
||||||
|
);
|
||||||
|
if (!jobDefinitionLink) {
|
||||||
|
throw new Error("Job definition URI was not found.");
|
||||||
|
}
|
||||||
|
const { result: jobDefinition } = await this.request<JobDefinition>(
|
||||||
|
`${this.serverUrl}${jobDefinitionLink.href}`,
|
||||||
|
headers
|
||||||
|
);
|
||||||
|
const linesToExecute = jobDefinition.code
|
||||||
|
.replace(/\r\n/g, "\n")
|
||||||
|
.split("\n");
|
||||||
|
return await this.executeScript(
|
||||||
|
sasJob,
|
||||||
|
linesToExecute,
|
||||||
|
contextName,
|
||||||
|
accessToken,
|
||||||
|
"",
|
||||||
|
false,
|
||||||
|
data,
|
||||||
|
debug
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes a job via the SAS Viya Job Execution API
|
* Executes a job via the SAS Viya Job Execution API
|
||||||
* @param sasJob - the relative path to the job.
|
* @param sasJob - the relative path to the job.
|
||||||
@@ -590,7 +724,6 @@ export class SASViyaApiClient {
|
|||||||
let files: any[] = [];
|
let files: any[] = [];
|
||||||
if (data && Object.keys(data).length) {
|
if (data && Object.keys(data).length) {
|
||||||
files = await this.uploadTables(data, accessToken);
|
files = await this.uploadTables(data, accessToken);
|
||||||
console.log("Uploaded table files: ", files);
|
|
||||||
}
|
}
|
||||||
const jobName = path.basename(sasJob);
|
const jobName = path.basename(sasJob);
|
||||||
const jobFolder = sasJob.replace(`/${jobName}`, "");
|
const jobFolder = sasJob.replace(`/${jobName}`, "");
|
||||||
@@ -608,7 +741,7 @@ export class SASViyaApiClient {
|
|||||||
headers.Authorization = `Bearer ${accessToken}`;
|
headers.Authorization = `Bearer ${accessToken}`;
|
||||||
}
|
}
|
||||||
requestInfo.headers = headers;
|
requestInfo.headers = headers;
|
||||||
const jobDefinition = await this.request<Job>(
|
const { result: jobDefinition } = await this.request<Job>(
|
||||||
`${this.serverUrl}${jobDefinitionLink}`,
|
`${this.serverUrl}${jobDefinitionLink}`,
|
||||||
requestInfo
|
requestInfo
|
||||||
);
|
);
|
||||||
@@ -647,24 +780,29 @@ export class SASViyaApiClient {
|
|||||||
arguments: jobArguments,
|
arguments: jobArguments,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
const postedJob = await this.request<Job>(
|
const { result: postedJob, etag } = await this.request<Job>(
|
||||||
`${this.serverUrl}/jobExecution/jobs?_action=wait`,
|
`${this.serverUrl}/jobExecution/jobs?_action=wait`,
|
||||||
postJobRequest
|
postJobRequest
|
||||||
);
|
);
|
||||||
const jobStatus = await this.pollJobState(postedJob, accessToken, true);
|
const jobStatus = await this.pollJobState(
|
||||||
const currentJob = await this.request<Job>(
|
postedJob,
|
||||||
|
etag,
|
||||||
|
accessToken,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
const { result: currentJob } = await this.request<Job>(
|
||||||
`${this.serverUrl}/jobExecution/jobs/${postedJob.id}`,
|
`${this.serverUrl}/jobExecution/jobs/${postedJob.id}`,
|
||||||
{ headers }
|
{ headers }
|
||||||
);
|
);
|
||||||
|
|
||||||
let result, log;
|
let jobResult, log;
|
||||||
if (jobStatus === "failed") {
|
if (jobStatus === "failed") {
|
||||||
return Promise.reject(currentJob.error);
|
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) {
|
||||||
result = await this.request<any>(
|
jobResult = await this.request<any>(
|
||||||
`${this.serverUrl}${resultLink}/content`,
|
`${this.serverUrl}${resultLink}/content`,
|
||||||
{ headers },
|
{ headers },
|
||||||
"text"
|
"text"
|
||||||
@@ -676,9 +814,11 @@ export class SASViyaApiClient {
|
|||||||
{
|
{
|
||||||
headers,
|
headers,
|
||||||
}
|
}
|
||||||
).then((res: any) => res.items.map((i: any) => i.line).join("\n"));
|
).then((res: any) =>
|
||||||
|
res.result.items.map((i: any) => i.line).join("\n")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return { result, log };
|
return { result: jobResult?.result, log };
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`The job ${sasJob} was not found at the location ${this.rootFolderName}`
|
`The job ${sasJob} was not found at the location ${this.rootFolderName}`
|
||||||
@@ -695,14 +835,14 @@ export class SASViyaApiClient {
|
|||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
requestInfo.headers = { Authorization: `Bearer ${accessToken}` };
|
requestInfo.headers = { Authorization: `Bearer ${accessToken}` };
|
||||||
}
|
}
|
||||||
const folder = await this.request<Folder>(
|
const { result: folder } = await this.request<Folder>(
|
||||||
`${this.serverUrl}${url}`,
|
`${this.serverUrl}${url}`,
|
||||||
requestInfo
|
requestInfo
|
||||||
);
|
);
|
||||||
if (!folder) {
|
if (!folder) {
|
||||||
throw new Error("Cannot populate RootFolderMap unless rootFolder exists");
|
throw new Error("Cannot populate RootFolderMap unless rootFolder exists");
|
||||||
}
|
}
|
||||||
const members = await this.request<{ items: any[] }>(
|
const { result: members } = await this.request<{ items: any[] }>(
|
||||||
`${this.serverUrl}/folders/folders/${folder.id}/members`,
|
`${this.serverUrl}/folders/folders/${folder.id}/members`,
|
||||||
requestInfo
|
requestInfo
|
||||||
);
|
);
|
||||||
@@ -717,7 +857,7 @@ export class SASViyaApiClient {
|
|||||||
this.rootFolderName +
|
this.rootFolderName +
|
||||||
"/" +
|
"/" +
|
||||||
member.name;
|
member.name;
|
||||||
const memberDetail = await this.request<Folder>(
|
const { result: memberDetail } = await this.request<Folder>(
|
||||||
`${this.serverUrl}${subFolderUrl}`,
|
`${this.serverUrl}${subFolderUrl}`,
|
||||||
requestInfo
|
requestInfo
|
||||||
);
|
);
|
||||||
@@ -726,7 +866,7 @@ export class SASViyaApiClient {
|
|||||||
(l: any) => l.rel === "members"
|
(l: any) => l.rel === "members"
|
||||||
);
|
);
|
||||||
|
|
||||||
const memberContents = await this.request<{ items: any[] }>(
|
const { result: memberContents } = await this.request<{ items: any[] }>(
|
||||||
`${this.serverUrl}${membersLink!.href}`,
|
`${this.serverUrl}${membersLink!.href}`,
|
||||||
requestInfo
|
requestInfo
|
||||||
);
|
);
|
||||||
@@ -752,26 +892,28 @@ export class SASViyaApiClient {
|
|||||||
requestInfo
|
requestInfo
|
||||||
).catch(() => null);
|
).catch(() => null);
|
||||||
|
|
||||||
this.rootFolder = rootFolder;
|
this.rootFolder = rootFolder?.result || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async pollJobState(
|
private async pollJobState(
|
||||||
postedJob: any,
|
postedJob: any,
|
||||||
|
etag: string | null,
|
||||||
accessToken?: string,
|
accessToken?: string,
|
||||||
silent = false
|
silent = false
|
||||||
) {
|
) {
|
||||||
const MAX_POLL_COUNT = 1000;
|
const MAX_POLL_COUNT = 1000;
|
||||||
const POLL_INTERVAL = 300;
|
const POLL_INTERVAL = 100;
|
||||||
let postedJobState = "";
|
let postedJobState = "";
|
||||||
let pollCount = 0;
|
let pollCount = 0;
|
||||||
const headers: any = {
|
const headers: any = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
"If-None-Match": etag,
|
||||||
};
|
};
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
headers.Authorization = `Bearer ${accessToken}`;
|
headers.Authorization = `Bearer ${accessToken}`;
|
||||||
}
|
}
|
||||||
const stateLink = postedJob.links.find((l: any) => l.rel === "state");
|
const stateLink = postedJob.links.find((l: any) => l.rel === "state");
|
||||||
return new Promise((resolve, _) => {
|
return new Promise(async (resolve, _) => {
|
||||||
const interval = setInterval(async () => {
|
const interval = setInterval(async () => {
|
||||||
if (
|
if (
|
||||||
postedJobState === "running" ||
|
postedJobState === "running" ||
|
||||||
@@ -782,8 +924,8 @@ export class SASViyaApiClient {
|
|||||||
if (!silent) {
|
if (!silent) {
|
||||||
console.log("Polling job status... \n");
|
console.log("Polling job status... \n");
|
||||||
}
|
}
|
||||||
const jobState = await this.request<string>(
|
const { result: jobState } = await this.request<string>(
|
||||||
`${this.serverUrl}${stateLink.href}?wait=30`,
|
`${this.serverUrl}${stateLink.href}?_action=wait&wait=30`,
|
||||||
{
|
{
|
||||||
headers,
|
headers,
|
||||||
},
|
},
|
||||||
@@ -807,6 +949,49 @@ export class SASViyaApiClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async waitForSession(
|
||||||
|
session: Session,
|
||||||
|
etag: string | null,
|
||||||
|
accessToken?: string,
|
||||||
|
silent = false
|
||||||
|
) {
|
||||||
|
let sessionState = session.state;
|
||||||
|
let pollCount = 0;
|
||||||
|
const headers: any = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"If-None-Match": etag,
|
||||||
|
};
|
||||||
|
if (accessToken) {
|
||||||
|
headers.Authorization = `Bearer ${accessToken}`;
|
||||||
|
}
|
||||||
|
const stateLink = session.links.find((l: any) => l.rel === "state");
|
||||||
|
return new Promise(async (resolve, _) => {
|
||||||
|
if (sessionState === "pending") {
|
||||||
|
if (stateLink) {
|
||||||
|
if (!silent) {
|
||||||
|
console.log("Polling session status... \n");
|
||||||
|
}
|
||||||
|
const { result: state } = await this.request<string>(
|
||||||
|
`${this.serverUrl}${stateLink.href}?wait=30`,
|
||||||
|
{
|
||||||
|
headers,
|
||||||
|
},
|
||||||
|
"text"
|
||||||
|
);
|
||||||
|
|
||||||
|
sessionState = state.trim();
|
||||||
|
if (!silent) {
|
||||||
|
console.log(`Current state: ${sessionState}\n`);
|
||||||
|
}
|
||||||
|
pollCount++;
|
||||||
|
resolve(sessionState);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resolve(sessionState);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private async uploadTables(data: any, accessToken?: string) {
|
private async uploadTables(data: any, accessToken?: string) {
|
||||||
const uploadedFiles = [];
|
const uploadedFiles = [];
|
||||||
const headers: any = {
|
const headers: any = {
|
||||||
@@ -830,7 +1015,7 @@ export class SASViyaApiClient {
|
|||||||
headers,
|
headers,
|
||||||
};
|
};
|
||||||
|
|
||||||
const file = await this.request<any>(
|
const { result: file } = await this.request<any>(
|
||||||
`${this.serverUrl}/files/files#rawUpload`,
|
`${this.serverUrl}/files/files#rawUpload`,
|
||||||
createFileRequest
|
createFileRequest
|
||||||
);
|
);
|
||||||
@@ -848,7 +1033,7 @@ export class SASViyaApiClient {
|
|||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
requestInfo.headers = { Authorization: `Bearer ${accessToken}` };
|
requestInfo.headers = { Authorization: `Bearer ${accessToken}` };
|
||||||
}
|
}
|
||||||
const folder = await this.request<Folder>(
|
const { result: folder } = await this.request<Folder>(
|
||||||
`${this.serverUrl}${url}`,
|
`${this.serverUrl}${url}`,
|
||||||
requestInfo
|
requestInfo
|
||||||
);
|
);
|
||||||
|
|||||||
542
src/SASjs.ts
542
src/SASjs.ts
@@ -32,6 +32,7 @@ const defaultConfig: SASjsConfig = {
|
|||||||
serverType: ServerType.SASViya,
|
serverType: ServerType.SASViya,
|
||||||
debug: true,
|
debug: true,
|
||||||
contextName: "SAS Job Execution compute context",
|
contextName: "SAS Job Execution compute context",
|
||||||
|
useComputeApi: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const requestRetryLimit = 5;
|
const requestRetryLimit = 5;
|
||||||
@@ -390,239 +391,30 @@ export default class SASjs {
|
|||||||
this.sasjsConfig.serverType === ServerType.SASViya &&
|
this.sasjsConfig.serverType === ServerType.SASViya &&
|
||||||
this.sasjsConfig.contextName
|
this.sasjsConfig.contextName
|
||||||
) {
|
) {
|
||||||
return await this.executeViaJesApi(
|
if (this.sasjsConfig.useComputeApi) {
|
||||||
|
return await this.executeJobViaComputeApi(
|
||||||
|
sasJob,
|
||||||
|
data,
|
||||||
|
params,
|
||||||
|
loginRequiredCallback,
|
||||||
|
accessToken
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return await this.executeJobViaJesApi(
|
||||||
|
sasJob,
|
||||||
|
data,
|
||||||
|
params,
|
||||||
|
loginRequiredCallback,
|
||||||
|
accessToken
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return await this.executeJobViaJes(
|
||||||
sasJob,
|
sasJob,
|
||||||
data,
|
data,
|
||||||
params,
|
params,
|
||||||
loginRequiredCallback,
|
loginRequiredCallback
|
||||||
accessToken
|
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
const sasjsWaitingRequest: SASjsWaitingRequest = {
|
|
||||||
requestPromise: {
|
|
||||||
promise: null,
|
|
||||||
resolve: null,
|
|
||||||
reject: null,
|
|
||||||
},
|
|
||||||
SASjob: sasJob,
|
|
||||||
data,
|
|
||||||
params,
|
|
||||||
};
|
|
||||||
const program = this.sasjsConfig.appLoc
|
|
||||||
? this.sasjsConfig.appLoc.replace(/\/?$/, "/") +
|
|
||||||
sasJob.replace(/^\//, "")
|
|
||||||
: sasJob;
|
|
||||||
const jobUri =
|
|
||||||
this.sasjsConfig.serverType === "SASVIYA"
|
|
||||||
? await this.getJobUri(sasJob)
|
|
||||||
: "";
|
|
||||||
const apiUrl = `${this.sasjsConfig.serverUrl}${this.jobsPath}/?${
|
|
||||||
jobUri.length > 0
|
|
||||||
? "__program=" + program + "&_job=" + jobUri
|
|
||||||
: "_program=" + program
|
|
||||||
}`;
|
|
||||||
|
|
||||||
const inputParams = params ? params : {};
|
|
||||||
const requestParams = {
|
|
||||||
...inputParams,
|
|
||||||
...this.getRequestParams(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
|
|
||||||
let isError = false;
|
|
||||||
let errorMsg = "";
|
|
||||||
|
|
||||||
if (data) {
|
|
||||||
console.log("Input data", data);
|
|
||||||
const stringifiedData = JSON.stringify(data);
|
|
||||||
if (
|
|
||||||
this.sasjsConfig.serverType === ServerType.SAS9 ||
|
|
||||||
stringifiedData.length > 500000 ||
|
|
||||||
stringifiedData.includes(";")
|
|
||||||
) {
|
|
||||||
// file upload approach
|
|
||||||
for (const tableName in data) {
|
|
||||||
console.log("TableName: ", tableName);
|
|
||||||
if (isError) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const name = tableName;
|
|
||||||
const csv = convertToCSV(data[tableName]);
|
|
||||||
console.log("Converted CSV", csv);
|
|
||||||
if (csv === "ERROR: LARGE STRING LENGTH") {
|
|
||||||
console.log("String too long");
|
|
||||||
isError = true;
|
|
||||||
errorMsg =
|
|
||||||
"The max length of a string value in SASjs is 32765 characters.";
|
|
||||||
}
|
|
||||||
|
|
||||||
const file = new Blob([csv], {
|
|
||||||
type: "application/csv",
|
|
||||||
});
|
|
||||||
console.log("File", file);
|
|
||||||
|
|
||||||
formData.append(name, file, `${name}.csv`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// param based approach
|
|
||||||
const sasjsTables = [];
|
|
||||||
let tableCounter = 0;
|
|
||||||
for (const tableName in data) {
|
|
||||||
if (isError) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
tableCounter++;
|
|
||||||
sasjsTables.push(tableName);
|
|
||||||
const csv = convertToCSV(data[tableName]);
|
|
||||||
if (csv === "ERROR: LARGE STRING LENGTH") {
|
|
||||||
isError = true;
|
|
||||||
errorMsg =
|
|
||||||
"The max length of a string value in SASjs is 32765 characters.";
|
|
||||||
}
|
|
||||||
// if csv has length more then 16k, send in chunks
|
|
||||||
if (csv.length > 16000) {
|
|
||||||
const csvChunks = splitChunks(csv);
|
|
||||||
// append chunks to form data with same key
|
|
||||||
csvChunks.map((chunk) => {
|
|
||||||
formData.append(`sasjs${tableCounter}data`, chunk);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
requestParams[`sasjs${tableCounter}data`] = csv;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
requestParams["sasjs_tables"] = sasjsTables.join(" ");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const key in requestParams) {
|
|
||||||
if (requestParams.hasOwnProperty(key)) {
|
|
||||||
formData.append(key, requestParams[key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Form data", formData);
|
|
||||||
|
|
||||||
let isRedirected = false;
|
|
||||||
|
|
||||||
sasjsWaitingRequest.requestPromise.promise = new Promise(
|
|
||||||
(resolve, reject) => {
|
|
||||||
if (isError) {
|
|
||||||
reject({ MESSAGE: errorMsg });
|
|
||||||
}
|
|
||||||
const headers: any = {};
|
|
||||||
if (this._csrfHeader && this._csrf) {
|
|
||||||
headers[this._csrfHeader] = this._csrf;
|
|
||||||
}
|
|
||||||
fetch(apiUrl, {
|
|
||||||
method: "POST",
|
|
||||||
body: formData,
|
|
||||||
referrerPolicy: "same-origin",
|
|
||||||
headers,
|
|
||||||
})
|
|
||||||
.then(async (response) => {
|
|
||||||
if (!response.ok) {
|
|
||||||
if (response.status === 403) {
|
|
||||||
const tokenHeader = response.headers.get("X-CSRF-HEADER");
|
|
||||||
|
|
||||||
if (tokenHeader) {
|
|
||||||
const token = response.headers.get(tokenHeader);
|
|
||||||
this._csrfHeader = tokenHeader;
|
|
||||||
this._csrf = token;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
response.redirected &&
|
|
||||||
this.sasjsConfig.serverType === ServerType.SAS9
|
|
||||||
) {
|
|
||||||
isRedirected = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.text();
|
|
||||||
})
|
|
||||||
.then((responseText) => {
|
|
||||||
if (
|
|
||||||
(needsRetry(responseText) || isRedirected) &&
|
|
||||||
!isLogInRequired(responseText)
|
|
||||||
) {
|
|
||||||
if (this.retryCount < requestRetryLimit) {
|
|
||||||
this.retryCount++;
|
|
||||||
this.request(sasJob, data, params).then(
|
|
||||||
(res: any) => resolve(res),
|
|
||||||
(err: any) => reject(err)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.retryCount = 0;
|
|
||||||
reject(responseText);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.retryCount = 0;
|
|
||||||
this.parseLogFromResponse(responseText, program);
|
|
||||||
|
|
||||||
if (isLogInRequired(responseText)) {
|
|
||||||
if (loginRequiredCallback) loginRequiredCallback(true);
|
|
||||||
sasjsWaitingRequest.requestPromise.resolve = resolve;
|
|
||||||
sasjsWaitingRequest.requestPromise.reject = reject;
|
|
||||||
this.sasjsWaitingRequests.push(sasjsWaitingRequest);
|
|
||||||
} else {
|
|
||||||
if (
|
|
||||||
this.sasjsConfig.serverType === ServerType.SAS9 &&
|
|
||||||
this.sasjsConfig.debug
|
|
||||||
) {
|
|
||||||
this.updateUsername(responseText);
|
|
||||||
const jsonResponseText = this.parseSAS9Response(
|
|
||||||
responseText
|
|
||||||
);
|
|
||||||
|
|
||||||
if (jsonResponseText !== "") {
|
|
||||||
resolve(JSON.parse(jsonResponseText));
|
|
||||||
} else {
|
|
||||||
reject({
|
|
||||||
MESSAGE: this.parseSAS9ErrorResponse(responseText),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (
|
|
||||||
this.sasjsConfig.serverType === ServerType.SASViya &&
|
|
||||||
this.sasjsConfig.debug
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
this.parseSASVIYADebugResponse(responseText).then(
|
|
||||||
(resText: any) => {
|
|
||||||
this.updateUsername(resText);
|
|
||||||
try {
|
|
||||||
resolve(JSON.parse(resText));
|
|
||||||
} catch (e) {
|
|
||||||
reject({ MESSAGE: resText });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
(err: any) => {
|
|
||||||
reject({ MESSAGE: err });
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
reject({ MESSAGE: responseText });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.updateUsername(responseText);
|
|
||||||
try {
|
|
||||||
const parsedJson = JSON.parse(responseText);
|
|
||||||
resolve(parsedJson);
|
|
||||||
} catch (e) {
|
|
||||||
reject({ MESSAGE: responseText });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((e: Error) => {
|
|
||||||
reject(e);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return sasjsWaitingRequest.requestPromise.promise;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -682,7 +474,62 @@ export default class SASjs {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async executeViaJesApi(
|
private async executeJobViaComputeApi(
|
||||||
|
sasJob: string,
|
||||||
|
data: any,
|
||||||
|
params?: any,
|
||||||
|
loginRequiredCallback?: any,
|
||||||
|
accessToken?: string
|
||||||
|
) {
|
||||||
|
const sasjsWaitingRequest: SASjsWaitingRequest = {
|
||||||
|
requestPromise: {
|
||||||
|
promise: null,
|
||||||
|
resolve: null,
|
||||||
|
reject: null,
|
||||||
|
},
|
||||||
|
SASjob: sasJob,
|
||||||
|
data,
|
||||||
|
params,
|
||||||
|
};
|
||||||
|
|
||||||
|
sasjsWaitingRequest.requestPromise.promise = new Promise(
|
||||||
|
async (resolve, reject) => {
|
||||||
|
const session = await this.checkSession();
|
||||||
|
|
||||||
|
if (!session.isLoggedIn) {
|
||||||
|
if (loginRequiredCallback) loginRequiredCallback(true);
|
||||||
|
sasjsWaitingRequest.requestPromise.resolve = resolve;
|
||||||
|
sasjsWaitingRequest.requestPromise.reject = reject;
|
||||||
|
this.sasjsWaitingRequests.push(sasjsWaitingRequest);
|
||||||
|
} else {
|
||||||
|
resolve(
|
||||||
|
await this.sasViyaApiClient
|
||||||
|
?.executeComputeJob(
|
||||||
|
sasJob,
|
||||||
|
this.sasjsConfig.contextName,
|
||||||
|
this.sasjsConfig.debug,
|
||||||
|
data,
|
||||||
|
accessToken
|
||||||
|
)
|
||||||
|
.then((response) => {
|
||||||
|
if (!this.sasjsConfig.debug) {
|
||||||
|
this.appendSasjsRequest(null, sasJob, null);
|
||||||
|
} else {
|
||||||
|
this.appendSasjsRequest(response, sasJob, null);
|
||||||
|
}
|
||||||
|
return JSON.parse(response!.result);
|
||||||
|
})
|
||||||
|
.catch((e) =>
|
||||||
|
reject({ MESSAGE: (e && e.message) || "Job execution failed" })
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return sasjsWaitingRequest.requestPromise.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async executeJobViaJesApi(
|
||||||
sasJob: string,
|
sasJob: string,
|
||||||
data: any,
|
data: any,
|
||||||
params?: any,
|
params?: any,
|
||||||
@@ -725,9 +572,11 @@ export default class SASjs {
|
|||||||
} else {
|
} else {
|
||||||
this.appendSasjsRequest(response, sasJob, null);
|
this.appendSasjsRequest(response, sasJob, null);
|
||||||
}
|
}
|
||||||
return JSON.parse(response.result);
|
return JSON.parse(response!.result);
|
||||||
})
|
})
|
||||||
.catch((e) => reject({ MESSAGE: e.message }))
|
.catch((e) =>
|
||||||
|
reject({ MESSAGE: (e && e.message) || "Job execution failed" })
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -735,6 +584,229 @@ export default class SASjs {
|
|||||||
return sasjsWaitingRequest.requestPromise.promise;
|
return sasjsWaitingRequest.requestPromise.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async executeJobViaJes(
|
||||||
|
sasJob: string,
|
||||||
|
data: any,
|
||||||
|
params?: any,
|
||||||
|
loginRequiredCallback?: any
|
||||||
|
) {
|
||||||
|
const sasjsWaitingRequest: SASjsWaitingRequest = {
|
||||||
|
requestPromise: {
|
||||||
|
promise: null,
|
||||||
|
resolve: null,
|
||||||
|
reject: null,
|
||||||
|
},
|
||||||
|
SASjob: sasJob,
|
||||||
|
data,
|
||||||
|
params,
|
||||||
|
};
|
||||||
|
const program = this.sasjsConfig.appLoc
|
||||||
|
? this.sasjsConfig.appLoc.replace(/\/?$/, "/") + sasJob.replace(/^\//, "")
|
||||||
|
: sasJob;
|
||||||
|
const jobUri =
|
||||||
|
this.sasjsConfig.serverType === "SASVIYA"
|
||||||
|
? await this.getJobUri(sasJob)
|
||||||
|
: "";
|
||||||
|
const apiUrl = `${this.sasjsConfig.serverUrl}${this.jobsPath}/?${
|
||||||
|
jobUri.length > 0
|
||||||
|
? "__program=" + program + "&_job=" + jobUri
|
||||||
|
: "_program=" + program
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const inputParams = params ? params : {};
|
||||||
|
const requestParams = {
|
||||||
|
...inputParams,
|
||||||
|
...this.getRequestParams(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
let isError = false;
|
||||||
|
let errorMsg = "";
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
const stringifiedData = JSON.stringify(data);
|
||||||
|
if (
|
||||||
|
this.sasjsConfig.serverType === ServerType.SAS9 ||
|
||||||
|
stringifiedData.length > 500000 ||
|
||||||
|
stringifiedData.includes(";")
|
||||||
|
) {
|
||||||
|
// file upload approach
|
||||||
|
for (const tableName in data) {
|
||||||
|
if (isError) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const name = tableName;
|
||||||
|
const csv = convertToCSV(data[tableName]);
|
||||||
|
if (csv === "ERROR: LARGE STRING LENGTH") {
|
||||||
|
isError = true;
|
||||||
|
errorMsg =
|
||||||
|
"The max length of a string value in SASjs is 32765 characters.";
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = new Blob([csv], {
|
||||||
|
type: "application/csv",
|
||||||
|
});
|
||||||
|
|
||||||
|
formData.append(name, file, `${name}.csv`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// param based approach
|
||||||
|
const sasjsTables = [];
|
||||||
|
let tableCounter = 0;
|
||||||
|
for (const tableName in data) {
|
||||||
|
if (isError) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tableCounter++;
|
||||||
|
sasjsTables.push(tableName);
|
||||||
|
const csv = convertToCSV(data[tableName]);
|
||||||
|
if (csv === "ERROR: LARGE STRING LENGTH") {
|
||||||
|
isError = true;
|
||||||
|
errorMsg =
|
||||||
|
"The max length of a string value in SASjs is 32765 characters.";
|
||||||
|
}
|
||||||
|
// if csv has length more then 16k, send in chunks
|
||||||
|
if (csv.length > 16000) {
|
||||||
|
const csvChunks = splitChunks(csv);
|
||||||
|
// append chunks to form data with same key
|
||||||
|
csvChunks.map((chunk) => {
|
||||||
|
formData.append(`sasjs${tableCounter}data`, chunk);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
requestParams[`sasjs${tableCounter}data`] = csv;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
requestParams["sasjs_tables"] = sasjsTables.join(" ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key in requestParams) {
|
||||||
|
if (requestParams.hasOwnProperty(key)) {
|
||||||
|
formData.append(key, requestParams[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let isRedirected = false;
|
||||||
|
|
||||||
|
sasjsWaitingRequest.requestPromise.promise = new Promise(
|
||||||
|
(resolve, reject) => {
|
||||||
|
if (isError) {
|
||||||
|
reject({ MESSAGE: errorMsg });
|
||||||
|
}
|
||||||
|
const headers: any = {};
|
||||||
|
if (this._csrfHeader && this._csrf) {
|
||||||
|
headers[this._csrfHeader] = this._csrf;
|
||||||
|
}
|
||||||
|
fetch(apiUrl, {
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
referrerPolicy: "same-origin",
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
.then(async (response) => {
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 403) {
|
||||||
|
const tokenHeader = response.headers.get("X-CSRF-HEADER");
|
||||||
|
|
||||||
|
if (tokenHeader) {
|
||||||
|
const token = response.headers.get(tokenHeader);
|
||||||
|
this._csrfHeader = tokenHeader;
|
||||||
|
this._csrf = token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
response.redirected &&
|
||||||
|
this.sasjsConfig.serverType === ServerType.SAS9
|
||||||
|
) {
|
||||||
|
isRedirected = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.text();
|
||||||
|
})
|
||||||
|
.then((responseText) => {
|
||||||
|
if (
|
||||||
|
(needsRetry(responseText) || isRedirected) &&
|
||||||
|
!isLogInRequired(responseText)
|
||||||
|
) {
|
||||||
|
if (this.retryCount < requestRetryLimit) {
|
||||||
|
this.retryCount++;
|
||||||
|
this.request(sasJob, data, params).then(
|
||||||
|
(res: any) => resolve(res),
|
||||||
|
(err: any) => reject(err)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.retryCount = 0;
|
||||||
|
reject(responseText);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.retryCount = 0;
|
||||||
|
this.parseLogFromResponse(responseText, program);
|
||||||
|
|
||||||
|
if (isLogInRequired(responseText)) {
|
||||||
|
if (loginRequiredCallback) loginRequiredCallback(true);
|
||||||
|
sasjsWaitingRequest.requestPromise.resolve = resolve;
|
||||||
|
sasjsWaitingRequest.requestPromise.reject = reject;
|
||||||
|
this.sasjsWaitingRequests.push(sasjsWaitingRequest);
|
||||||
|
} else {
|
||||||
|
if (
|
||||||
|
this.sasjsConfig.serverType === ServerType.SAS9 &&
|
||||||
|
this.sasjsConfig.debug
|
||||||
|
) {
|
||||||
|
this.updateUsername(responseText);
|
||||||
|
const jsonResponseText = this.parseSAS9Response(responseText);
|
||||||
|
|
||||||
|
if (jsonResponseText !== "") {
|
||||||
|
resolve(JSON.parse(jsonResponseText));
|
||||||
|
} else {
|
||||||
|
reject({
|
||||||
|
MESSAGE: this.parseSAS9ErrorResponse(responseText),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
this.sasjsConfig.serverType === ServerType.SASViya &&
|
||||||
|
this.sasjsConfig.debug
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
this.parseSASVIYADebugResponse(responseText).then(
|
||||||
|
(resText: any) => {
|
||||||
|
this.updateUsername(resText);
|
||||||
|
try {
|
||||||
|
resolve(JSON.parse(resText));
|
||||||
|
} catch (e) {
|
||||||
|
reject({ MESSAGE: resText });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(err: any) => {
|
||||||
|
reject({ MESSAGE: err });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
reject({ MESSAGE: responseText });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.updateUsername(responseText);
|
||||||
|
try {
|
||||||
|
const parsedJson = JSON.parse(responseText);
|
||||||
|
resolve(parsedJson);
|
||||||
|
} catch (e) {
|
||||||
|
reject({ MESSAGE: responseText });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e: Error) => {
|
||||||
|
reject(e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return sasjsWaitingRequest.requestPromise.promise;
|
||||||
|
}
|
||||||
|
|
||||||
private async resendWaitingRequests() {
|
private async resendWaitingRequests() {
|
||||||
for (const sasjsWaitingRequest of this.sasjsWaitingRequests) {
|
for (const sasjsWaitingRequest of this.sasjsWaitingRequests) {
|
||||||
this.request(
|
this.request(
|
||||||
@@ -930,7 +1002,7 @@ export default class SASjs {
|
|||||||
try {
|
try {
|
||||||
jsonResponse = JSON.parse(this.parseSAS9Response(response));
|
jsonResponse = JSON.parse(this.parseSAS9Response(response));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await this.parseSASVIYADebugResponse(response).then(
|
await this.parseSASVIYADebugResponse(response).then(
|
||||||
@@ -938,11 +1010,11 @@ export default class SASjs {
|
|||||||
try {
|
try {
|
||||||
jsonResponse = JSON.parse(resText);
|
jsonResponse = JSON.parse(resText);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
(err: any) => {
|
(err: any) => {
|
||||||
console.log(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
3
src/types/JobDefinition.ts
Normal file
3
src/types/JobDefinition.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export interface JobDefinition {
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
@@ -27,4 +27,5 @@ export class SASjsConfig {
|
|||||||
*/
|
*/
|
||||||
debug: boolean = true;
|
debug: boolean = true;
|
||||||
contextName: string = "";
|
contextName: string = "";
|
||||||
|
useComputeApi = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import { Link } from "./Link";
|
||||||
|
|
||||||
export interface Session {
|
export interface Session {
|
||||||
id: string;
|
id: string;
|
||||||
|
state: string;
|
||||||
|
links: Link[];
|
||||||
}
|
}
|
||||||
|
|||||||
33
src/utils/formatDataForRequest.ts
Normal file
33
src/utils/formatDataForRequest.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { convertToCSV } from "./convertToCsv";
|
||||||
|
import { splitChunks } from "./splitChunks";
|
||||||
|
|
||||||
|
export const formatDataForRequest = (data: any) => {
|
||||||
|
const sasjsTables = [];
|
||||||
|
let tableCounter = 0;
|
||||||
|
const result: any = {};
|
||||||
|
|
||||||
|
for (const tableName in data) {
|
||||||
|
tableCounter++;
|
||||||
|
sasjsTables.push(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."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// if csv has length more then 16k, send in chunks
|
||||||
|
if (csv.length > 16000) {
|
||||||
|
const csvChunks = splitChunks(csv);
|
||||||
|
// append chunks to form data with same key
|
||||||
|
result[`sasjs${tableCounter}data0`] = csvChunks.length;
|
||||||
|
csvChunks.forEach((chunk, index) => {
|
||||||
|
result[`sasjs${tableCounter}data${index + 1}`] = chunk;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
result[`sasjs${tableCounter}data`] = csv;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result["sasjs_tables"] = sasjsTables.join(" ");
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
@@ -5,11 +5,12 @@ export async function makeRequest<T>(
|
|||||||
request: RequestInit,
|
request: RequestInit,
|
||||||
callback: (value: CsrfToken) => any,
|
callback: (value: CsrfToken) => any,
|
||||||
contentType: "text" | "json" = "json"
|
contentType: "text" | "json" = "json"
|
||||||
): Promise<T> {
|
): Promise<{ result: T; etag: string | null }> {
|
||||||
const responseTransform =
|
const responseTransform =
|
||||||
contentType === "json"
|
contentType === "json"
|
||||||
? (res: Response) => res.json()
|
? (res: Response) => res.json()
|
||||||
: (res: Response) => res.text();
|
: (res: Response) => res.text();
|
||||||
|
let etag = null;
|
||||||
const result = await fetch(url, request).then((response) => {
|
const result = await fetch(url, request).then((response) => {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
if (response.status === 403) {
|
if (response.status === 403) {
|
||||||
@@ -26,12 +27,16 @@ export async function makeRequest<T>(
|
|||||||
...request,
|
...request,
|
||||||
headers: { ...request.headers, [tokenHeader]: token },
|
headers: { ...request.headers, [tokenHeader]: token },
|
||||||
};
|
};
|
||||||
return fetch(url, retryRequest).then(responseTransform);
|
return fetch(url, retryRequest).then((res) => {
|
||||||
|
etag = res.headers.get("ETag");
|
||||||
|
return responseTransform(res);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
etag = response.headers.get("ETag");
|
||||||
return responseTransform(response);
|
return responseTransform(response);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return result;
|
return { result, etag };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user