mirror of
https://github.com/sasjs/adapter.git
synced 2026-01-07 20:40:05 +00:00
feat(*): recreate package with new name
This commit is contained in:
51
src/SAS9ApiClient.ts
Normal file
51
src/SAS9ApiClient.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* A client for interfacing with the SAS9 REST API
|
||||
*
|
||||
*/
|
||||
export class SAS9ApiClient {
|
||||
constructor(private serverUrl: string) {}
|
||||
|
||||
/**
|
||||
* returns on object containing the server URL
|
||||
*/
|
||||
public getConfig() {
|
||||
return {
|
||||
serverUrl: this.serverUrl,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates serverurl which is not null
|
||||
* @param serverUrl - the URL of the server.
|
||||
*/
|
||||
public setConfig(serverUrl: string) {
|
||||
if (serverUrl) this.serverUrl = serverUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes code on a SAS9 server.
|
||||
* @param linesOfCode - an array of lines of code to execute
|
||||
* @param serverName - the server to execute the code on
|
||||
* @param repositoryName - the repository to execute the code on
|
||||
*/
|
||||
public async executeScript(
|
||||
linesOfCode: string[],
|
||||
serverName: string,
|
||||
repositoryName: string
|
||||
) {
|
||||
const requestPayload = linesOfCode.join("\n");
|
||||
const executeScriptRequest = {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: `command=${requestPayload}`,
|
||||
};
|
||||
const executeScriptResponse = await fetch(
|
||||
`${this.serverUrl}/sas/servers/${serverName}/cmd?repositoryName=${repositoryName}`,
|
||||
executeScriptRequest
|
||||
).then((res) => res.text());
|
||||
|
||||
return executeScriptResponse;
|
||||
}
|
||||
}
|
||||
850
src/SASViyaApiClient.ts
Normal file
850
src/SASViyaApiClient.ts
Normal file
@@ -0,0 +1,850 @@
|
||||
import {
|
||||
isAuthorizeFormRequired,
|
||||
parseAndSubmitAuthorizeForm,
|
||||
convertToCSV,
|
||||
makeRequest,
|
||||
} from "./utils";
|
||||
import * as NodeFormData from "form-data";
|
||||
import * as path from "path";
|
||||
import { Job, Session, Context, Folder } from "./types";
|
||||
|
||||
/**
|
||||
* A client for interfacing with the SAS Viya REST API
|
||||
*
|
||||
*/
|
||||
export class SASViyaApiClient {
|
||||
constructor(
|
||||
private serverUrl: string,
|
||||
private rootFolderName: string,
|
||||
private rootFolderMap = new Map<string, Job[]>()
|
||||
) {
|
||||
if (!rootFolderName) {
|
||||
throw new Error("Root folder must be provided.");
|
||||
}
|
||||
}
|
||||
private csrfToken: { headerName: string; value: string } | null = null;
|
||||
private rootFolder: Folder | null = null;
|
||||
|
||||
/**
|
||||
* 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 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 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 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 createdSession = 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(
|
||||
fileName: string,
|
||||
linesOfCode: string[],
|
||||
contextName: string,
|
||||
accessToken?: string,
|
||||
sessionId = "",
|
||||
silent = false
|
||||
) {
|
||||
const headers: any = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
if (accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`;
|
||||
}
|
||||
if (this.csrfToken) {
|
||||
headers[this.csrfToken.headerName] = this.csrfToken.value;
|
||||
}
|
||||
const 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) {
|
||||
// Request new session in context or use the ID passed in
|
||||
let executionSessionId: string;
|
||||
if (sessionId) {
|
||||
executionSessionId = sessionId;
|
||||
} else {
|
||||
const createSessionRequest = {
|
||||
method: "POST",
|
||||
headers,
|
||||
};
|
||||
const createdSession = await this.request<Session>(
|
||||
`${this.serverUrl}/compute/contexts/${executionContext.id}/sessions`,
|
||||
createSessionRequest
|
||||
);
|
||||
|
||||
executionSessionId = createdSession.id;
|
||||
}
|
||||
// Execute job in session
|
||||
const postJobRequest = {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
name: fileName,
|
||||
description: "Powered by SASjs",
|
||||
code: linesOfCode,
|
||||
}),
|
||||
};
|
||||
const postedJob = 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, accessToken, silent);
|
||||
const logLink = postedJob.links.find((l: any) => l.rel === "log");
|
||||
if (logLink) {
|
||||
const log = await this.request(
|
||||
`${this.serverUrl}${logLink.href}?limit=100000`,
|
||||
{
|
||||
headers,
|
||||
}
|
||||
);
|
||||
|
||||
return { jobStatus, 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(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 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 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 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.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 postedJob = await this.request<Job>(
|
||||
`${this.serverUrl}/jobExecution/jobs?_action=wait`,
|
||||
postJobRequest
|
||||
);
|
||||
const jobStatus = await this.pollJobState(postedJob, accessToken, true);
|
||||
const currentJob = await this.request<Job>(
|
||||
`${this.serverUrl}/jobExecution/jobs/${postedJob.id}`,
|
||||
{ headers }
|
||||
);
|
||||
const resultLink = currentJob.results["_webout.json"];
|
||||
if (resultLink) {
|
||||
const result = await this.request<any>(
|
||||
`${this.serverUrl}${resultLink}/content`,
|
||||
{ headers }
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
return postedJob;
|
||||
} 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 folder = await this.request<Folder>(
|
||||
`${this.serverUrl}${url}`,
|
||||
requestInfo
|
||||
);
|
||||
if (!folder){
|
||||
throw new Error("Cannot populate RootFolderMap unless rootFolder exists");
|
||||
}
|
||||
const 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 memberDetail = await this.request<Folder>(
|
||||
`${this.serverUrl}${subFolderUrl}`,
|
||||
requestInfo
|
||||
);
|
||||
|
||||
const membersLink = memberDetail.links.find(
|
||||
(l: any) => l.rel === "members"
|
||||
);
|
||||
|
||||
const 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}` };
|
||||
}
|
||||
const rootFolder = await this.request<Folder>(
|
||||
`${this.serverUrl}${url}`,
|
||||
requestInfo
|
||||
).catch(() => null);
|
||||
|
||||
this.rootFolder = rootFolder;
|
||||
}
|
||||
|
||||
private async pollJobState(
|
||||
postedJob: any,
|
||||
accessToken?: string,
|
||||
silent = false
|
||||
) {
|
||||
let postedJobState = "";
|
||||
let pollCount = 0;
|
||||
const headers: any = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
if (accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`;
|
||||
}
|
||||
const stateLink = postedJob.links.find((l: any) => l.rel === "state");
|
||||
return new Promise((resolve, _) => {
|
||||
const interval = setInterval(async () => {
|
||||
if (
|
||||
postedJobState === "running" ||
|
||||
postedJobState === "" ||
|
||||
postedJobState === "pending"
|
||||
) {
|
||||
if (stateLink) {
|
||||
if (!silent) {
|
||||
console.log("Polling job status... \n");
|
||||
}
|
||||
const jobState = await this.request<string>(
|
||||
`${this.serverUrl}${stateLink.href}?wait=30`,
|
||||
{
|
||||
headers,
|
||||
},
|
||||
"text"
|
||||
);
|
||||
|
||||
postedJobState = jobState.trim();
|
||||
if (!silent) {
|
||||
console.log(`Current state: ${postedJobState}\n`);
|
||||
}
|
||||
pollCount++;
|
||||
if (pollCount >= 100) {
|
||||
resolve(postedJobState);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
clearInterval(interval);
|
||||
resolve(postedJobState);
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
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 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 folder = await this.request<Folder>(
|
||||
`${this.serverUrl}${url}`,
|
||||
requestInfo
|
||||
);
|
||||
if (!folder)
|
||||
return undefined;
|
||||
return `/folders/folders/${folder.id}`;
|
||||
}
|
||||
|
||||
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,
|
||||
(csrfToken) => (this.csrfToken = csrfToken),
|
||||
contentType
|
||||
);
|
||||
}
|
||||
}
|
||||
48
src/SASjs.spec.ts
Normal file
48
src/SASjs.spec.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import SASjs from "./index";
|
||||
|
||||
const adapter = new SASjs();
|
||||
|
||||
it("should parse SAS9 source code", async done => {
|
||||
expect(sampleResponse).toBeTruthy();
|
||||
const parsedSourceCode = (adapter as any).parseSAS9SourceCode(sampleResponse);
|
||||
expect(parsedSourceCode).toBeTruthy();
|
||||
const sourceCodeLines = parsedSourceCode.split("\r\n");
|
||||
expect(sourceCodeLines.length).toEqual(5);
|
||||
expect(sourceCodeLines[0].startsWith("6")).toBeTruthy();
|
||||
expect(sourceCodeLines[1].startsWith("7")).toBeTruthy();
|
||||
expect(sourceCodeLines[2].startsWith("8")).toBeTruthy();
|
||||
expect(sourceCodeLines[3].startsWith("9")).toBeTruthy();
|
||||
expect(sourceCodeLines[4].startsWith("10")).toBeTruthy();
|
||||
done();
|
||||
});
|
||||
|
||||
it("should parse generated code", async done => {
|
||||
expect(sampleResponse).toBeTruthy();
|
||||
const parsedGeneratedCode = (adapter as any).parseGeneratedCode(
|
||||
sampleResponse
|
||||
);
|
||||
expect(parsedGeneratedCode).toBeTruthy();
|
||||
const generatedCodeLines = parsedGeneratedCode.split("\r\n");
|
||||
expect(generatedCodeLines.length).toEqual(5);
|
||||
expect(generatedCodeLines[0].startsWith("MPRINT(MM_WEBIN)")).toBeTruthy();
|
||||
expect(generatedCodeLines[1].startsWith("MPRINT(MM_WEBLEFT)")).toBeTruthy();
|
||||
expect(generatedCodeLines[2].startsWith("MPRINT(MM_WEBOUT)")).toBeTruthy();
|
||||
expect(generatedCodeLines[3].startsWith("MPRINT(MM_WEBRIGHT)")).toBeTruthy();
|
||||
expect(generatedCodeLines[4].startsWith("MPRINT(MM_WEBOUT)")).toBeTruthy();
|
||||
done();
|
||||
});
|
||||
|
||||
/* tslint:disable */
|
||||
const sampleResponse = `<meta http-equiv="Content-Type" content="text/html; charset=windows-1252"/>
|
||||
6 @file mm_webout.sas
|
||||
7 @brief Send data to/from SAS Stored Processes
|
||||
8 @details This macro should be added to the start of each Stored Process,
|
||||
9 **immediately** followed by a call to:
|
||||
10 %webout(OPEN)
|
||||
MPRINT(MM_WEBIN): ;
|
||||
MPRINT(MM_WEBLEFT): filename _temp temp lrecl=999999;
|
||||
MPRINT(MM_WEBOUT): data _null_;
|
||||
MPRINT(MM_WEBRIGHT): file _temp;
|
||||
MPRINT(MM_WEBOUT): if upcase(symget('_debug'))='LOG' then put '>>weboutBEGIN<<';
|
||||
`;
|
||||
/* tslint:enable */
|
||||
1041
src/SASjs.ts
Normal file
1041
src/SASjs.ts
Normal file
File diff suppressed because it is too large
Load Diff
5
src/index.ts
Normal file
5
src/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import SASjs from "./SASjs";
|
||||
export * from "./types";
|
||||
export * from "./SASViyaApiClient";
|
||||
export * from "./SAS9ApiClient";
|
||||
export default SASjs;
|
||||
6
src/types/Context.ts
Normal file
6
src/types/Context.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface Context {
|
||||
name: string;
|
||||
id: string;
|
||||
createdBy: string;
|
||||
version: number;
|
||||
}
|
||||
4
src/types/CsrfToken.ts
Normal file
4
src/types/CsrfToken.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface CsrfToken {
|
||||
headerName: string;
|
||||
value: string;
|
||||
}
|
||||
7
src/types/Folder.ts
Normal file
7
src/types/Folder.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Link } from "./Link";
|
||||
|
||||
export interface Folder {
|
||||
id: string;
|
||||
uri: string;
|
||||
links: Link[];
|
||||
}
|
||||
11
src/types/Job.ts
Normal file
11
src/types/Job.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Link } from "./Link";
|
||||
import { JobResult } from "./JobResult";
|
||||
|
||||
export interface Job {
|
||||
id: string;
|
||||
name: string;
|
||||
uri: string;
|
||||
createdBy: string;
|
||||
links: Link[];
|
||||
results: JobResult;
|
||||
}
|
||||
3
src/types/JobResult.ts
Normal file
3
src/types/JobResult.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export interface JobResult {
|
||||
"_webout.json": string;
|
||||
}
|
||||
7
src/types/Link.ts
Normal file
7
src/types/Link.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export interface Link {
|
||||
method: string;
|
||||
rel: string;
|
||||
href: string;
|
||||
uri: string;
|
||||
type: string;
|
||||
}
|
||||
30
src/types/SASjsConfig.ts
Normal file
30
src/types/SASjsConfig.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { ServerType } from "./ServerType";
|
||||
|
||||
/**
|
||||
* Specifies the configuration for the SASjs instance.
|
||||
*
|
||||
*/
|
||||
export class SASjsConfig {
|
||||
/**
|
||||
* The location (including http protocol and port) of the SAS Server.
|
||||
* Can be omitted, eg if serving directly from the SAS Web Server or being
|
||||
* streamed.
|
||||
*/
|
||||
serverUrl: string = "";
|
||||
pathSAS9: string = "";
|
||||
pathSASViya: string = "";
|
||||
/**
|
||||
* The appLoc is the parent folder under which the SAS services (STPs or Job
|
||||
* Execution Services) are stored.
|
||||
*/
|
||||
appLoc: string = "";
|
||||
/**
|
||||
* Can be SAS9 or SASVIYA
|
||||
*/
|
||||
serverType: ServerType | null = null;
|
||||
/**
|
||||
* Set to `true` to enable additional debugging.
|
||||
*/
|
||||
debug: boolean = true;
|
||||
contextName: string = "";
|
||||
}
|
||||
12
src/types/SASjsRequest.ts
Normal file
12
src/types/SASjsRequest.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Represents a SASjs request, its response and logs.
|
||||
*
|
||||
*/
|
||||
export interface SASjsRequest {
|
||||
serviceLink: string;
|
||||
timestamp: Date;
|
||||
sourceCode: string;
|
||||
generatedCode: string;
|
||||
logFile: string;
|
||||
SASWORK: any;
|
||||
}
|
||||
14
src/types/SASjsWaitingRequest.ts
Normal file
14
src/types/SASjsWaitingRequest.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Represents requests that are queued, pending a signon event
|
||||
*
|
||||
*/
|
||||
export interface SASjsWaitingRequest {
|
||||
requestPromise: {
|
||||
promise: any;
|
||||
resolve: any;
|
||||
reject: any;
|
||||
};
|
||||
SASjob: string;
|
||||
data: any;
|
||||
params?: any;
|
||||
}
|
||||
8
src/types/ServerType.ts
Normal file
8
src/types/ServerType.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Server type - Viya or SAS9.
|
||||
*
|
||||
*/
|
||||
export enum ServerType {
|
||||
SASViya = "SASVIYA",
|
||||
SAS9 = "SAS9",
|
||||
}
|
||||
3
src/types/Session.ts
Normal file
3
src/types/Session.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export interface Session {
|
||||
id: string;
|
||||
}
|
||||
10
src/types/index.ts
Normal file
10
src/types/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export * from "./Context";
|
||||
export * from "./CsrfToken";
|
||||
export * from "./Folder";
|
||||
export * from "./Job";
|
||||
export * from "./Link";
|
||||
export * from "./SASjsConfig";
|
||||
export * from "./SASjsRequest";
|
||||
export * from "./SASjsWaitingRequest";
|
||||
export * from "./ServerType";
|
||||
export * from "./Session";
|
||||
5
src/utils/asyncForEach.ts
Normal file
5
src/utils/asyncForEach.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export async function asyncForEach(array: any[], callback: any) {
|
||||
for (let index = 0; index < array.length; index++) {
|
||||
await callback(array[index], index, array);
|
||||
}
|
||||
}
|
||||
9
src/utils/compareTimestamps.ts
Normal file
9
src/utils/compareTimestamps.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { SASjsRequest } from "../types/SASjsRequest";
|
||||
|
||||
/**
|
||||
* Comparator for SASjs request timestamps
|
||||
*
|
||||
*/
|
||||
export const compareTimestamps = (a: SASjsRequest, b: SASjsRequest) => {
|
||||
return b.timestamp.getTime() - a.timestamp.getTime();
|
||||
};
|
||||
133
src/utils/convertToCsv.ts
Normal file
133
src/utils/convertToCsv.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* Converts the given JSON object to a CSV string.
|
||||
* @param data - the JSON object to convert.
|
||||
*/
|
||||
export const convertToCSV = (data: any) => {
|
||||
const replacer = (key: any, value: any) => (value === null ? "" : value);
|
||||
const headerFields = Object.keys(data[0]);
|
||||
let csvTest;
|
||||
let invalidString = false;
|
||||
const headers = headerFields.map((field) => {
|
||||
let firstFoundType: string | null = null;
|
||||
let hasMixedTypes: boolean = false;
|
||||
let rowNumError: number = -1;
|
||||
|
||||
const longestValueForField = data
|
||||
.map((row: any, index: number) => {
|
||||
if (row[field] || row[field] === "") {
|
||||
if (firstFoundType) {
|
||||
let currentFieldType =
|
||||
row[field] === "" || typeof row[field] === "string"
|
||||
? "chars"
|
||||
: "number";
|
||||
|
||||
if (!hasMixedTypes) {
|
||||
hasMixedTypes = currentFieldType !== firstFoundType;
|
||||
rowNumError = hasMixedTypes ? index + 1 : -1;
|
||||
}
|
||||
} else {
|
||||
if (row[field] === "") {
|
||||
firstFoundType = "chars";
|
||||
} else {
|
||||
firstFoundType =
|
||||
typeof row[field] === "string" ? "chars" : "number";
|
||||
}
|
||||
}
|
||||
|
||||
let byteSize;
|
||||
|
||||
if (typeof row[field] === "string") {
|
||||
let doubleQuotesFound = row[field]
|
||||
.split("")
|
||||
.filter((char: any) => char === '"');
|
||||
|
||||
byteSize = getByteSize(row[field]);
|
||||
|
||||
if (doubleQuotesFound.length > 0) {
|
||||
byteSize += doubleQuotesFound.length;
|
||||
}
|
||||
}
|
||||
|
||||
return byteSize;
|
||||
}
|
||||
})
|
||||
.sort((a: number, b: number) => b - a)[0];
|
||||
if (longestValueForField && longestValueForField > 32765) {
|
||||
invalidString = true;
|
||||
}
|
||||
if (hasMixedTypes) {
|
||||
console.error(
|
||||
`Row (${rowNumError}), Column (${field}) has mixed types: ERROR`
|
||||
);
|
||||
}
|
||||
|
||||
return `${field}:${firstFoundType === "chars" ? "$" : ""}${
|
||||
longestValueForField
|
||||
? longestValueForField
|
||||
: firstFoundType === "chars"
|
||||
? "1"
|
||||
: "best"
|
||||
}.`;
|
||||
});
|
||||
|
||||
if (invalidString) {
|
||||
return "ERROR: LARGE STRING LENGTH";
|
||||
}
|
||||
csvTest = data.map((row: any) => {
|
||||
const fields = Object.keys(row).map((fieldName, index) => {
|
||||
let value;
|
||||
let containsSpecialChar = false;
|
||||
const currentCell = row[fieldName];
|
||||
|
||||
if (JSON.stringify(currentCell).search(/(\\t|\\n|\\r)/gm) > -1) {
|
||||
value = currentCell.toString();
|
||||
containsSpecialChar = true;
|
||||
} else {
|
||||
value = JSON.stringify(currentCell, replacer);
|
||||
}
|
||||
|
||||
value = value.replace(/\\\\/gm, "\\");
|
||||
|
||||
if (containsSpecialChar) {
|
||||
if (value.includes(",") || value.includes('"')) {
|
||||
value = '"' + value + '"';
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
!value.includes(",") &&
|
||||
value.includes('"') &&
|
||||
!value.includes('\\"')
|
||||
) {
|
||||
value = value.substring(1, value.length - 1);
|
||||
}
|
||||
|
||||
value = value.replace(/\\"/gm, '""');
|
||||
}
|
||||
|
||||
value = value.replace(/\r\n/gm, "\n");
|
||||
|
||||
if (value === "" && headers[index].includes("best")) {
|
||||
value = ".";
|
||||
}
|
||||
|
||||
return value;
|
||||
});
|
||||
return fields.join(",");
|
||||
});
|
||||
|
||||
let finalCSV =
|
||||
headers.join(",").replace(/,/g, " ") + "\r\n" + csvTest.join("\r\n");
|
||||
|
||||
return finalCSV;
|
||||
};
|
||||
|
||||
const getByteSize = (str: string) => {
|
||||
let byteSize = str.length;
|
||||
for (let i = str.length - 1; i >= 0; i--) {
|
||||
const code = str.charCodeAt(i);
|
||||
if (code > 0x7f && code <= 0x7ff) byteSize++;
|
||||
else if (code > 0x7ff && code <= 0xffff) byteSize += 2;
|
||||
if (code >= 0xdc00 && code <= 0xdfff) i--; //trail surrogate
|
||||
}
|
||||
return byteSize;
|
||||
};
|
||||
14
src/utils/index.ts
Normal file
14
src/utils/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export * from "./asyncForEach";
|
||||
export * from "./compareTimestamps";
|
||||
export * from "./convertToCsv";
|
||||
export * from "./isAuthorizeFormRequired";
|
||||
export * from "./isLoginRequired";
|
||||
export * from "./isLoginSuccess";
|
||||
export * from "./makeRequest";
|
||||
export * from "./needsRetry";
|
||||
export * from "./parseAndSubmitAuthorizeForm";
|
||||
export * from "./parseGeneratedCode";
|
||||
export * from "./parseSourceCode";
|
||||
export * from "./parseSasViyaLog";
|
||||
export * from "./serialize";
|
||||
export * from "./splitChunks";
|
||||
3
src/utils/isAuthorizeFormRequired.ts
Normal file
3
src/utils/isAuthorizeFormRequired.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const isAuthorizeFormRequired = (response: string): boolean => {
|
||||
return /<form.+action="(.*Logon\/oauth\/authorize[^"]*).*>/gm.test(response);
|
||||
};
|
||||
5
src/utils/isLoginRequired.ts
Normal file
5
src/utils/isLoginRequired.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export const isLogInRequired = (response: string): boolean => {
|
||||
const pattern: RegExp = /<form.+action="(.*Logon[^"]*).*>/gm;
|
||||
const matches = pattern.test(response);
|
||||
return matches;
|
||||
};
|
||||
2
src/utils/isLoginSuccess.ts
Normal file
2
src/utils/isLoginSuccess.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const isLogInSuccess = (response: string): boolean =>
|
||||
/You have signed in/gm.test(response);
|
||||
37
src/utils/makeRequest.ts
Normal file
37
src/utils/makeRequest.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { CsrfToken } from "../types";
|
||||
|
||||
export async function makeRequest<T>(
|
||||
url: string,
|
||||
request: RequestInit,
|
||||
callback: (value: CsrfToken) => any,
|
||||
contentType: "text" | "json" = "json"
|
||||
): Promise<T> {
|
||||
const responseTransform =
|
||||
contentType === "json"
|
||||
? (res: Response) => res.json()
|
||||
: (res: Response) => res.text();
|
||||
const result = await fetch(url, request).then((response) => {
|
||||
if (!response.ok) {
|
||||
if (response.status === 403) {
|
||||
const tokenHeader = response.headers.get("X-CSRF-HEADER");
|
||||
|
||||
if (tokenHeader) {
|
||||
const token = response.headers.get(tokenHeader);
|
||||
callback({
|
||||
headerName: tokenHeader,
|
||||
value: token || "",
|
||||
});
|
||||
|
||||
const retryRequest = {
|
||||
...request,
|
||||
headers: { ...request.headers, [tokenHeader]: token },
|
||||
};
|
||||
return fetch(url, retryRequest).then(responseTransform);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return responseTransform(response);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
11
src/utils/needsRetry.ts
Normal file
11
src/utils/needsRetry.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export const needsRetry = (responseText: string): boolean => {
|
||||
return (
|
||||
(responseText.includes('"errorCode":403') &&
|
||||
responseText.includes("_csrf") &&
|
||||
responseText.includes("X-CSRF-TOKEN")) ||
|
||||
(responseText.includes('"status":403') &&
|
||||
responseText.includes('"error":"Forbidden"')) ||
|
||||
(responseText.includes('"status":449') &&
|
||||
responseText.includes("Authentication success, retry original request"))
|
||||
);
|
||||
};
|
||||
49
src/utils/parseAndSubmitAuthorizeForm.ts
Normal file
49
src/utils/parseAndSubmitAuthorizeForm.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
export const parseAndSubmitAuthorizeForm = async (
|
||||
response: string,
|
||||
serverUrl: string
|
||||
) => {
|
||||
let authUrl: string | null = null;
|
||||
const params: any = {};
|
||||
|
||||
const responseBody = response.split("<body>")[1].split("</body>")[0];
|
||||
const bodyElement = document.createElement("div");
|
||||
bodyElement.innerHTML = responseBody;
|
||||
|
||||
const form = bodyElement.querySelector("#application_authorization");
|
||||
authUrl = form ? serverUrl + form.getAttribute("action") : null;
|
||||
|
||||
const inputs: any = form?.querySelectorAll("input");
|
||||
|
||||
for (const input of inputs) {
|
||||
if (input.name === "user_oauth_approval") {
|
||||
input.value = "true";
|
||||
}
|
||||
|
||||
params[input.name] = input.value;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
for (const key in params) {
|
||||
if (params.hasOwnProperty(key)) {
|
||||
formData.append(key, params[key]);
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (authUrl) {
|
||||
fetch(authUrl, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: formData,
|
||||
referrerPolicy: "same-origin",
|
||||
})
|
||||
.then((res) => res.text())
|
||||
.then((res) => {
|
||||
resolve(res);
|
||||
});
|
||||
} else {
|
||||
reject("Auth form url is null");
|
||||
}
|
||||
});
|
||||
};
|
||||
7
src/utils/parseGeneratedCode.ts
Normal file
7
src/utils/parseGeneratedCode.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export const parseGeneratedCode = (log: string) => {
|
||||
const startsWith = "MPRINT";
|
||||
const isGeneratedCodeLine = (line: string) =>
|
||||
line.trim().startsWith(startsWith);
|
||||
const logLines = log.split("\n").filter(isGeneratedCodeLine);
|
||||
return logLines.join("\r\n");
|
||||
};
|
||||
12
src/utils/parseSasViyaLog.ts
Normal file
12
src/utils/parseSasViyaLog.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export const parseSasViyaLog = (logResponse: { items: any[] }) => {
|
||||
let log;
|
||||
try {
|
||||
log = logResponse.items
|
||||
? logResponse.items.map((i) => i.line).join("\n")
|
||||
: JSON.stringify(logResponse);
|
||||
} catch (e) {
|
||||
console.error("An error has occurred while parsing the log response", e);
|
||||
log = logResponse;
|
||||
}
|
||||
return log;
|
||||
};
|
||||
6
src/utils/parseSourceCode.ts
Normal file
6
src/utils/parseSourceCode.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export const parseSourceCode = (log: string): string => {
|
||||
const isSourceCodeLine = (line: string) =>
|
||||
line.trim().substring(0, 10).trimStart().match(/^\d/);
|
||||
const logLines = log.split("\n").filter(isSourceCodeLine);
|
||||
return logLines.join("\r\n");
|
||||
};
|
||||
15
src/utils/serialize.ts
Normal file
15
src/utils/serialize.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export const serialize = (obj: any) => {
|
||||
const str: any[] = [];
|
||||
for (const p in obj) {
|
||||
if (obj.hasOwnProperty(p)) {
|
||||
if (obj[p] instanceof Array) {
|
||||
for (let i = 0, n = obj[p].length; i < n; i++) {
|
||||
str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p][i]));
|
||||
}
|
||||
} else {
|
||||
str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
|
||||
}
|
||||
}
|
||||
}
|
||||
return str.join("&");
|
||||
};
|
||||
12
src/utils/splitChunks.ts
Normal file
12
src/utils/splitChunks.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export const splitChunks = (content: string) => {
|
||||
const size = 16000;
|
||||
|
||||
const numChunks = Math.ceil(content.length / size);
|
||||
const chunks = new Array(numChunks);
|
||||
|
||||
for (let i = 0, o = 0; i < numChunks; ++i, o += size) {
|
||||
chunks[i] = content.substr(o, size);
|
||||
}
|
||||
|
||||
return chunks;
|
||||
};
|
||||
Reference in New Issue
Block a user