1
0
mirror of https://github.com/sasjs/adapter.git synced 2026-01-03 18:50:05 +00:00
Files
adapter/src/SASViyaApiClient.ts
2020-07-29 13:20:03 +02:00

1045 lines
31 KiB
TypeScript

import {
isAuthorizeFormRequired,
parseAndSubmitAuthorizeForm,
convertToCSV,
makeRequest,
} from "./utils";
import * as NodeFormData from "form-data";
import * as path from "path";
import { Job, Session, Context, Folder, CsrfToken } from "./types";
import { JobDefinition } from "./types/JobDefinition";
import { formatDataForRequest } from "./utils/formatDataForRequest";
import { SessionManager } from "./SessionManager";
/**
* A client for interfacing with the SAS Viya REST API
*
*/
export class SASViyaApiClient {
constructor(
private serverUrl: string,
private rootFolderName: string,
private contextName: string,
private setCsrfToken: (csrfToken: CsrfToken) => void,
private rootFolderMap = new Map<string, Job[]>()
) {
if (!rootFolderName) {
throw new Error("Root folder must be provided.");
}
}
private csrfToken: CsrfToken | null = null;
private rootFolder: Folder | null = null;
private sessionManager = new SessionManager(this.serverUrl, this.contextName, this.setCsrfToken);
/**
* Returns a map containing the directory structure in the currently set root folder.
*/
public async getAppLocMap() {
if (this.rootFolderMap.size) {
return this.rootFolderMap;
}
this.populateRootFolderMap();
return this.rootFolderMap;
}
/**
* returns an object containing the Server URL and root folder name
*/
public getConfig() {
return {
serverUrl: this.serverUrl,
rootFolderName: this.rootFolderName,
};
}
/**
* Updates server URL or root folder name when not null
* @param serverUrl - the URL of the server.
* @param rootFolderName - the name for rootFolderName.
*/
public setConfig(serverUrl: string, rootFolderName: string) {
if (serverUrl) this.serverUrl = serverUrl;
if (rootFolderName) this.rootFolderName = rootFolderName;
}
/**
* Returns all available compute contexts on this server.
* @param accessToken - an access token for an authorized user.
*/
public async getAllContexts(accessToken?: string) {
const headers: any = {
"Content-Type": "application/json",
};
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`;
}
const { result: contexts } = await this.request<{ items: Context[] }>(
`${this.serverUrl}/compute/contexts`,
{ headers }
);
const contextsList = contexts && contexts.items ? contexts.items : [];
return contextsList.map((context: any) => ({
createdBy: context.createdBy,
id: context.id,
name: context.name,
version: context.version,
attributes: {},
}));
}
/**
* Returns all compute contexts on this server that the user has access to.
* @param accessToken - an access token for an authorized user.
*/
public async getExecutableContexts(accessToken?: string) {
const headers: any = {
"Content-Type": "application/json",
};
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`;
}
const { result: contexts } = await this.request<{ items: Context[] }>(
`${this.serverUrl}/compute/contexts`,
{ headers }
);
const contextsList = contexts && contexts.items ? contexts.items : [];
const executableContexts: any[] = [];
const promises = contextsList.map((context: any) => {
const linesOfCode = ["%put &=sysuserid;"];
return this.executeScript(
`test-${context.name}`,
linesOfCode,
context.name,
accessToken,
undefined,
true
).catch(() => null);
});
const results = await Promise.all(promises);
results.forEach((result: any, index: number) => {
if (result && result.jobStatus === "completed") {
let sysUserId = "";
if (result && result.log && result.log.items) {
const sysUserIdLog = result.log.items.find((i: any) =>
i.line.startsWith("SYSUSERID=")
);
if (sysUserIdLog) {
sysUserId = sysUserIdLog.line.replace("SYSUSERID=", "");
}
}
executableContexts.push({
createdBy: contextsList[index].createdBy,
id: contextsList[index].id,
name: contextsList[index].name,
version: contextsList[index].version,
attributes: {
sysUserId,
},
});
}
});
return executableContexts;
}
/**
* Creates a session on the given context.
* @param contextName - the name of the context to create a session on.
* @param accessToken - an access token for an authorized user.
*/
public async createSession(contextName: string, accessToken?: string) {
const headers: any = {
"Content-Type": "application/json",
};
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`;
}
const { result: contexts } = await this.request<{ items: Context[] }>(
`${this.serverUrl}/compute/contexts`,
{ headers }
);
const executionContext =
contexts.items && contexts.items.length
? contexts.items.find((c: any) => c.name === contextName)
: null;
if (!executionContext) {
throw new Error(`Execution context ${contextName} not found.`);
}
const createSessionRequest = {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
};
const { result: createdSession } = await this.request<Session>(
`${this.serverUrl}/compute/contexts/${executionContext.id}/sessions`,
createSessionRequest
);
return createdSession;
}
/**
* Executes code on the current SAS Viya server.
* @param fileName - a name for the file being submitted for execution.
* @param linesOfCode - an array of lines of code to execute.
* @param contextName - the context to execute the code in.
* @param accessToken - an access token for an authorized user.
* @param sessionId - optional session ID to reuse.
* @param silent - optional flag to turn of logging.
*/
public async executeScript(
jobName: string,
linesOfCode: string[],
contextName: string,
accessToken?: string,
sessionId = "",
silent = false,
data = null,
debug = false
) {
const headers: any = {
"Content-Type": "application/json",
};
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`;
}
let executionSessionId: string;
const session = await this.sessionManager.getSession(accessToken);
executionSessionId = session!.id;
const 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: this.rootFolderName + "/" + 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
const postJobRequest = {
method: "POST",
headers,
body: JSON.stringify({
name: fileName,
description: "Powered by SASjs",
code: linesOfCode,
variables: jobVariables,
arguments: jobArguments,
}),
};
const { result: postedJob, etag } = await this.request<Job>(
`${this.serverUrl}/compute/sessions/${executionSessionId}/jobs`,
postJobRequest
);
if (!silent) {
console.log(`Job has been submitted for ${fileName}`);
console.log(
`You can monitor the job progress at ${this.serverUrl}${
postedJob.links.find((l: any) => l.rel === "state")!.href
}`
);
}
const jobStatus = await this.pollJobState(
postedJob,
etag,
accessToken,
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,
}
).then((res: any) => res.result.items.map((i: any) => i.line).join("\n"));
}
return { result: jobResult?.result, log };
// } else {
// console.error(
// `Unable to find execution context ${contextName}.\nPlease check the contextName in the tgtDeployVars and try again.`
// );
// console.error("Response from server: ", JSON.stringify(this.contexts));
// }
}
/**
* Creates a folder in the specified location. Either parentFolderPath or
* parentFolderUri must be provided.
* @param folderName - the name of the new folder.
* @param parentFolderPath - the full path to the parent folder. If not
* provided, the parentFolderUri must be provided.
* @param parentFolderUri - the URI (eg /folders/folders/UUID) of the parent
* folder. If not provided, the parentFolderPath must be provided.
*/
public async createFolder(
folderName: string,
parentFolderPath?: string,
parentFolderUri?: string,
accessToken?: string
): Promise<Folder> {
if (!parentFolderPath && !parentFolderUri) {
throw new Error("Parent folder path or uri is required");
}
if (!parentFolderUri && parentFolderPath) {
parentFolderUri = await this.getFolderUri(parentFolderPath, accessToken);
if (!parentFolderUri) {
console.log(`Parent folder is not present: ${parentFolderPath}`);
const newParentFolderPath = parentFolderPath.substring(
0,
parentFolderPath.lastIndexOf("/")
);
const newFolderName = `${parentFolderPath.split("/").pop()}`;
if (newParentFolderPath === "") {
throw new Error("Root Folder should have been present on server");
}
console.log(
`Creating Parent Folder:\n${newFolderName} in ${newParentFolderPath}`
);
const parentFolder = await this.createFolder(
newFolderName,
newParentFolderPath,
undefined,
accessToken
);
console.log(`Parent Folder "${newFolderName}" successfully created.`);
parentFolderUri = `/folders/folders/${parentFolder.id}`;
}
}
const createFolderRequest: RequestInit = {
method: "POST",
body: JSON.stringify({
name: folderName,
type: "folder",
}),
};
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 rootFolderMap with newly created folder.
await this.populateRootFolderMap(accessToken);
return createFolderResponse;
}
/**
* Creates a Job in the specified folder (or folder uri).
* @param parentFolderPath - the location of the new job.
* @param parentFolderUri - the URI location of the new job. The function is a
* little faster if the folder URI is supplied instead of the path.
* @param jobName - the name of the new job to be created.
* @param code - the SAS code for the new job.
*/
public async createJobDefinition(
jobName: string,
code: string,
parentFolderPath?: string,
parentFolderUri?: string,
accessToken?: string
) {
if (!parentFolderPath && !parentFolderUri) {
throw new Error(
"Either parentFolderPath or parentFolderUri must be provided"
);
}
if (!parentFolderUri && parentFolderPath) {
parentFolderUri = await this.getFolderUri(parentFolderPath, accessToken);
}
const createJobDefinitionRequest: RequestInit = {
method: "POST",
headers: {
"Content-Type": "application/vnd.sas.job.definition+json",
Accept: "application/vnd.sas.job.definition+json",
},
body: JSON.stringify({
name: jobName,
parameters: [
{
name: "_addjesbeginendmacros",
type: "CHARACTER",
defaultValue: "false",
},
],
type: "Compute",
code,
}),
};
if (accessToken) {
createJobDefinitionRequest!.headers = {
...createJobDefinitionRequest.headers,
Authorization: `Bearer ${accessToken}`,
};
}
return await this.request<Job>(
`${this.serverUrl}/jobDefinitions/definitions?parentFolderUri=${parentFolderUri}`,
createJobDefinitionRequest
);
}
/**
* Performs a login redirect and returns an auth code for the given client
* @param clientId - the client ID to authenticate with.
*/
public async getAuthCode(clientId: string) {
const authUrl = `${this.serverUrl}/SASLogon/oauth/authorize?client_id=${clientId}&response_type=code`;
const authCode = await fetch(authUrl, {
referrerPolicy: "same-origin",
credentials: "include",
})
.then((response) => response.text())
.then(async (response) => {
let code = "";
if (isAuthorizeFormRequired(response)) {
const formResponse: any = await parseAndSubmitAuthorizeForm(
response,
this.serverUrl
);
const responseBody = formResponse
.split("<body>")[1]
.split("</body>")[0];
const bodyElement: any = document.createElement("div");
bodyElement.innerHTML = responseBody;
code = bodyElement.querySelector(".infobox h4").innerText;
return code;
} else {
const responseBody = response.split("<body>")[1].split("</body>")[0];
const bodyElement: any = document.createElement("div");
bodyElement.innerHTML = responseBody;
if (bodyElement) {
code = bodyElement.querySelector(".infobox h4").innerText;
}
return code;
}
})
.catch(() => null);
return authCode;
}
/**
* Exchanges the auth code for an access token for the given client.
* @param clientId - the client ID to authenticate with.
* @param clientSecret - the client secret to authenticate with.
* @param authCode - the auth code received from the server.
*/
public async getAccessToken(
clientId: string,
clientSecret: string,
authCode: string
) {
const url = this.serverUrl + "/SASLogon/oauth/token";
let token;
if (typeof Buffer === "undefined") {
token = btoa(clientId + ":" + clientSecret);
} else {
token = Buffer.from(clientId + ":" + clientSecret).toString("base64");
}
const headers = {
Authorization: "Basic " + token,
};
let formData;
if (typeof FormData === "undefined") {
formData = new NodeFormData();
formData.append("grant_type", "authorization_code");
formData.append("code", authCode);
} else {
formData = new FormData();
formData.append("grant_type", "authorization_code");
formData.append("code", authCode);
}
const authResponse = await fetch(url, {
method: "POST",
credentials: "include",
headers,
body: formData as any,
referrerPolicy: "same-origin",
}).then((res) => res.json());
return authResponse;
}
/**
* Exchanges the refresh token for an access token for the given client.
* @param clientId - the client ID to authenticate with.
* @param clientSecret - the client secret to authenticate with.
* @param authCode - the refresh token received from the server.
*/
public async refreshTokens(
clientId: string,
clientSecret: string,
refreshToken: string
) {
const url = this.serverUrl + "/SASLogon/oauth/token";
let token;
if (typeof Buffer === "undefined") {
token = btoa(clientId + ":" + clientSecret);
} else {
token = Buffer.from(clientId + ":" + clientSecret).toString("base64");
}
const headers = {
Authorization: "Basic " + token,
};
let formData;
if (typeof FormData === "undefined") {
formData = new NodeFormData();
formData.append("grant_type", "refresh_token");
formData.append("refresh_token", refreshToken);
} else {
formData = new FormData();
formData.append("grant_type", "refresh_token");
formData.append("refresh_token", refreshToken);
}
const authResponse = await fetch(url, {
method: "POST",
credentials: "include",
headers,
body: formData as any,
referrerPolicy: "same-origin",
}).then((res) => res.json());
return authResponse;
}
/**
* Deletes the client representing the supplied ID.
* @param clientId - the client ID to authenticate with.
* @param accessToken - an access token for an authorized user.
*/
public async deleteClient(clientId: string, accessToken?: string) {
const url = this.serverUrl + `/oauth/clients/${clientId}`;
const headers: any = {};
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`;
}
const deleteResponse = await this.request(url, {
method: "DELETE",
credentials: "include",
headers,
});
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,
"",
true,
data,
debug
);
}
/**
* Executes a job via the SAS Viya Job Execution 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 executeJob(
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}`
);
}
let files: any[] = [];
if (data && Object.keys(data).length) {
files = await this.uploadTables(data, accessToken);
}
const jobName = path.basename(sasJob);
const jobFolder = sasJob.replace(`/${jobName}`, "");
const allJobsInFolder = this.rootFolderMap.get(jobFolder.replace("/", ""));
if (allJobsInFolder) {
const jobSpec = allJobsInFolder.find((j: Job) => j.name === jobName);
const jobDefinitionLink = jobSpec?.links.find(
(l) => l.rel === "getResource"
)?.href;
const requestInfo: any = {
method: "GET",
};
const headers: any = { "Content-Type": "application/json" };
if (!!accessToken) {
headers.Authorization = `Bearer ${accessToken}`;
}
requestInfo.headers = headers;
const { result: jobDefinition } = await this.request<Job>(
`${this.serverUrl}${jobDefinitionLink}`,
requestInfo
);
const jobArguments: { [key: string]: any } = {
_contextName: contextName,
_program: `${this.rootFolderName}/${sasJob}`,
_webin_file_count: files.length,
_OMITJSONLISTING: true,
_OMITJSONLOG: true,
_OMITSESSIONRESULTS: true,
_OMITTEXTLISTING: true,
_OMITTEXTLOG: true,
};
if (debug) {
jobArguments["_OMITTEXTLOG"] = "false";
jobArguments["_OMITSESSIONRESULTS"] = "false";
jobArguments["_DEBUG"] = 131;
}
files.forEach((fileInfo, index) => {
jobArguments[
`_webin_fileuri${index + 1}`
] = `/files/files/${fileInfo.file.id}`;
jobArguments[`_webin_name${index + 1}`] = fileInfo.tableName;
});
const postJobRequest = {
method: "POST",
headers,
body: JSON.stringify({
name: `exec-${jobName}`,
description: "Powered by SASjs",
jobDefinition,
arguments: jobArguments,
}),
};
const { result: postedJob, etag } = await this.request<Job>(
`${this.serverUrl}/jobExecution/jobs?_action=wait`,
postJobRequest
);
const jobStatus = await this.pollJobState(
postedJob,
etag,
accessToken,
true
);
const { result: currentJob } = await this.request<Job>(
`${this.serverUrl}/jobExecution/jobs/${postedJob.id}`,
{ headers }
);
let jobResult, log;
if (jobStatus === "failed") {
return Promise.reject(currentJob.error);
}
const resultLink = currentJob.results["_webout.json"];
const logLink = currentJob.links.find((l) => l.rel === "log");
if (resultLink) {
jobResult = await this.request<any>(
`${this.serverUrl}${resultLink}/content`,
{ headers },
"text"
);
}
if (debug && logLink) {
log = await this.request<any>(
`${this.serverUrl}${logLink.href}/content`,
{
headers,
}
).then((res: any) =>
res.result.items.map((i: any) => i.line).join("\n")
);
}
return { result: jobResult?.result, log };
} else {
throw new Error(
`The job ${sasJob} was not found at the location ${this.rootFolderName}`
);
}
}
private async populateRootFolderMap(accessToken?: string) {
const allItems = new Map<string, Job[]>();
const url = "/folders/folders/@item?path=" + this.rootFolderName;
const requestInfo: any = {
method: "GET",
};
if (accessToken) {
requestInfo.headers = { Authorization: `Bearer ${accessToken}` };
}
const { result: folder } = await this.request<Folder>(
`${this.serverUrl}${url}`,
requestInfo
);
if (!folder) {
throw new Error("Cannot populate RootFolderMap unless rootFolder exists");
}
const { result: members } = await this.request<{ items: any[] }>(
`${this.serverUrl}/folders/folders/${folder.id}/members`,
requestInfo
);
const itemsAtRoot = members.items;
allItems.set("", itemsAtRoot);
const subfolderRequests = members.items
.filter((i: any) => i.contentType === "folder")
.map(async (member: any) => {
const subFolderUrl =
"/folders/folders/@item?path=" +
this.rootFolderName +
"/" +
member.name;
const { result: memberDetail } = await this.request<Folder>(
`${this.serverUrl}${subFolderUrl}`,
requestInfo
);
const membersLink = memberDetail.links.find(
(l: any) => l.rel === "members"
);
const { result: memberContents } = await this.request<{ items: any[] }>(
`${this.serverUrl}${membersLink!.href}`,
requestInfo
);
const itemsInFolder = memberContents.items as any[];
allItems.set(member.name, itemsInFolder);
return itemsInFolder;
});
await Promise.all(subfolderRequests);
this.rootFolderMap = allItems;
}
private async populateRootFolder(accessToken?: string) {
const url = "/folders/folders/@item?path=" + this.rootFolderName;
const requestInfo: RequestInit = {
method: "GET",
};
if (accessToken) {
requestInfo.headers = { Authorization: `Bearer ${accessToken}` };
}
let error;
const rootFolder = await this.request<Folder>(
`${this.serverUrl}${url}`,
requestInfo
);
this.rootFolder = rootFolder?.result || null;
if (error) {
throw new Error(JSON.stringify(error));
}
}
private async pollJobState(
postedJob: any,
etag: string | null,
accessToken?: string,
silent = false
) {
const MAX_POLL_COUNT = 1000;
const POLL_INTERVAL = 100;
let postedJobState = "";
let pollCount = 0;
const headers: any = {
"Content-Type": "application/json",
"If-None-Match": etag,
};
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`;
}
const stateLink = postedJob.links.find((l: any) => l.rel === "state");
return new Promise(async (resolve, _) => {
const interval = setInterval(async () => {
if (
postedJobState === "running" ||
postedJobState === "" ||
postedJobState === "pending"
) {
if (stateLink) {
if (!silent) {
console.log("Polling job status... \n");
}
const { result: jobState } = await this.request<string>(
`${this.serverUrl}${stateLink.href}?_action=wait&wait=30`,
{
headers,
},
"text"
);
postedJobState = jobState.trim();
if (!silent) {
console.log(`Current state: ${postedJobState}\n`);
}
pollCount++;
if (pollCount >= MAX_POLL_COUNT) {
resolve(postedJobState);
}
}
} else {
clearInterval(interval);
resolve(postedJobState);
}
}, POLL_INTERVAL);
});
}
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) {
const uploadedFiles = [];
const headers: any = {
"Content-Type": "application/json",
};
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`;
}
for (const tableName in data) {
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."
);
}
const createFileRequest = {
method: "POST",
body: csv,
headers,
};
const { result: file } = await this.request<any>(
`${this.serverUrl}/files/files#rawUpload`,
createFileRequest
);
uploadedFiles.push({ tableName, file });
}
return uploadedFiles;
}
private async getFolderUri(folderPath: string, accessToken?: string) {
const url = "/folders/folders/@item?path=" + folderPath;
const requestInfo: any = {
method: "GET",
};
if (accessToken) {
requestInfo.headers = { Authorization: `Bearer ${accessToken}` };
}
const { result: folder } = await this.request<Folder>(
`${this.serverUrl}${url}`,
requestInfo
);
if (!folder) return undefined;
return `/folders/folders/${folder.id}`;
}
setCsrfTokenLocal = (csrfToken: CsrfToken) => {
this.csrfToken = csrfToken;
this.setCsrfToken(csrfToken);
};
private async request<T>(
url: string,
options: RequestInit,
contentType: "text" | "json" = "json"
) {
if (this.csrfToken) {
options.headers = {
...options.headers,
[this.csrfToken.headerName]: this.csrfToken.value,
};
}
return await makeRequest<T>(url, options, this.setCsrfTokenLocal, contentType);
}
}