1
0
mirror of https://github.com/sasjs/adapter.git synced 2025-12-11 01:14:36 +00:00

Compare commits

...

7 Commits

Author SHA1 Message Date
Allan Bowe
01af5eb634 Merge pull request #791 from sasjs/sas9-mini
feat: create a minified version for sas9  and web based appications
2023-03-16 13:36:08 +00:00
0c3797e2de chore: deleted package-lock.json and recreated by npm install 2023-03-16 16:22:26 +05:00
c33c509207 chore: merge master 2023-03-16 13:40:22 +05:00
af351d7375 chore: bump webpack 2023-03-16 12:39:39 +05:00
2b53406cac chore: no need for forked version of tough cookie 2023-03-16 00:28:06 +05:00
99cfb8b2af feat: created a minified version of adapter for executing web jobs on sas9 2023-03-16 00:26:08 +05:00
Yury Shkoda
22fa185715 feat: minified adapter for SAS9 2023-03-14 10:45:03 +03:00
7 changed files with 2672 additions and 2241 deletions

4440
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -64,7 +64,7 @@
"process": "0.11.10",
"rimraf": "3.0.2",
"semantic-release": "19.0.3",
"terser-webpack-plugin": "5.3.1",
"terser-webpack-plugin": "5.3.6",
"ts-jest": "27.1.3",
"ts-loader": "9.4.0",
"tslint": "6.1.3",
@@ -72,7 +72,7 @@
"typedoc": "0.23.24",
"typedoc-plugin-rename-defaults": "0.6.4",
"typescript": "4.8.3",
"webpack": "5.69.0",
"webpack": "5.76.2",
"webpack-cli": "4.9.2"
},
"main": "index.js",

View File

@@ -4,8 +4,7 @@ import {
UploadFile,
EditContextInput,
PollOptions,
LoginMechanism,
ExecutionQuery
LoginMechanism
} from './types'
import { SASViyaApiClient } from './SASViyaApiClient'
import { SAS9ApiClient } from './SAS9ApiClient'
@@ -17,7 +16,6 @@ import {
AuthConfig,
ExtraResponseAttributes,
SasAuthResponse,
ServicePackSASjs,
AuthConfigSas9
} from '@sasjs/utils/types'
import { RequestClient } from './request/RequestClient'

273
src/minified/sas9/SASjs.ts Normal file
View File

