├── .npmrc
├── .gitattributes
├── src
├── types
│ ├── system
│ │ └── global.d.ts
│ ├── JobDefinition.ts
│ ├── JobResult.ts
│ ├── CsrfToken.ts
│ ├── ExecuteScript.ts
│ ├── LogStatistics.ts
│ ├── Link.ts
│ ├── Process.d.ts
│ ├── File.ts
│ ├── Folder.ts
│ ├── UploadFile.ts
│ ├── errors
│ │ ├── ArgumentError.ts
│ │ ├── InvalidJsonError.ts
│ │ ├── NotFoundError.ts
│ │ ├── AuthorizeError.ts
│ │ ├── JsonParseArrayError.ts
│ │ ├── InternalServerError.ts
│ │ ├── WeboutResponseError.ts
│ │ ├── InvalidSASjsCsrfError.ts
│ │ ├── SAS9AuthError.ts
│ │ ├── ComputeJobExecutionError.ts
│ │ ├── JobExecutionError.ts
│ │ ├── JobStatePollError.ts
│ │ ├── LoginRequiredError.ts
│ │ ├── CertificateError.ts
│ │ ├── NoSessionStateError.ts
│ │ ├── index.ts
│ │ ├── ErrorResponse.ts
│ │ ├── RootFolderNotFoundError.ts
│ │ ├── spec
│ │ │ └── SAS9AuthError.spec.ts
│ │ └── RootFolderNotFoundError.spec.ts
│ ├── PollOptions.ts
│ ├── WriteStream.ts
│ ├── Login.ts
│ ├── Job.ts
│ ├── index.ts
│ ├── FileResource.ts
│ ├── Session.ts
│ ├── Tables.ts
│ ├── Context.ts
│ ├── Tables.spec.ts
│ ├── RequestClient.ts
│ └── SASjsConfig.ts
├── minified
│ └── sas9
│ │ └── index.ts
├── utils
│ ├── delay.ts
│ ├── isRelativePath.ts
│ ├── isNode.ts
│ ├── isUri.ts
│ ├── getFormData.ts
│ ├── asyncForEach.ts
│ ├── createAxiosInstance.ts
│ ├── compareTimestamps.ts
│ ├── parseSourceCode.ts
│ ├── parseGeneratedCode.ts
│ ├── splitChunks.ts
│ ├── parseSasViyaLog.ts
│ ├── isUrl.ts
│ ├── serialize.ts
│ ├── getUserLanguage.ts
│ ├── needsRetry.ts
│ ├── parseWeboutResponse.ts
│ ├── appendExtraResponseAttributes.ts
│ ├── spec
│ │ ├── getFormData.spec.ts
│ │ ├── parseSasViyaLog.spec.ts
│ │ ├── parseViyaDebugResponse.spec.ts
│ │ ├── validateInput.spec.ts
│ │ └── extractUserLongNameSas9.spec.ts
│ ├── getValidJson.ts
│ ├── isIeOrEdge.ts
│ ├── index.ts
│ ├── sas9
│ │ └── extractUserLongNameSas9.ts
│ ├── formatDataForRequest.ts
│ ├── parseViyaDebugResponse.ts
│ ├── fetchLogByChunks.ts
│ └── validateInput.ts
├── auth
│ ├── index.ts
│ ├── isAuthorizeFormRequired.ts
│ ├── isLoginRequired.ts
│ ├── verifySas9Login.ts
│ ├── openWebPage.ts
│ ├── spec
│ │ ├── verifySas9Login.spec.ts
│ │ ├── verifySasViyaLogin.spec.ts
│ │ ├── refreshTokensForSasjs.spec.ts
│ │ ├── mockResponses.ts
│ │ ├── openWebPage.spec.ts
│ │ ├── getAccessTokenForSasjs.spec.ts
│ │ ├── getTokenRequestErrorPrefix.spec.ts
│ │ ├── getAccessTokenForViya.spec.ts
│ │ ├── loginHeader.spec.ts
│ │ ├── getTokens.spec.ts
│ │ └── refreshTokensForViya.spec.ts
│ ├── verifySasViyaLogin.ts
│ ├── refreshTokensForSasjs.ts
│ ├── getAccessTokenForSasjs.ts
│ ├── getTokens.ts
│ ├── getAccessTokenForViya.ts
│ ├── refreshTokensForViya.ts
│ └── getTokenRequestErrorPrefix.ts
├── __mocks__
│ └── axios.ts
├── job-execution
│ ├── index.ts
│ ├── JobExecutor.ts
│ ├── ComputeJobExecutor.ts
│ └── JesJobExecutor.ts
├── index.ts
├── api
│ └── viya
│ │ ├── writeStream.ts
│ │ ├── getFileStream.ts
│ │ ├── uploadTables.ts
│ │ ├── spec
│ │ ├── getFileStream.spec.ts
│ │ ├── mockResponses.ts
│ │ ├── writeStream.spec.ts
│ │ ├── uploadTables.spec.ts
│ │ └── saveLog.spec.ts
│ │ └── saveLog.ts
├── test
│ ├── utils
│ │ ├── isUrl.spec.ts
│ │ ├── parseSourceCode.spec.ts
│ │ ├── getValidJson.spec.ts
│ │ └── parseGeneratedCode.spec.ts
│ ├── SAS_server_app.ts
│ └── FolderOperations.spec.ts
├── file
│ ├── generateTableUploadForm.ts
│ └── generateFileUploadForm.ts
├── SASViyaApiClient.spec.ts
├── request
│ └── SasjsRequestClient.ts
└── SAS9ApiClient.ts
├── .gitpod.yml
├── .vscode
└── settings.json
├── sasjs-tests
├── src
│ ├── types
│ │ ├── index.ts
│ │ ├── context.ts
│ │ └── test.ts
│ ├── images
│ │ └── favicon.png
│ ├── core
│ │ ├── index.ts
│ │ ├── runTest.ts
│ │ └── AppContext.ts
│ ├── components
│ │ ├── index.ts
│ │ ├── TestSuite.css
│ │ ├── LoginForm.css
│ │ ├── TestsView.css
│ │ ├── TestCard.css
│ │ ├── LoginForm.ts
│ │ ├── TestCard.ts
│ │ └── TestSuite.ts
│ ├── config
│ │ └── loader.ts
│ └── testSuites
│ │ ├── FileUpload.ts
│ │ └── SasjsRequests.ts
├── public
│ ├── robots.txt
│ └── config.json
├── sasjs
│ ├── doxy
│ │ ├── logo.png
│ │ ├── new_stylesheet.css
│ │ ├── favicon.ico
│ │ ├── new_footer.html
│ │ ├── Doxyfile
│ │ └── new_header.html
│ ├── common
│ │ ├── sendMacVars.sas
│ │ ├── invalidJSON.sas
│ │ ├── makeErr.sas
│ │ ├── sendArr.sas
│ │ └── sendObj.sas
│ └── sasjsconfig.json
├── vite.config.js
├── .gitignore
├── index.html
├── sasjs-cypress-run.sh
├── .sasjslint
├── tsconfig.json
├── index.css
└── package.json
├── .env.example
├── .gitignore
├── cypress
├── videos
│ └── sasjs.tests.ts.mp4
├── tsconfig.json
├── screenshots
│ └── sasjs.tests.ts
│ │ └── sasjs-tests -- Should have all tests successfull -- before all hook (failed).png
├── webpack.config.js
├── plugins
│ ├── cy-ts-preprocessor.js
│ └── index.js
├── support
│ ├── index.js
│ └── commands.js
└── integration
│ └── sasjs.tests.ts
├── .prettierrc
├── screenshots
├── subsequent-session-request.png
└── session-manager-first-request.png
├── .github
├── dependabot.yml
├── reviewer-lottery.yml
├── workflows
│ ├── assign-reviewer.yml
│ ├── generateDocs.yml
│ ├── build-unit-tests.yml
│ └── npmpublish.yml
├── issue_template.md
├── vpn
│ └── config.ovpn
├── PULL_REQUEST_TEMPLATE.md
└── CONTRIBUTING.md
├── tslint.json
├── .npmignore
├── typedoc.json
├── .git-hooks
├── pre-commit
└── commit-msg
├── tsconfig.json
├── checkNodeVersion.js
├── cypress.config.js
├── createTSDocs.js
├── LICENSE
├── webpack.config.js
└── .all-contributorsrc
/.npmrc:
--------------------------------------------------------------------------------
1 | ignore-scripts=true
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto eol=lf
--------------------------------------------------------------------------------
/src/types/system/global.d.ts:
--------------------------------------------------------------------------------
1 | import 'jest-extended'
2 |
--------------------------------------------------------------------------------
/.gitpod.yml:
--------------------------------------------------------------------------------
1 | tasks:
2 | - init: npm install && npm run build
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "cSpell.words": ["SASVIYA"]
3 | }
4 |
--------------------------------------------------------------------------------
/sasjs-tests/src/types/index.ts:
--------------------------------------------------------------------------------
1 | export * from './test'
2 | export * from './context'
3 |
--------------------------------------------------------------------------------
/src/types/JobDefinition.ts:
--------------------------------------------------------------------------------
1 | export interface JobDefinition {
2 | code: string
3 | }
4 |
--------------------------------------------------------------------------------
/src/types/JobResult.ts:
--------------------------------------------------------------------------------
1 | export interface JobResult {
2 | '_webout.json': string
3 | }
4 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | SERVER_URL=https://server.com
2 | DEFAULT_COMPUTE_CONTEXT=SAS Job Execution compute context
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | build
3 |
4 | docs
5 |
6 | .env
7 |
8 | /coverage
9 |
10 | .DS_Store
--------------------------------------------------------------------------------
/sasjs-tests/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/types/CsrfToken.ts:
--------------------------------------------------------------------------------
1 | export interface CsrfToken {
2 | headerName: string
3 | value: string
4 | }
5 |
--------------------------------------------------------------------------------
/sasjs-tests/sasjs/doxy/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sasjs/adapter/master/sasjs-tests/sasjs/doxy/logo.png
--------------------------------------------------------------------------------
/cypress/videos/sasjs.tests.ts.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sasjs/adapter/master/cypress/videos/sasjs.tests.ts.mp4
--------------------------------------------------------------------------------
/sasjs-tests/sasjs/doxy/new_stylesheet.css:
--------------------------------------------------------------------------------
1 | #projectlogo img {
2 | border: 0px none;
3 | max-height: 70px;
4 | }
5 |
--------------------------------------------------------------------------------
/src/minified/sas9/index.ts:
--------------------------------------------------------------------------------
1 | import SASjs from './SASjs'
2 | export * from '../../types'
3 | export default SASjs
4 |
--------------------------------------------------------------------------------
/src/types/ExecuteScript.ts:
--------------------------------------------------------------------------------
1 | export interface ExecutionQuery {
2 | _program: string
3 | _debug?: number
4 | }
5 |
--------------------------------------------------------------------------------
/src/utils/delay.ts:
--------------------------------------------------------------------------------
1 | export const delay = (ms: number) =>
2 | new Promise((resolve) => setTimeout(resolve, ms))
3 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "none",
3 | "tabWidth": 2,
4 | "semi": false,
5 | "singleQuote": true
6 | }
7 |
--------------------------------------------------------------------------------
/sasjs-tests/sasjs/doxy/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sasjs/adapter/master/sasjs-tests/sasjs/doxy/favicon.ico
--------------------------------------------------------------------------------
/sasjs-tests/src/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sasjs/adapter/master/sasjs-tests/src/images/favicon.png
--------------------------------------------------------------------------------
/src/utils/isRelativePath.ts:
--------------------------------------------------------------------------------
1 | export const isRelativePath = (uri: string): boolean =>
2 | !!uri && !uri.startsWith('/')
3 |
--------------------------------------------------------------------------------
/sasjs-tests/src/core/index.ts:
--------------------------------------------------------------------------------
1 | export * from './runTest'
2 | export * from './TestRunner'
3 | export * from './AppContext'
4 |
--------------------------------------------------------------------------------
/src/types/LogStatistics.ts:
--------------------------------------------------------------------------------
1 | export interface LogStatistics {
2 | lineCount: number
3 | modifiedTimeStamp: string
4 | }
5 |
--------------------------------------------------------------------------------
/screenshots/subsequent-session-request.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sasjs/adapter/master/screenshots/subsequent-session-request.png
--------------------------------------------------------------------------------
/screenshots/session-manager-first-request.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sasjs/adapter/master/screenshots/session-manager-first-request.png
--------------------------------------------------------------------------------
/src/types/Link.ts:
--------------------------------------------------------------------------------
1 | export interface Link {
2 | method: string
3 | rel: string
4 | href: string
5 | uri: string
6 | type: string
7 | }
8 |
--------------------------------------------------------------------------------
/src/types/Process.d.ts:
--------------------------------------------------------------------------------
1 | declare namespace NodeJS {
2 | export interface Process {
3 | logger?: import('@sasjs/utils/logger').Logger
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/utils/isNode.ts:
--------------------------------------------------------------------------------
1 | export const isNode = () =>
2 | typeof process !== 'undefined' &&
3 | process.versions != null &&
4 | process.versions.node != null
5 |
--------------------------------------------------------------------------------
/src/auth/index.ts:
--------------------------------------------------------------------------------
1 | export * from './AuthManager'
2 | export * from './isAuthorizeFormRequired'
3 | export * from './isLoginRequired'
4 | export * from './loginHeader'
5 |
--------------------------------------------------------------------------------
/src/types/File.ts:
--------------------------------------------------------------------------------
1 | import { Link } from './Link'
2 |
3 | export interface File {
4 | id: string
5 | name: string
6 | parentUri: string
7 | links: Link[]
8 | }
9 |
--------------------------------------------------------------------------------
/src/types/Folder.ts:
--------------------------------------------------------------------------------
1 | import { Link } from './Link'
2 |
3 | export interface Folder {
4 | id: string
5 | uri: string
6 | links: Link[]
7 | memberCount: number
8 | }
9 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "npm"
4 | directory: "/"
5 | schedule:
6 | interval: "monthly"
7 | open-pull-requests-limit: 1
8 |
--------------------------------------------------------------------------------
/src/types/UploadFile.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Represents an object that is passed to the file uploader.
3 | *
4 | */
5 | export interface UploadFile {
6 | file: File
7 | fileName: string
8 | }
9 |
--------------------------------------------------------------------------------
/sasjs-tests/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | export default defineConfig({
3 | build: {
4 | assetsInlineLimit: 0,
5 | assetsDir: ''
6 | },
7 | base: ''
8 | })
9 |
--------------------------------------------------------------------------------
/src/utils/isUri.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Checks if string is in URI format.
3 | * @param str - string to check.
4 | */
5 | export const isUri = (str: string): boolean => /^\/folders\/folders\//.test(str)
6 |
--------------------------------------------------------------------------------
/src/auth/isAuthorizeFormRequired.ts:
--------------------------------------------------------------------------------
1 | export const isAuthorizeFormRequired = (response: string): boolean => {
2 | return /
/gm.test(response)
3 | }
4 |
--------------------------------------------------------------------------------
/src/utils/getFormData.ts:
--------------------------------------------------------------------------------
1 | import { isNode } from './'
2 | import NodeFormData from 'form-data'
3 |
4 | export const getFormData = (): NodeFormData | FormData =>
5 | isNode() ? new NodeFormData() : new FormData()
6 |
--------------------------------------------------------------------------------
/src/__mocks__/axios.ts:
--------------------------------------------------------------------------------
1 | import { AxiosStatic } from 'axios'
2 |
3 | const mockAxios = jest.genMockFromModule('axios') as AxiosStatic
4 |
5 | mockAxios.create = jest.fn(() => mockAxios)
6 |
7 | export default mockAxios
8 |
--------------------------------------------------------------------------------
/src/utils/asyncForEach.ts:
--------------------------------------------------------------------------------
1 | export async function asyncForEach(array: any[], callback: any) {
2 | for (let index = 0; index < array.length; index++) {
3 | await callback(array[index], index, array)
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/auth/isLoginRequired.ts:
--------------------------------------------------------------------------------
1 | export const isLogInRequired = (response: string): boolean => {
2 | const pattern: RegExp = //gm
3 | const matches = pattern.test(response)
4 | return matches
5 | }
6 |
--------------------------------------------------------------------------------
/src/utils/createAxiosInstance.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import * as https from 'https'
3 |
4 | export const createAxiosInstance = (
5 | baseURL: string,
6 | httpsAgent?: https.Agent
7 | ) => axios.create({ baseURL, httpsAgent })
8 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["tslint:recommended", "tslint-config-prettier"],
3 | "rules": {
4 | "no-string-literal": false,
5 | "forin": false,
6 | "no-console": false,
7 | "max-classes-per-file": false
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/types/errors/ArgumentError.ts:
--------------------------------------------------------------------------------
1 | export class ArgumentError extends Error {
2 | constructor(public message: string) {
3 | super(message)
4 | this.name = 'ArgumentError'
5 | Object.setPrototypeOf(this, ArgumentError.prototype)
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/cypress/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true,
4 | "baseUrl": "../node_modules",
5 | "target": "es6",
6 | "lib": ["es2019", "dom"],
7 | "types": ["cypress"]
8 | },
9 | "include": ["**/*.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/sasjs-tests/src/components/index.ts:
--------------------------------------------------------------------------------
1 | export { LoginForm } from './LoginForm'
2 | export { TestCard } from './TestCard'
3 | export { TestSuiteElement } from './TestSuite'
4 | export { TestsView } from './TestsView'
5 | export { RequestsModal } from './RequestsModal'
6 |
--------------------------------------------------------------------------------
/.github/reviewer-lottery.yml:
--------------------------------------------------------------------------------
1 | groups:
2 | - name: SASjs Devs # name of the group
3 | reviewers: 1 # how many reviewers do you want to assign?
4 | usernames: # github usernames of the reviewers
5 | - YuryShkoda
6 | - medjedovicm
7 | - sabhas
8 |
--------------------------------------------------------------------------------
/sasjs-tests/sasjs/common/sendMacVars.sas:
--------------------------------------------------------------------------------
1 | /**
2 | @file
3 | @brief Returns Macro Variables
4 |
5 | SAS Macros
6 | **/
7 |
8 | data work.macvars;
9 | set sashelp.vmacro;
10 | run;
11 | %webout(OPEN)
12 | %webout(OBJ,macvars)
13 | %webout(CLOSE)
--------------------------------------------------------------------------------
/src/types/errors/InvalidJsonError.ts:
--------------------------------------------------------------------------------
1 | export class InvalidJsonError extends Error {
2 | constructor() {
3 | super('Error: invalid Json string')
4 | this.name = 'InvalidJsonError'
5 | Object.setPrototypeOf(this, InvalidJsonError.prototype)
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/cypress/screenshots/sasjs.tests.ts/sasjs-tests -- Should have all tests successfull -- before all hook (failed).png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sasjs/adapter/master/cypress/screenshots/sasjs.tests.ts/sasjs-tests -- Should have all tests successfull -- before all hook (failed).png
--------------------------------------------------------------------------------
/sasjs-tests/sasjs/common/invalidJSON.sas:
--------------------------------------------------------------------------------
1 | /**
2 | @file
3 | @brief Makes an invalid JSON file
4 |
5 | SAS Macros
6 | **/
7 |
8 | %webout(OPEN)
9 | data _null_;
10 | file _webout;
11 | put ' the discovery channel ';
12 | run;
13 | %webout(CLOSE)
14 |
--------------------------------------------------------------------------------
/src/job-execution/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ComputeJobExecutor'
2 | export * from './FileUploader'
3 | export * from './JesJobExecutor'
4 | export * from './JobExecutor'
5 | export * from './Sas9JobExecutor'
6 | export * from './WebJobExecutor'
7 | export * from './SasjsJobExecutor'
8 |
--------------------------------------------------------------------------------
/src/types/PollOptions.ts:
--------------------------------------------------------------------------------
1 | export interface PollOptions {
2 | maxPollCount: number
3 | pollInterval: number // milliseconds
4 | pollStrategy?: PollStrategy
5 | streamLog?: boolean
6 | logFolderPath?: string
7 | }
8 |
9 | export type PollStrategy = PollOptions[]
10 |
--------------------------------------------------------------------------------
/src/types/errors/NotFoundError.ts:
--------------------------------------------------------------------------------
1 | export class NotFoundError extends Error {
2 | constructor(public url: string) {
3 | super(`Error: Resource at ${url} was not found`)
4 | this.name = 'NotFoundError'
5 | Object.setPrototypeOf(this, NotFoundError.prototype)
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | sasjs-tests/
2 | docs/
3 | .github/
4 | *.md
5 | *.spec.ts
6 | .all-contributorsrc
7 | cypress/
8 | .gitpod.yml
9 | .prettierrc
10 | cypress.json
11 | jest.config.js
12 | sasjs-cypress-run.sh
13 | tsconfig.json
14 | tslint.json
15 | typedoc.json
16 | webpack.config.js
17 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import SASjs from './SASjs'
2 | export * from './types'
3 | export * from './types/errors'
4 | export * from './SASViyaApiClient'
5 | export * from './SAS9ApiClient'
6 | export * from './SASjsApiClient'
7 | export * from './request/SasjsRequestClient'
8 | export default SASjs
9 |
--------------------------------------------------------------------------------
/src/types/errors/AuthorizeError.ts:
--------------------------------------------------------------------------------
1 | export class AuthorizeError extends Error {
2 | constructor(public message: string, public confirmUrl: string) {
3 | super(message)
4 | this.name = 'AuthorizeError'
5 | Object.setPrototypeOf(this, AuthorizeError.prototype)
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/types/errors/JsonParseArrayError.ts:
--------------------------------------------------------------------------------
1 | export class JsonParseArrayError extends Error {
2 | constructor() {
3 | super('Can not parse array object to json.')
4 | this.name = 'JsonParseArrayError'
5 | Object.setPrototypeOf(this, JsonParseArrayError.prototype)
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/utils/compareTimestamps.ts:
--------------------------------------------------------------------------------
1 | import { SASjsRequest } from '../types'
2 |
3 | /**
4 | * Comparator for SASjs request timestamps.
5 | *
6 | */
7 | export const compareTimestamps = (a: SASjsRequest, b: SASjsRequest) => {
8 | return b.timestamp.getTime() - a.timestamp.getTime()
9 | }
10 |
--------------------------------------------------------------------------------
/src/types/errors/InternalServerError.ts:
--------------------------------------------------------------------------------
1 | export class InternalServerError extends Error {
2 | constructor() {
3 | super('Error: Internal server error.')
4 |
5 | this.name = 'InternalServerError'
6 |
7 | Object.setPrototypeOf(this, InternalServerError.prototype)
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/utils/parseSourceCode.ts:
--------------------------------------------------------------------------------
1 | export const parseSourceCode = (log: string): string => {
2 | const isSourceCodeLine = (line: string) =>
3 | line.trim().substring(0, 10).trimStart().match(/^\d/)
4 | const logLines = log.split('\n').filter(isSourceCodeLine)
5 | return logLines.join('\r\n')
6 | }
7 |
--------------------------------------------------------------------------------
/src/types/errors/WeboutResponseError.ts:
--------------------------------------------------------------------------------
1 | export class WeboutResponseError extends Error {
2 | constructor(public url: string) {
3 | super(`Error: error while parsing response from ${url}`)
4 | this.name = 'WeboutResponseError'
5 | Object.setPrototypeOf(this, WeboutResponseError.prototype)
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/utils/parseGeneratedCode.ts:
--------------------------------------------------------------------------------
1 | export const parseGeneratedCode = (log: string) => {
2 | const startsWith = 'MPRINT'
3 | const isGeneratedCodeLine = (line: string) =>
4 | line.trim().startsWith(startsWith)
5 | const logLines = log.split('\n').filter(isGeneratedCodeLine)
6 | return logLines.join('\r\n')
7 | }
8 |
--------------------------------------------------------------------------------
/sasjs-tests/sasjs/common/makeErr.sas:
--------------------------------------------------------------------------------
1 | /**
2 | @file
3 | @brief Makes an error
4 |
5 | SAS Macros
6 | **/
7 |
8 | If you can keep your head when all about you
9 | Are losing theirs and blaming it on you,
10 | If you can trust yourself when all men doubt you,
11 | But make allowance for their doubting too;
--------------------------------------------------------------------------------
/sasjs-tests/src/types/context.ts:
--------------------------------------------------------------------------------
1 | import type SASjs from '@sasjs/adapter'
2 | import type { SASjsConfig } from '@sasjs/adapter'
3 |
4 | export interface AppConfig {
5 | sasJsConfig: SASjsConfig
6 | }
7 |
8 | export interface AppState {
9 | config: AppConfig | null
10 | adapter: SASjs | null
11 | isLoggedIn: boolean
12 | }
13 |
--------------------------------------------------------------------------------
/src/types/errors/InvalidSASjsCsrfError.ts:
--------------------------------------------------------------------------------
1 | export class InvalidSASjsCsrfError extends Error {
2 | constructor() {
3 | const message = 'Invalid CSRF token!'
4 |
5 | super(`Auth error: ${message}`)
6 | this.name = 'InvalidSASjsCsrfError'
7 | Object.setPrototypeOf(this, InvalidSASjsCsrfError.prototype)
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/typedoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "out": "docs",
3 | "exclude": ["**/*+(index|.spec|.e2e).ts"],
4 | "excludeExternals": true,
5 | "excludePrivate": true,
6 | "entryPoints": [
7 | "src/SASjs.ts",
8 | "src/SAS9ApiClient.ts",
9 | "src/SASjsApiClient.ts",
10 | "src/SASViyaApiClient.ts",
11 | "src/types"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/src/types/errors/SAS9AuthError.ts:
--------------------------------------------------------------------------------
1 | export class SAS9AuthError extends Error {
2 | constructor() {
3 | super(
4 | 'The credentials you provided cannot be authenticated. Please provide a valid set of credentials.'
5 | )
6 | this.name = 'AuthorizeError'
7 | Object.setPrototypeOf(this, SAS9AuthError.prototype)
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/.github/workflows/assign-reviewer.yml:
--------------------------------------------------------------------------------
1 | name: 'Assign Reviewer'
2 | on:
3 | pull_request:
4 | types: [opened]
5 |
6 | jobs:
7 | test:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/checkout@v2
11 | - uses: uesteibar/reviewer-lottery@v1
12 | with:
13 | repo-token: ${{ secrets.GITHUB_TOKEN }}
14 |
--------------------------------------------------------------------------------
/src/types/WriteStream.ts:
--------------------------------------------------------------------------------
1 | import { WriteStream as FsWriteStream } from 'fs'
2 |
3 | export interface WriteStream extends FsWriteStream {
4 | write(
5 | chunk: any,
6 | encoding?: BufferEncoding | ((error: Error | null | undefined) => void),
7 | cb?: (error: Error | null | undefined) => void
8 | ): boolean
9 | path: string | Buffer
10 | }
11 |
--------------------------------------------------------------------------------
/src/utils/splitChunks.ts:
--------------------------------------------------------------------------------
1 | export const splitChunks = (content: string) => {
2 | const size = 16000
3 |
4 | const numChunks = Math.ceil(content.length / size)
5 | const chunks = new Array(numChunks)
6 |
7 | for (let i = 0, o = 0; i < numChunks; ++i, o += size) {
8 | chunks[i] = content.substr(o, size)
9 | }
10 |
11 | return chunks
12 | }
13 |
--------------------------------------------------------------------------------
/src/types/Login.ts:
--------------------------------------------------------------------------------
1 | export interface LoginOptions {
2 | onLoggedOut?: () => Promise
3 | }
4 |
5 | export interface LoginResult {
6 | isLoggedIn: boolean
7 | userName: string
8 | userLongName: string
9 | }
10 | export interface LoginResultInternal {
11 | isLoggedIn: boolean
12 | userName: string
13 | userLongName: string
14 | loginForm?: any
15 | }
16 |
--------------------------------------------------------------------------------
/src/types/errors/ComputeJobExecutionError.ts:
--------------------------------------------------------------------------------
1 | import { Job } from '../Job'
2 |
3 | export class ComputeJobExecutionError extends Error {
4 | constructor(public job: Job, public log: string) {
5 | super('Error: Job execution failed')
6 | this.name = 'ComputeJobExecutionError'
7 | Object.setPrototypeOf(this, ComputeJobExecutionError.prototype)
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/types/Job.ts:
--------------------------------------------------------------------------------
1 | import { Link } from './Link'
2 | import { JobResult } from './JobResult'
3 | import { LogStatistics } from './LogStatistics'
4 |
5 | export interface Job {
6 | id: string
7 | name: string
8 | uri: string
9 | createdBy: string
10 | code?: string
11 | links: Link[]
12 | results: JobResult
13 | error?: any
14 | logStatistics: LogStatistics
15 | }
16 |
--------------------------------------------------------------------------------
/sasjs-tests/public/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "userName": "",
3 | "password": "",
4 | "sasJsConfig": {
5 | "loginMechanism": "Redirected",
6 | "serverUrl": "",
7 | "appLoc": "/Public/app/adapter-tests/services",
8 | "serverType": "SASVIYA",
9 | "debug": false,
10 | "contextName": "SAS Job Execution compute context",
11 | "useComputeApi": true
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/types/errors/JobExecutionError.ts:
--------------------------------------------------------------------------------
1 | export class JobExecutionError extends Error {
2 | constructor(
3 | public errorCode: number,
4 | public errorMessage: string,
5 | public result: string
6 | ) {
7 | super(`Error Code ${errorCode}: ${errorMessage}`)
8 | this.name = 'JobExecutionError'
9 | Object.setPrototypeOf(this, JobExecutionError.prototype)
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/types/errors/JobStatePollError.ts:
--------------------------------------------------------------------------------
1 | export class JobStatePollError extends Error {
2 | constructor(id: string, public originalError: Error) {
3 | super(
4 | `Error while polling job state for job ${id}: ${
5 | originalError.message || originalError
6 | }`
7 | )
8 | this.name = 'JobStatePollError'
9 | Object.setPrototypeOf(this, JobStatePollError.prototype)
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/.git-hooks/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # Using `--silent` helps for showing any errs in the first line of the response
4 | # The first line is picked up by the VS Code GIT UI popup when rc is not 0
5 |
6 | if npm run --silent lint:silent ; then
7 | exit 0
8 | else
9 | npm run --silent lint:fix
10 | echo "❌ Prettier check failed! We ran lint:fix for you. Please add & commit again."
11 | exit 1
12 | fi
13 |
--------------------------------------------------------------------------------
/src/utils/parseSasViyaLog.ts:
--------------------------------------------------------------------------------
1 | export const parseSasViyaLog = (logResponse: { items: any[] }) => {
2 | let log
3 | try {
4 | log = logResponse.items
5 | ? logResponse.items.map((i) => i.line).join('\n')
6 | : JSON.stringify(logResponse)
7 | } catch (e: any) {
8 | console.error('An error has occurred while parsing the log response', e)
9 | log = logResponse
10 | }
11 | return log
12 | }
13 |
--------------------------------------------------------------------------------
/src/types/errors/LoginRequiredError.ts:
--------------------------------------------------------------------------------
1 | export class LoginRequiredError extends Error {
2 | constructor(details?: any) {
3 | const message = details
4 | ? JSON.stringify(details, null, 2)
5 | : 'You must be logged in to access this resource'
6 |
7 | super(`Auth error: ${message}`)
8 | this.name = 'LoginRequiredError'
9 | Object.setPrototypeOf(this, LoginRequiredError.prototype)
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/utils/isUrl.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Checks if string is in URL format.
3 | * @param str - string to check.
4 | */
5 | export const isUrl = (str: string): boolean => {
6 | const supportedProtocols = ['http:', 'https:']
7 |
8 | try {
9 | const url = new URL(str)
10 |
11 | if (!supportedProtocols.includes(url.protocol)) return false
12 | } catch (_) {
13 | return false
14 | }
15 |
16 | return true
17 | }
18 |
--------------------------------------------------------------------------------
/.github/issue_template.md:
--------------------------------------------------------------------------------
1 | ## Expected behaviour
2 | *Describe what should be happening*
3 |
4 | ## Current behaviour
5 | *Describe what is actually happening*
6 |
7 | ## Environment info
8 | **Client tech stack**: *Angular, React, Vue, VanillaJS, NodeJS etc.*
9 | **Server type**: SASJS|SASVIYA|SAS9
10 | **Login mechanism**: Default|Redirected
11 | **Debug**: true|false
12 | **Use Compute Api (relevant only on VIYA)**: true|false
13 |
--------------------------------------------------------------------------------
/sasjs-tests/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
26 |
27 | # sasjs
28 | sasjsbuild
29 | sasjsresults
--------------------------------------------------------------------------------
/sasjs-tests/src/config/loader.ts:
--------------------------------------------------------------------------------
1 | import type { AppConfig } from '../types'
2 |
3 | export interface ConfigWithCredentials extends AppConfig {
4 | userName?: string
5 | password?: string
6 | }
7 |
8 | export async function loadConfig(): Promise {
9 | const response = await fetch('config.json')
10 | if (!response.ok) {
11 | throw new Error('Failed to load config.json')
12 | }
13 | return response.json()
14 | }
15 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["ES2018", "DOM", "ES2019.String"],
4 | "target": "es6",
5 | "module": "commonjs",
6 | "declaration": true,
7 | "outDir": "./build",
8 | "esModuleInterop": true,
9 | "strict": true,
10 | "sourceMap": true,
11 | "inlineSources": true,
12 | "typeRoots": ["./node_modules/@types", "./src/types/system"]
13 | },
14 | "include": ["src"],
15 | "exclude": ["node_modules"]
16 | }
17 |
--------------------------------------------------------------------------------
/src/types/errors/CertificateError.ts:
--------------------------------------------------------------------------------
1 | const instructionsToFix =
2 | 'https://github.com/sasjs/cli/issues/1181#issuecomment-1090638584'
3 |
4 | export class CertificateError extends Error {
5 | constructor(message: string) {
6 | super(
7 | `${message}\nPlease visit the link below for further information on this issue:\n- ${instructionsToFix}\n`
8 | )
9 | this.name = 'CertificateError'
10 | Object.setPrototypeOf(this, CertificateError.prototype)
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/sasjs-tests/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | SASjs tests
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/api/viya/writeStream.ts:
--------------------------------------------------------------------------------
1 | import { WriteStream } from '../../types'
2 |
3 | export const writeStream = async (
4 | stream: WriteStream,
5 | content: string
6 | ): Promise => {
7 | return new Promise((resolve, reject) => {
8 | stream.write(content + '\n', (err: Error | null | undefined) => {
9 | if (err) {
10 | reject(err) // Reject on write error
11 | } else {
12 | resolve(true) // Resolve on successful write
13 | }
14 | })
15 | })
16 | }
17 |
--------------------------------------------------------------------------------
/checkNodeVersion.js:
--------------------------------------------------------------------------------
1 | const result = process.versions
2 | if (result && result.node) {
3 | if (parseInt(result.node) < 14) {
4 | console.log(
5 | '\x1b[31m%s\x1b[0m',
6 | `❌ Process failed due to Node Version,\nPlease install and use Node Version >= 14\nYour current Node Version is: ${result.node}`
7 | )
8 | process.exit(1)
9 | }
10 | } else {
11 | console.log(
12 | '\x1b[31m%s\x1b[0m',
13 | 'Something went wrong while checking Node version'
14 | )
15 | process.exit(1)
16 | }
17 |
--------------------------------------------------------------------------------
/src/types/errors/NoSessionStateError.ts:
--------------------------------------------------------------------------------
1 | export class NoSessionStateError extends Error {
2 | constructor(
3 | public serverResponseStatus: number,
4 | public sessionStateUrl: string,
5 | public logUrl: string
6 | ) {
7 | super(
8 | `Could not get session state. Server responded with ${serverResponseStatus} whilst checking state: ${sessionStateUrl}`
9 | )
10 |
11 | this.name = 'NoSessionStatus'
12 |
13 | Object.setPrototypeOf(this, NoSessionStateError.prototype)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/utils/serialize.ts:
--------------------------------------------------------------------------------
1 | export const serialize = (obj: any) => {
2 | const str: any[] = []
3 | for (const p in obj) {
4 | if (obj.hasOwnProperty(p)) {
5 | if (obj[p] instanceof Array) {
6 | for (let i = 0, n = obj[p].length; i < n; i++) {
7 | str.push(encodeURIComponent(p) + '=' + encodeURIComponent(obj[p][i]))
8 | }
9 | } else {
10 | str.push(encodeURIComponent(p) + '=' + encodeURIComponent(obj[p]))
11 | }
12 | }
13 | }
14 | return str.join('&')
15 | }
16 |
--------------------------------------------------------------------------------
/cypress/webpack.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | mode: 'development',
3 | resolve: {
4 | extensions: ['.ts', '.js']
5 | },
6 | module: {
7 | rules: [
8 | {
9 | test: /\.ts$/,
10 | exclude: [/node_modules/],
11 | use: [
12 | {
13 | loader: 'ts-loader',
14 | options: {
15 | // skip typechecking for speed
16 | transpileOnly: true
17 | }
18 | }
19 | ]
20 | }
21 | ]
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/cypress.config.js:
--------------------------------------------------------------------------------
1 | const { defineConfig } = require('cypress')
2 |
3 | module.exports = defineConfig({
4 | e2e: {
5 | includeShadowDom: true,
6 | chromeWebSecurity: false,
7 | defaultCommandTimeout: 20000,
8 | specPattern: 'cypress/integration/**/*.ts',
9 | supportFile: 'cypress/support/index.js'
10 | },
11 | env: {
12 | sasjsTestsUrl: 'http://localhost:5173',
13 | username: '',
14 | password: '',
15 | screenshotOnRunFailure: false,
16 | testingFinishTimeout: 600000
17 | }
18 | })
19 |
--------------------------------------------------------------------------------
/src/utils/getUserLanguage.ts:
--------------------------------------------------------------------------------
1 | interface IEnavigator {
2 | userLanguage?: string
3 | }
4 |
5 | /**
6 | * Provides preferred language of the user.
7 | * @returns A string representing the preferred language of the user, usually the language of the browser UI. Examples of valid language codes include "en", "en-US", "fr", "fr-FR", "es-ES". More info available https://datatracker.ietf.org/doc/html/rfc5646
8 | */
9 | export const getUserLanguage = () =>
10 | window.navigator.language || (window.navigator as IEnavigator).userLanguage
11 |
--------------------------------------------------------------------------------
/sasjs-tests/sasjs/common/sendArr.sas:
--------------------------------------------------------------------------------
1 | /**
2 | @file
3 | @brief Returns JSON in Array format
4 |
5 | SAS Macros
6 | **/
7 |
8 | %webout(FETCH)
9 | %webout(OPEN)
10 | %macro x();
11 | %if %symexist(sasjs_tables) %then
12 | %do i=1 %to %sysfunc(countw(&sasjs_tables));
13 | %let table=%scan(&sasjs_tables,&i);
14 | %webout(ARR,&table,missing=STRING,showmeta=YES)
15 | %end;
16 | %else %do i=1 %to &_webin_file_count;
17 | %webout(ARR,&&_webin_name&i,missing=STRING,showmeta=YES)
18 | %end;
19 | %mend x;
20 | %x()
21 | %webout(CLOSE)
--------------------------------------------------------------------------------
/sasjs-tests/sasjs/common/sendObj.sas:
--------------------------------------------------------------------------------
1 | /**
2 | @file
3 | @brief Returns JSON in Object format
4 |
5 | SAS Macros
6 | **/
7 |
8 | %webout(FETCH)
9 | %webout(OPEN)
10 | %macro x();
11 | %if %symexist(sasjs_tables) %then
12 | %do i=1 %to %sysfunc(countw(&sasjs_tables));
13 | %let table=%scan(&sasjs_tables,&i);
14 | %webout(OBJ,&table,missing=STRING,showmeta=YES)
15 | %end;
16 | %else %do i=1 %to &_webin_file_count;
17 | %webout(OBJ,&&_webin_name&i,missing=STRING,showmeta=YES)
18 | %end;
19 | %mend x;
20 | %x()
21 | %webout(CLOSE)
--------------------------------------------------------------------------------
/src/utils/needsRetry.ts:
--------------------------------------------------------------------------------
1 | export const needsRetry = (responseText: string): boolean => {
2 | return (
3 | !!responseText &&
4 | ((responseText.includes('"errorCode":403') &&
5 | responseText.includes('_csrf') &&
6 | responseText.includes('X-CSRF-TOKEN')) ||
7 | (responseText.includes('"status":403') &&
8 | responseText.includes('"error":"Forbidden"')) ||
9 | (responseText.includes('"status":449') &&
10 | responseText.includes(
11 | 'Authentication success, retry original request'
12 | )))
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/sasjs-tests/sasjs-cypress-run.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | if npm run cy:run -- --spec "cypress/integration/sasjs.tests.ts" ; then
4 | echo "Cypress sasjs testing passed!"
5 | else
6 | echo '{"msgtype":"m.text", "body":"Automated sasjs-tests failed on the @sasjs/adapter PR: '$2'"}'
7 | curl -XPOST -d '{"msgtype":"m.text", "body":"Automated sasjs-tests failed on the @sasjs/adapter PR: '$2'"}' https://matrix.4gl.io/_matrix/client/r0/rooms/%21jRebyiGmHZlpfDwYXN:4gl.io/send/m.room.message?access_token=$1
8 | echo "Cypress sasjs testing failed!"
9 | exit 1
10 | fi
11 |
--------------------------------------------------------------------------------
/src/types/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Context'
2 | export * from './CsrfToken'
3 | export * from './Folder'
4 | export * from './File'
5 | export * from './Job'
6 | export * from './JobDefinition'
7 | export * from './JobResult'
8 | export * from './Link'
9 | export * from './Login'
10 | export * from './SASjsConfig'
11 | export * from './RequestClient'
12 | export * from './Session'
13 | export * from './UploadFile'
14 | export * from './PollOptions'
15 | export * from './WriteStream'
16 | export * from './ExecuteScript'
17 | export * from './errors'
18 | export * from './Tables'
19 |
--------------------------------------------------------------------------------
/src/utils/parseWeboutResponse.ts:
--------------------------------------------------------------------------------
1 | import { WeboutResponseError } from '../types/errors'
2 |
3 | export const parseWeboutResponse = (response: string, url?: string): string => {
4 | let sasResponse = ''
5 |
6 | if (response.includes('>>weboutBEGIN<<')) {
7 | try {
8 | sasResponse = response
9 | .split('>>weboutBEGIN<<')[1]
10 | .split('>>weboutEND<<')[0]
11 | } catch (e: any) {
12 | if (url) throw new WeboutResponseError(url)
13 |
14 | sasResponse = ''
15 | console.error(e)
16 | }
17 | }
18 |
19 | return sasResponse
20 | }
21 |
--------------------------------------------------------------------------------
/.github/vpn/config.ovpn:
--------------------------------------------------------------------------------
1 | # Client
2 | client
3 | tls-client
4 | dev tun
5 | # this will connect with whatever proto DNS tells us (https://community.openvpn.net/openvpn/ticket/934)
6 | proto udp
7 | remote vpn.4gl.io 7194
8 | resolv-retry infinite
9 | # this will fallback from udp6 to udp4 as well
10 | connect-timeout 5
11 | data-ciphers AES-256-CBC:AES-256-GCM
12 | auth SHA256
13 | script-security 2
14 | keepalive 10 120
15 | remote-cert-tls server
16 |
17 | # Keys
18 | ca ca.crt
19 | cert user.crt
20 | key user.key
21 | tls-auth tls.key 1
22 |
23 | # Security
24 | nobind
25 | persist-key
26 | persist-tun
27 | verb 3
28 |
--------------------------------------------------------------------------------
/src/types/errors/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ArgumentError'
2 | export * from './AuthorizeError'
3 | export * from './CertificateError'
4 | export * from './ComputeJobExecutionError'
5 | export * from './ErrorResponse'
6 | export * from './InternalServerError'
7 | export * from './InvalidJsonError'
8 | export * from './JobExecutionError'
9 | export * from './JobStatePollError'
10 | export * from './JsonParseArrayError'
11 | export * from './LoginRequiredError'
12 | export * from './NoSessionStateError'
13 | export * from './NotFoundError'
14 | export * from './RootFolderNotFoundError'
15 | export * from './WeboutResponseError'
16 |
--------------------------------------------------------------------------------
/src/types/errors/ErrorResponse.ts:
--------------------------------------------------------------------------------
1 | export class ErrorResponse {
2 | error: ErrorBody
3 |
4 | constructor(message: string, details?: any, raw?: any) {
5 | let detailsString = details
6 |
7 | if (typeof details !== 'object') {
8 | try {
9 | detailsString = JSON.parse(details)
10 | } catch {
11 | raw = details
12 | detailsString = ''
13 | }
14 | }
15 |
16 | this.error = {
17 | message,
18 | details: detailsString,
19 | raw
20 | }
21 | }
22 | }
23 |
24 | export interface ErrorBody {
25 | message: string
26 | details: any
27 | raw: any
28 | }
29 |
--------------------------------------------------------------------------------
/sasjs-tests/.sasjslint:
--------------------------------------------------------------------------------
1 | {
2 | "lineEndings": "off",
3 | "noTrailingSpaces": true,
4 | "noEncodedPasswords": true,
5 | "hasDoxygenHeader": true,
6 | "noSpacesInFileNames": true,
7 | "lowerCaseFileNames": true,
8 | "maxLineLength": 80,
9 | "maxHeaderLineLength": 80,
10 | "maxDataLineLength": 80,
11 | "noTabIndentation": true,
12 | "indentationMultiple": 2,
13 | "hasMacroNameInMend": true,
14 | "noNestedMacros": true,
15 | "hasMacroParentheses": true,
16 | "strictMacroDefinition": true,
17 | "noGremlins": true,
18 | "defaultHeader": "/**{lineEnding} @file{lineEnding} @brief {lineEnding} SAS Macros
{lineEnding}**/"
19 | }
--------------------------------------------------------------------------------
/src/utils/appendExtraResponseAttributes.ts:
--------------------------------------------------------------------------------
1 | import { ExtraResponseAttributes } from '@sasjs/utils/types'
2 |
3 | export async function appendExtraResponseAttributes(
4 | response: any,
5 | extraResponseAttributes: ExtraResponseAttributes[]
6 | ) {
7 | let responseObject = {}
8 |
9 | if (extraResponseAttributes?.length) {
10 | const extraAttributes = extraResponseAttributes.reduce(
11 | (map: any, obj: any) => ((map[obj] = response[obj]), map),
12 | {}
13 | )
14 |
15 | responseObject = {
16 | result: response.result,
17 | ...extraAttributes
18 | }
19 | } else responseObject = response.result
20 |
21 | return responseObject
22 | }
23 |
--------------------------------------------------------------------------------
/sasjs-tests/src/components/TestSuite.css:
--------------------------------------------------------------------------------
1 | :host {
2 | display: block;
3 | background: white;
4 | border-radius: 8px;
5 | padding: 20px;
6 | margin-bottom: 20px;
7 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
8 | }
9 |
10 | .header {
11 | display: flex;
12 | justify-content: space-between;
13 | align-items: center;
14 | margin-bottom: 15px;
15 | padding-bottom: 10px;
16 | border-bottom: 1px solid #ecf0f1;
17 | }
18 |
19 | h2 {
20 | color: #2c3e50;
21 | font-size: 20px;
22 | margin: 0;
23 | }
24 |
25 | .stats {
26 | font-size: 14px;
27 | color: #7f8c8d;
28 | }
29 |
30 | .tests {
31 | display: grid;
32 | grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
33 | gap: 15px;
34 | }
35 |
--------------------------------------------------------------------------------
/sasjs-tests/src/types/test.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 | export interface Test {
3 | title: string
4 | description: string
5 | beforeTest?: (...args: any) => Promise
6 | afterTest?: (...args: any) => Promise
7 | test: (context: any) => Promise
8 | assertion: (...args: any) => boolean
9 | }
10 |
11 | export interface TestSuite {
12 | name: string
13 | tests: Test[]
14 | beforeAll?: (...args: any) => Promise
15 | afterAll?: (...args: any) => Promise
16 | }
17 |
18 | export interface TestResult {
19 | result: boolean
20 | error: Error | null
21 | executionTime: number
22 | }
23 |
24 | export type TestStatus = 'pending' | 'running' | 'passed' | 'failed'
25 |
--------------------------------------------------------------------------------
/cypress/plugins/cy-ts-preprocessor.js:
--------------------------------------------------------------------------------
1 | const wp = require('@cypress/webpack-preprocessor')
2 |
3 | const webpackOptions = {
4 | resolve: {
5 | extensions: ['.ts', '.js']
6 | },
7 | module: {
8 | rules: [
9 | {
10 | test: /\.ts$/,
11 | loaders: ['ts-loader'],
12 | exclude: [/node_modules/]
13 | },
14 | {
15 | test: /\.(html|css)$/,
16 | loader: 'raw-loader',
17 | exclude: /\.async\.(html|css)$/
18 | },
19 | {
20 | test: /\.async\.(html|css)$/,
21 | loaders: ['file?name=[name].[hash].[ext]', 'extract']
22 | }
23 | ]
24 | }
25 | }
26 |
27 | const options = {
28 | webpackOptions
29 | }
30 |
31 | module.exports = wp(options)
32 |
--------------------------------------------------------------------------------
/.git-hooks/commit-msg:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | RED="\033[1;31m"
3 | GREEN="\033[1;32m"
4 |
5 | # Get the commit message (the parameter we're given is just the path to the
6 | # temporary file which holds the message).
7 | commit_message=$(cat "$1")
8 |
9 | if (echo "$commit_message" | grep -Eq "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z0-9 -\*]+\))?!?: .+$") then
10 | echo "${GREEN} ✔ Commit message meets Conventional Commit standards"
11 | exit 0
12 | fi
13 |
14 | echo "${RED}❌ Commit message does not meet the Conventional Commit standard!"
15 | echo "An example of a valid message is:"
16 | echo " feat(login): add the 'remember me' button"
17 | echo "ℹ More details at: https://www.conventionalcommits.org/en/v1.0.0/#summary"
18 | exit 1
--------------------------------------------------------------------------------
/src/api/viya/getFileStream.ts:
--------------------------------------------------------------------------------
1 | import { isFolder } from '@sasjs/utils/file'
2 | import { generateTimestamp } from '@sasjs/utils/time'
3 | import { Job } from '../../types'
4 |
5 | export const getFileStream = async (job: Job, filePath?: string) => {
6 | const { createWriteStream } = require('@sasjs/utils/file')
7 | const logPath = filePath || process.cwd()
8 | const isFolderPath = await isFolder(logPath)
9 | if (isFolderPath) {
10 | const logFileName = `${job.name || 'job'}-${generateTimestamp()}.log`
11 | const path = require('path')
12 | const logFilePath = path.join(filePath || process.cwd(), logFileName)
13 | return await createWriteStream(logFilePath)
14 | } else {
15 | return await createWriteStream(logPath)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/sasjs-tests/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "useDefineForClassFields": true,
5 | "module": "ESNext",
6 | "lib": ["ES2022", "DOM", "DOM.Iterable"],
7 | "types": ["vite/client"],
8 | "skipLibCheck": true,
9 |
10 | /* Bundler mode */
11 | "moduleResolution": "bundler",
12 | "allowImportingTsExtensions": true,
13 | "verbatimModuleSyntax": true,
14 | "moduleDetection": "force",
15 | "noEmit": true,
16 |
17 | /* Linting */
18 | "strict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "erasableSyntaxOnly": true,
22 | "noFallthroughCasesInSwitch": true,
23 | "noUncheckedSideEffectImports": true
24 | },
25 | "include": ["src"]
26 | }
27 |
--------------------------------------------------------------------------------
/src/utils/spec/getFormData.spec.ts:
--------------------------------------------------------------------------------
1 | import { getFormData } from '..'
2 | import * as isNodeModule from '../isNode'
3 | import NodeFormData from 'form-data'
4 |
5 | describe('getFormData', () => {
6 | it('should return NodeFormData if environment is Node', () => {
7 | jest.spyOn(isNodeModule, 'isNode').mockImplementation(() => true)
8 |
9 | expect(getFormData() instanceof NodeFormData).toEqual(true)
10 | })
11 |
12 | it('should return FormData if environment is not Node', () => {
13 | // Ensure FormData is globally available
14 | ;(global as any).FormData = class FormData {}
15 |
16 | jest.spyOn(isNodeModule, 'isNode').mockImplementation(() => false)
17 |
18 | expect(getFormData() instanceof FormData).toEqual(true)
19 | })
20 | })
21 |
--------------------------------------------------------------------------------
/cypress/support/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/index.js is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import './commands'
18 |
19 | // Alternatively you can use CommonJS syntax:
20 | // require('./commands')
21 |
--------------------------------------------------------------------------------
/src/utils/getValidJson.ts:
--------------------------------------------------------------------------------
1 | import { JsonParseArrayError, InvalidJsonError } from '../types/errors'
2 |
3 | /**
4 | * if string passed then parse the string to json else if throw error for all other types unless it is not a valid json object.
5 | * @param str - string to check.
6 | */
7 | export const getValidJson = (str: string | object): object => {
8 | try {
9 | if (str === null || str === undefined) throw new InvalidJsonError()
10 |
11 | if (Array.isArray(str)) throw new JsonParseArrayError()
12 |
13 | if (typeof str === 'object') return str
14 |
15 | if (str === '') return {}
16 |
17 | return JSON.parse(str)
18 | } catch (e: any) {
19 | if (e instanceof JsonParseArrayError) throw e
20 |
21 | throw new InvalidJsonError()
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/types/FileResource.ts:
--------------------------------------------------------------------------------
1 | export interface FileResource {
2 | creationTimeStamp: string
3 | modifiedTimeStamp: string
4 | createdBy: string
5 | modifiedBy: string
6 | id: string
7 | properties: Properties
8 | contentDisposition: string
9 | contentType: string
10 | encoding: string
11 | links: Link[]
12 | name: string
13 | size: number
14 | searchable: boolean
15 | fileStatus: string
16 | fileVersion: number
17 | typeDefName: string
18 | version: number
19 | virusDetected: boolean
20 | urlDetected: boolean
21 | quarantine: boolean
22 | }
23 |
24 | export interface Link {
25 | method: string
26 | rel: string
27 | href: string
28 | uri: string
29 | type?: string
30 | responseType?: string
31 | }
32 |
33 | export interface Properties {}
34 |
--------------------------------------------------------------------------------
/src/auth/verifySas9Login.ts:
--------------------------------------------------------------------------------
1 | import { delay } from '../utils'
2 | import { getExpectedLogInSuccessHeader } from './'
3 |
4 | export async function verifySas9Login(loginPopup: Window): Promise<{
5 | isLoggedIn: boolean
6 | }> {
7 | let isLoggedIn = false
8 | let startTime = new Date()
9 | let elapsedSeconds = 0
10 |
11 | do {
12 | await delay(1000)
13 | if (loginPopup.closed) break
14 |
15 | isLoggedIn =
16 | loginPopup.window.location.href.includes('SASLogon') &&
17 | loginPopup.window.document.body.innerText.includes(
18 | getExpectedLogInSuccessHeader()
19 | )
20 |
21 | elapsedSeconds = (new Date().valueOf() - startTime.valueOf()) / 1000
22 | } while (!isLoggedIn && elapsedSeconds < 5 * 60)
23 |
24 | return { isLoggedIn }
25 | }
26 |
--------------------------------------------------------------------------------
/src/types/Session.ts:
--------------------------------------------------------------------------------
1 | import { Link } from './Link'
2 | import { SessionManager } from '../SessionManager'
3 |
4 | export enum SessionState {
5 | Completed = 'completed',
6 | Running = 'running',
7 | Pending = 'pending',
8 | Idle = 'idle',
9 | Unavailable = 'unavailable',
10 | NoState = '',
11 | Failed = 'failed',
12 | Error = 'error'
13 | }
14 |
15 | export interface Session {
16 | id: string
17 | state: SessionState
18 | stateUrl: string
19 | links: Link[]
20 | attributes: {
21 | sessionInactiveTimeout: number
22 | }
23 | creationTimeStamp: string
24 | etag: string
25 | }
26 |
27 | export interface SessionVariable {
28 | value: string
29 | }
30 |
31 | export interface JobSessionManager {
32 | session: Session
33 | sessionManager: SessionManager
34 | }
35 |
--------------------------------------------------------------------------------
/sasjs-tests/index.css:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | margin: 0;
4 | padding: 0;
5 | }
6 |
7 | body {
8 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
9 | Ubuntu, Cantarell, sans-serif;
10 | line-height: 1.6;
11 | color: #333;
12 | background: #f5f5f5;
13 | }
14 |
15 | .app__error {
16 | max-width: 800px;
17 | margin: 50px auto;
18 | padding: 30px;
19 | background: white;
20 | border-radius: 8px;
21 | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
22 |
23 | h1 {
24 | color: #e74c3c;
25 | margin-bottom: 15px;
26 | }
27 |
28 | p {
29 | margin-bottom: 15px;
30 | }
31 |
32 | pre {
33 | background: #2c3e50;
34 | color: #ecf0f1;
35 | padding: 15px;
36 | border-radius: 4px;
37 | overflow-x: auto;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/utils/isIeOrEdge.ts:
--------------------------------------------------------------------------------
1 | export function isIEorEdgeOrOldFirefox() {
2 | if (typeof window === 'undefined') {
3 | return false
4 | }
5 | const ua = window.navigator.userAgent
6 |
7 | if (ua.indexOf('Firefox') > 0) {
8 | const version = parseInt(
9 | ua.substring(ua.lastIndexOf('Firefox/') + 8, ua.length),
10 | 10
11 | )
12 | return version <= 60
13 | }
14 |
15 | const msie = ua.indexOf('MSIE ')
16 | if (msie > 0) {
17 | // IE 10 or older => return version number
18 | return true
19 | }
20 |
21 | const trident = ua.indexOf('Trident/')
22 | if (trident > 0) {
23 | return true
24 | }
25 |
26 | const edge = ua.indexOf('Edge/')
27 | if (edge > 0) {
28 | // Edge (IE 12+) => return version number
29 | return true
30 | }
31 |
32 | // other browser
33 | return false
34 | }
35 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## Issue
2 |
3 | Link any related issue(s) in this section.
4 |
5 | ## Intent
6 |
7 | What this PR intends to achieve.
8 |
9 | ## Implementation
10 |
11 | What code changes have been made to achieve the intent.
12 |
13 | ## Checks
14 |
15 | No PR (that involves a non-trivial code change) should be merged, unless all items below are confirmed! If an urgent fix is needed - use a tar file.
16 |
17 | - [ ] Unit tests coverage has been increased and a new threshold is set.
18 | - [ ] All `sasjs-cli` unit tests are passing (`npm test`).
19 | - (CI Runs this) All `sasjs-tests` are passing. If you want to run it manually (instructions available [here](https://github.com/sasjs/adapter/blob/master/sasjs-tests/README.md)).
20 | - [ ] [Data Controller](https://datacontroller.io) builds and is functional on both SAS 9 and Viya
21 |
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from './appendExtraResponseAttributes'
2 | export * from './asyncForEach'
3 | export * from './compareTimestamps'
4 | export * from './convertToCsv'
5 | export * from './createAxiosInstance'
6 | export * from './delay'
7 | export * from './fetchLogByChunks'
8 | export * from './getValidJson'
9 | export * from './isNode'
10 | export * from './isRelativePath'
11 | export * from './isUri'
12 | export * from './isUrl'
13 | export * from './needsRetry'
14 | export * from './parseGeneratedCode'
15 | export * from './parseSasViyaLog'
16 | export * from './parseSourceCode'
17 | export * from './parseViyaDebugResponse'
18 | export * from './parseWeboutResponse'
19 | export * from './serialize'
20 | export * from './splitChunks'
21 | export * from './validateInput'
22 | export * from './getFormData'
23 | export * from './getUserLanguage'
24 |
--------------------------------------------------------------------------------
/sasjs-tests/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sasjs-tests-new",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "start": "vite",
8 | "build": "tsc && vite build",
9 | "preview": "vite preview",
10 | "update:adapter": "cd .. && npm run package:lib && cd sasjs-tests && npm i ../build/sasjs-adapter-5.0.0.tgz",
11 | "deploy:tests": "rsync -avhe ssh ./dist/* --delete $SSH_ACCOUNT:$DEPLOY_PATH || npm run deploy:tests-win",
12 | "deploy:tests-win": "scp %DEPLOY_PATH% ./dist/*",
13 | "deploy": "npm run update:adapter && npm run build && npm run deploy:tests"
14 | },
15 | "devDependencies": {
16 | "typescript": "~5.9.3",
17 | "vite": "npm:rolldown-vite@7.2.2"
18 | },
19 | "overrides": {
20 | "vite": "npm:rolldown-vite@7.2.2"
21 | },
22 | "dependencies": {
23 | "@sasjs/adapter": "^4.14.0"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/types/errors/RootFolderNotFoundError.ts:
--------------------------------------------------------------------------------
1 | import { decodeToken } from '@sasjs/utils/auth'
2 |
3 | export class RootFolderNotFoundError extends Error {
4 | constructor(
5 | parentFolderPath: string,
6 | serverUrl: string,
7 | accessToken?: string
8 | ) {
9 | let message: string =
10 | `Root folder ${parentFolderPath} was not found.` +
11 | `\nPlease check ${serverUrl}/SASDrive.` +
12 | `\nIf the folder DOES exist then it is likely a permission problem.\n`
13 | if (accessToken) {
14 | const decodedToken = decodeToken(accessToken)
15 | let scope = decodedToken.scope
16 | scope = scope.map((element) => '* ' + element)
17 | message +=
18 | `Your access token contains the following scopes:\n` + scope.join('\n')
19 | }
20 | super(message)
21 | this.name = 'RootFolderNotFoundError'
22 | Object.setPrototypeOf(this, RootFolderNotFoundError.prototype)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/types/Tables.ts:
--------------------------------------------------------------------------------
1 | import { ArgumentError } from './errors'
2 |
3 | export class Tables {
4 | _tables: { [macroName: string]: Record }
5 |
6 | constructor(table: Record, macroName: string) {
7 | this._tables = {}
8 |
9 | this.add(table, macroName)
10 | }
11 |
12 | add(table: Record | null, macroName: string) {
13 | if (table && macroName) {
14 | if (!(table instanceof Array)) {
15 | throw new ArgumentError('First argument must be array')
16 | }
17 | if (typeof macroName !== 'string') {
18 | throw new ArgumentError('Second argument must be string')
19 | }
20 | if (!isNaN(Number(macroName[macroName.length - 1]))) {
21 | throw new ArgumentError('Macro name cannot have number at the end')
22 | }
23 | } else {
24 | throw new ArgumentError('Missing arguments')
25 | }
26 |
27 | this._tables[macroName] = table
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/cypress/support/commands.js:
--------------------------------------------------------------------------------
1 | // ***********************************************
2 | // This example commands.js shows you how to
3 | // create various custom commands and overwrite
4 | // existing commands.
5 | //
6 | // For more comprehensive examples of custom
7 | // commands please read more here:
8 | // https://on.cypress.io/custom-commands
9 | // ***********************************************
10 | //
11 | //
12 | // -- This is a parent command --
13 | // Cypress.Commands.add('login', (email, password) => { ... })
14 | //
15 | //
16 | // -- This is a child command --
17 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
18 | //
19 | //
20 | // -- This is a dual command --
21 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
22 | //
23 | //
24 | // -- This will overwrite an existing command --
25 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
26 |
--------------------------------------------------------------------------------
/src/types/Context.ts:
--------------------------------------------------------------------------------
1 | export interface Context {
2 | name: string
3 | id: string
4 | createdBy: string
5 | version: number
6 | attributes?: any
7 | }
8 |
9 | export interface EditContextInput {
10 | name?: string
11 | description?: string
12 | launchContext?: { name: string }
13 | environment?: { options?: string[]; autoExecLines?: string[] }
14 | attributes?: any
15 | authorizedUsers?: string[]
16 | authorizeAllAuthenticatedUsers?: boolean
17 | id?: string
18 | }
19 |
20 | export interface ContextAllAttributes {
21 | attributes: {
22 | reuseServerProcesses: boolean
23 | runServerAs: string
24 | }
25 | modifiedTimeStamp: string
26 | createdBy: string
27 | creationTimeStamp: string
28 | launchType: string
29 | environment: {
30 | autoExecLines: [string]
31 | }
32 | launchContext: {
33 | contextName: string
34 | }
35 | modifiedBy: string
36 | id: string
37 | version: number
38 | name: string
39 | }
40 |
--------------------------------------------------------------------------------
/src/utils/spec/parseSasViyaLog.spec.ts:
--------------------------------------------------------------------------------
1 | import { parseSasViyaLog } from '../parseSasViyaLog'
2 |
3 | describe('parseSasViyaLog', () => {
4 | it('should parse sas viya log if environment is Node', () => {
5 | const logResponse = {
6 | items: [{ line: 'Line 1' }, { line: 'Line 2' }, { line: 'Line 3' }]
7 | }
8 |
9 | const expectedLog = 'Line 1\nLine 2\nLine 3'
10 | const result = parseSasViyaLog(logResponse)
11 | expect(result).toEqual(expectedLog)
12 | })
13 |
14 | it('should handle exceptions and return the original logResponse', () => {
15 | // Create a logResponse that will cause an error in the mapping process.
16 | const logResponse: any = {
17 | items: null
18 | }
19 | // Since logResponse.items is null, the ternary operator returns the else branch.
20 | const expectedLog = JSON.stringify(logResponse)
21 | const result = parseSasViyaLog(logResponse)
22 | expect(result).toEqual(expectedLog)
23 | })
24 | })
25 |
--------------------------------------------------------------------------------
/createTSDocs.js:
--------------------------------------------------------------------------------
1 | const td = require('typedoc')
2 | const ts = require('typescript')
3 |
4 | const typedocJson = require('./typedoc.json')
5 |
6 | async function createTSDocs() {
7 | if (!typedocJson.entryPoints?.length) {
8 | throw new Error(
9 | 'Typedoc error: entryPoints option is missing in typedoc configuration.'
10 | )
11 | }
12 |
13 | if (!typedocJson.out) {
14 | throw new Error(
15 | 'Typedoc error: out option is missing in typedoc configuration.'
16 | )
17 | }
18 | const app = new td.Application()
19 | app.options.addReader(new td.TSConfigReader())
20 |
21 | app.bootstrap({
22 | ...typedocJson,
23 | tsconfig: 'tsconfig.json'
24 | })
25 |
26 | const project = app.converter.convert(app.getEntryPoints() ?? [])
27 |
28 | if (project) {
29 | await app.generateDocs(project, typedocJson.out)
30 | } else {
31 | throw new Error('Typedoc error: error creating the TS docs.')
32 | }
33 | }
34 |
35 | createTSDocs()
36 |
--------------------------------------------------------------------------------
/src/utils/sas9/extractUserLongNameSas9.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Dictionary should contain only languages in SAS where `logout` text
3 | * is represented with more then one word
4 | */
5 | const dictionary = ['Log Off']
6 |
7 | /**
8 | * Extracts user full name assuming the first word after "title" means log off if not found otherwise in the dictionary
9 | * @param response SAS response content
10 | * @returns user full name
11 | */
12 | export const extractUserLongNameSas9 = (response: string) => {
13 | const regex = /"title":\s?".*?"/
14 |
15 | const matched = response?.match(regex)
16 | let fullName = matched?.[0].split(':')[1].trim()
17 | let breakIndex = fullName?.indexOf(' ')
18 |
19 | if (!fullName) return 'unknown'
20 |
21 | dictionary.map((logoutWord) => {
22 | const index = fullName?.indexOf(logoutWord) || -1
23 |
24 | if (index > -1) {
25 | breakIndex = index + logoutWord.length
26 | }
27 | })
28 |
29 | //Cut only name
30 | return fullName.slice(breakIndex, -1).trim()
31 | }
32 |
--------------------------------------------------------------------------------
/src/types/Tables.spec.ts:
--------------------------------------------------------------------------------
1 | import SASjs from '../SASjs'
2 |
3 | describe('Tables - basic coverage', () => {
4 | const adapter = new SASjs()
5 |
6 | it('should throw an error if first argument is not an array', () => {
7 | expect(() => adapter.Tables({}, 'test')).toThrow('First argument')
8 | })
9 |
10 | it('should throw an error if second argument is not a string', () => {
11 | // @ts-expect-error
12 | expect(() => adapter.Tables([], 1234)).toThrow('Second argument')
13 | })
14 |
15 | it('should throw an error if macro name ends with a number', () => {
16 | expect(() => adapter.Tables([], 'test1')).toThrow('number at the end')
17 | })
18 |
19 | it('should throw an error if no arguments are passed', () => {
20 | // @ts-expect-error
21 | expect(() => adapter.Tables()).toThrow('Missing arguments')
22 | })
23 |
24 | it('should create Tables class successfully with _tables property', () => {
25 | const tables = adapter.Tables([], 'test')
26 | expect(tables).toHaveProperty('_tables')
27 | })
28 | })
29 |
--------------------------------------------------------------------------------
/sasjs-tests/sasjs/doxy/new_footer.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | $navpath
8 |
15 | -
16 | For more information visit the
17 | SASjs cli documentation.
18 |
19 |
20 |
21 |
22 |
23 |
24 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/src/types/errors/spec/SAS9AuthError.spec.ts:
--------------------------------------------------------------------------------
1 | import { SAS9AuthError } from '../SAS9AuthError'
2 |
3 | describe('SAS9AuthError', () => {
4 | it('should have the correct error message', () => {
5 | const error = new SAS9AuthError()
6 | expect(error.message).toBe(
7 | 'The credentials you provided cannot be authenticated. Please provide a valid set of credentials.'
8 | )
9 | })
10 |
11 | it('should have the correct error name', () => {
12 | const error = new SAS9AuthError()
13 | expect(error.name).toBe('AuthorizeError')
14 | })
15 |
16 | it('should be an instance of SAS9AuthError', () => {
17 | const error = new SAS9AuthError()
18 | expect(error).toBeInstanceOf(SAS9AuthError)
19 | })
20 |
21 | it('should be an instance of Error', () => {
22 | const error = new SAS9AuthError()
23 | expect(error).toBeInstanceOf(Error)
24 | })
25 |
26 | it('should set the prototype correctly', () => {
27 | const error = new SAS9AuthError()
28 | expect(Object.getPrototypeOf(error)).toBe(SAS9AuthError.prototype)
29 | })
30 | })
31 |
--------------------------------------------------------------------------------
/src/test/utils/isUrl.spec.ts:
--------------------------------------------------------------------------------
1 | import { isUrl } from '../../utils/isUrl'
2 |
3 | describe('urlValidator', () => {
4 | it('should return true with an HTTP URL', () => {
5 | const url = 'http://google.com'
6 |
7 | expect(isUrl(url)).toEqual(true)
8 | })
9 |
10 | it('should return true with an HTTPS URL', () => {
11 | const url = 'https://google.com'
12 |
13 | expect(isUrl(url)).toEqual(true)
14 | })
15 |
16 | it('should return true when the URL is blank', () => {
17 | const url = ''
18 |
19 | expect(isUrl(url)).toEqual(false)
20 | })
21 |
22 | it('should return false when the URL has not supported protocol', () => {
23 | const url = 'htpps://google.com'
24 |
25 | expect(isUrl(url)).toEqual(false)
26 | })
27 |
28 | it('should return false when the URL is null', () => {
29 | const url = null
30 |
31 | expect(isUrl(url as unknown as string)).toEqual(false)
32 | })
33 |
34 | it('should return false when the URL is undefined', () => {
35 | const url = undefined
36 |
37 | expect(isUrl(url as unknown as string)).toEqual(false)
38 | })
39 | })
40 |
--------------------------------------------------------------------------------
/sasjs-tests/src/core/runTest.ts:
--------------------------------------------------------------------------------
1 | import type { Test, TestResult } from '../types'
2 |
3 | export async function runTest(
4 | testToRun: Test,
5 | context: unknown
6 | ): Promise {
7 | const { test, assertion, beforeTest, afterTest } = testToRun
8 | const beforeTestFunction = beforeTest ? beforeTest : () => Promise.resolve()
9 | const afterTestFunction = afterTest ? afterTest : () => Promise.resolve()
10 |
11 | const startTime = new Date().valueOf()
12 |
13 | return beforeTestFunction()
14 | .then(() => test(context))
15 | .then((res) => {
16 | return Promise.resolve(assertion(res, context))
17 | })
18 | .then((testResult) => {
19 | afterTestFunction()
20 | const endTime = new Date().valueOf()
21 | const executionTime = (endTime - startTime) / 1000
22 | return { result: testResult, error: null, executionTime }
23 | })
24 | .catch((e) => {
25 | console.error(e)
26 | const endTime = new Date().valueOf()
27 | const executionTime = (endTime - startTime) / 1000
28 | return { result: false, error: e, executionTime }
29 | })
30 | }
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Macro People
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/sasjs-tests/src/components/LoginForm.css:
--------------------------------------------------------------------------------
1 | :host {
2 | display: block;
3 | max-width: 400px;
4 | margin: 100px auto;
5 | padding: 40px;
6 | background: white;
7 | border-radius: 8px;
8 | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
9 | }
10 |
11 | h1 {
12 | text-align: center;
13 | margin-bottom: 30px;
14 | color: #2c3e50;
15 | }
16 |
17 | form {
18 | display: flex;
19 | flex-direction: column;
20 | gap: 15px;
21 | }
22 |
23 | label {
24 | font-weight: 600;
25 | margin-bottom: 5px;
26 | }
27 |
28 | input {
29 | padding: 10px;
30 | border: 1px solid #ddd;
31 | border-radius: 4px;
32 | font-size: 14px;
33 |
34 | &:focus {
35 | outline: none;
36 | border-color: #3498db;
37 | }
38 | }
39 |
40 | button {
41 | padding: 12px;
42 | background: #3498db;
43 | color: white;
44 | border: none;
45 | border-radius: 4px;
46 | font-size: 16px;
47 | font-weight: 600;
48 | cursor: pointer;
49 | transition: background 0.2s;
50 |
51 | &:hover:not(:disabled) {
52 | background: #2980b9;
53 | }
54 |
55 | &:disabled {
56 | opacity: 0.6;
57 | cursor: not-allowed;
58 | }
59 | }
60 |
61 | .error {
62 | color: #e74c3c;
63 | font-size: 14px;
64 | min-height: 20px;
65 | }
66 |
--------------------------------------------------------------------------------
/src/auth/openWebPage.ts:
--------------------------------------------------------------------------------
1 | import { openLoginPrompt } from '../utils/loginPrompt'
2 |
3 | interface WindowFeatures {
4 | width: number
5 | height: number
6 | }
7 |
8 | const defaultWindowFeatures: WindowFeatures = { width: 500, height: 600 }
9 |
10 | export async function openWebPage(
11 | url: string,
12 | windowName: string = '',
13 | WindowFeatures: WindowFeatures = defaultWindowFeatures,
14 | onLoggedOut?: () => Promise
15 | ): Promise {
16 | const { width, height } = WindowFeatures
17 | const left = screen.width / 2 - width / 2
18 | const top = screen.height / 2 - height / 2
19 |
20 | const loginPopup = window.open(
21 | url,
22 | windowName,
23 | `toolbar=0,location=0,menubar=0,width=${width},height=${height},left=${left},top=${top}`
24 | )
25 |
26 | if (!loginPopup) {
27 | const getUserAction: () => Promise = onLoggedOut ?? openLoginPrompt
28 |
29 | const doLogin = await getUserAction()
30 | return doLogin
31 | ? window.open(
32 | url,
33 | windowName,
34 | `toolbar=0,location=0,menubar=0,width=${width},height=${height},left=${left},top=${top}`
35 | )
36 | : null
37 | }
38 |
39 | return loginPopup
40 | }
41 |
--------------------------------------------------------------------------------
/src/auth/spec/verifySas9Login.spec.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @jest-environment jsdom
3 | */
4 | import { verifySas9Login } from '../verifySas9Login'
5 | import * as delayModule from '../../utils/delay'
6 | import { getExpectedLogInSuccessHeader } from '../'
7 |
8 | describe('verifySas9Login', () => {
9 | const serverUrl = 'http://test-server.com'
10 |
11 | beforeAll(() => {
12 | jest.mock('../../utils')
13 | jest
14 | .spyOn(delayModule, 'delay')
15 | .mockImplementation(() => Promise.resolve({}))
16 | })
17 |
18 | it('should return isLoggedIn true by checking state of popup', async () => {
19 | const popup = {
20 | window: {
21 | location: { href: serverUrl + `/SASLogon` },
22 | document: {
23 | body: { innerText: `${getExpectedLogInSuccessHeader()}
` }
24 | }
25 | }
26 | } as unknown as Window
27 |
28 | await expect(verifySas9Login(popup)).resolves.toEqual({
29 | isLoggedIn: true
30 | })
31 | })
32 |
33 | it('should return isLoggedIn false if user closed popup, already', async () => {
34 | const popup: Window = { closed: true } as unknown as Window
35 |
36 | await expect(verifySas9Login(popup)).resolves.toEqual({
37 | isLoggedIn: false
38 | })
39 | })
40 | })
41 |
--------------------------------------------------------------------------------
/src/test/SAS_server_app.ts:
--------------------------------------------------------------------------------
1 | import express = require('express')
2 | import cors from 'cors'
3 |
4 | export const app = express()
5 |
6 | app.use(
7 | cors({
8 | origin: 'http://localhost', // Allow requests only from this origin
9 | credentials: true // Allow credentials (cookies, auth headers, etc.)
10 | })
11 | )
12 |
13 | export const mockedAuthResponse = {
14 | access_token: 'access_token',
15 | token_type: 'bearer',
16 | id_token: 'id_token',
17 | refresh_token: 'refresh_token',
18 | expires_in: 43199,
19 | scope: 'openid',
20 | jti: 'jti'
21 | }
22 |
23 | app.get('/', (req: any, res: any) => {
24 | res.send('Hello World')
25 | })
26 |
27 | app.post('/SASLogon/oauth/token', (req: any, res: any) => {
28 | let valid = true
29 |
30 | // capture the encoded form data
31 | req.on('data', (data: any) => {
32 | const resData = data.toString()
33 |
34 | if (resData.includes('incorrect')) valid = false
35 | })
36 |
37 | // send a response when finished reading
38 | // the encoded form data
39 | req.on('end', () => {
40 | if (valid) res.status(200).send(mockedAuthResponse)
41 | else
42 | res.status(401).send({
43 | error: 'unauthorized',
44 | error_description: 'Bad credentials'
45 | })
46 | })
47 | })
48 |
--------------------------------------------------------------------------------
/sasjs-tests/src/testSuites/FileUpload.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable prefer-const */
2 | /* eslint-disable @typescript-eslint/no-explicit-any */
3 | import SASjs from '@sasjs/adapter'
4 | import type { TestSuite } from '../types'
5 |
6 | export const fileUploadTests = (adapter: SASjs): TestSuite => ({
7 | name: 'File Upload Tests',
8 | tests: [
9 | {
10 | title: 'Upload File',
11 | description: 'Should upload the file to VIYA',
12 | test: async () => {
13 | let blob: any = new Blob(['test'], { type: 'text/html' })
14 | blob['lastModifiedDate'] = ''
15 | blob['name'] = 'macvars_testfile'
16 | let file = blob
17 |
18 | const filesToUpload = [
19 | {
20 | file: file,
21 | fileName: file.name
22 | }
23 | ]
24 |
25 | return adapter.uploadFile('common/sendMacVars', filesToUpload, null)
26 | },
27 | assertion: (response: any) =>
28 | (response.macvars as any[]).findIndex(
29 | (el: any) => el.NAME === '_WEBIN_FILE_COUNT' && el.VALUE === '1'
30 | ) > -1 &&
31 | (response.macvars as any[]).findIndex(
32 | (el: any) =>
33 | el.NAME === '_WEBIN_FILENAME' && el.VALUE === 'macvars_testfile'
34 | ) > -1
35 | }
36 | ]
37 | })
38 |
--------------------------------------------------------------------------------
/src/auth/verifySasViyaLogin.ts:
--------------------------------------------------------------------------------
1 | import { delay } from '../utils'
2 | import { getExpectedLogInSuccessHeader } from './'
3 |
4 | export async function verifySasViyaLogin(loginPopup: Window): Promise<{
5 | isLoggedIn: boolean
6 | }> {
7 | let isLoggedIn = false
8 | let startTime = new Date()
9 | let elapsedSeconds = 0
10 |
11 | do {
12 | await delay(1000)
13 |
14 | if (loginPopup.closed) break
15 |
16 | isLoggedIn = isLoggedInSASVIYA()
17 |
18 | elapsedSeconds = (new Date().valueOf() - startTime.valueOf()) / 1000
19 | } while (!isLoggedIn && elapsedSeconds < 5 * 60)
20 |
21 | let isAuthorized = false
22 |
23 | startTime = new Date()
24 |
25 | do {
26 | await delay(1000)
27 |
28 | if (loginPopup.closed) break
29 |
30 | isAuthorized =
31 | loginPopup.window.location.href.includes('SASLogon') ||
32 | loginPopup.window.document.body?.innerText?.includes(
33 | getExpectedLogInSuccessHeader()
34 | )
35 |
36 | elapsedSeconds = (new Date().valueOf() - startTime.valueOf()) / 1000
37 | } while (!isAuthorized && elapsedSeconds < 5 * 60)
38 |
39 | return { isLoggedIn: isLoggedIn && isAuthorized }
40 | }
41 |
42 | export const isLoggedInSASVIYA = () =>
43 | document.cookie.includes('Current-User') && document.cookie.includes('userId')
44 |
--------------------------------------------------------------------------------
/src/auth/spec/verifySasViyaLogin.spec.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @jest-environment jsdom
3 | */
4 | import { verifySasViyaLogin } from '../verifySasViyaLogin'
5 | import * as delayModule from '../../utils/delay'
6 | import { getExpectedLogInSuccessHeader } from '../'
7 |
8 | describe('verifySasViyaLogin', () => {
9 | const serverUrl = 'http://test-server.com'
10 |
11 | beforeAll(() => {
12 | jest.mock('../../utils')
13 | jest
14 | .spyOn(delayModule, 'delay')
15 | .mockImplementation(() => Promise.resolve({}))
16 | document.cookie = encodeURIComponent('Current-User={"userId":"user-hash"}')
17 | })
18 |
19 | it('should return isLoggedIn true by checking state of popup', async () => {
20 | const popup = {
21 | window: {
22 | location: { href: serverUrl + `/SASLogon` },
23 | document: {
24 | body: { innerText: `${getExpectedLogInSuccessHeader()}
` }
25 | }
26 | }
27 | } as unknown as Window
28 |
29 | await expect(verifySasViyaLogin(popup)).resolves.toEqual({
30 | isLoggedIn: true
31 | })
32 | })
33 |
34 | it('should return isLoggedIn false if user closed popup, already', async () => {
35 | const popup: Window = { closed: true } as unknown as Window
36 |
37 | await expect(verifySasViyaLogin(popup)).resolves.toEqual({
38 | isLoggedIn: false
39 | })
40 | })
41 | })
42 |
--------------------------------------------------------------------------------
/src/file/generateTableUploadForm.ts:
--------------------------------------------------------------------------------
1 | import NodeFormData from 'form-data'
2 | import { convertToCSV, isFormatsTable } from '../utils/convertToCsv'
3 | import { splitChunks } from '../utils/splitChunks'
4 |
5 | export const generateTableUploadForm = (
6 | formData: FormData | NodeFormData,
7 | data: any
8 | ) => {
9 | const sasjsTables = []
10 | const requestParams: any = {}
11 | let tableCounter = 0
12 |
13 | for (const tableName in data) {
14 | tableCounter++
15 |
16 | // Formats table should not be sent as part of 'sasjs_tables'
17 | if (!isFormatsTable(tableName)) sasjsTables.push(tableName)
18 |
19 | const csv = convertToCSV(data, tableName)
20 |
21 | if (csv === 'ERROR: LARGE STRING LENGTH') {
22 | throw new Error(
23 | 'The max length of a string value in SASjs is 32765 characters.'
24 | )
25 | }
26 |
27 | // if csv has length more then 16k, send in chunks
28 | if (csv.length > 16000) {
29 | const csvChunks = splitChunks(csv)
30 |
31 | // append chunks to form data with same key
32 | csvChunks.map((chunk) => {
33 | formData.append(`sasjs${tableCounter}data`, chunk)
34 | })
35 | } else {
36 | requestParams[`sasjs${tableCounter}data`] = csv
37 | }
38 | }
39 |
40 | requestParams['sasjs_tables'] = sasjsTables.join(' ')
41 |
42 | return { formData, requestParams }
43 | }
44 |
--------------------------------------------------------------------------------
/src/test/utils/parseSourceCode.spec.ts:
--------------------------------------------------------------------------------
1 | import { parseSourceCode } from '../../utils/index'
2 |
3 | it('should parse SAS9 source code', async () => {
4 | expect(sampleResponse).toBeTruthy()
5 |
6 | const parsedSourceCode = parseSourceCode(sampleResponse)
7 |
8 | expect(parsedSourceCode).toBeTruthy()
9 |
10 | const sourceCodeLines = parsedSourceCode.split('\r\n')
11 |
12 | expect(sourceCodeLines.length).toEqual(5)
13 | expect(sourceCodeLines[0].startsWith('6')).toBeTruthy()
14 | expect(sourceCodeLines[1].startsWith('7')).toBeTruthy()
15 | expect(sourceCodeLines[2].startsWith('8')).toBeTruthy()
16 | expect(sourceCodeLines[3].startsWith('9')).toBeTruthy()
17 | expect(sourceCodeLines[4].startsWith('10')).toBeTruthy()
18 | })
19 |
20 | /* tslint:disable */
21 | const sampleResponse = `
22 | 6 @file mm_webout.sas
23 | 7 @brief Send data to/from SAS Stored Processes
24 | 8 @details This macro should be added to the start of each Stored Process,
25 | 9 **immediately** followed by a call to:
26 | 10 %webout(OPEN)
27 | MPRINT(MM_WEBIN): ;
28 | MPRINT(MM_WEBLEFT): filename _temp temp lrecl=999999;
29 | MPRINT(MM_WEBOUT): data _null_;
30 | MPRINT(MM_WEBRIGHT): file _temp;
31 | MPRINT(MM_WEBOUT): if upcase(symget('_debug'))='LOG' then put '>>weboutBEGIN<<';
32 | `
33 | /* tslint:enable */
34 |
--------------------------------------------------------------------------------
/sasjs-tests/sasjs/sasjsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://cli.sasjs.io/sasjsconfig-schema.json",
3 | "serviceConfig": {
4 | "serviceFolders": ["sasjs/common"]
5 | },
6 | "defaultTarget": "4gl",
7 | "targets": [
8 | {
9 | "name": "4gl",
10 | "serverUrl": "https://sas9.4gl.io",
11 | "serverType": "SASJS",
12 | "httpsAgentOptions": {
13 | "allowInsecureRequests": false
14 | },
15 | "appLoc": "/Public/app/adapter-tests",
16 | "deployConfig": {
17 | "deployServicePack": true,
18 | "deployScripts": []
19 | },
20 | "streamConfig": {
21 | "streamWeb": true,
22 | "streamWebFolder": "webv",
23 | "webSourcePath": "dist",
24 | "streamServiceName": "adapter-tests",
25 | "assetPaths": []
26 | }
27 | },
28 | {
29 | "name": "viya",
30 | "serverUrl": "",
31 | "serverType": "SASVIYA",
32 | "httpsAgentOptions": {
33 | "allowInsecureRequests": false
34 | },
35 | "appLoc": "/Public/app/adapter-tests",
36 | "deployConfig": {
37 | "deployServicePack": true,
38 | "deployScripts": []
39 | },
40 | "streamConfig": {
41 | "streamWeb": true,
42 | "streamWebFolder": "webv",
43 | "webSourcePath": "dist",
44 | "streamServiceName": "adapter-tests",
45 | "assetPaths": []
46 | }
47 | }
48 | ]
49 | }
50 |
--------------------------------------------------------------------------------
/src/api/viya/uploadTables.ts:
--------------------------------------------------------------------------------
1 | import { prefixMessage } from '@sasjs/utils/error'
2 | import { RequestClient } from '../../request/RequestClient'
3 | import { convertToCSV } from '../../utils/convertToCsv'
4 |
5 | /**
6 | * Uploads tables to SAS as specially formatted CSVs.
7 | * This is more compact than JSON, and easier to read within SAS.
8 | * @param requestClient - the pre-configured HTTP request client
9 | * @param data - the JSON representation of the data to be uploaded
10 | * @param accessToken - an optional access token for authentication/authorization
11 | * The access token is not required when uploading tables from the browser.
12 | */
13 | export async function uploadTables(
14 | requestClient: RequestClient,
15 | data: any,
16 | accessToken?: string
17 | ) {
18 | const uploadedFiles = []
19 |
20 | for (const tableName in data) {
21 | const csv = convertToCSV(data, tableName)
22 | if (csv === 'ERROR: LARGE STRING LENGTH') {
23 | throw new Error(
24 | 'The max length of a string value in SASjs is 32765 characters.'
25 | )
26 | }
27 |
28 | const uploadResponse = await requestClient
29 | .uploadFile(`/files/files#rawUpload`, csv, accessToken)
30 | .catch((err) => {
31 | throw prefixMessage(err, 'Error while uploading file. ')
32 | })
33 |
34 | uploadedFiles.push({ tableName, file: uploadResponse.result })
35 | }
36 | return uploadedFiles
37 | }
38 |
--------------------------------------------------------------------------------
/sasjs-tests/sasjs/doxy/Doxyfile:
--------------------------------------------------------------------------------
1 | ALPHABETICAL_INDEX = NO
2 |
3 | ENABLE_PREPROCESSING = NO
4 | EXTENSION_MAPPING = sas=Java ddl=Java
5 | EXTRACT_LOCAL_CLASSES = NO
6 | FILE_PATTERNS = *.sas \
7 | *.ddl \
8 | *.dox
9 | GENERATE_LATEX = NO
10 | GENERATE_TREEVIEW = YES
11 | HIDE_FRIEND_COMPOUNDS = YES
12 | HIDE_IN_BODY_DOCS = YES
13 | HIDE_SCOPE_NAMES = YES
14 | HIDE_UNDOC_CLASSES = YES
15 | HIDE_UNDOC_MEMBERS = YES
16 | HTML_OUTPUT = $(DOXY_HTML_OUTPUT)
17 | HTML_HEADER = $(HTML_HEADER)
18 | HTML_EXTRA_FILES = $(HTML_EXTRA_FILES)
19 | HTML_FOOTER = $(HTML_FOOTER)
20 | HTML_EXTRA_STYLESHEET = $(HTML_EXTRA_STYLESHEET)
21 | INHERIT_DOCS = NO
22 | INLINE_INFO = NO
23 | INPUT = $(DOXY_INPUT)
24 | LAYOUT_FILE = $(LAYOUT_FILE)
25 | USE_MDFILE_AS_MAINPAGE = README.md
26 | MAX_INITIALIZER_LINES = 0
27 | PROJECT_NAME = $(PROJECT_NAME)
28 | PROJECT_LOGO = $(PROJECT_LOGO)
29 | PROJECT_BRIEF = $(PROJECT_BRIEF)
30 | RECURSIVE = YES
31 | REPEAT_BRIEF = NO
32 | SHOW_NAMESPACES = NO
33 | SHOW_USED_FILES = NO
34 | SOURCE_BROWSER = YES
35 | SOURCE_TOOLTIPS = NO
36 | STRICT_PROTO_MATCHING = YES
37 | STRIP_CODE_COMMENTS = NO
38 | SUBGROUPING = NO
39 | TAB_SIZE = 2
40 | VERBATIM_HEADERS = NO
--------------------------------------------------------------------------------
/src/auth/refreshTokensForSasjs.ts:
--------------------------------------------------------------------------------
1 | import { prefixMessage } from '@sasjs/utils/error'
2 | import { RequestClient } from '../request/RequestClient'
3 | import { getTokenRequestErrorPrefix } from './getTokenRequestErrorPrefix'
4 | import { ServerType } from '@sasjs/utils/types'
5 |
6 | /**
7 | * Exchanges the refresh token for an access token for the given client.
8 | * @param requestClient - the pre-configured HTTP request client
9 | * @param refreshToken - the refresh token received from the server.
10 | */
11 | export async function refreshTokensForSasjs(
12 | requestClient: RequestClient,
13 | refreshToken: string
14 | ) {
15 | const url = '/SASjsApi/auth/refresh'
16 | const headers = {
17 | Authorization: 'Bearer ' + refreshToken
18 | }
19 |
20 | const authResponse = await requestClient
21 | .post(url, undefined, undefined, undefined, headers)
22 | .then((res) => {
23 | const sasAuth = res.result as {
24 | accessToken: string
25 | refreshToken: string
26 | }
27 | return {
28 | access_token: sasAuth.accessToken,
29 | refresh_token: sasAuth.refreshToken
30 | }
31 | })
32 | .catch((err) => {
33 | throw prefixMessage(
34 | err,
35 | getTokenRequestErrorPrefix(
36 | 'refreshing tokens',
37 | 'refreshTokensForSasjs',
38 | ServerType.Sasjs,
39 | url
40 | )
41 | )
42 | })
43 |
44 | return authResponse
45 | }
46 |
--------------------------------------------------------------------------------
/.github/workflows/generateDocs.yml:
--------------------------------------------------------------------------------
1 | name: Generate docs and Push to docs Branch
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | jobs:
9 | generate_and_push_docs:
10 | runs-on: ubuntu-latest
11 |
12 | strategy:
13 | matrix:
14 | node-version: [lts/iron]
15 |
16 | steps:
17 | - name: Checkout
18 | uses: actions/checkout@v2
19 |
20 | - name: Setup Node
21 | uses: actions/setup-node@v2
22 | with:
23 | node-version:
24 | ${{ matrix.node-version }}
25 |
26 | # 2. Restore npm cache manually
27 | - name: Restore npm cache
28 | uses: actions/cache@v3
29 | id: npm-cache
30 | with:
31 | path: ~/.npm
32 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
33 | restore-keys: |
34 | ${{ runner.os }}-node-
35 |
36 | - name: Install Dependencies
37 | run: npm ci
38 |
39 | - name: Ensure docs folder exists
40 | run: |
41 | rm -rf docs || true # avoid error if docs folder does not exist
42 | mkdir docs
43 |
44 | - name: Generate Docs
45 | run: npm run typedoc
46 |
47 | - name: Push generated docs
48 | uses: peaceiris/actions-gh-pages@v3
49 | with:
50 | github_token: ${{ secrets.GITHUB_TOKEN }}
51 | publish_branch: gh-pages
52 | publish_dir: ./docs
53 | cname: adapter.sasjs.io
54 |
--------------------------------------------------------------------------------
/src/utils/formatDataForRequest.ts:
--------------------------------------------------------------------------------
1 | import { convertToCSV, isFormatsTable } from './convertToCsv'
2 | import { splitChunks } from './splitChunks'
3 |
4 | export const formatDataForRequest = (data: any) => {
5 | const sasjsTables = []
6 | let tableCounter = 0
7 | const result: any = {}
8 |
9 | for (const tableName in data) {
10 | if (
11 | isFormatsTable(tableName) &&
12 | Object.keys(data).includes(tableName.replace(/^\$/, ''))
13 | ) {
14 | continue
15 | }
16 |
17 | tableCounter++
18 |
19 | // Formats table should not be sent as part of 'sasjs_tables'
20 | if (!isFormatsTable(tableName)) sasjsTables.push(tableName)
21 |
22 | const csv = convertToCSV(data, tableName)
23 |
24 | if (csv === 'ERROR: LARGE STRING LENGTH') {
25 | throw new Error(
26 | 'The max length of a string value in SASjs is 32765 characters.'
27 | )
28 | }
29 |
30 | // if csv has length more then 16k, send in chunks
31 | if (csv.length > 16000) {
32 | const csvChunks = splitChunks(csv)
33 |
34 | // append chunks to form data with same key
35 | result[`sasjs${tableCounter}data0`] = csvChunks.length
36 |
37 | csvChunks.forEach((chunk, index) => {
38 | result[`sasjs${tableCounter}data${index + 1}`] = chunk
39 | })
40 | } else {
41 | result[`sasjs${tableCounter}data`] = csv
42 | }
43 | }
44 |
45 | result['sasjs_tables'] = sasjsTables.join(' ')
46 |
47 | return result
48 | }
49 |
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Contributions to SASjs are very welcome! When making a PR, test cases should be included.
4 |
5 | ## Code Style
6 |
7 | This repository uses `Prettier` to ensure a uniform code style.
8 | If you are using VS Code for development, you can automatically fix your code to match the style as follows:
9 |
10 | - Install the `Prettier` extension for VS Code.
11 | - Open your `settings.json` file by choosing 'Preferences: Open Settings (JSON)' from the command palette.
12 | - Add the following items to the JSON.
13 | ```
14 | "editor.formatOnSave": true,
15 | "editor.formatOnPaste": true,
16 | ```
17 |
18 | If you are using another editor, or are unable to install the extension, you can run `npm run lint:fix` to fix the formatting after you've made your changes.
19 |
20 | ## Testing
21 |
22 | This repository contains a suite of tests built using [@sasjs/test-framework](https://github.com/sasjs/test-framework).
23 |
24 | Detailed instructions for creating and running the tests can be found [here](https://github.com/sasjs/adapter/blob/master/sasjs-tests/README.md).
25 |
26 | If you'd like to test your changes in an app that uses the adapter, you can do so as follows:
27 |
28 | 1. Run `npm run package:lib` from the root folder in this repository.
29 | This creates a tarball in the `/build` folder.
30 | 2. In your app's root folder, run `npm install `.
31 | This will install the changed version of the adapter in your app.
32 |
--------------------------------------------------------------------------------
/src/auth/getAccessTokenForSasjs.ts:
--------------------------------------------------------------------------------
1 | import { prefixMessage } from '@sasjs/utils/error'
2 | import { RequestClient } from '../request/RequestClient'
3 | import { getTokenRequestErrorPrefix } from './getTokenRequestErrorPrefix'
4 | import { ServerType } from '@sasjs/utils/types'
5 |
6 | /**
7 | * Exchanges the auth code for an access token for the given client.
8 | * @param requestClient - the pre-configured HTTP request client
9 | * @param clientId - the client ID to authenticate with.
10 | * @param authCode - the auth code received from the server.
11 | */
12 | export async function getAccessTokenForSasjs(
13 | requestClient: RequestClient,
14 | clientId: string,
15 | authCode: string
16 | ) {
17 | const url = '/SASjsApi/auth/token'
18 | const data = {
19 | clientId,
20 | code: authCode
21 | }
22 |
23 | return await requestClient
24 | .post(url, data, undefined)
25 | .then((res) => {
26 | const sasAuth = res.result as {
27 | accessToken: string
28 | refreshToken: string
29 | }
30 | return {
31 | access_token: sasAuth.accessToken,
32 | refresh_token: sasAuth.refreshToken
33 | }
34 | })
35 | .catch((err) => {
36 | throw prefixMessage(
37 | err,
38 | getTokenRequestErrorPrefix(
39 | 'fetching access token',
40 | 'getAccessTokenForSasjs',
41 | ServerType.Sasjs,
42 | url,
43 | data,
44 | clientId
45 | )
46 | )
47 | })
48 | }
49 |
--------------------------------------------------------------------------------
/src/test/utils/getValidJson.spec.ts:
--------------------------------------------------------------------------------
1 | import { getValidJson } from '../../utils'
2 | import { JsonParseArrayError, InvalidJsonError } from '../../types/errors'
3 |
4 | describe('jsonValidator', () => {
5 | it('should not throw an error with a valid json', () => {
6 | const json = {
7 | test: 'test'
8 | }
9 |
10 | expect(getValidJson(json)).toBe(json)
11 | })
12 |
13 | it('should not throw an error with a valid json string', () => {
14 | const json = {
15 | test: 'test'
16 | }
17 |
18 | expect(getValidJson(JSON.stringify(json))).toStrictEqual(json)
19 | })
20 |
21 | it('should throw an error with an invalid json', () => {
22 | const json = `{\"test\":\"test\"\"test2\":\"test\"}`
23 | const test = () => {
24 | getValidJson(json)
25 | }
26 | expect(test).toThrowError(InvalidJsonError)
27 | })
28 |
29 | it('should throw an error when an array is passed', () => {
30 | const array = ['hello', 'world']
31 | const test = () => {
32 | getValidJson(array)
33 | }
34 | expect(test).toThrow(JsonParseArrayError)
35 | })
36 |
37 | it('should throw an error when null is passed', () => {
38 | const test = () => {
39 | getValidJson(null as any)
40 | }
41 | expect(test).toThrow(InvalidJsonError)
42 | })
43 |
44 | it('should throw an error when undefined is passed', () => {
45 | const test = () => {
46 | getValidJson(undefined as any)
47 | }
48 | expect(test).toThrow(InvalidJsonError)
49 | })
50 | })
51 |
--------------------------------------------------------------------------------
/src/api/viya/spec/getFileStream.spec.ts:
--------------------------------------------------------------------------------
1 | import { Logger, LogLevel } from '@sasjs/utils/logger'
2 | import * as path from 'path'
3 | import * as fileModule from '@sasjs/utils/file'
4 | import { getFileStream } from '../getFileStream'
5 | import { mockJob } from './mockResponses'
6 | import { WriteStream } from '../../../types'
7 |
8 | describe('getFileStream', () => {
9 | beforeEach(() => {
10 | ;(process as any).logger = new Logger(LogLevel.Off)
11 | setupMocks()
12 | })
13 | it('should use the given log path if it points to a file', async () => {
14 | const { createWriteStream } = require('@sasjs/utils/file')
15 |
16 | await getFileStream(mockJob, path.join(__dirname, 'test.log'))
17 |
18 | expect(createWriteStream).toHaveBeenCalledWith(
19 | path.join(__dirname, 'test.log')
20 | )
21 | })
22 |
23 | it('should generate a log file path with a timestamp if it points to a folder', async () => {
24 | const { createWriteStream } = require('@sasjs/utils/file')
25 |
26 | await getFileStream(mockJob, __dirname)
27 |
28 | expect(createWriteStream).not.toHaveBeenCalledWith(__dirname)
29 | expect(createWriteStream).toHaveBeenCalledWith(
30 | expect.stringContaining(path.join(__dirname, 'test job-20'))
31 | )
32 | })
33 | })
34 |
35 | const setupMocks = () => {
36 | jest.restoreAllMocks()
37 | jest.mock('@sasjs/utils/file/file')
38 | jest
39 | .spyOn(fileModule, 'createWriteStream')
40 | .mockImplementation(() => Promise.resolve({} as unknown as WriteStream))
41 | }
42 |
--------------------------------------------------------------------------------
/src/test/utils/parseGeneratedCode.spec.ts:
--------------------------------------------------------------------------------
1 | import { parseGeneratedCode } from '../../utils/index'
2 |
3 | it('should parse generated code', () => {
4 | expect(sampleResponse).toBeTruthy()
5 |
6 | const parsedGeneratedCode = parseGeneratedCode(sampleResponse)
7 |
8 | expect(parsedGeneratedCode).toBeTruthy()
9 |
10 | const generatedCodeLines = parsedGeneratedCode.split('\r\n')
11 |
12 | expect(generatedCodeLines.length).toEqual(5)
13 | expect(generatedCodeLines[0].startsWith('MPRINT(MM_WEBIN)')).toBeTruthy()
14 | expect(generatedCodeLines[1].startsWith('MPRINT(MM_WEBLEFT)')).toBeTruthy()
15 | expect(generatedCodeLines[2].startsWith('MPRINT(MM_WEBOUT)')).toBeTruthy()
16 | expect(generatedCodeLines[3].startsWith('MPRINT(MM_WEBRIGHT)')).toBeTruthy()
17 | expect(generatedCodeLines[4].startsWith('MPRINT(MM_WEBOUT)')).toBeTruthy()
18 | })
19 |
20 | /* tslint:disable */
21 | const sampleResponse = `
22 | 6 @file mm_webout.sas
23 | 7 @brief Send data to/from SAS Stored Processes
24 | 8 @details This macro should be added to the start of each Stored Process,
25 | 9 **immediately** followed by a call to:
26 | 10 %webout(OPEN)
27 | MPRINT(MM_WEBIN): ;
28 | MPRINT(MM_WEBLEFT): filename _temp temp lrecl=999999;
29 | MPRINT(MM_WEBOUT): data _null_;
30 | MPRINT(MM_WEBRIGHT): file _temp;
31 | MPRINT(MM_WEBOUT): if upcase(symget('_debug'))='LOG' then put '>>weboutBEGIN<<';
32 | `
33 | /* tslint:enable */
34 |
--------------------------------------------------------------------------------
/src/utils/parseViyaDebugResponse.ts:
--------------------------------------------------------------------------------
1 | import { RequestClient } from '../request/RequestClient'
2 | import { getValidJson } from '../utils'
3 |
4 | /**
5 | * When querying a Viya job using the Web approach (as opposed to using the APIs) with _DEBUG enabled,
6 | * the first response contains the log with the content in an iframe. Therefore when debug is enabled,
7 | * and the serverType is VIYA, and useComputeApi is null (WEB), we call this function to extract the
8 | * (_webout) content from the iframe.
9 | * @param response - first response from viya job
10 | * @param requestClient
11 | * @param serverUrl
12 | * @returns
13 | */
14 | export const parseSasViyaDebugResponse = async (
15 | response: string,
16 | requestClient: RequestClient,
17 | serverUrl: string
18 | ) => {
19 | // On viya 3.5, iframe is like
20 | // On viya 4, iframe is like
21 |
22 | const iframeStart = response.split(
23 | /