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

feat(compute-api): implement job execution via compute API

This commit is contained in:
Krishna Acondy
2020-07-16 09:06:24 +01:00
parent 92504b0c16
commit 7d84033ad4
11 changed files with 584 additions and 280 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,7 +391,8 @@ 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, sasJob,
data, data,
params, params,
@@ -398,6 +400,196 @@ export default class SASjs {
accessToken accessToken
); );
} else { } else {
return await this.executeJobViaJesApi(
sasJob,
data,
params,
loginRequiredCallback,
accessToken
);
}
} else {
return await this.executeJobViaJes(
sasJob,
data,
params,
loginRequiredCallback
);
}
}
/**
* Creates the folders and services in the provided JSON on the given location
* (appLoc) on the given server (serverUrl).
* @param serviceJson - the JSON specifying the folders and services to be created.
* @param appLoc - the base folder in which to create the new folders and
* services. If not provided, is taken from SASjsConfig.
* @param serverUrl - the server on which to deploy the folders and services.
* If not provided, is taken from SASjsConfig.
* @param accessToken - an optional access token to be passed in when
* using this function from the command line.
*/
public async deployServicePack(
serviceJson: any,
appLoc?: string,
serverUrl?: string,
accessToken?: string
) {
if (this.sasjsConfig.serverType !== ServerType.SASViya) {
throw new Error("This operation is only supported on SAS Viya servers.");
}
let sasApiClient: any = null;
if (serverUrl || appLoc) {
if (!serverUrl) {
serverUrl = this.sasjsConfig.serverUrl;
}
if (!appLoc) {
appLoc = this.sasjsConfig.appLoc;
}
if (this.sasjsConfig.serverType === ServerType.SASViya) {
sasApiClient = new SASViyaApiClient(serverUrl, appLoc);
} else if (this.sasjsConfig.serverType === ServerType.SAS9) {
sasApiClient = new SAS9ApiClient(serverUrl);
}
} else {
let sasClientConfig: any = null;
if (this.sasjsConfig.serverType === ServerType.SASViya) {
sasClientConfig = this.sasViyaApiClient!.getConfig();
} else if (this.sasjsConfig.serverType === ServerType.SAS9) {
sasClientConfig = this.sas9ApiClient!.getConfig();
}
serverUrl = sasClientConfig.serverUrl;
appLoc = sasClientConfig.rootFolderName as string;
}
const members =
serviceJson.members[0].name === "services"
? serviceJson.members[0].members
: serviceJson.members;
await this.createFoldersAndServices(
appLoc,
members,
accessToken,
sasApiClient
);
}
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,
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
?.executeJob(
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 executeJobViaJes(
sasJob: string,
data: any,
params?: any,
loginRequiredCallback?: any
) {
const sasjsWaitingRequest: SASjsWaitingRequest = { const sasjsWaitingRequest: SASjsWaitingRequest = {
requestPromise: { requestPromise: {
promise: null, promise: null,
@@ -409,8 +601,7 @@ export default class SASjs {
params, params,
}; };
const program = this.sasjsConfig.appLoc const program = this.sasjsConfig.appLoc
? this.sasjsConfig.appLoc.replace(/\/?$/, "/") + ? this.sasjsConfig.appLoc.replace(/\/?$/, "/") + sasJob.replace(/^\//, "")
sasJob.replace(/^\//, "")
: sasJob; : sasJob;
const jobUri = const jobUri =
this.sasjsConfig.serverType === "SASVIYA" this.sasjsConfig.serverType === "SASVIYA"
@@ -434,7 +625,6 @@ export default class SASjs {
let errorMsg = ""; let errorMsg = "";
if (data) { if (data) {
console.log("Input data", data);
const stringifiedData = JSON.stringify(data); const stringifiedData = JSON.stringify(data);
if ( if (
this.sasjsConfig.serverType === ServerType.SAS9 || this.sasjsConfig.serverType === ServerType.SAS9 ||
@@ -443,15 +633,12 @@ export default class SASjs {
) { ) {
// file upload approach // file upload approach
for (const tableName in data) { for (const tableName in data) {
console.log("TableName: ", tableName);
if (isError) { if (isError) {
return; return;
} }
const name = tableName; const name = tableName;
const csv = convertToCSV(data[tableName]); const csv = convertToCSV(data[tableName]);
console.log("Converted CSV", csv);
if (csv === "ERROR: LARGE STRING LENGTH") { if (csv === "ERROR: LARGE STRING LENGTH") {
console.log("String too long");
isError = true; isError = true;
errorMsg = errorMsg =
"The max length of a string value in SASjs is 32765 characters."; "The max length of a string value in SASjs is 32765 characters.";
@@ -460,7 +647,6 @@ export default class SASjs {
const file = new Blob([csv], { const file = new Blob([csv], {
type: "application/csv", type: "application/csv",
}); });
console.log("File", file);
formData.append(name, file, `${name}.csv`); formData.append(name, file, `${name}.csv`);
} }
@@ -501,8 +687,6 @@ export default class SASjs {
} }
} }
console.log("Form data", formData);
let isRedirected = false; let isRedirected = false;
sasjsWaitingRequest.requestPromise.promise = new Promise( sasjsWaitingRequest.requestPromise.promise = new Promise(
@@ -572,9 +756,7 @@ export default class SASjs {
this.sasjsConfig.debug this.sasjsConfig.debug
) { ) {
this.updateUsername(responseText); this.updateUsername(responseText);
const jsonResponseText = this.parseSAS9Response( const jsonResponseText = this.parseSAS9Response(responseText);
responseText
);
if (jsonResponseText !== "") { if (jsonResponseText !== "") {
resolve(JSON.parse(jsonResponseText)); resolve(JSON.parse(jsonResponseText));
@@ -624,116 +806,6 @@ export default class SASjs {
return sasjsWaitingRequest.requestPromise.promise; return sasjsWaitingRequest.requestPromise.promise;
} }
}
/**
* Creates the folders and services in the provided JSON on the given location
* (appLoc) on the given server (serverUrl).
* @param serviceJson - the JSON specifying the folders and services to be created.
* @param appLoc - the base folder in which to create the new folders and
* services. If not provided, is taken from SASjsConfig.
* @param serverUrl - the server on which to deploy the folders and services.
* If not provided, is taken from SASjsConfig.
* @param accessToken - an optional access token to be passed in when
* using this function from the command line.
*/
public async deployServicePack(
serviceJson: any,
appLoc?: string,
serverUrl?: string,
accessToken?: string
) {
if (this.sasjsConfig.serverType !== ServerType.SASViya) {
throw new Error("This operation is only supported on SAS Viya servers.");
}
let sasApiClient: any = null;
if (serverUrl || appLoc) {
if (!serverUrl) {
serverUrl = this.sasjsConfig.serverUrl;
}
if (!appLoc) {
appLoc = this.sasjsConfig.appLoc;
}
if (this.sasjsConfig.serverType === ServerType.SASViya) {
sasApiClient = new SASViyaApiClient(serverUrl, appLoc);
} else if (this.sasjsConfig.serverType === ServerType.SAS9) {
sasApiClient = new SAS9ApiClient(serverUrl);
}
} else {
let sasClientConfig: any = null;
if (this.sasjsConfig.serverType === ServerType.SASViya) {
sasClientConfig = this.sasViyaApiClient!.getConfig();
} else if (this.sasjsConfig.serverType === ServerType.SAS9) {
sasClientConfig = this.sas9ApiClient!.getConfig();
}
serverUrl = sasClientConfig.serverUrl;
appLoc = sasClientConfig.rootFolderName as string;
}
const members =
serviceJson.members[0].name === "services"
? serviceJson.members[0].members
: serviceJson.members;
await this.createFoldersAndServices(
appLoc,
members,
accessToken,
sasApiClient
);
}
private async executeViaJesApi(
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
?.executeJob(
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.message }))
);
}
}
);
return sasjsWaitingRequest.requestPromise.promise;
}
private async resendWaitingRequests() { private async resendWaitingRequests() {
for (const sasjsWaitingRequest of this.sasjsWaitingRequests) { for (const sasjsWaitingRequest of this.sasjsWaitingRequests) {
@@ -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);
} }
); );
} }

View File

@@ -0,0 +1,3 @@
export interface JobDefinition {
code: string;
}

View File

@@ -27,4 +27,5 @@ export class SASjsConfig {
*/ */
debug: boolean = true; debug: boolean = true;
contextName: string = ""; contextName: string = "";
useComputeApi = false;
} }

View File

@@ -1,3 +1,7 @@
import { Link } from "./Link";
export interface Session { export interface Session {
id: string; id: string;
state: string;
links: Link[];
} }

View 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;
};

View File

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