@@ -0,0 +1,273 @@
import { validateInput, compareTimestamps } from '../../utils'
import { SASjsConfig, UploadFile, LoginMechanism } from '../../types'
import { AuthManager } from '../../auth'
import {
ServerType,
AuthConfig,
ExtraResponseAttributes
} from '@sasjs/utils/types'
import { RequestClient } from '../../request/RequestClient'
import { FileUploader } from '../../job-execution/FileUploader'
import { WebJobExecutor } from './WebJobExecutor'
import { ErrorResponse } from '../../types/errors/ErrorResponse'
import { LoginOptions, LoginResult } from '../../types/Login'
const defaultConfig: SASjsConfig = {
serverUrl: '',
pathSASJS: '/SASjsApi/stp/execute',
pathSAS9: '/SASStoredProcess/do',
pathSASViya: '/SASJobExecution',
appLoc: '/Public/seedapp',
serverType: ServerType.Sas9,
debug: false,
contextName: 'SAS Job Execution compute context',
useComputeApi: null,
loginMechanism: LoginMechanism.Default
}
/**
* SASjs is a JavaScript adapter for SAS.
*
*/
export default class SASjs {
private sasjsConfig: SASjsConfig = new SASjsConfig()
private jobsPath: string = ''
private fileUploader: FileUploader | null = null
private authManager: AuthManager | null = null
private requestClient: RequestClient | null = null
private webJobExecutor: WebJobExecutor | null = null
constructor(config?: Partial<SASjsConfig>) {
this.sasjsConfig = {
...defaultConfig,
...config
}
this.setupConfiguration()
}
/**
* Logs into the SAS server with the supplied credentials.
* @param username - a string representing the username.
* @param password - a string representing the password.
* @param clientId - a string representing the client ID.
*/
public async logIn(
username?: string,
password?: string,
clientId?: string,
options: LoginOptions = {}
): Promise<LoginResult> {
if (this.sasjsConfig.loginMechanism === LoginMechanism.Default) {
if (!username || !password)
throw new Error(
'A username and password are required when using the default login mechanism.'
)
return this.authManager!.logIn(username, password)
}
if (typeof window === typeof undefined) {
throw new Error(
'The redirected login mechanism is only available for use in the browser.'
)
}
return this.authManager!.redirectedLogIn(options)
}
/**
* Logs out of the configured SAS server.
*/
public logOut() {
return this.authManager!.logOut()
}
/**
* Returns the current SASjs configuration.
*
*/
public getSasjsConfig() {
return this.sasjsConfig
}
/**
* this method returns an array of SASjsRequest
* @returns SASjsRequest[]
*/
public getSasRequests() {
const requests = [...this.requestClient!.getRequests()]
const sortedRequests = requests.sort(compareTimestamps)
return sortedRequests
}
/**
* Sets the debug state. Turning this on will enable additional logging in the adapter.
* @param value - boolean indicating debug state (on/off).
*/
public setDebugState(value: boolean) {
this.sasjsConfig.debug = value
}
/**
* Uploads a file to the given service.
* @param sasJob - the path to the SAS program (ultimately resolves to
* the SAS `_program` parameter to run a Job Definition or SAS 9 Stored
* Process). Is prepended at runtime with the value of `appLoc`.
* @param files - array of files to be uploaded, including File object and file name.
* @param params - request URL parameters.
* @param config - provide any changes to the config here, for instance to
* enable/disable `debug`. Any change provided will override the global config,
* for that particular function call.
* @param loginRequiredCallback - a function that is called if the
* user is not logged in (eg to display a login form). The request will be
* resubmitted after successful login.
*/
public async uploadFile(
sasJob: string,
files: UploadFile[],
params: { [key: string]: any } | null,
config: { [key: string]: any } = {},
loginRequiredCallback?: () => any
) {
config = {
...this.sasjsConfig,
...config
}
const data = { files, params }
return await this.fileUploader!.execute(
sasJob,
data,
config,
loginRequiredCallback
)
}
/**
* Makes a request to program specified in `SASjob` (could be a Viya Job, a
* SAS 9 Stored Process, or a SASjs Server Stored Program). The response
* object will always contain table names in lowercase, and column names in
* uppercase. Values are returned formatted by default, unformatted
* values can be configured as an option in the `%webout` macro.
*
* @param sasJob - the path to the SAS program (ultimately resolves to
* the SAS `_program` parameter to run a Job Definition or SAS 9 Stored
* Process). Is prepended at runtime with the value of `appLoc`.
* @param data - a JSON object containing one or more tables to be sent to
* SAS. For an example of the table structure, see the project README. This
* value can be `null` if no inputs are required.
* @param config - provide any changes to the config here, for instance to
* enable/disable `debug`. Any change provided will override the global config,
* for that particular function call.
* @param loginRequiredCallback - a function that is called if the
* user is not logged in (eg to display a login form). The request will be
* resubmitted after successful login.
* When using a `loginRequiredCallback`, the call to the request will look, for example, like so:
* `await request(sasJobPath, data, config, () => setIsLoggedIn(false))`
* If you are not passing in any data and configuration, it will look like so:
* `await request(sasJobPath, {}, {}, () => setIsLoggedIn(false))`
* @param extraResponseAttributes - a array of predefined values that are used
* to provide extra attributes (same names as those values) to be added in response
* Supported values are declared in ExtraResponseAttributes type.
*/
public async request(
sasJob: string,
data: { [key: string]: any } | null,
config: { [key: string]: any } = {},
loginRequiredCallback?: () => any,
authConfig?: AuthConfig,
extraResponseAttributes: ExtraResponseAttributes[] = []
) {
config = {
...this.sasjsConfig,
...config
}
const validationResult = validateInput(data)
// status is true if the data passes validation checks above
if (validationResult.status) {
return await this.webJobExecutor!.execute(
sasJob,
data,
config,
loginRequiredCallback,
authConfig,
extraResponseAttributes
)
} else {
return Promise.reject(new ErrorResponse(validationResult.msg))
}
}
/**
* Checks whether a session is active, or login is required.
* @returns - a promise which resolves with an object containing two values - a boolean `isLoggedIn`, and a string `userName`.
*/
public async checkSession() {
return this.authManager!.checkSession()
}
private setupConfiguration() {
if (
this.sasjsConfig.serverUrl === undefined ||
this.sasjsConfig.serverUrl === ''
) {
if (typeof location !== 'undefined') {
let url = `${location.protocol}//${location.hostname}`
if (location.port) url = `${url}:${location.port}`
this.sasjsConfig.serverUrl = url
} else {
this.sasjsConfig.serverUrl = ''
}
}
if (this.sasjsConfig.serverUrl.slice(-1) === '/') {
this.sasjsConfig.serverUrl = this.sasjsConfig.serverUrl.slice(0, -1)
}
if (!this.requestClient) {
this.requestClient = new RequestClient(
this.sasjsConfig.serverUrl,
this.sasjsConfig.httpsAgentOptions,
this.sasjsConfig.requestHistoryLimit
)
} else {
this.requestClient.setConfig(
this.sasjsConfig.serverUrl,
this.sasjsConfig.httpsAgentOptions
)
}
this.jobsPath = this.sasjsConfig.pathSAS9
this.authManager = new AuthManager(
this.sasjsConfig.serverUrl,
this.sasjsConfig.serverType!,
this.requestClient,
this.resendWaitingRequests
)
this.fileUploader = new FileUploader(
this.sasjsConfig.serverUrl,
this.sasjsConfig.serverType!,
this.jobsPath,
this.requestClient
)
this.webJobExecutor = new WebJobExecutor(
this.sasjsConfig.serverUrl,
this.sasjsConfig.serverType!,
this.jobsPath,
this.requestClient
)
}
private resendWaitingRequests = async () => {
await this.webJobExecutor?.resendWaitingRequests()
await this.fileUploader?.resendWaitingRequests()
}
}

