1
0
mirror of https://github.com/sasjs/adapter.git synced 2026-01-03 18:50:05 +00:00

Compare commits

..

32 Commits

Author SHA1 Message Date
Krishna Acondy
fe907e1c43 Merge pull request #384 from sasjs/sas9-support
feat(sas9-support): add support for SAS9 job execution outside of the browser
2021-05-28 07:46:47 +01:00
Allan Bowe
e95e894365 Merge branch 'master' into sas9-support 2021-05-27 12:29:30 +03:00
Allan Bowe
82414d8b8b Merge pull request #379 from sasjs/dependabot/npm_and_yarn/sasjs/utils-2.14.0
chore(deps): bump @sasjs/utils from 2.10.2 to 2.14.0
2021-05-27 12:29:13 +03:00
Allan Bowe
456fa68f0f Merge branch 'master' into dependabot/npm_and_yarn/sasjs/utils-2.14.0 2021-05-27 11:55:31 +03:00
Allan Bowe
076adc1f6a Merge pull request #334 from sasjs/dependabot/npm_and_yarn/typedoc-0.20.36
chore(deps-dev): bump typedoc from 0.20.35 to 0.20.36
2021-05-27 11:54:52 +03:00
Krishna Acondy
9676488ff2 chore(refactor): remove unnecessary variables, use jobs path from config 2021-05-27 08:40:50 +01:00
Krishna Acondy
e9affb862d chore(merge): update branch 2021-05-27 08:32:11 +01:00
Krishna Acondy
e04371510e chore(update): update branch with changes from master 2021-05-27 08:30:20 +01:00
dependabot-preview[bot]
19657a1c12 chore(deps-dev): bump typedoc from 0.20.35 to 0.20.36
Bumps [typedoc](https://github.com/TypeStrong/TypeDoc) from 0.20.35 to 0.20.36.
- [Release notes](https://github.com/TypeStrong/TypeDoc/releases)
- [Commits](https://github.com/TypeStrong/TypeDoc/compare/v0.20.35...v0.20.36)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2021-05-27 07:22:22 +00:00
dependabot-preview[bot]
6424c82ac9 chore(deps): bump @sasjs/utils from 2.10.2 to 2.14.0
Bumps [@sasjs/utils](https://github.com/sasjs/utils) from 2.10.2 to 2.14.0.
- [Release notes](https://github.com/sasjs/utils/releases)
- [Commits](https://github.com/sasjs/utils/compare/v2.10.2...v2.14.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2021-05-27 07:22:21 +00:00
Allan Bowe
fcab18191f Merge pull request #382 from sasjs/dependabot/npm_and_yarn/browserslist-4.16.6
chore(deps): [security] bump browserslist from 4.16.4 to 4.16.6
2021-05-27 10:20:21 +03:00
Krishna Acondy
f157612a0e Merge branch 'master' into sas9-support 2021-05-27 08:16:49 +01:00
Krishna Acondy
b8cb7d52e7 chore(*): remove unused loader 2021-05-27 08:08:47 +01:00
Krishna Acondy
d8d1968162 chore(*): fix formatting 2021-05-27 08:06:21 +01:00
Krishna Acondy
0e1d1f1d99 chore(dep): remove unused dependency 2021-05-27 08:04:19 +01:00
Krishna Acondy
0b055dd05f feat(sas9-support): add support for SAS9 via username/password login 2021-05-27 08:00:15 +01:00
dependabot-preview[bot]
ba91c29ba8 chore(deps): [security] bump browserslist from 4.16.4 to 4.16.6
Bumps [browserslist](https://github.com/browserslist/browserslist) from 4.16.4 to 4.16.6. **This update includes a security fix.**
- [Release notes](https://github.com/browserslist/browserslist/releases)
- [Changelog](https://github.com/browserslist/browserslist/blob/main/CHANGELOG.md)
- [Commits](https://github.com/browserslist/browserslist/compare/4.16.4...4.16.6)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2021-05-26 08:24:56 +00:00
Allan Bowe
bd19457c2a Merge branch 'master' of github.com:sasjs/adapter 2021-05-26 11:23:13 +03:00
Allan Bowe
b0570e1cd9 chore: automated commit 2021-05-26 11:23:08 +03:00
Allan Bowe
a5f1b59f7b Merge pull request #374 from sasjs/git-hooks
feat(git): enabled git hook enforcing conventional commits
2021-05-20 09:23:25 +03:00
Yury Shkoda
01ca29fc01 feat(git): enabled git hook enforcing conventional commits 2021-05-20 08:22:40 +03:00
Allan Bowe
ed9648fdf9 chore: automated commit 2021-05-16 22:02:23 +03:00
Allan Bowe
7e17aa6eb3 chore: automated commit 2021-05-16 22:00:10 +03:00
Allan Bowe
9caee9941a Merge pull request #371 from sasjs/qualityfixes
fix: readme badges
2021-05-16 21:59:15 +03:00
Allan Bowe
e309e7a4f4 fix: readme badges 2021-05-16 21:56:26 +03:00
Allan Bowe
c47441d6d4 Merge pull request #370 from sasjs/qualityfixes
fix: metadata in package.json
2021-05-16 21:41:11 +03:00
Allan Bowe
1844bc48ac Merge branch 'master' into qualityfixes 2021-05-16 21:40:40 +03:00
Allan Bowe
7a5adebdb5 fix: metadata in package.json 2021-05-16 21:40:14 +03:00
Allan Bowe
b39f0c577b Merge pull request #369 from sasjs/qualityfixes
Qualityfixes
2021-05-16 21:34:55 +03:00
Allan Bowe
15f4065cd8 fix: metadata updates (readme, changelog, url in package.json) 2021-05-16 20:25:56 +03:00
Allan Bowe
4c67665b4d fix: adding npmignore (should reduce the bundle size from 8.88mb) 2021-05-16 13:52:13 +03:00
Krishna Acondy
f8c6318a88 chore(*): attempt SAS9 job executor 2021-05-11 08:15:48 +01:00
15 changed files with 13903 additions and 101 deletions

18
.git-hooks/commit-msg Executable file
View File

@@ -0,0 +1,18 @@
#!/bin/sh
RED="\033[1;31m"
GREEN="\033[1;32m"
# Get the commit message (the parameter we're given is just the path to the
# temporary file which holds the message).
commit_message=$(cat "$1")
if (echo "$commit_message" | grep -Eq "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z \-]+\))?!?: .+$") then
echo "${GREEN} ✔ Commit message meets Conventional Commit standards"
exit 0
fi
echo "${RED}❌ Commit message does not meet the Conventional Commit standard!"
echo "An example of a valid message is:"
echo " feat(login): add the 'remember me' button"
echo " More details at: https://www.conventionalcommits.org/en/v1.0.0/#summary"
exit 1

4
.npmignore Normal file
View File

@@ -0,0 +1,4 @@
sasjs-tests/
docs/
.github/
CONTRIBUTING.md

View File

@@ -1,5 +1,9 @@
# Change Log
Since March 2020 the changelog is managed by github releases - see [https://github.com/sasjs/adapter/releases](https://github.com/sasjs/adapter/releases).
## Changes up to 5th March 2020
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
<a name="1.9.0"></a>

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2020 Macro People
Copyright (c) 2021 Macro People
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,7 +1,23 @@
[![](https://data.jsdelivr.com/v1/package/npm/@sasjs/adapter/badge)](https://www.jsdelivr.com/package/npm/@sasjs/adapter)
# @sasjs/adapter
[![npm package][npm-image]][npm-url]
[![Github Workflow][githubworkflow-image]][githubworkflow-url]
[![Dependency Status][dependency-image]][dependency-url]
[![npm](https://img.shields.io/npm/dt/@sasjs/adapter)]()
![Snyk Vulnerabilities for npm package](https://img.shields.io/snyk/vulnerabilities/npm/@sasjs/adapter)
[![License](https://img.shields.io/apm/l/atomic-design-ui.svg)](/LICENSE)
![GitHub top language](https://img.shields.io/github/languages/top/sasjs/adapter)
![GitHub issues](https://img.shields.io/github/issues/sasjs/adapter)
[![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-908a85?logo=gitpod)](https://gitpod.io/#https://github.com/sasjs/adapter)
[npm-image]:https://img.shields.io/npm/v/@sasjs/adapter.svg
[npm-url]:http://npmjs.org/package/@sasjs/adapter
[githubworkflow-image]:https://github.com/sasjs/adapter/actions/workflows/build.yml/badge.svg
[githubworkflow-url]:https://github.com/sasjs/adapter/blob/main/.github/workflows/build.yml
[dependency-image]:https://david-dm.org/sasjs/adapter.svg
[dependency-url]:https://github.com/sasjs/adapter/blob/main/package.json
SASjs is a open-source framework for building Web Apps on SAS® platforms. You can use as much or as little of it as you like. This repository contains the JS adapter, the part that handles the to/from SAS communication on the client side. There are 3 ways to install it:
1 - `npm install @sasjs/adapter` - for use in a node project
@@ -198,8 +214,15 @@ This approach is by far the fastest, as a result of the optimisations we have bu
# More resources
For more information and examples specific to this adapter you can check out the [user guide](https://sasjs.io/sasjs-adapter/) or the [technical](http://adapter.sasjs.io/) documentation.
For more information and examples specific to this adapter you can check out the [user guide](https://sasjs.io/sasjs-adapter/) or the [technical](http://adapter.sasjs.io/) documentation.
For more information on building web apps in general, check out these [resources](https://sasjs.io/training/resources/) or contact the [author](https://www.linkedin.com/in/allanbowe/) directly.
If you are a SAS 9 or SAS Viya customer you can also request a copy of [Data Controller](https://datacontroller.io) - free for up to 5 users, this tool makes use of all parts of the SASjs framework.
## Star Gazing
If you find this library useful, help us grow our star graph!
![](https://starchart.cc/sasjs/adapter.svg)

13655
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
{
"name": "@sasjs/adapter",
"description": "JavaScript adapter for SAS",
"homepage": "https://adapter.sasjs.io",
"scripts": {
"build": "rimraf build && rimraf node && mkdir node && cp -r src/* node && webpack && rimraf build/src && rimraf node",
"package:lib": "npm run build && cp ./package.json build && cd build && npm version \"5.0.0\" && npm pack",
@@ -11,7 +12,8 @@
"prepublishOnly": "cp -r ./build/* . && rm -rf ./build",
"postpublish": "git clean -fd",
"semantic-release": "semantic-release",
"typedoc": "typedoc"
"typedoc": "typedoc",
"postinstall": "[ -d .git ] && git config core.hooksPath ./.git-hooks || true"
},
"publishConfig": {
"access": "public"
@@ -37,6 +39,7 @@
"license": "ISC",
"devDependencies": {
"@types/jest": "^26.0.22",
"@types/tough-cookie": "^4.0.0",
"cp": "^0.2.0",
"dotenv": "^8.2.0",
"jest": "^26.6.3",
@@ -49,7 +52,7 @@
"ts-loader": "^9.1.2",
"tslint": "^6.1.3",
"tslint-config-prettier": "^1.18.0",
"typedoc": "^0.20.35",
"typedoc": "^0.20.36",
"typedoc-neo-theme": "^1.1.0",
"typedoc-plugin-external-module-name": "^4.0.6",
"typescript": "^3.9.9",
@@ -58,9 +61,12 @@
},
"main": "index.js",
"dependencies": {
"@sasjs/utils": "^2.10.2",
"@sasjs/utils": "^2.14.0",
"axios": "^0.21.1",
"axios-cookiejar-support": "^1.0.1",
"form-data": "^4.0.0",
"https": "^1.0.0"
"https": "^1.0.0",
"tough-cookie": "^4.0.0",
"url": "^0.11.0"
}
}

View File

@@ -55,6 +55,7 @@ filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas";
%inc mc;
filename ft15f001 temp;
parmcards4;
%webout(FETCH)
%webout(OPEN)
%macro x();
%do i=1 %to &_webin_file_count; %webout(OBJ,&&_webin_name&i) %end;
@@ -63,6 +64,7 @@ parmcards4;
;;;;
%mm_createwebservice(path=/Public/app/common,name=sendObj)
parmcards4;
%webout(FETCH)
%webout(OPEN)
%macro x();
%do i=1 %to &_webin_file_count; %webout(ARR,&&_webin_name&i) %end;

View File

@@ -2378,9 +2378,9 @@
}
},
"@types/jest": {
"version": "26.0.22",
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.22.tgz",
"integrity": "sha512-eeWwWjlqxvBxc4oQdkueW5OF/gtfSceKk4OnOAGlUSwS/liBRtZppbJuz1YkgbrbfGOoeBHun9fOvXnjNwrSOw==",
"version": "26.0.20",
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.20.tgz",
"integrity": "sha512-9zi2Y+5USJRxd0FsahERhBwlcvFh6D2GLQnY2FH2BzK8J9s9omvNHIbvABwIluXa0fD8XVKMLTO0aOEuUfACAA==",
"requires": {
"jest-diff": "^26.0.0",
"pretty-format": "^26.0.0"

View File

@@ -6,7 +6,7 @@
"dependencies": {
"@sasjs/adapter": "file:../build/sasjs-adapter-5.0.0.tgz",
"@sasjs/test-framework": "^1.4.0",
"@types/jest": "^26.0.22",
"@types/jest": "^26.0.20",
"@types/node": "^14.14.25",
"@types/react": "^17.0.1",
"@types/react-dom": "^17.0.0",

View File

@@ -10,7 +10,8 @@ import {
JobExecutor,
WebJobExecutor,
ComputeJobExecutor,
JesJobExecutor
JesJobExecutor,
Sas9JobExecutor
} from './job-execution'
import { ErrorResponse } from './types/errors'
@@ -41,6 +42,7 @@ export default class SASjs {
private webJobExecutor: JobExecutor | null = null
private computeJobExecutor: JobExecutor | null = null
private jesJobExecutor: JobExecutor | null = null
private sas9JobExecutor: JobExecutor | null = null
constructor(config?: any) {
this.sasjsConfig = {
@@ -569,6 +571,12 @@ export default class SASjs {
accessToken
)
}
} else if (
config.serverType === ServerType.Sas9 &&
config.username &&
config.password
) {
return await this.sas9JobExecutor!.execute(sasJob, data, config)
} else {
return await this.webJobExecutor!.execute(
sasJob,
@@ -823,6 +831,12 @@ export default class SASjs {
this.sasViyaApiClient!
)
this.sas9JobExecutor = new Sas9JobExecutor(
this.sasjsConfig.serverUrl,
this.sasjsConfig.serverType!,
this.jobsPath
)
this.computeJobExecutor = new ComputeJobExecutor(
this.sasjsConfig.serverUrl,
this.sasViyaApiClient!

View File

@@ -0,0 +1,110 @@
import { ServerType } from '@sasjs/utils/types'
import * as NodeFormData from 'form-data'
import { ErrorResponse } from '../types/errors'
import { convertToCSV, isRelativePath } from '../utils'
import { BaseJobExecutor } from './JobExecutor'
import { Sas9RequestClient } from '../request/Sas9RequestClient'
/**
* Job executor for SAS9 servers for use in Node.js environments.
* Initiates login with the provided username and password from the config
* The cookies are stored in the request client and used in subsequent
* job execution requests.
*/
export class Sas9JobExecutor extends BaseJobExecutor {
private requestClient: Sas9RequestClient
constructor(
serverUrl: string,
serverType: ServerType,
private jobsPath: string
) {
super(serverUrl, serverType)
this.requestClient = new Sas9RequestClient(serverUrl, false)
}
async execute(sasJob: string, data: any, config: any) {
const program = isRelativePath(sasJob)
? config.appLoc
? config.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '')
: sasJob
: sasJob
let apiUrl = `${config.serverUrl}${this.jobsPath}?${'_program=' + program}`
apiUrl = `${apiUrl}${
config.username && config.password
? '&_username=' + config.username + '&_password=' + config.password
: ''
}`
let requestParams = {
...this.getRequestParams(config)
}
let formData = new NodeFormData()
if (data) {
try {
formData = generateFileUploadForm(formData, data)
} catch (e) {
return Promise.reject(new ErrorResponse(e?.message, e))
}
}
for (const key in requestParams) {
if (requestParams.hasOwnProperty(key)) {
formData.append(key, requestParams[key])
}
}
await this.requestClient.login(
config.username,
config.password,
this.jobsPath
)
const contentType =
data && Object.keys(data).length
? 'multipart/form-data; boundary=' + (formData as any)._boundary
: 'text/plain'
return await this.requestClient!.post(
apiUrl,
formData,
undefined,
contentType,
{
Accept: '*/*',
Connection: 'Keep-Alive'
}
)
}
private getRequestParams(config: any): any {
const requestParams: any = {}
if (config.debug) {
requestParams['_debug'] = 131
}
return requestParams
}
}
const generateFileUploadForm = (
formData: NodeFormData,
data: any
): NodeFormData => {
for (const tableName in data) {
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.'
)
}
formData.append(name, csv, {
filename: `${name}.csv`,
contentType: 'application/csv'
})
}
return formData
}

View File

@@ -1,4 +1,5 @@
export * from './ComputeJobExecutor'
export * from './JesJobExecutor'
export * from './JobExecutor'
export * from './Sas9JobExecutor'
export * from './WebJobExecutor'

View File

@@ -44,11 +44,11 @@ export interface HttpClient {
}
export class RequestClient implements HttpClient {
private csrfToken: CsrfToken = { headerName: '', value: '' }
private fileUploadCsrfToken: CsrfToken | undefined
private httpClient: AxiosInstance
protected csrfToken: CsrfToken = { headerName: '', value: '' }
protected fileUploadCsrfToken: CsrfToken | undefined
protected httpClient: AxiosInstance
constructor(private baseUrl: string, allowInsecure = false) {
constructor(protected baseUrl: string, allowInsecure = false) {
const https = require('https')
if (allowInsecure && https.Agent) {
this.httpClient = axios.create({
@@ -290,7 +290,7 @@ export class RequestClient implements HttpClient {
})
}
private getHeaders = (
protected getHeaders = (
accessToken: string | undefined,
contentType: string
) => {
@@ -315,7 +315,7 @@ export class RequestClient implements HttpClient {
return headers
}
private parseAndSetFileUploadCsrfToken = (response: AxiosResponse) => {
protected parseAndSetFileUploadCsrfToken = (response: AxiosResponse) => {
const token = this.parseCsrfToken(response)
if (token) {
@@ -323,7 +323,7 @@ export class RequestClient implements HttpClient {
}
}
private parseAndSetCsrfToken = (response: AxiosResponse) => {
protected parseAndSetCsrfToken = (response: AxiosResponse) => {
const token = this.parseCsrfToken(response)
if (token) {
@@ -347,7 +347,7 @@ export class RequestClient implements HttpClient {
}
}
private handleError = async (
protected handleError = async (
e: any,
callback: any,
debug: boolean = false
@@ -405,7 +405,7 @@ export class RequestClient implements HttpClient {
throw e
}
private parseResponse<T>(response: AxiosResponse<any>) {
protected parseResponse<T>(response: AxiosResponse<any>) {
const etag = response?.headers ? response.headers['etag'] : ''
let parsedResponse
let includeSAS9Log: boolean = false
@@ -439,7 +439,7 @@ export class RequestClient implements HttpClient {
}
}
const throwIfError = (response: AxiosResponse) => {
export const throwIfError = (response: AxiosResponse) => {
if (response.status === 401) {
throw new LoginRequiredError()
}

View File

@@ -0,0 +1,121 @@
import { AxiosRequestConfig } from 'axios'
import axiosCookieJarSupport from 'axios-cookiejar-support'
import * as tough from 'tough-cookie'
import { prefixMessage } from '@sasjs/utils/error'
import { RequestClient, throwIfError } from './RequestClient'
/**
* Specific request client for SAS9 in Node.js environments.
* Handles redirects and cookie management.
*/
export class Sas9RequestClient extends RequestClient {
constructor(baseUrl: string, allowInsecure = false) {
super(baseUrl, allowInsecure)
this.httpClient.defaults.maxRedirects = 0
this.httpClient.defaults.validateStatus = (status) =>
status >= 200 && status < 303
if (axiosCookieJarSupport) {
axiosCookieJarSupport(this.httpClient)
this.httpClient.defaults.jar = new tough.CookieJar()
}
}
public async login(username: string, password: string, jobsPath: string) {
const codeInjectorPath = `/User Folders/${username}/My Folder/sasjs/runner`
if (this.httpClient.defaults.jar) {
;(this.httpClient.defaults.jar as tough.CookieJar).removeAllCookies()
await this.get(
`${jobsPath}?_program=${codeInjectorPath}&_username=${username}&_password=${password}`,
undefined,
'text/plain'
)
}
}
public async get<T>(
url: string,
accessToken: string | undefined,
contentType: string = 'application/json',
overrideHeaders: { [key: string]: string | number } = {},
debug: boolean = false
): Promise<{ result: T; etag: string }> {
const headers = {
...this.getHeaders(accessToken, contentType),
...overrideHeaders
}
const requestConfig: AxiosRequestConfig = {
headers,
responseType: contentType === 'text/plain' ? 'text' : 'json',
withCredentials: true
}
if (contentType === 'text/plain') {
requestConfig.transformResponse = undefined
}
return this.httpClient
.get<T>(url, requestConfig)
.then((response) => {
if (response.status === 302) {
return this.get(
response.headers['location'],
accessToken,
contentType
)
}
throwIfError(response)
return this.parseResponse<T>(response)
})
.catch(async (e) => {
return await this.handleError(
e,
() =>
this.get<T>(url, accessToken, contentType, overrideHeaders).catch(
(err) => {
throw prefixMessage(
err,
'Error while executing handle error callback. '
)
}
),
debug
).catch((err) => {
throw prefixMessage(err, 'Error while handling error. ')
})
})
}
public post<T>(
url: string,
data: any,
accessToken: string | undefined,
contentType = 'application/json',
overrideHeaders: { [key: string]: string | number } = {}
): Promise<{ result: T; etag: string }> {
const headers = {
...this.getHeaders(accessToken, contentType),
...overrideHeaders
}
return this.httpClient
.post<T>(url, data, { headers, withCredentials: true })
.then(async (response) => {
if (response.status === 302) {
return await this.get(
response.headers['location'],
undefined,
contentType,
overrideHeaders
)
}
throwIfError(response)
return this.parseResponse<T>(response)
})
.catch(async (e) => {
return await this.handleError(e, () =>
this.post<T>(url, data, accessToken, contentType, overrideHeaders)
)
})
}
}