View File

@@ -0,0 +1,157 @@
import {
AuthConfig,
ExtraResponseAttributes,
ServerType
} from '@sasjs/utils/types'
import {
ErrorResponse,
JobExecutionError,
LoginRequiredError
} from '../../types/errors'
import { RequestClient } from '../../request/RequestClient'
import {
isRelativePath,
parseSasViyaDebugResponse,
appendExtraResponseAttributes,
convertToCSV
} from '../../utils'
import { BaseJobExecutor } from '../../job-execution/JobExecutor'
import { parseWeboutResponse } from '../../utils/parseWeboutResponse'
export interface WaitingRequstPromise {
promise: Promise<any> | null
resolve: any
reject: any
}
export class WebJobExecutor extends BaseJobExecutor {
constructor(
serverUrl: string,
serverType: ServerType,
private jobsPath: string,
private requestClient: RequestClient
) {
super(serverUrl, serverType)
}
async execute(
sasJob: string,
data: any,
config: any,
loginRequiredCallback?: any,
authConfig?: AuthConfig,
extraResponseAttributes: ExtraResponseAttributes[] = []
) {
const loginCallback = loginRequiredCallback
const program = isRelativePath(sasJob)
? config.appLoc
? config.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '')
: sasJob
: sasJob
let apiUrl = `${config.serverUrl}${this.jobsPath}/?${'_program=' + program}`
let requestParams = {
...this.getRequestParams(config)
}
let formData = new FormData()
if (data) {
try {
formData = generateFileUploadForm(formData, data)
} catch (e: any) {
return Promise.reject(new ErrorResponse(e?.message, e))
}
}
for (const key in requestParams) {
if (requestParams.hasOwnProperty(key)) {
formData.append(key, requestParams[key])
}
}
const requestPromise = new Promise((resolve, reject) => {
this.requestClient!.post(apiUrl, formData, authConfig?.access_token)
.then(async (res: any) => {
this.requestClient!.appendRequest(res, sasJob, config.debug)
const jsonResponse =
config.debug && typeof res.result === 'string'
? parseWeboutResponse(res.result, apiUrl)
: res.result
const responseObject = appendExtraResponseAttributes(
{ result: jsonResponse, log: res.log },
extraResponseAttributes
)
resolve(responseObject)
})
.catch(async (e: Error) => {
if (e instanceof JobExecutionError) {
this.requestClient!.appendRequest(e, sasJob, config.debug)
reject(new ErrorResponse(e?.message, e))
}
if (e instanceof LoginRequiredError) {
if (!loginRequiredCallback) {
reject(
new ErrorResponse(
'Request is not authenticated. Make sure .env file exists with valid credentials.',
e
)
)
}
this.appendWaitingRequest(() => {
return this.execute(
sasJob,
data,
config,
loginRequiredCallback,
authConfig,
extraResponseAttributes
).then(
(res: any) => {
resolve(res)
},
(err: any) => {
reject(err)
}
)
})
if (loginCallback) await loginCallback()
} else reject(new ErrorResponse(e?.message, e))
})
})
return requestPromise
}
}
/**
* One of the approaches SASjs takes to send tables-formatted JSON (see README)
* to SAS is as multipart form data, where each table is provided as a specially
* formatted CSV file.
*/
const generateFileUploadForm = (formData: FormData, data: any): FormData => {
for (const tableName in data) {
if (!Array.isArray(data[tableName])) continue
const name = 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.'
)
}
const file = new Blob([csv], {
type: 'application/csv'
})
formData.append(name, file, `${name}.csv`)
}
return formData
}

View File

@@ -0,0 +1,3 @@
import SASjs from './SASjs'
export * from '../../types'
export default SASjs

View File

@@ -23,8 +23,16 @@ const optimization = {
}
const browserConfig = {
entry: './src/index.ts',
devtool: 'inline-source-map',
entry: {
index: './src/index.ts',
minified_sas9: './src/minified/sas9/index.ts'
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'build'),
libraryTarget: 'umd',
library: 'SASjs'
},
mode: 'production',
optimization: optimization,
module: {
@@ -40,12 +48,6 @@ const browserConfig = {
extensions: ['.ts', '.js'],
fallback: { https: false, fs: false, readline: false }
},
output: {
filename: 'index.js',
path: path.resolve(__dirname, 'build'),
libraryTarget: 'umd',
library: 'SASjs'
},
plugins: [
...defaultPlugins,
new webpack.ProvidePlugin({
@@ -55,6 +57,18 @@ const browserConfig = {
]
}
const browserConfigWithDevTool = {
...browserConfig,
entry: './src/index.ts',
output: {
filename: 'index-dev.js',
path: path.resolve(__dirname, 'build'),
libraryTarget: 'umd',
library: 'SASjs'
},
devtool: 'inline-source-map'
}
const browserConfigWithoutProcessPlugin = {
entry: browserConfig.entry,
devtool: browserConfig.devtool,
@@ -76,4 +90,4 @@ const nodeConfig = {
}
}
module.exports = [browserConfig, nodeConfig]
module.exports = [browserConfig, browserConfigWithDevTool, nodeConfig]