├── .npmrc ├── .gitattributes ├── src ├── commands │ ├── compile │ │ ├── spec │ │ │ ├── service.sas │ │ │ ├── no-header.sas │ │ │ ├── programs │ │ │ │ ├── test.ddl │ │ │ │ └── test.sas │ │ │ ├── macros │ │ │ │ ├── doesnothing.sas │ │ │ │ └── examplemacro.sas │ │ │ ├── nested-deps.sas │ │ │ ├── non-sas-dependency.sas │ │ │ ├── empty-list.sas │ │ │ ├── missing-dependency.sas │ │ │ ├── example.sas │ │ │ ├── missing-fileref.sas │ │ │ ├── newlines.sas │ │ │ ├── duplicates.sas │ │ │ ├── duplicates-extensions.sas │ │ │ ├── example-reversed.sas │ │ │ ├── spacing.sas │ │ │ ├── services │ │ │ │ ├── example1.sas │ │ │ │ └── example.sas │ │ │ └── extra-spacing-sas-programs.sas │ │ └── internal │ │ │ ├── spec │ │ │ └── testFiles │ │ │ │ ├── tests │ │ │ │ ├── testinit.sas │ │ │ │ ├── testterm.sas │ │ │ │ ├── testsetup.sas │ │ │ │ └── sub │ │ │ │ │ └── testteardown.sas │ │ │ │ ├── jobs │ │ │ │ └── testJob.test.sas │ │ │ │ ├── macros │ │ │ │ └── testMacro.test.sas │ │ │ │ └── services │ │ │ │ ├── random.test.0.sas │ │ │ │ └── random.test.sas │ │ │ ├── index.ts │ │ │ ├── copySyncFolder.ts │ │ │ ├── identifySasFile.ts │ │ │ ├── getDestinationPath.ts │ │ │ ├── getAllFolders.ts │ │ │ └── compileFile.ts │ ├── snippets │ │ ├── spec │ │ │ ├── empty │ │ │ │ └── .gitkeep │ │ │ ├── testMacros │ │ │ │ ├── badMacro.sas │ │ │ │ ├── example.test.sas │ │ │ │ ├── example.sas │ │ │ │ ├── textMacro.txt │ │ │ │ └── subtestMacros │ │ │ │ │ ├── subMacro.sas │ │ │ │ │ └── subSubTestMacros │ │ │ │ │ └── subSubMacro.sas │ │ │ └── testMacros2 │ │ │ │ └── macro2.sas │ │ └── snippetsCommand.ts │ ├── job │ │ ├── spec │ │ │ ├── testSource │ │ │ │ ├── invalid.json │ │ │ │ └── source.json │ │ │ ├── testJob │ │ │ │ ├── job.sas │ │ │ │ ├── logJob.sas │ │ │ │ ├── failingJob.sas │ │ │ │ ├── jobWithWarning.sas │ │ │ │ └── largeLogJob.sas │ │ │ ├── mocks.ts │ │ │ ├── testServices │ │ │ │ ├── serviceterm.sas │ │ │ │ └── serviceinit.sas │ │ │ └── getContextName.spec.ts │ │ └── internal │ │ │ └── execute │ │ │ ├── index.ts │ │ │ └── sasjs.ts │ ├── request │ │ └── spec │ │ │ ├── runRequest │ │ │ ├── err.sas │ │ │ ├── sendArr.sas │ │ │ └── sendObj.sas │ │ │ └── utils.ts │ ├── testing │ │ └── spec │ │ │ ├── testMacros │ │ │ ├── shouldFail.test.sas │ │ │ └── examplemacro.test.sas │ │ │ ├── testJobsFiles │ │ │ ├── exampleprogram.sas │ │ │ ├── standalone.test.sas │ │ │ └── exampleprogram.test.sas │ │ │ ├── testServicesFiles │ │ │ ├── dostuff.test.0.sas │ │ │ └── dostuff.test.1.sas │ │ │ ├── testFiles │ │ │ ├── testterm.sas │ │ │ ├── testinit.sas │ │ │ ├── testsetup.sas │ │ │ └── testteardown.sas │ │ │ └── mockedAdapter │ │ │ └── testResponses.ts │ ├── deploy │ │ ├── spec │ │ │ ├── testJob │ │ │ │ ├── job.sas │ │ │ │ ├── logJob.sas │ │ │ │ ├── failingJob.sas │ │ │ │ └── jobWithWarning.sas │ │ │ ├── testScript │ │ │ │ └── copyscript.sh │ │ │ ├── testServices │ │ │ │ ├── serviceterm.sas │ │ │ │ └── serviceinit.sas │ │ │ ├── testConfig │ │ │ │ └── config.json │ │ │ └── getDeployScripts.spec.ts │ │ ├── internal │ │ │ ├── index.ts │ │ │ ├── getDeployScripts.ts │ │ │ ├── executeDeployScript.ts │ │ │ └── executeNonSasScript.ts │ │ └── deployCommand.ts │ ├── flow │ │ ├── spec │ │ │ ├── testJob │ │ │ │ ├── job.sas │ │ │ │ ├── logJob.sas │ │ │ │ ├── failingJob.sas │ │ │ │ ├── jobWithWarning.sas │ │ │ │ └── largeLogJob.sas │ │ │ ├── sourceFiles │ │ │ │ ├── not_valid_1.json │ │ │ │ ├── not_valid_2.json │ │ │ │ ├── not_valid_3.json │ │ │ │ ├── testFlow_1.json │ │ │ │ ├── testFlow_3.json │ │ │ │ ├── testFlow_8.json │ │ │ │ ├── testFlow_2.json │ │ │ │ ├── testFlow_4.json │ │ │ │ ├── testFlow_5.json │ │ │ │ ├── testFlow_6.json │ │ │ │ └── testFlow_7.json │ │ │ └── testServices │ │ │ │ ├── serviceterm.sas │ │ │ │ └── serviceinit.sas │ │ └── internal │ │ │ ├── csvColumns.ts │ │ │ ├── generateFileName.ts │ │ │ ├── normalizeFilePath.ts │ │ │ ├── printError.ts │ │ │ ├── index.ts │ │ │ ├── failAllSuccessors.ts │ │ │ ├── parseJobDetails.ts │ │ │ ├── saveToCsv.ts │ │ │ ├── checkPredecessorDeadlock.ts │ │ │ ├── examples.ts │ │ │ ├── allFlowsCompleted.ts │ │ │ ├── spec │ │ │ ├── failAllSuccessors.spec.ts │ │ │ └── checkPredecessorDeadlock.spec.ts │ │ │ └── saveLog.ts │ ├── web │ │ ├── internal │ │ │ ├── sasViya │ │ │ │ ├── index.ts │ │ │ │ └── createClickMeFile.ts │ │ │ ├── spec │ │ │ │ ├── testFiles │ │ │ │ │ ├── script2.js │ │ │ │ │ ├── script1.js │ │ │ │ │ └── style1.css │ │ │ │ ├── updateSasjstag.spec.ts │ │ │ │ ├── updateLinkTag.spec.ts │ │ │ │ └── updateStyleTag.spec.ts │ │ │ ├── sas9 │ │ │ │ ├── index.ts │ │ │ │ ├── generateAssetService.ts │ │ │ │ ├── createClickMeService.ts │ │ │ │ └── getWebServiceContent.ts │ │ │ ├── updateSasjsTag.ts │ │ │ ├── modifyLinksInContent.ts │ │ │ ├── getAssetPath.ts │ │ │ ├── updateLinkTag.ts │ │ │ └── getTags.ts │ │ └── webCommand.ts │ ├── run │ │ └── spec │ │ │ ├── testServices │ │ │ ├── logJob.sas │ │ │ └── logJob.js │ │ │ ├── run.spec.server.sasjs.ts │ │ │ └── run.spec.server.sas9.ts │ ├── create │ │ ├── createTemplate.ts │ │ ├── spec │ │ │ └── minimalAppFiles.ts │ │ ├── create.ts │ │ ├── createCommand.ts │ │ └── internal │ │ │ └── createReadme.ts │ ├── version │ │ ├── spec │ │ │ ├── version.spec.ts │ │ │ └── versionCommand.spec.ts │ │ ├── version.ts │ │ └── versionCommand.ts │ ├── docs │ │ ├── initDocs.ts │ │ ├── spec │ │ │ ├── testJobHavingDoubleQoutes │ │ │ │ └── runjob1.sas │ │ │ ├── testJobHavingBackSlashes │ │ │ │ └── runjob2.sas │ │ │ ├── testJobs │ │ │ │ ├── jobinit.sas │ │ │ │ └── jobterm.sas │ │ │ ├── initDocs.spec.ts │ │ │ └── getDocConfig.spec.ts │ │ ├── internal │ │ │ ├── populateNodeDictionary.ts │ │ │ ├── getFileOutputs.ts │ │ │ ├── getFileInputs.ts │ │ │ ├── populateParamNodeTypes.ts │ │ │ ├── createDotFiles.ts │ │ │ ├── getDocConfig.ts │ │ │ └── getFoldersForDocs.ts │ │ └── generateDot.ts │ ├── context │ │ ├── internal │ │ │ ├── parseConfig.ts │ │ │ └── validateConfigPath.ts │ │ ├── spec │ │ │ └── mocks.ts │ │ ├── delete.ts │ │ ├── edit.ts │ │ ├── create.ts │ │ └── export.ts │ ├── servicepack │ │ ├── spec │ │ │ ├── testServicepack.json │ │ │ └── servicepack.spec.server.viya.ts │ │ └── deploy.ts │ ├── folder │ │ ├── spec │ │ │ └── mocks.ts │ │ ├── delete.ts │ │ ├── list.ts │ │ ├── move.ts │ │ └── create.ts │ ├── lint │ │ └── initLint.ts │ ├── shared │ │ ├── createLintConfigFile.ts │ │ ├── createConfigFile.ts │ │ ├── fetchLogFileContent.ts │ │ ├── createFileStructure.ts │ │ └── deployToSasViyaWithServicePack.ts │ ├── add │ │ └── internal │ │ │ └── saveConfig.ts │ ├── help │ │ ├── helpCommand.ts │ │ └── spec │ │ │ ├── help.spec.ts │ │ │ └── helpCommand.spec.ts │ ├── db │ │ ├── spec │ │ │ ├── db.spec.ts │ │ │ └── dbCommand.spec.ts │ │ └── dbCommand.ts │ ├── index.ts │ ├── init │ │ ├── spec │ │ │ ├── initFiles.ts │ │ │ └── initCommand.spec.ts │ │ ├── initCommand.ts │ │ └── init.ts │ ├── build │ │ ├── buildCommand.ts │ │ └── internal │ │ │ ├── config.ts │ │ │ └── getLaunchPageCode.ts │ └── fs │ │ └── internal │ │ └── executeCode.ts ├── types │ ├── system │ │ ├── global.d.ts │ │ └── process.d.ts │ ├── targetScope.ts │ ├── command │ │ ├── commandExample.ts │ │ ├── returnCode.ts │ │ ├── index.ts │ │ ├── unalias.ts │ │ ├── commandAliases.ts │ │ ├── parse.ts │ │ └── targetCommand.ts │ ├── log.ts │ ├── folder.ts │ ├── file.ts │ ├── commonFields.ts │ ├── index.ts │ ├── flow.ts │ └── testing.ts ├── doxy │ ├── logo.png │ ├── favicon.ico │ ├── new_stylesheet.css │ ├── new_footer.html │ └── Doxyfile ├── index.ts ├── utils │ ├── createSASjsInstance.ts │ ├── getBaseCommand.ts │ ├── index.ts │ ├── prefixAppLoc.ts │ ├── fileStructures │ │ ├── dbFiles.ts │ │ ├── compiledFiles.ts │ │ ├── compiledFilesCustom1.ts │ │ ├── builtFiles.ts │ │ └── builtFilesCustom1.ts │ ├── getLogFilePath.ts │ ├── validateTargetName.ts │ ├── compressAndSave.ts │ ├── setProjectDir.ts │ ├── loadEnvVariables.ts │ ├── parseSourceFile.ts │ ├── spec │ │ ├── file.spec.ts │ │ └── saveLog.spec.ts │ ├── saveLog.ts │ └── displayResult.ts ├── constants.ts └── cli.ts ├── .prettierignore ├── .vscode ├── settings.json └── launch.json ├── bin └── sasjs ├── .github ├── FUNDING.yml ├── dependabot.yml ├── PULL_REQUEST_TEMPLATE.md ├── CHANGELOG.md ├── SECURITY.md ├── workflows │ └── npmpublish.yml └── CONTRIBUTING.md ├── nodemon.json ├── tsconfig.doc.json ├── .gitignore ├── .prettierrc ├── .gitpod.dockerfile ├── .gitpod.yml ├── jest.env.js ├── babel.config.js ├── npm-production-install.sh ├── .npmignore ├── mocks └── sasjs │ ├── .sasjslint │ └── sasjsconfig.json ├── .env.example ├── .git-hooks └── commit-msg ├── tsconfig.json ├── LICENSE └── test.sh /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-scripts=true -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf -------------------------------------------------------------------------------- /src/commands/compile/spec/service.sas: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/commands/snippets/spec/empty/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/commands/job/spec/testSource/invalid.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /src/commands/request/spec/runRequest/err.sas: -------------------------------------------------------------------------------- 1 | %abort; -------------------------------------------------------------------------------- /src/commands/compile/spec/no-header.sas: -------------------------------------------------------------------------------- 1 | %put stuff; 2 | -------------------------------------------------------------------------------- /src/types/system/global.d.ts: -------------------------------------------------------------------------------- 1 | import 'jest-extended' 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | test/commands/flow/sourceFiles/not_valid_1.json -------------------------------------------------------------------------------- /src/commands/compile/spec/programs/test.ddl: -------------------------------------------------------------------------------- 1 | proc sql; 2 | quit; -------------------------------------------------------------------------------- /src/commands/compile/spec/programs/test.sas: -------------------------------------------------------------------------------- 1 | %put 'Hello, world!'; -------------------------------------------------------------------------------- /src/commands/testing/spec/testMacros/shouldFail.test.sas: -------------------------------------------------------------------------------- 1 | shouldFail -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "viya" 4 | ] 5 | } -------------------------------------------------------------------------------- /bin/sasjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require("../src/cli").cli(process.argv); 4 | -------------------------------------------------------------------------------- /src/commands/deploy/spec/testJob/job.sas: -------------------------------------------------------------------------------- 1 | data _null_; 2 | rc=sleep(1000); 3 | run; -------------------------------------------------------------------------------- /src/commands/flow/spec/testJob/job.sas: -------------------------------------------------------------------------------- 1 | data _null_; 2 | rc=sleep(1000); 3 | run; -------------------------------------------------------------------------------- /src/commands/job/spec/testJob/job.sas: -------------------------------------------------------------------------------- 1 | data _null_; 2 | rc=sleep(1000); 3 | run; -------------------------------------------------------------------------------- /src/commands/web/internal/sasViya/index.ts: -------------------------------------------------------------------------------- 1 | export * from './createClickMeFile' 2 | -------------------------------------------------------------------------------- /src/doxy/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sasjs/cli/main/src/doxy/logo.png -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('./cli').cli(process.argv) 4 | -------------------------------------------------------------------------------- /src/commands/deploy/spec/testScript/copyscript.sh: -------------------------------------------------------------------------------- 1 | echo "sasjs: example deploy script" -------------------------------------------------------------------------------- /src/commands/testing/spec/testJobsFiles/exampleprogram.sas: -------------------------------------------------------------------------------- 1 | %put 'exampleprogram.sas'; -------------------------------------------------------------------------------- /src/commands/web/internal/spec/testFiles/script2.js: -------------------------------------------------------------------------------- 1 | ;['Ħ', 'ƕ', 'Ң', 'Һ', 'Ӈ', 'Ԋ'] 2 | -------------------------------------------------------------------------------- /src/doxy/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sasjs/cli/main/src/doxy/favicon.ico -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [sasjs] 4 | -------------------------------------------------------------------------------- /src/commands/flow/spec/testJob/logJob.sas: -------------------------------------------------------------------------------- 1 | data; 2 | do x=1 to 100; 3 | output; 4 | end; 5 | run; -------------------------------------------------------------------------------- /src/commands/job/spec/testJob/logJob.sas: -------------------------------------------------------------------------------- 1 | data; 2 | do x=1 to 100; 3 | output; 4 | end; 5 | run; -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "js, ts", 4 | "exec": "ts-node src/index.ts" 5 | } 6 | -------------------------------------------------------------------------------- /src/commands/deploy/spec/testJob/logJob.sas: -------------------------------------------------------------------------------- 1 | data; 2 | do x=1 to 100; 3 | output; 4 | end; 5 | run; -------------------------------------------------------------------------------- /src/commands/run/spec/testServices/logJob.sas: -------------------------------------------------------------------------------- 1 | data; 2 | do x=1 to 100; 3 | output; 4 | end; 5 | run; -------------------------------------------------------------------------------- /src/doxy/new_stylesheet.css: -------------------------------------------------------------------------------- 1 | #projectlogo img { 2 | border: 0px none; 3 | max-height: 70px; 4 | } 5 | -------------------------------------------------------------------------------- /src/types/targetScope.ts: -------------------------------------------------------------------------------- 1 | export enum TargetScope { 2 | Global = 'Global', 3 | Local = 'Local' 4 | } 5 | -------------------------------------------------------------------------------- /src/commands/run/spec/testServices/logJob.js: -------------------------------------------------------------------------------- 1 | Array(100) 2 | .fill() 3 | .forEach((_, i) => console.log(i)) 4 | -------------------------------------------------------------------------------- /src/commands/flow/spec/testJob/failingJob.sas: -------------------------------------------------------------------------------- 1 | data; 2 | %abort; 3 | do x=1 to 1e6; 4 | output; 5 | end; 6 | run; -------------------------------------------------------------------------------- /src/commands/job/spec/testJob/failingJob.sas: -------------------------------------------------------------------------------- 1 | data; 2 | %abort; 3 | do x=1 to 1e6; 4 | output; 5 | end; 6 | run; -------------------------------------------------------------------------------- /src/commands/compile/spec/macros/doesnothing.sas: -------------------------------------------------------------------------------- 1 | %macro doesnothing(); 2 | %put check this, nothing happened!; 3 | %mend; 4 | -------------------------------------------------------------------------------- /src/commands/deploy/spec/testJob/failingJob.sas: -------------------------------------------------------------------------------- 1 | data; 2 | %abort; 3 | do x=1 to 1e6; 4 | output; 5 | end; 6 | run; -------------------------------------------------------------------------------- /src/commands/job/internal/execute/index.ts: -------------------------------------------------------------------------------- 1 | export * from './viya' 2 | export * from './sasjs' 3 | export * from './sas9' 4 | -------------------------------------------------------------------------------- /src/types/command/commandExample.ts: -------------------------------------------------------------------------------- 1 | export interface CommandExample { 2 | command: string 3 | description: string 4 | } 5 | -------------------------------------------------------------------------------- /src/commands/web/internal/spec/testFiles/script1.js: -------------------------------------------------------------------------------- 1 | ;['═', '╤', '╔', '╗', '═', '╧', '╚', '╝', '║', '╟', '─', '┼', '║', '╢', '│'] 2 | -------------------------------------------------------------------------------- /src/types/command/returnCode.ts: -------------------------------------------------------------------------------- 1 | export enum ReturnCode { 2 | Success, 3 | InvalidCommand, 4 | InternalError, 5 | LintError 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.doc.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/**/*.ts"], 3 | "exclude": ["src/**/spec", "src/**/*.spec.ts", "src/**/*.spec.server.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /build 3 | sas 4 | sasbuild 5 | sasjsbuild 6 | .env 7 | .env.* 8 | .DS_Store 9 | /coverage 10 | /documentation 11 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true, 6 | "endOfLine": "auto" 7 | } 8 | -------------------------------------------------------------------------------- /src/commands/deploy/spec/testJob/jobWithWarning.sas: -------------------------------------------------------------------------------- 1 | data yury; was='here'; 2 | data new; was='toolong'; 3 | proc append base=yury data=new force; 4 | run; -------------------------------------------------------------------------------- /src/commands/flow/spec/testJob/jobWithWarning.sas: -------------------------------------------------------------------------------- 1 | data yury; was='here'; 2 | data new; was='toolong'; 3 | proc append base=yury data=new force; 4 | run; -------------------------------------------------------------------------------- /src/commands/job/spec/testJob/jobWithWarning.sas: -------------------------------------------------------------------------------- 1 | data yury; was='here'; 2 | data new; was='toolong'; 3 | proc append base=yury data=new force; 4 | run; -------------------------------------------------------------------------------- /src/types/log.ts: -------------------------------------------------------------------------------- 1 | export interface LogLine { 2 | line: string 3 | } 4 | 5 | export interface LogJson { 6 | items: LogLine[] 7 | error?: object 8 | } 9 | -------------------------------------------------------------------------------- /src/commands/job/spec/testSource/source.json: -------------------------------------------------------------------------------- 1 | { 2 | "macroVars": { 3 | "test_var_1": "test_var_value_1", 4 | "test_var_2": "test_var_value_2" 5 | } 6 | } -------------------------------------------------------------------------------- /src/types/command/index.ts: -------------------------------------------------------------------------------- 1 | export { CommandExample } from './commandExample' 2 | export { parse } from './parse' 3 | export { ReturnCode } from './returnCode' 4 | -------------------------------------------------------------------------------- /src/types/folder.ts: -------------------------------------------------------------------------------- 1 | import { File } from './file' 2 | export interface Folder { 3 | folderName: string 4 | subFolders: Folder[] 5 | files: File[] 6 | } 7 | -------------------------------------------------------------------------------- /src/commands/web/internal/sas9/index.ts: -------------------------------------------------------------------------------- 1 | export * from './createClickMeService' 2 | export * from './generateAssetService' 3 | export * from './getWebServiceContent' 4 | -------------------------------------------------------------------------------- /.gitpod.dockerfile: -------------------------------------------------------------------------------- 1 | FROM gitpod/workspace-full 2 | 3 | RUN sudo apt-get update \ 4 | && sudo apt-get install -y \ 5 | doxygen \ 6 | && sudo rm -rf /var/lib/apt/lists/* -------------------------------------------------------------------------------- /src/commands/testing/spec/testJobsFiles/standalone.test.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @brief 4 |

SAS Macros

5 | **/ 6 | %put 'standalone.test.sas'; -------------------------------------------------------------------------------- /src/commands/compile/internal/spec/testFiles/tests/testinit.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @brief setting up the test 4 | 5 |

SAS Macros

6 | **/ 7 | 8 | %put testing, init; -------------------------------------------------------------------------------- /src/commands/compile/internal/spec/testFiles/tests/testterm.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @brief ending the test 4 | 5 |

SAS Macros

6 | **/ 7 | 8 | %put testing, termed; -------------------------------------------------------------------------------- /src/commands/testing/spec/testJobsFiles/exampleprogram.test.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @brief 4 |

SAS Macros

5 | **/ 6 | %put 'exampleprogram.test.sas'; -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | - init: nvm install --latest-npm && npm i -g @sasjs/cli && npm i 3 | 4 | image: 5 | file: .gitpod.dockerfile 6 | vscode: 7 | extensions: 8 | - sasjs.sasjs-for-vscode -------------------------------------------------------------------------------- /jest.env.js: -------------------------------------------------------------------------------- 1 | // Define global TextEncoder and TextDecoder for `jsdom` 2 | const { TextEncoder, TextDecoder } = require('util') 3 | global.TextEncoder = TextEncoder 4 | global.TextDecoder = TextDecoder 5 | -------------------------------------------------------------------------------- /src/commands/create/createTemplate.ts: -------------------------------------------------------------------------------- 1 | export enum CreateTemplate { 2 | React = 'react', 3 | Angular = 'angular', 4 | Minimal = 'minimal', 5 | SasOnly = 'sasonly', 6 | Jobs = 'jobs' 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/createSASjsInstance.ts: -------------------------------------------------------------------------------- 1 | import SASjs, { SASjsConfig } from '@sasjs/adapter/node' 2 | 3 | export const createSASjsInstance = (config: Partial): SASjs => 4 | new SASjs(config) 5 | -------------------------------------------------------------------------------- /src/types/file.ts: -------------------------------------------------------------------------------- 1 | export interface File { 2 | fileName: string 3 | content?: string 4 | } 5 | 6 | export interface FilePath { 7 | absolutePath: string 8 | relativePath: string 9 | } 10 | -------------------------------------------------------------------------------- /src/commands/web/internal/spec/testFiles/style1.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: myFirstFont; 3 | src: url(assets/fa-solid-900.ttf); 4 | } 5 | 6 | h2 { 7 | font-family: myFirstFont; 8 | color: darkgreen; 9 | } 10 | -------------------------------------------------------------------------------- /src/commands/flow/spec/testJob/largeLogJob.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @brief 4 |

SAS Macros

5 | **/ 6 | data _null_; 7 | do x=1 to 21000; 8 | putlog x=; 9 | end; 10 | run; 11 | 12 | %put _all_; -------------------------------------------------------------------------------- /src/commands/job/spec/testJob/largeLogJob.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @brief 4 |

SAS Macros

5 | **/ 6 | data _null_; 7 | do x=1 to 21000; 8 | putlog x=; 9 | end; 10 | run; 11 | 12 | %put _all_; -------------------------------------------------------------------------------- /src/commands/version/spec/version.spec.ts: -------------------------------------------------------------------------------- 1 | import { printVersion } from '../version' 2 | 3 | describe('printVersion', () => { 4 | it('should return sasjs version', async () => { 5 | await expect(printVersion()).toResolve() 6 | }) 7 | }) 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: '/' 5 | schedule: 6 | interval: monthly 7 | open-pull-requests-limit: 3 8 | allow: 9 | - dependency-type: "production" 10 | -------------------------------------------------------------------------------- /src/commands/job/spec/mocks.ts: -------------------------------------------------------------------------------- 1 | import { AuthConfig } from '@sasjs/utils/types' 2 | 3 | export const mockAuthConfig: AuthConfig = { 4 | client: 'cl13nt', 5 | secret: '53cr3t', 6 | access_token: 'acc355', 7 | refresh_token: 'r3fr35h' 8 | } 9 | -------------------------------------------------------------------------------- /src/commands/flow/internal/csvColumns.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | id: 'id', 3 | flow: 'Flow', 4 | predecessors: 'Predecessors', 5 | name: 'Location', 6 | status: 'Status', 7 | logLocation: 'Log location', 8 | details: 'Details' 9 | } 10 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | node: 'current' 8 | } 9 | } 10 | ], 11 | '@babel/preset-typescript' 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /npm-production-install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if npm ci --omit=dev --ignore-scripts 2>&1 | grep -i warn; 4 | then 5 | echo "Warnings are found when doing production install" 6 | exit 1 7 | else 8 | echo "No warnings found when doing production install" 9 | fi -------------------------------------------------------------------------------- /src/commands/compile/spec/nested-deps.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file example.sas 3 | @brief example file 4 | @details This service does stuff. Like - ya know - stuff. 5 | 6 |

SAS Macros

7 | @li mm_createwebservice.sas 8 | 9 | **/ 10 | 11 | %put stuff; 12 | -------------------------------------------------------------------------------- /src/commands/testing/spec/testServicesFiles/dostuff.test.0.sas: -------------------------------------------------------------------------------- 1 | %put 'dostuff.test.0.sas'; 2 | data TEST_RESULTS; 3 | test_description="dostuff 0 test description"; 4 | test_result="FAIL"; 5 | output; 6 | run; 7 | %WEBOUT(OPEN) 8 | %WEBOUT(OBJ, TEST_RESULTS) 9 | %WEBOUT(CLOSE) -------------------------------------------------------------------------------- /src/commands/testing/spec/testServicesFiles/dostuff.test.1.sas: -------------------------------------------------------------------------------- 1 | %put 'dostuff.test.1.sas'; 2 | data TEST_RESULTS; 3 | test_description="dostuff 1 test description"; 4 | test_result="PASS"; 5 | output; 6 | run; 7 | %WEBOUT(OPEN) 8 | %WEBOUT(OBJ, TEST_RESULTS) 9 | %WEBOUT(CLOSE) -------------------------------------------------------------------------------- /src/commands/testing/spec/testMacros/examplemacro.test.sas: -------------------------------------------------------------------------------- 1 | %put 'examplemacro.test.1.sas'; 2 | data work.test_results; 3 | test_description="examplemacro test.1 description"; 4 | test_result="PASS"; 5 | output; 6 | run; 7 | %webout(OPEN) 8 | %webout(OBJ, test_results) 9 | %webout(CLOSE) -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github/ 2 | .vscode/ 3 | .gitignore 4 | .prettierignore 5 | .prettierrc 6 | *.md 7 | .all-contributorsrc 8 | 9 | jest.config.js 10 | jest.server.config.js 11 | jest.env.js 12 | test.sh 13 | *.spec.ts 14 | *.spec.server.ts 15 | src/utils/test.ts 16 | */spec/ 17 | mocks/ -------------------------------------------------------------------------------- /src/commands/request/spec/runRequest/sendArr.sas: -------------------------------------------------------------------------------- 1 | %webout(FETCH) 2 | %webout(OPEN) 3 | %macro x(); 4 | %do i=1 %to %sysfunc(countw(&sasjs_tables)); 5 | %let table=%scan(&sasjs_tables,&i); 6 | %webout(ARR,&table) 7 | %end; 8 | %mend; 9 | %x() 10 | %webout(CLOSE) -------------------------------------------------------------------------------- /src/commands/request/spec/runRequest/sendObj.sas: -------------------------------------------------------------------------------- 1 | %webout(FETCH) 2 | %webout(OPEN) 3 | %macro x(); 4 | %do i=1 %to %sysfunc(countw(&sasjs_tables)); 5 | %let table=%scan(&sasjs_tables,&i); 6 | %webout(OBJ,&table) 7 | %end; 8 | %mend; 9 | %x() 10 | %webout(CLOSE) -------------------------------------------------------------------------------- /src/commands/compile/spec/non-sas-dependency.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file example.sas 3 | @brief example file 4 | @details This service does stuff. Like - ya know - stuff. 5 | 6 |

SAS Macros

7 | @li mv_createfolder.sas 8 | @li foobar 9 | 10 | **/ 11 | 12 | %put stuff; 13 | -------------------------------------------------------------------------------- /src/commands/snippets/spec/testMacros/badMacro.sas: -------------------------------------------------------------------------------- 1 | 2 | %macro whaaat; 3 | %put this macro has no header; 4 | %put it has poor indentation; 5 | %put it has trailing spaces; 6 | %put the filename has uppercase chars; 7 | %put it needs a jolly good lint!!!; 8 | %mend; 9 | -------------------------------------------------------------------------------- /src/commands/compile/spec/empty-list.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file example.sas 3 | @brief example file 4 | @details This service does stuff. Like - ya know - stuff. 5 | 6 |

SAS Macros

7 | @li mv_createfolder.sas 8 | 9 |

SAS Programs

10 | **/ 11 | 12 | %put stuff; 13 | -------------------------------------------------------------------------------- /src/commands/docs/initDocs.ts: -------------------------------------------------------------------------------- 1 | import { setupDoxygen } from '../../utils/utils' 2 | 3 | /** 4 | * Initiates or reset doxy folder in current sasjs application 5 | */ 6 | export async function initDocs() { 7 | const parentFolderName = '.' 8 | await setupDoxygen(parentFolderName) 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/getBaseCommand.ts: -------------------------------------------------------------------------------- 1 | import { CommandBase } from '../types' 2 | import { defaultCommandOptions } from '../types/command/commandBase' 3 | 4 | export const getBaseCommand = (args: string[]): CommandBase => { 5 | return new CommandBase(args, { ...defaultCommandOptions, strict: false }) 6 | } 7 | -------------------------------------------------------------------------------- /src/commands/context/internal/parseConfig.ts: -------------------------------------------------------------------------------- 1 | export function parseConfig(config: string) { 2 | try { 3 | const parsedConfig = JSON.parse(config) 4 | 5 | return parsedConfig 6 | } catch (err) { 7 | throw new Error('Context config file is not a valid JSON file.') 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/commands/compile/spec/missing-dependency.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file example.sas 3 | @brief example file 4 | @details This service does stuff. Like - ya know - stuff. 5 | 6 |

SAS Macros

7 | @li foobar.sas 8 | @li mv_createfolder.sas 9 | @li foobar2.sas 10 | 11 | **/ 12 | 13 | %put stuff; 14 | -------------------------------------------------------------------------------- /src/types/command/unalias.ts: -------------------------------------------------------------------------------- 1 | import { aliasMap } from './commandAliases' 2 | 3 | export const unalias = (name: string) => { 4 | const entry = [...aliasMap.entries()].find( 5 | ([k, v]) => k === name || v.includes(name) 6 | ) 7 | if (!entry) { 8 | return name 9 | } 10 | return entry[0] 11 | } 12 | -------------------------------------------------------------------------------- /src/commands/compile/spec/example.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file example.sas 3 | @brief example file 4 | @details This service does stuff. Like - ya know - stuff. 5 | 6 |

SAS Macros

7 | @li mv_createfolder.sas 8 | 9 |

SAS Programs

10 | @li test.sas TEST 11 | 12 | **/ 13 | 14 | %put stuff; 15 | -------------------------------------------------------------------------------- /src/commands/compile/spec/missing-fileref.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file example.sas 3 | @brief example file 4 | @details This service does stuff. Like - ya know - stuff. 5 | 6 |

SAS Macros

7 | @li mv_createfolder.sas 8 | 9 |

SAS Programs

10 | @li test.sas 11 | 12 | **/ 13 | 14 | %put stuff; 15 | -------------------------------------------------------------------------------- /src/commands/compile/spec/newlines.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file example.sas 3 | @brief example file 4 | @details This service does stuff. Like - ya know - stuff. 5 | 6 | 7 |

SAS Programs

8 | @li test.sas TEST 9 | 10 |

SAS Macros

11 | @li mv_createfolder.sas 12 | 13 | **/ 14 | 15 | %put stuff; 16 | -------------------------------------------------------------------------------- /src/commands/flow/internal/generateFileName.ts: -------------------------------------------------------------------------------- 1 | import { generateTimestamp } from '@sasjs/utils' 2 | 3 | export const generateFileName = (flowName: string, jobLocation: string) => 4 | `${flowName}_${jobLocation 5 | .split('/') 6 | .splice(-1, 1) 7 | .join('') 8 | .replace(/\W/g, '_')}_${generateTimestamp('_')}.log` 9 | -------------------------------------------------------------------------------- /src/commands/flow/internal/normalizeFilePath.ts: -------------------------------------------------------------------------------- 1 | import { getRealPath } from '@sasjs/utils' 2 | import path from 'path' 3 | 4 | export const normalizeFilePath = (filePath: string) => { 5 | const pathSepRegExp = new RegExp(path.sep.replace(/\\/g, '\\\\'), 'g') 6 | 7 | return getRealPath(filePath).replace(pathSepRegExp, '/') 8 | } 9 | -------------------------------------------------------------------------------- /src/types/commonFields.ts: -------------------------------------------------------------------------------- 1 | import { ServerType, TargetJson } from '@sasjs/utils/types' 2 | import { TargetScope } from './targetScope' 3 | 4 | export interface CommonFields { 5 | scope: TargetScope 6 | serverType: ServerType 7 | name: string 8 | appLoc: string 9 | serverUrl: string 10 | existingTarget?: TargetJson 11 | } 12 | -------------------------------------------------------------------------------- /src/commands/context/internal/validateConfigPath.ts: -------------------------------------------------------------------------------- 1 | import { fileExists } from '@sasjs/utils/file' 2 | 3 | export async function validateConfigPath(path: string) { 4 | if (!path) return false 5 | 6 | const isJsonFile = /\.json$/i.test(path) 7 | 8 | if (!isJsonFile) return false 9 | 10 | return await fileExists(path) 11 | } 12 | -------------------------------------------------------------------------------- /src/commands/testing/spec/testFiles/testterm.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file testterm.sas 3 | @brief this file is called at the end of every test 4 | @details This file is included at the *end* of every test. 5 | 6 |

SAS Macros

7 | @li mf_abort.sas 8 | @li mf_existds.sas 9 | 10 | **/ 11 | 12 | %put test is finishing. Thanks, SASjs!; -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth' 2 | export * from './config' 3 | export * from './displayResult' 4 | export * from './file' 5 | export * from './loadEnvVariables' 6 | export * from './saveLog' 7 | export * from './setConstants' 8 | export * from './setProjectDir' 9 | export * from './utils' 10 | export * from './validateTargetName' 11 | -------------------------------------------------------------------------------- /src/commands/compile/spec/duplicates.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file example.sas 3 | @brief example file 4 | @details This service does stuff. Like - ya know - stuff. 5 | 6 |

SAS Macros

7 | @li mv_createfolder.sas 8 | 9 |

SAS Programs

10 | @li test.sas TEST 11 | @li test.sas TEST2 12 | 13 | **/ 14 | 15 | %put stuff; 16 | -------------------------------------------------------------------------------- /src/commands/compile/internal/spec/testFiles/jobs/testJob.test.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @sastype_test 4 | @brief testing runjob1 5 | 6 |

SAS Macros

7 | @li mp_assert.sas 8 | **/ 9 | 10 | %put this is a test; 11 | 12 | %assert(msg=My Test for runjob1,result=FAIL) 13 | 14 | %webout(OPEN) 15 | %webout(OBJ,test_results) 16 | %webout(CLOSE) -------------------------------------------------------------------------------- /src/commands/compile/internal/spec/testFiles/macros/testMacro.test.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @sastype_test 4 | @brief testing macro 5 | 6 |

SAS Macros

7 | @li mp_assert.sas 8 | **/ 9 | 10 | %put this is a test; 11 | 12 | %assert(msg=My Test for macro,result=FAIL) 13 | 14 | %webout(OPEN) 15 | %webout(OBJ,test_results) 16 | %webout(CLOSE) -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "attach", 7 | "name": "Node: Nodemon", 8 | "processId": "${command:PickProcess}", 9 | "restart": true, 10 | "protocol": "inspector", 11 | }, 12 | ] 13 | } -------------------------------------------------------------------------------- /src/commands/compile/spec/duplicates-extensions.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file example.sas 3 | @brief example file 4 | @details This service does stuff. Like - ya know - stuff. 5 | 6 |

SAS Macros

7 | @li mv_createfolder.sas 8 | 9 |

SAS Programs

10 | @li test.sas TEST 11 | @li test.ddl TEST2 12 | 13 | **/ 14 | 15 | %put stuff; 16 | -------------------------------------------------------------------------------- /src/commands/deploy/spec/testServices/serviceterm.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file serviceterm.sas 3 | @brief this file is called at the end of every service 4 | @details This file is included at the *end* of every service. 5 | 6 |

SAS Macros

7 | @li mf_abort.sas 8 | @li mf_existds.sas 9 | 10 | **/ 11 | 12 | %put service is finishing. Thanks, SASjs!; -------------------------------------------------------------------------------- /src/commands/flow/spec/sourceFiles/not_valid_1.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "myAmazingFlow", 3 | "flows": { 4 | "firstFlow": { 5 | "jobs": [ 6 | { 7 | "location": "jobs/jobs/job" 8 | } 9 | { 10 | "location": "jobs/jobs/job" 11 | } 12 | ], 13 | "predecessors": [] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/commands/flow/spec/sourceFiles/not_valid_2.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "myAmazingFlow", 3 | "ERROR": { 4 | "firstFlow": { 5 | "jobs": [ 6 | { 7 | "location": "jobs/jobs/job" 8 | }, 9 | { 10 | "location": "jobs/jobs/job" 11 | } 12 | ], 13 | "predecessors": [] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/commands/flow/spec/sourceFiles/not_valid_3.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "myAmazingFlow", 3 | "flows": { 4 | "firstFlow": { 5 | "ERROR": [ 6 | { 7 | "location": "jobs/jobs/job" 8 | }, 9 | { 10 | "location": "jobs/jobs/job" 11 | } 12 | ], 13 | "predecessors": [] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/commands/flow/spec/testServices/serviceterm.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file serviceterm.sas 3 | @brief this file is called at the end of every service 4 | @details This file is included at the *end* of every service. 5 | 6 |

SAS Macros

7 | @li mf_abort.sas 8 | @li mf_existds.sas 9 | 10 | **/ 11 | 12 | %put service is finishing. Thanks, SASjs!; -------------------------------------------------------------------------------- /src/commands/job/spec/testServices/serviceterm.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file serviceterm.sas 3 | @brief this file is called at the end of every service 4 | @details This file is included at the *end* of every service. 5 | 6 |

SAS Macros

7 | @li mf_abort.sas 8 | @li mf_existds.sas 9 | 10 | **/ 11 | 12 | %put service is finishing. Thanks, SASjs!; -------------------------------------------------------------------------------- /src/utils/prefixAppLoc.ts: -------------------------------------------------------------------------------- 1 | export const prefixAppLoc = (appLoc = '', path = '') => { 2 | if (!path) return '' 3 | 4 | if (!/^\//.test(appLoc)) appLoc = '/' + appLoc 5 | 6 | if (Array.isArray(path)) path = path.join(' ') 7 | 8 | return path 9 | .split(' ') 10 | .map((p) => (/^\//.test(p) ? p : `${appLoc}/${p}`)) 11 | .join(' ') 12 | } 13 | -------------------------------------------------------------------------------- /src/commands/flow/spec/sourceFiles/testFlow_1.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "myAmazingFlow", 3 | "flows": { 4 | "firstFlow": { 5 | "jobs": [ 6 | { 7 | "location": "jobs/testJob/job" 8 | }, 9 | { 10 | "location": "jobs/testJob/job" 11 | } 12 | ], 13 | "predecessors": [] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/commands/compile/internal/spec/testFiles/services/random.test.0.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @brief 4 |

SAS Macros

5 | **/ 6 | %put 'random.test.0.sas'; 7 | data work.test_results; 8 | test_description="random 0 test description"; 9 | test_result="FAIL"; 10 | output; 11 | run; 12 | %webout(OPEN) 13 | %webout(OBJ, test_results) 14 | %webout(CLOSE) -------------------------------------------------------------------------------- /src/commands/compile/spec/example-reversed.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file example.sas 3 | @brief example file 4 | @details This service does stuff. Like - ya know - stuff. 5 |

SAS Macros

6 | @li mv_createfolder.sas 7 | 8 | 9 | 10 |

SAS Programs

11 | @li test.sas TEST 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | **/ 21 | 22 | %put stuff; 23 | -------------------------------------------------------------------------------- /src/commands/flow/spec/sourceFiles/testFlow_3.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "myAmazingFlow", 3 | "flows": { 4 | "firstFlow": { 5 | "jobs": [ 6 | { 7 | "location": "jobs/testJob/job" 8 | }, 9 | { 10 | "location": "jobs/testJob/DOES_NOT_EXIST" 11 | } 12 | ], 13 | "predecessors": [] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/commands/compile/internal/index.ts: -------------------------------------------------------------------------------- 1 | export * from './checkCompileStatus' 2 | export * from './compareFolders' 3 | export * from './compileFile' 4 | export * from './compileTestFile' 5 | export * from './copySyncFolder' 6 | export * from './getAllFolders' 7 | export * from './getDestinationPath' 8 | export * from './identifySasFile' 9 | export * from './loadDependencies' 10 | -------------------------------------------------------------------------------- /src/commands/deploy/internal/index.ts: -------------------------------------------------------------------------------- 1 | export * from './deployToSASJSWithServicePack' 2 | export * from './getDeployScripts' 3 | export * from './executeDeployScript' 4 | export * from './executeSasScript' 5 | export * from './executeNonSasScript' 6 | export * from './executeDeployScriptSasViya' 7 | export * from './executeDeployScriptSas9' 8 | export * from './executeDeployScriptSasjs' 9 | -------------------------------------------------------------------------------- /src/commands/compile/spec/spacing.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file example.sas 3 | @brief example file 4 | @details This service does stuff. Like - ya know - stuff. 5 | 6 | 7 |

SAS Programs

8 | @li test.sas TEST 9 | @li test2.sas TEST2 10 | 11 |

SAS Macros

12 | @li mv_createfolder.sas 13 | @li mv_createfolder2.sas 14 | 15 | **/ 16 | 17 | %put stuff; 18 | -------------------------------------------------------------------------------- /src/types/system/process.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace NodeJS { 2 | export interface Process { 3 | csvFileAbleToSave: boolean 4 | projectDir: string 5 | currentDir: string 6 | logger: import('@sasjs/utils/logger').Logger 7 | sasjsConstants: import('../../constants').Constants 8 | sasjsConfig: import('@sasjs/utils/types/configuration').Configuration 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /mocks/sasjs/.sasjslint: -------------------------------------------------------------------------------- 1 | { 2 | "noEncodedPasswords": true, 3 | "hasDoxygenHeader": true, 4 | "hasMacroNameInMend": true, 5 | "hasMacroParentheses": true, 6 | "indentationMultiple": 2, 7 | "lowerCaseFileNames": true, 8 | "maxLineLength": 107, 9 | "noNestedMacros": true, 10 | "noSpacesInFileNames": true, 11 | "noTabIndentation": true, 12 | "noTrailingSpaces": true 13 | } -------------------------------------------------------------------------------- /src/commands/compile/internal/spec/testFiles/services/random.test.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @brief 4 |

SAS Macros

5 | @li mf_abort.sas 6 | **/ 7 | %put 'random.test.sas'; 8 | data work.test_results; 9 | test_description="random test description"; 10 | test_result="PASS"; 11 | output; 12 | run; 13 | %webout(OPEN) 14 | %webout(OBJ, test_results) 15 | %webout(CLOSE) -------------------------------------------------------------------------------- /src/commands/servicepack/spec/testServicepack.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "services", 3 | "type": "folder", 4 | "members": [ 5 | { 6 | "name": "admin", 7 | "type": "folder", 8 | "members": [ 9 | { 10 | "name": "exportconfig", 11 | "type": "service", 12 | "code": "%put hello world;" 13 | } 14 | ] 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | SAS_USERNAME=SAS username 2 | SAS_PASSWORD=SAS password 3 | VIYA_SERVER_URL=https://sas.analytium.co.uk 4 | SAS9_SERVER_URL=https://sas.analytium.co.uk:8343 5 | SASJS_SERVER_URL=https://sas.analytium.co.uk:5000 6 | CLIENT=clientXYZ 7 | SECRET=secretXYZ 8 | ACCESS_TOKEN=QWERTY!@#$% 9 | REFRESH_TOKEN=QWERTY!@#$% 10 | LOG_LEVEL= Trace / Debug / Info / Warn / Error / Off 11 | VERBOSE=off 12 | -------------------------------------------------------------------------------- /src/commands/compile/internal/spec/testFiles/tests/testsetup.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @brief setting up the test suite 4 |

SAS Macros

5 | **/ 6 | 7 | %put testing, setup everything; 8 | 9 | data work.test_results; 10 | test_description="some description"; 11 | test_result="PASS"; 12 | output; 13 | run; 14 | 15 | %webout(OPEN) 16 | %webout(OBJ, test_results) 17 | %webout(CLOSE) -------------------------------------------------------------------------------- /mocks/sasjs/sasjsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/sasjs/utils/main/src/types/sasjsconfig-schema.json", 3 | "defaultTarget": "server", 4 | "syncFolder": "sasjs", 5 | "targets": [ 6 | { 7 | "name": "server", 8 | "serverUrl": "http://localhost:5000", 9 | "serverType": "SASJS", 10 | "appLoc": "/User Folders/sasdemo/My Folder" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /src/commands/compile/internal/spec/testFiles/tests/sub/testteardown.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @brief tearing down the test suite 4 |

SAS Macros

5 | **/ 6 | 7 | %put testing, tear down everything; 8 | 9 | data work.test_results; 10 | test_description="some description"; 11 | test_result="FAIL"; 12 | output; 13 | run; 14 | 15 | %webout(OPEN) 16 | %webout(OBJ, test_results) 17 | %webout(CLOSE) -------------------------------------------------------------------------------- /src/commands/snippets/spec/testMacros/example.test.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @sastype_test 4 | @brief testing example.sas macro 5 | 6 |

SAS Macros

7 | @li example.sas 8 | @li mp_assertscope.sas 9 | 10 | **/ 11 | 12 | 13 | %let testvar=this is a test; 14 | %mp_assertscope(SNAPSHOT) 15 | %example(some message) 16 | %mp_assertscope(COMPARE,desc=Checking macro variables against previous snapshot) 17 | -------------------------------------------------------------------------------- /src/commands/snippets/spec/testMacros/example.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @brief An example macro 4 | @details prints an arbitrary message to the log 5 | 6 | @param msg The message to be printed 7 | @author Allan Bowe 8 | 9 | **/ 10 | 11 | %macro example(msg); 12 | 13 | %let testvar=%sysfunc(ranuni(0)); 14 | 15 | data work.example; 16 | msg=symget('msg'); 17 | putlog msg=; 18 | run; 19 | 20 | %mend ; 21 | -------------------------------------------------------------------------------- /src/commands/snippets/spec/testMacros/textMacro.txt: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @brief An example macro 4 | @details prints an arbitrary message to the log 5 | 6 | @param msg The message to be printed 7 | @author Allan Bowe 8 | 9 | **/ 10 | 11 | %macro example(msg); 12 | 13 | %let testvar=%sysfunc(ranuni(0)); 14 | 15 | data work.example; 16 | msg=symget('msg'); 17 | putlog msg=; 18 | run; 19 | 20 | %mend ; 21 | -------------------------------------------------------------------------------- /src/commands/web/internal/updateSasjsTag.ts: -------------------------------------------------------------------------------- 1 | import { Target } from '@sasjs/utils' 2 | import { JSDOM } from 'jsdom' 3 | import { getSasjsTags } from './getTags' 4 | 5 | /** 6 | * Updates sasjs adapter's config in index.html if present. 7 | */ 8 | export const updateSasjsTag = (tag: Element, target: Target) => { 9 | tag.setAttribute('appLoc', target.appLoc) 10 | tag.setAttribute('serverType', target.serverType) 11 | } 12 | -------------------------------------------------------------------------------- /src/commands/context/spec/mocks.ts: -------------------------------------------------------------------------------- 1 | import { AuthConfig } from '@sasjs/utils/types' 2 | 3 | export const mockAuthConfig: AuthConfig = { 4 | client: 'cl13nt', 5 | secret: '53cr3t', 6 | access_token: 'acc355', 7 | refresh_token: 'r3fr35h' 8 | } 9 | 10 | export const mockContext = { 11 | name: 'testContext', 12 | launchContext: { 13 | contextName: 'test launcher context' 14 | }, 15 | launchType: 'service' 16 | } 17 | -------------------------------------------------------------------------------- /src/commands/folder/spec/mocks.ts: -------------------------------------------------------------------------------- 1 | import { AuthConfig } from '@sasjs/utils/types' 2 | 3 | export const mockAuthConfig: AuthConfig = { 4 | client: 'cl13nt', 5 | secret: '53cr3t', 6 | access_token: 'acc355', 7 | refresh_token: 'r3fr35h' 8 | } 9 | 10 | export const mockContext = { 11 | name: 'testContext', 12 | launchContext: { 13 | contextName: 'test launcher context' 14 | }, 15 | launchType: 'service' 16 | } 17 | -------------------------------------------------------------------------------- /src/commands/flow/spec/sourceFiles/testFlow_8.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "myAmazingFlow", 3 | "flows": { 4 | "firstFlow": { 5 | "jobs": [ 6 | { 7 | "location": "jobs/testJob/largeLogJob", 8 | "macroVars": { 9 | "test_var_1": "test_var_value_1", 10 | "test_var_2": "test_var_value_2" 11 | } 12 | } 13 | ], 14 | "predecessors": [] 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/commands/flow/spec/sourceFiles/testFlow_2.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "myAmazingFlow", 3 | "flows": { 4 | "firstFlow": { 5 | "jobs": [ 6 | { 7 | "location": "jobs/testJob/job" 8 | }, 9 | { 10 | "location": "jobs/testJob/failingJob" 11 | }, 12 | { 13 | "location": "jobs/testJob/job" 14 | } 15 | ], 16 | "predecessors": [] 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/commands/snippets/spec/testMacros/subtestMacros/subMacro.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @brief A sub macro 4 | @details prints an arbitrary message to the log 5 | 6 | @param msg The message to be printed 7 | @author Allan Bowe 8 | 9 | **/ 10 | 11 | %macro example(msg); 12 | 13 | %let testvar=%sysfunc(ranuni(0)); 14 | 15 | data work.example; 16 | msg=symget('msg'); 17 | putlog msg=; 18 | run; 19 | 20 | %mend example ; 21 | -------------------------------------------------------------------------------- /src/utils/fileStructures/dbFiles.ts: -------------------------------------------------------------------------------- 1 | import { Folder } from '../../types' 2 | 3 | export const dbFiles: Folder = { 4 | folderName: 'sasjsbuild', 5 | files: [], 6 | subFolders: [ 7 | { 8 | folderName: 'db', 9 | files: [ 10 | { 11 | fileName: 'LIBREF1.ddl' 12 | }, 13 | { 14 | fileName: 'LIBREF1.sas' 15 | } 16 | ], 17 | subFolders: [] 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /src/commands/compile/spec/services/example1.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file example1.sas 3 | @brief example service - for example 4 | @details This is a longer description. 5 | 6 |

SAS Macros

7 | @li mf_nobs.sas 8 | @li examplemacro.sas 9 | @li yetanothermacro.sas 10 | 11 | **/ 12 | 13 | %put %mf_nobs(sashelp.class); 14 | 15 | %examplemacro() 16 | %yetanothermacro() 17 | 18 | %webout(OPEN) 19 | %webout(OBJ,areas) 20 | %webout(CLOSE) 21 | -------------------------------------------------------------------------------- /src/commands/deploy/internal/getDeployScripts.ts: -------------------------------------------------------------------------------- 1 | import { Target } from '@sasjs/utils' 2 | 3 | export async function getDeployScripts(target: Target) { 4 | const configuration = process.sasjsConfig 5 | 6 | const allDeployScripts: string[] = [ 7 | ...(configuration?.deployConfig?.deployScripts || []), 8 | ...(target.deployConfig?.deployScripts || []) 9 | ] 10 | 11 | return [...new Set(allDeployScripts.filter((d) => !!d))] 12 | } 13 | -------------------------------------------------------------------------------- /src/commands/web/internal/modifyLinksInContent.ts: -------------------------------------------------------------------------------- 1 | import { AssetPathMap } from './createAssetServices' 2 | 3 | export const modifyLinksInContent = ( 4 | _content: string, 5 | assetPathMap: AssetPathMap[] 6 | ) => { 7 | let content = _content 8 | assetPathMap.forEach((pathEntry) => { 9 | content = content.replace( 10 | new RegExp(pathEntry.source, 'g'), 11 | pathEntry.target 12 | ) 13 | }) 14 | return content 15 | } 16 | -------------------------------------------------------------------------------- /src/commands/testing/spec/testFiles/testinit.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file testinit.sas 3 | @brief this file is called with every test 4 | @details This file is included in *every* test, *after* the macros and *before* the test code. 5 | 6 |

SAS Macros

7 | @li mf_abort.sas 8 | 9 | **/ 10 | 11 | options 12 | DATASTMTCHK=ALLKEYWORDS /* some sites have this enabled */ 13 | PS=MAX /* reduce log size slightly */ 14 | ; 15 | %put test is starting!!; -------------------------------------------------------------------------------- /src/commands/testing/spec/testFiles/testsetup.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file testsetup.sas 3 | @brief this file is called with every test 4 | @details This file is included in *every* test, *after* the macros and *before* the test code. 5 | 6 |

SAS Macros

7 | @li mf_abort.sas 8 | 9 | **/ 10 | 11 | options 12 | DATASTMTCHK=ALLKEYWORDS /* some sites have this enabled */ 13 | PS=MAX /* reduce log size slightly */ 14 | ; 15 | %put test is seting up!!; -------------------------------------------------------------------------------- /src/commands/snippets/spec/testMacros/subtestMacros/subSubTestMacros/subSubMacro.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @brief A sub sub macro 4 | @details prints an arbitrary message to the log 5 | 6 | @param msg The message to be printed 7 | @author Allan Bowe 8 | 9 | **/ 10 | 11 | %macro example(msg); 12 | 13 | %let testvar=%sysfunc(ranuni(0)); 14 | 15 | data work.example; 16 | msg=symget('msg'); 17 | putlog msg=; 18 | run; 19 | 20 | %mend example ; 21 | -------------------------------------------------------------------------------- /src/commands/testing/spec/testFiles/testteardown.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file testteardown.sas 3 | @brief this file is called with every test 4 | @details This file is included in *every* test, *after* the macros and *before* the test code. 5 | 6 |

SAS Macros

7 | @li mf_abort.sas 8 | 9 | **/ 10 | 11 | options 12 | DATASTMTCHK=ALLKEYWORDS /* some sites have this enabled */ 13 | PS=MAX /* reduce log size slightly */ 14 | ; 15 | %put test tear down!!; -------------------------------------------------------------------------------- /src/commands/deploy/spec/testServices/serviceinit.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file serviceinit.sas 3 | @brief this file is called with every service 4 | @details This file is included in *every* service, *after* the macros and *before* the service code. 5 | 6 |

SAS Macros

7 | @li mf_abort.sas 8 | 9 | **/ 10 | 11 | options 12 | DATASTMTCHK=ALLKEYWORDS /* some sites have this enabled */ 13 | PS=MAX /* reduce log size slightly */ 14 | ; 15 | %put service is starting!!; -------------------------------------------------------------------------------- /src/commands/flow/spec/testServices/serviceinit.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file serviceinit.sas 3 | @brief this file is called with every service 4 | @details This file is included in *every* service, *after* the macros and *before* the service code. 5 | 6 |

SAS Macros

7 | @li mf_abort.sas 8 | 9 | **/ 10 | 11 | options 12 | DATASTMTCHK=ALLKEYWORDS /* some sites have this enabled */ 13 | PS=MAX /* reduce log size slightly */ 14 | ; 15 | %put service is starting!!; -------------------------------------------------------------------------------- /src/commands/job/spec/testServices/serviceinit.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file serviceinit.sas 3 | @brief this file is called with every service 4 | @details This file is included in *every* service, *after* the macros and *before* the service code. 5 | 6 |

SAS Macros

7 | @li mf_abort.sas 8 | 9 | **/ 10 | 11 | options 12 | DATASTMTCHK=ALLKEYWORDS /* some sites have this enabled */ 13 | PS=MAX /* reduce log size slightly */ 14 | ; 15 | %put service is starting!!; -------------------------------------------------------------------------------- /src/commands/compile/spec/services/example.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file example.sas 3 | @brief example service - for example 4 | @details This is a longer description. 5 | 6 |

SAS Macros

7 | @li mf_nobs.sas 8 | @li examplemacro.sas 9 | @li yetanothermacro.sas 10 | 11 |

SAS Includes

12 | @li doesnotexist.sas SOMEREF 13 | 14 | **/ 15 | 16 | %put %mf_nobs(sashelp.class); 17 | 18 | %examplemacro() 19 | %yetanothermacro() 20 | 21 | %webout(OPEN) 22 | %webout(OBJ,areas) 23 | %webout(CLOSE) 24 | -------------------------------------------------------------------------------- /src/commands/deploy/internal/executeDeployScript.ts: -------------------------------------------------------------------------------- 1 | import { Target, StreamConfig } from '@sasjs/utils' 2 | import { isSasFile } from '../../../utils/file' 3 | import { executeSasScript, executeNonSasScript } from './' 4 | 5 | export async function executeDeployScript( 6 | scriptPath: string, 7 | target: Target, 8 | streamConfig: StreamConfig 9 | ) { 10 | if (isSasFile(scriptPath)) 11 | return await executeSasScript(scriptPath, target, streamConfig) 12 | 13 | return executeNonSasScript(scriptPath) 14 | } 15 | -------------------------------------------------------------------------------- /src/types/command/commandAliases.ts: -------------------------------------------------------------------------------- 1 | export const aliasMap = new Map([ 2 | ['add', ['auth']], 3 | ['build', ['b']], 4 | ['compile', ['c']], 5 | ['compilebuild', ['cb']], 6 | ['compilebuilddeploy', ['cbd']], 7 | ['db', ['DB', 'build-DB', 'build-db']], 8 | ['deploy', ['d']], 9 | ['doc', ['docs']], 10 | ['help', ['h']], 11 | ['request', ['rq']], 12 | ['run', ['r']], 13 | ['version', ['v']], 14 | ['web', ['w']] 15 | ]) 16 | 17 | export const getAllSupportedAliases = () => aliasMap.keys() 18 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export { CommandBase } from './command/commandBase' 2 | export { File, FilePath } from './file' 3 | export { Folder } from './folder' 4 | export { TargetScope } from './targetScope' 5 | export { 6 | TestFlow, 7 | Coverage, 8 | CoverageType, 9 | CoverageState, 10 | TestResults, 11 | TestResultStatus, 12 | TestDescription, 13 | TestResult, 14 | TestResultDescription, 15 | TestResultCsv 16 | } from './testing' 17 | export { Flow, FlowWave, FlowWaveJob } from './flow' 18 | export * from './log' 19 | -------------------------------------------------------------------------------- /src/commands/docs/spec/testJobHavingDoubleQoutes/runjob1.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @brief runjob1 " t"est" 4 | @details In the flow this is defined to run after the dependent flows have 5 | finished (successfully) 6 | 7 | 8 |

SAS Macros

9 | @li example.sas 10 | @li mf_nobs.sas 11 | 12 |

Data Outputs

13 | @li sas9hrdb.test 14 | 15 | **/ 16 | 17 | %example(runjob1 is executing) 18 | 19 | /* here we are using one of the @sasjs/core macros */ 20 | data work.somedata; 21 | x=%mf_nobs(sashelp.class); 22 | run; 23 | 24 | -------------------------------------------------------------------------------- /src/commands/flow/spec/sourceFiles/testFlow_4.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "myAmazingFlow", 3 | "flows": { 4 | "firstFlow": { 5 | "jobs": [ 6 | { 7 | "location": "jobs/testJob/job" 8 | }, 9 | { 10 | "location": "jobs/testJob/failingJob" 11 | } 12 | ], 13 | "predecessors": [] 14 | }, 15 | "secondFlow": { 16 | "jobs": [ 17 | { 18 | "location": "jobs/testJob/job" 19 | } 20 | ], 21 | "predecessors": ["firstFlow"] 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/commands/compile/spec/macros/examplemacro.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file examplemacro.sas 3 | @brief an example of a macro to be used in a service 4 | @details This macro is great. Yadda yadda yadda. Usage: 5 | 6 | * code formatting applies when indented by 4 spaces; 7 | %examplemacro() 8 | 9 |

SAS Macros

10 | @li doesnothing.sas 11 | 12 | @author Allan Bowe 13 | **/ 14 | 15 | %macro examplemacro(); 16 | 17 | proc sql; 18 | create table areas 19 | as select area 20 | from sashelp.springs; 21 | 22 | %doesnothing() 23 | 24 | %mend; 25 | -------------------------------------------------------------------------------- /src/utils/getLogFilePath.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | export const getLogFilePath = (logArg: unknown, jobPath: string) => { 4 | if (logArg === undefined || !jobPath || jobPath === '') return undefined 5 | 6 | if (logArg) { 7 | const currentDirPath = path.isAbsolute(logArg as string) 8 | ? '' 9 | : process.projectDir 10 | 11 | return path.join(currentDirPath, logArg as string) 12 | } 13 | 14 | const logFileName = `${jobPath.split('/').slice(-1).pop()}.log` 15 | 16 | return path.join(process.projectDir, logFileName) 17 | } 18 | -------------------------------------------------------------------------------- /src/commands/docs/internal/populateNodeDictionary.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Populates Dictionary Map of All Nodes 3 | * @param {Map} nodeDictionary- Map for all Nodes having Alpha-Numeric name(, acceptable for dot) 4 | * @param {Map} nodes- Map for params(Inputs/Outputs) Or files 5 | */ 6 | export function populateNodeDictionary( 7 | nodeDictionary: Map, 8 | nodes: Map 9 | ) { 10 | nodes.forEach((node, key) => { 11 | if (!nodeDictionary.has(key)) 12 | nodeDictionary.set(key, `n${nodeDictionary.size}`) 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /src/commands/flow/internal/printError.ts: -------------------------------------------------------------------------------- 1 | import { FlowWaveJob } from '../../../types' 2 | 3 | export const printError = ( 4 | job: FlowWaveJob, 5 | flowName: string, 6 | err: { name?: string; message?: string } | string 7 | ) => { 8 | process.logger?.error( 9 | `An error has occurred when executing '${flowName}' flow's job located at: '${ 10 | job.location 11 | }'. ${ 12 | typeof err === 'object' 13 | ? err?.name === 'NotFoundError' 14 | ? 'Job was not found.' 15 | : err?.message || '' 16 | : '\n' + err 17 | }` 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export interface Constants { 2 | buildSourceFolder: string 3 | buildSourceDbFolder: string 4 | buildDestinationFolder: string 5 | buildDestinationServicesFolder: string 6 | buildDestinationJobsFolder: string 7 | buildDestinationDbFolder: string 8 | buildDestinationDocsFolder: string 9 | buildDestinationResultsFolder: string 10 | buildDestinationResultsLogsFolder: string 11 | buildDestinationTestFolder: string 12 | macroCorePath: string 13 | contextName: string 14 | sas9CredentialsError: string 15 | invalidSasError: string 16 | sas9GUID: string 17 | } 18 | -------------------------------------------------------------------------------- /src/types/command/parse.ts: -------------------------------------------------------------------------------- 1 | import { getBaseCommand } from '../../utils/getBaseCommand' 2 | import { CommandBase } from './commandBase' 3 | import { commandFactory } from './commandFactory' 4 | 5 | export const parse = (args: string[]): CommandBase => { 6 | const baseCommand = getBaseCommand(args) 7 | 8 | if (!Array.from(commandFactory.keys()).includes(baseCommand.name)) { 9 | process.logger?.error( 10 | 'Invalid SASjs command! Run `sasjs help` for a full list of available commands.' 11 | ) 12 | 13 | process.exit(1) 14 | } 15 | 16 | return commandFactory.get(baseCommand.name)!(args) 17 | } 18 | -------------------------------------------------------------------------------- /src/types/flow.ts: -------------------------------------------------------------------------------- 1 | import { MacroVar } from '@sasjs/utils' 2 | 3 | export interface Flow { 4 | name: string 5 | flows: { 6 | [key: string]: FlowWave 7 | } 8 | } 9 | 10 | export interface FlowWave { 11 | name?: string 12 | jobs: FlowWaveJob[] 13 | predecessors?: string[] 14 | execution?: 'started' | 'finished' | 'failedByPredecessor' 15 | } 16 | 17 | export enum FlowWaveJobStatus { 18 | Runnning = 'running', 19 | Success = 'success', 20 | Failure = 'failure' 21 | } 22 | 23 | export interface FlowWaveJob { 24 | location: string 25 | macroVars?: MacroVar 26 | status?: FlowWaveJobStatus 27 | } 28 | -------------------------------------------------------------------------------- /src/commands/compile/internal/copySyncFolder.ts: -------------------------------------------------------------------------------- 1 | import { copy, folderExists, getAbsolutePath } from '@sasjs/utils' 2 | 3 | export const copySyncFolder = async (syncFolder: string) => { 4 | const { buildSourceFolder, buildDestinationFolder } = process.sasjsConstants 5 | const syncFolderPath = getAbsolutePath(syncFolder, buildSourceFolder) 6 | 7 | process.logger?.info(`Syncing files from ${syncFolderPath} .`) 8 | 9 | if (!(await folderExists(syncFolderPath))) { 10 | process.logger?.error(`${syncFolderPath} doesn't exist.`) 11 | return 12 | } 13 | await copy(syncFolderPath, buildDestinationFolder) 14 | } 15 | -------------------------------------------------------------------------------- /src/commands/flow/internal/index.ts: -------------------------------------------------------------------------------- 1 | export { executeFlow } from './executeFlow' 2 | export { checkPredecessorDeadlock } from './checkPredecessorDeadlock' 3 | export { printError } from './printError' 4 | export { validateParams } from './validateParams' 5 | export { failAllSuccessors } from './failAllSuccessors' 6 | export { normalizeFilePath } from './normalizeFilePath' 7 | export { saveLog } from './saveLog' 8 | export { generateFileName } from './generateFileName' 9 | export { allFlowsCompleted } from './allFlowsCompleted' 10 | export { parseJobDetails } from './parseJobDetails' 11 | export { saveToCsv } from './saveToCsv' 12 | -------------------------------------------------------------------------------- /src/commands/flow/spec/sourceFiles/testFlow_5.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "myAmazingFlow", 3 | "flows": { 4 | "firstFlow": { 5 | "jobs": [ 6 | { 7 | "location": "jobs/testJob/job" 8 | }, 9 | { 10 | "location": "jobs/testJob/job" 11 | } 12 | ], 13 | "predecessors": [] 14 | }, 15 | "secondFlow": { 16 | "jobs": [ 17 | { 18 | "location": "jobs/testJob/job" 19 | }, 20 | { 21 | "location": "jobs/testJob/failingJob" 22 | } 23 | ], 24 | "predecessors": ["firstFlow"] 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/commands/lint/initLint.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | import { fileExists } from '@sasjs/utils' 4 | import { createLintConfigFile } from '../shared/createLintConfigFile' 5 | 6 | /** 7 | * Creates a .sasjslint configuration file in the current SASjs project if one doesn't already exist 8 | */ 9 | export async function initLint(): Promise<{ fileAlreadyExisted: boolean }> { 10 | const lintConfigPath = path.join(process.projectDir, '.sasjslint') 11 | if (await fileExists(lintConfigPath)) return { fileAlreadyExisted: true } 12 | else await createLintConfigFile('.') 13 | return { fileAlreadyExisted: false } 14 | } 15 | -------------------------------------------------------------------------------- /src/commands/web/internal/sasViya/createClickMeFile.ts: -------------------------------------------------------------------------------- 1 | import { createFile } from '@sasjs/utils' 2 | import path from 'path' 3 | 4 | /** 5 | * Creates index file for SASVIYA server only. 6 | * @param {string} indexHtmlContent contents of index file. 7 | * @param {string} fileName name of index file. 8 | */ 9 | export const createClickMeFile = async ( 10 | indexHtmlContent: string, 11 | fileName: string 12 | ) => { 13 | const { buildDestinationServicesFolder } = process.sasjsConstants 14 | await createFile( 15 | path.join(buildDestinationServicesFolder, `${fileName}.html`), 16 | indexHtmlContent 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /src/commands/version/version.ts: -------------------------------------------------------------------------------- 1 | import shelljs from 'shelljs' 2 | 3 | export async function printVersion() { 4 | const result = shelljs.exec(`npm list -g @sasjs/cli`, { 5 | silent: true 6 | }) 7 | 8 | const line = result.split('\n').find((l) => l.includes('@sasjs/cli')) || '' 9 | const version = line.split('@')[2].trim() 10 | const message = version.includes('->') 11 | ? `You are using a linked version of SASjs CLI running from sources at ${version 12 | .replace('->', '') 13 | .trim()}` 14 | : `You are using SASjs CLI v${version}` 15 | 16 | process.logger?.info(message) 17 | 18 | return message 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/validateTargetName.ts: -------------------------------------------------------------------------------- 1 | export const validateTargetName = (targetName: string): string => { 2 | // if targetName contain falsy Value just return that value 3 | if (!targetName) return targetName 4 | 5 | targetName = targetName.trim() 6 | if (targetName.includes(' ')) 7 | throw new Error( 8 | 'Target names cannot include spaces. Please try again with a valid target name.' 9 | ) 10 | 11 | if (!/^[a-zA-Z0-9][a-zA-Z0-9\-]+$/i.test(targetName)) 12 | throw Error( 13 | 'Target names can only contain alphanumeric characters. Please try again with a valid target name.' 14 | ) 15 | 16 | return targetName 17 | } 18 | -------------------------------------------------------------------------------- /src/commands/deploy/spec/testConfig/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "macroFolders": ["sasjs/macros"], 3 | "serviceConfig": { 4 | "serviceFolders": ["sasjs/services"] 5 | }, 6 | "targets": [ 7 | { 8 | "name": "", 9 | "serverType": "", 10 | "serverUrl": "", 11 | "appLoc": "/Public/app/cli-tests", 12 | "deployConfig": { 13 | "deployScripts": ["sasjs/build/copyscript.sh"], 14 | "deployServicePack": true 15 | }, 16 | "streamConfig": { 17 | "assetPaths": [], 18 | "streamWeb": true, 19 | "streamWebFolder": "webv", 20 | "webSourcePath": "src" 21 | } 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | import { LogLevel, Logger } from '@sasjs/utils/logger' 2 | import { loadProjectEnvVariables, setProjectDir } from './utils' 3 | import { parse } from './types/command/parse' 4 | 5 | export async function cli(args: string[]) { 6 | await setProjectDir(args) 7 | await instantiateLogger() 8 | await loadProjectEnvVariables() 9 | 10 | const command = parse(args) 11 | const returnCode = await command.execute() 12 | process.exit(returnCode) 13 | } 14 | 15 | export async function instantiateLogger() { 16 | const logLevel = (process.env.LOG_LEVEL || LogLevel.Info) as LogLevel 17 | const logger = new Logger(logLevel) 18 | process.logger = logger 19 | } 20 | -------------------------------------------------------------------------------- /src/commands/flow/internal/failAllSuccessors.ts: -------------------------------------------------------------------------------- 1 | import { FlowWave, FlowWaveJob } from '../../../types' 2 | import { FlowWaveJobStatus } from '../../../types/flow' 3 | 4 | export const failAllSuccessors = ( 5 | flows: { [key: string]: FlowWave }, 6 | flowName: string 7 | ) => { 8 | const successors = Object.keys(flows).filter((flow: string) => 9 | flows[flow]?.predecessors?.includes(flowName) 10 | ) 11 | 12 | successors.forEach((successor: string) => { 13 | flows[successor].jobs.map( 14 | (job: FlowWaveJob) => (job.status = FlowWaveJobStatus.Failure) 15 | ) 16 | flows[successor].execution = 'failedByPredecessor' 17 | failAllSuccessors(flows, successor) 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /src/commands/docs/spec/testJobHavingBackSlashes/runjob2.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @brief runjob2 \some text\\more 4 | @details Get a list of Viya users 5 | 6 | 7 |

SAS Macros

8 | @li example.sas 9 | @li mv_getusers.sas 10 | 11 |

Data Inputs

12 | @li sas9hrdb.test 13 | @li mylib.example 14 | @li mylib.demotable3 15 | 16 |

Data Outputs

17 | @li mylib.users 18 | 19 | **/ 20 | 21 | %let input1=sas9hrdb.test; /* example */ 22 | %let output1=&mylib..users; 23 | 24 | %example(runjob2 is executing) 25 | 26 | /* here we are using one of the @sasjs/core macros */ 27 | %mv_getusers(outds=users) 28 | 29 | data &output1; 30 | set work.users; 31 | run; 32 | 33 | -------------------------------------------------------------------------------- /src/commands/job/spec/getContextName.spec.ts: -------------------------------------------------------------------------------- 1 | import { Target } from '@sasjs/utils' 2 | import { contextName } from '../../../utils' 3 | import { getContextName } from '../internal/execute' 4 | 5 | describe('getContextName', () => { 6 | it('should return the context name if specified in the target', async () => { 7 | const target = { contextName: 'Test Context' } 8 | 9 | expect(getContextName(target as Target)).toEqual('Test Context') 10 | }) 11 | 12 | it('should return the default context if context name is not specified', async () => { 13 | const target = { contextName: undefined } 14 | 15 | expect(getContextName(target as unknown as Target)).toEqual(contextName) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /src/utils/compressAndSave.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import AdmZip from 'adm-zip' 3 | 4 | /** 5 | * Creates a zip file. 6 | * Having single JSON file in it. 7 | * @param {string} saveTo - full path to save the file. 8 | * @param {string} contents - contents of JSON file. 9 | */ 10 | export const compressAndSave = async (saveTo: string, contents: string) => { 11 | const zip = new AdmZip() 12 | 13 | const filenameInZip = path.basename(saveTo, path.extname(saveTo)) 14 | 15 | // add file directly 16 | zip.addFile( 17 | filenameInZip, 18 | Buffer.from(contents, 'utf8'), 19 | 'entry comment goes here' 20 | ) 21 | 22 | await zip.writeZipPromise(saveTo, { overwrite: true }) 23 | } 24 | -------------------------------------------------------------------------------- /src/commands/docs/spec/testJobs/jobinit.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @brief This code is inserted into the beginning of each Viya Job. 4 | @details Inserted during the `sasjs compile` step. Add any code here that 5 | should go at the beginning of every deployed job. 6 | 7 | The path to this file should be listed in the `jobInit` property of the 8 | sasjsconfig file. 9 | 10 |

Data Inputs

11 | @li LIB.test_input_1 12 | @li LIBf.test_input_4 13 | @li LIBf.test_input_5 14 | 15 |

Data Outputs

16 | @li LND.test_output_1 17 | @li BOTH.as_input_and_output 18 | 19 |

SAS Macros

20 | @li example.sas 21 | 22 | **/ 23 | 24 | %example(Job Init is executing!) 25 | 26 | %let mylib=WORK; -------------------------------------------------------------------------------- /src/commands/shared/createLintConfigFile.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | import { DefaultLintConfiguration } from '@sasjs/lint/utils/getLintConfig' 4 | import { createFile } from '@sasjs/utils' 5 | 6 | /** 7 | * Creates a SASjs Lint configuration file. 8 | * Its name will be of the form '.sasjslint' 9 | * @param {string} parentFolderName- the name of the project folder. 10 | */ 11 | export const createLintConfigFile = async (parentFolderName: string) => { 12 | const configDestinationPath = path.join( 13 | process.projectDir, 14 | parentFolderName, 15 | '.sasjslint' 16 | ) 17 | await createFile( 18 | configDestinationPath, 19 | JSON.stringify(DefaultLintConfiguration, null, 2) 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /src/commands/add/internal/saveConfig.ts: -------------------------------------------------------------------------------- 1 | import { Target } from '@sasjs/utils' 2 | import { TargetScope } from '../../../types' 3 | import { saveToGlobalConfig, saveToLocalConfig } from '../../../utils' 4 | 5 | export async function saveConfig( 6 | scope: TargetScope, 7 | target: Target, 8 | isDefault: boolean, 9 | saveWithDefaultValues: boolean 10 | ) { 11 | let filePath = '' 12 | 13 | if (scope === TargetScope.Local) { 14 | filePath = await saveToLocalConfig(target, isDefault, saveWithDefaultValues) 15 | } else if (scope === TargetScope.Global) { 16 | filePath = await saveToGlobalConfig( 17 | target, 18 | isDefault, 19 | saveWithDefaultValues 20 | ) 21 | } 22 | 23 | return filePath 24 | } 25 | -------------------------------------------------------------------------------- /src/utils/fileStructures/compiledFiles.ts: -------------------------------------------------------------------------------- 1 | import { Folder } from '../../types' 2 | 3 | export const compiledFiles: Folder = { 4 | folderName: 'sasjsbuild', 5 | files: [], 6 | subFolders: [ 7 | { 8 | folderName: 'services', 9 | files: [], 10 | subFolders: [ 11 | { 12 | folderName: 'admin', 13 | files: [{ fileName: 'dostuff.sas' }], 14 | subFolders: [] 15 | }, 16 | { 17 | folderName: 'common', 18 | files: [ 19 | { fileName: 'appinit.sas' }, 20 | { fileName: 'example.sas' }, 21 | { fileName: 'getdata.sas' } 22 | ], 23 | subFolders: [] 24 | } 25 | ] 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /src/utils/setProjectDir.ts: -------------------------------------------------------------------------------- 1 | import { getProjectRoot } from './config' 2 | import { getBaseCommand } from './getBaseCommand' 3 | 4 | export const setProjectDir = async (args: string[]) => { 5 | const baseCommand = getBaseCommand(args) 6 | const nonProjectCommands = ['create', 'init'] 7 | 8 | if (nonProjectCommands.includes(baseCommand.name)) { 9 | if (!process.projectDir) process.projectDir = process.cwd() 10 | } else { 11 | process.currentDir = process.cwd() 12 | if (!process.projectDir) { 13 | process.projectDir = process.cwd() 14 | 15 | const rootDir = await getProjectRoot() 16 | 17 | if (rootDir !== process.projectDir) { 18 | process.projectDir = rootDir 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/utils/fileStructures/compiledFilesCustom1.ts: -------------------------------------------------------------------------------- 1 | import { Folder } from '../../types' 2 | 3 | export const compiledFilesCustom1: Folder = { 4 | folderName: 'sasjsbuild', 5 | files: [], 6 | subFolders: [ 7 | { 8 | folderName: 'services', 9 | files: [], 10 | subFolders: [ 11 | { 12 | folderName: 'admin', 13 | files: [{ fileName: 'dostuff.sas' }], 14 | subFolders: [] 15 | }, 16 | { 17 | folderName: 'common', 18 | files: [ 19 | { fileName: 'appinit.sas' }, 20 | { fileName: 'example.sas' }, 21 | { fileName: 'get.sasdata.sas' } 22 | ], 23 | subFolders: [] 24 | } 25 | ] 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /src/commands/folder/delete.ts: -------------------------------------------------------------------------------- 1 | import SASjs from '@sasjs/adapter/node' 2 | 3 | /** 4 | * Deletes folder. 5 | * @param {string} path - folder path. 6 | * @param {object} sasjs - configuration object of SAS adapter. 7 | * @param {string} accessToken - an access token for an authorized user. 8 | */ 9 | export const deleteFolder = async ( 10 | path: string, 11 | sasjs: SASjs, 12 | accessToken: string 13 | ) => { 14 | const deletedFolder = await sasjs 15 | .deleteFolder(path, accessToken) 16 | .catch((err) => { 17 | process.logger?.error(`Error deleting folder ${path}: `, err) 18 | throw err 19 | }) 20 | 21 | if (deletedFolder) { 22 | process.logger?.success(`Folder '${path}' has been moved to 'Recycle Bin'.`) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.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 | echo commit_message 9 | 10 | if (echo "$commit_message" | grep -Eq "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z \-]+\))?!?: .+$") then 11 | echo "${GREEN} ✔ Commit message meets Conventional Commit standards" 12 | exit 0 13 | fi 14 | 15 | echo "${RED}❌ Commit message does not meet the Conventional Commit standard!" 16 | echo "An example of a valid message is:" 17 | echo " feat(login): add the 'remember me' button" 18 | echo "ℹ More details at: https://www.conventionalcommits.org/en/v1.0.0/#summary" 19 | exit 1 -------------------------------------------------------------------------------- /src/commands/shared/createConfigFile.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | import { getConfiguration } from '../../utils/config' 4 | import { createFile } from '@sasjs/utils' 5 | 6 | /** 7 | * Creates a SASjs configuration file. 8 | * Its name will be of the form 'sasjsconfig.json' 9 | * @param {string} parentFolderName- the name of the project folder. 10 | */ 11 | export const createConfigFile = async (parentFolderName: string) => { 12 | const config = await getConfiguration( 13 | path.join(__dirname, '../../config.json') 14 | ) 15 | const configDestinationPath = path.join( 16 | process.projectDir, 17 | parentFolderName, 18 | 'sasjs', 19 | 'sasjsconfig.json' 20 | ) 21 | await createFile(configDestinationPath, JSON.stringify(config, null, 2)) 22 | } 23 | -------------------------------------------------------------------------------- /src/commands/docs/spec/testJobs/jobterm.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @brief This code is inserted at the end of each Viya Job. 4 | @details Inserted during the `sasjs compile` step. Add any code here that 5 | should go at the end of every deployed job. 6 | 7 | The path to this file should be listed in the `jobTerm` property of the 8 | sasjsconfig file. 9 | 10 |

Data Inputs

11 | @li LIB.test_input_1 12 | @li LIB.test_input_2 13 | @li LIBr.test_input_3 14 | @li LIBf.test_input_4 15 | @li BOTH.as_input_and_output 16 | 17 |

Data Outputs

18 | @li LND.test_output_1 19 | @li LND.test_output_2 20 | @li LND.test_output_3 21 | @li LND.test_output_4 22 | 23 |

SAS Macros

24 | @li example.sas 25 | 26 | **/ 27 | 28 | %example(Job Term is executing!) -------------------------------------------------------------------------------- /src/commands/snippets/spec/testMacros2/macro2.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @BRIEF Macro 2 with uppercase @BRIEF 4 | @details prints an arbitrary message to the log 5 | 6 | @param msg The message to be printed 7 | @PARAM [in] indlm= ( ) Uppercase @PARAM 8 | @param [OUT] outdlm= ( ) Uppercase [OUT] 9 | @param[out] outdlm= ( ) No space after @param 10 | @param [out]outdlm= ( ) No space after [out] 11 | @param [out]outdlm= ( ) No space after @param and [out] 12 | @param [in,out] scopeds= () with [in,out] 13 | @param [anything] anything= () with [anything] 14 | @author Allan Bowe 15 | 16 | **/ 17 | 18 | %macro example(msg); 19 | 20 | %let testvar=%sysfunc(ranuni(0)); 21 | 22 | data work.example; 23 | msg=symget('msg'); 24 | putlog msg=; 25 | run; 26 | 27 | %mend example ; 28 | -------------------------------------------------------------------------------- /src/commands/web/internal/getAssetPath.ts: -------------------------------------------------------------------------------- 1 | import { ServerType } from '@sasjs/utils' 2 | 3 | export const getAssetPath = ( 4 | appLoc: string, 5 | serverType: ServerType, 6 | streamWebFolder: string, 7 | fileName: string 8 | ) => { 9 | const { sas9GUID } = process.sasjsConstants 10 | const storedProcessPath = 11 | // the appLoc is inserted dynamically 12 | // for SAS 9 files are Base 64 encoded into STPs, with 13 | // dynamic runtime replacement of appLoc (see sasjsout.ts) 14 | // for Viya, fileName is a FILE, with replacement harcoded in build.sas 15 | serverType === ServerType.SasViya 16 | ? `/SASJobExecution?_FILE=${appLoc}/services` 17 | : `/SASStoredProcess/?_PROGRAM=${sas9GUID}` 18 | return `${storedProcessPath}/${streamWebFolder}/${fileName}` 19 | } 20 | -------------------------------------------------------------------------------- /.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 | - [ ] Code is formatted correctly (`npm run lint:fix`). 16 | - [ ] Any new functionality has been unit tested. 17 | - [ ] All unit tests are passing (`npm test`). 18 | - [ ] Unit tests coverage has been increased and a new threshold is set. 19 | - [ ] All CI checks are green. 20 | - [ ] Development comments have been added or updated. 21 | - [ ] Development documentation coverage has been increased and a new threshold is set. 22 | - [ ] Reviewer is assigned. 23 | 24 | ### Reviewer checks 25 | 26 | - [ ] Any new code is documented. 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "allowJs": true, 5 | "module": "commonjs", 6 | "downlevelIteration": true, 7 | "lib": ["ES2018", "DOM", "ES2019.String", "ES2019.Array"], 8 | "declaration": true, 9 | "outDir": "build", 10 | "rootDir": "src", 11 | "strict": true, 12 | "typeRoots": ["./node_modules/@types", "./src/types/system"], 13 | "esModuleInterop": true, 14 | "resolveJsonModule": true 15 | }, 16 | "exclude": [ 17 | "test/**/*", 18 | "node_modules", 19 | "src/utils/test.ts", 20 | "src/utils/fileStructures", 21 | "PULL_REQUEST_TEMPLATE.md", 22 | "test.sh", 23 | "jest.config.js", 24 | "babel.config.js", 25 | ".env*", 26 | ".prettierrc" 27 | ], 28 | "include": ["src/**/*"] 29 | } 30 | -------------------------------------------------------------------------------- /src/commands/web/internal/spec/updateSasjstag.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | 5 | import { ServerType, Target } from '@sasjs/utils' 6 | import { updateSasjsTag } from '../updateSasjsTag' 7 | 8 | describe('updateSasjsTag', () => { 9 | it(`should update links in js script`, async () => { 10 | const target = { 11 | serverType: ServerType.Sasjs, 12 | appLoc: '/public/app/test' 13 | } as any as Target 14 | 15 | const tag = document.createElement('sasjs') 16 | 17 | tag.setAttribute('appLoc', 'need/to/update') 18 | tag.setAttribute('serverType', 'sas') 19 | 20 | updateSasjsTag(tag, target) 21 | 22 | expect(tag.getAttribute('appLoc')).toEqual('/public/app/test') 23 | expect(tag.getAttribute('serverType')).toEqual(ServerType.Sasjs) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /src/commands/context/delete.ts: -------------------------------------------------------------------------------- 1 | import SASjs from '@sasjs/adapter/node' 2 | 3 | /** 4 | * Removes compute context. 5 | * @param {string} contextName - name of the context to delete. 6 | * @param {object} sasjs - configuration object of SAS adapter. 7 | * @param {string} accessToken - an access token for an authorized user. 8 | */ 9 | export async function deleteContext( 10 | contextName: string, 11 | sasjs: SASjs, 12 | accessToken: string 13 | ) { 14 | const deletedContext = await sasjs 15 | .deleteComputeContext(contextName, accessToken) 16 | .catch((err) => { 17 | process.logger?.error(`Error deleting context '${contextName}': `, err) 18 | throw err 19 | }) 20 | 21 | if (deletedContext) { 22 | process.logger?.success(`Context '${contextName}' has been deleted!`) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/commands/testing/spec/mockedAdapter/testResponses.ts: -------------------------------------------------------------------------------- 1 | export const testResponses: { [key: string]: any } = { 2 | 'dostuff.test.0': [ 3 | { 4 | TEST_DESCRIPTION: 'dostuff 0 test description', 5 | TEST_RESULT: 'FAIL' 6 | } 7 | ], 8 | 'dostuff.test.1': [ 9 | { 10 | TEST_DESCRIPTION: 'dostuff 1 test description', 11 | TEST_RESULT: 'PASS' 12 | } 13 | ], 14 | 'examplemacro.test': [ 15 | { 16 | TEST_DESCRIPTION: 'examplemacro test.1 description', 17 | TEST_RESULT: 'PASS' 18 | } 19 | ], 20 | 'notexisting.test': [ 21 | { 22 | TEST_DESCRIPTION: 'examplemacro test.1 description', 23 | TEST_RESULT: 'PASS' 24 | }, 25 | { 26 | TEST_DESCRIPTION: 'examplemacro test.1 description', 27 | TEST_RESULT: 'FAIL' 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /src/commands/flow/spec/sourceFiles/testFlow_6.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "myAmazingFlow", 3 | "flows": { 4 | "firstFlow": { 5 | "jobs": [ 6 | { 7 | "location": "jobs/testJob/job" 8 | }, 9 | { 10 | "location": "jobs/testJob/job" 11 | } 12 | ], 13 | "predecessors": [] 14 | }, 15 | "secondFlow": { 16 | "jobs": [ 17 | { 18 | "location": "jobs/testJob/failingJob" 19 | } 20 | ], 21 | "predecessors": ["firstFlow"] 22 | }, 23 | "thirdFlow": { 24 | "jobs": [ 25 | { 26 | "location": "jobs/testJob/job" 27 | }, 28 | { 29 | "location": "jobs/testJob/failingJob" 30 | } 31 | ], 32 | "predecessors": ["firstFlow", "secondFlow"] 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/commands/flow/internal/parseJobDetails.ts: -------------------------------------------------------------------------------- 1 | export const parseJobDetails = (response: any) => { 2 | if (!response) return 3 | 4 | let details = '' 5 | 6 | const concatDetails = (data: any, title: string) => { 7 | if (data) 8 | details = details.concat( 9 | details.length ? ' | ' : '', 10 | `${title}: ${Object.keys(data) 11 | .map((key) => `${key}: ${data[key]}`) 12 | .join('; ')}` 13 | ) 14 | } 15 | 16 | concatDetails(response.statistics, 'Statistics') 17 | concatDetails(response.listingStatistics, 'Listing Statistics') 18 | concatDetails(response.logStatistics, 'Log Statistics') 19 | 20 | let lineCount = 1000000 21 | 22 | if (response.logStatistics && response.logStatistics.lineCount) { 23 | lineCount = parseInt(response.logStatistics.lineCount) 24 | } 25 | 26 | return { details, lineCount } 27 | } 28 | -------------------------------------------------------------------------------- /src/commands/help/helpCommand.ts: -------------------------------------------------------------------------------- 1 | import { printHelpText } from './help' 2 | import { CommandExample, ReturnCode } from '../../types/command' 3 | import { CommandBase } from '../../types' 4 | 5 | const syntax = 'help' 6 | const usage = 'sasjs help' 7 | const description = 'Print SASjs CLI help text.' 8 | const examples: CommandExample[] = [ 9 | { 10 | command: 'sasjs help', 11 | description: '' 12 | } 13 | ] 14 | 15 | export class HelpCommand extends CommandBase { 16 | constructor(args: string[]) { 17 | super(args, { 18 | syntax, 19 | usage, 20 | description, 21 | examples 22 | }) 23 | } 24 | 25 | public async execute() { 26 | return await printHelpText() 27 | .then(() => { 28 | return ReturnCode.Success 29 | }) 30 | .catch(() => { 31 | return ReturnCode.InternalError 32 | }) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/commands/docs/internal/getFileOutputs.ts: -------------------------------------------------------------------------------- 1 | import { DependencyHeader, getList } from '@sasjs/utils' 2 | 3 | /** 4 | * Returns list of Outputs and populates Map of Nodes 5 | * @param {string} fileContent- Contents of the file from which Outputs need to extract 6 | * @param {Map} paramNodes- Map for params(Inputs/Outputs) 7 | */ 8 | export function getFileOutputs( 9 | fileContent: string, 10 | paramNodes: Map 11 | ) { 12 | const fileOutputs = getList(DependencyHeader.DataOutput, fileContent) 13 | .map((output) => output.toUpperCase()) 14 | .filter((output) => !output.endsWith('.DLL')) 15 | 16 | fileOutputs.forEach((outputParam) => { 17 | if (!paramNodes.has(outputParam)) 18 | paramNodes.set(outputParam, { 19 | edges: [], 20 | label: outputParam 21 | }) 22 | }) 23 | return fileOutputs 24 | } 25 | -------------------------------------------------------------------------------- /src/commands/compile/internal/identifySasFile.ts: -------------------------------------------------------------------------------- 1 | import { Target, isTestFile } from '@sasjs/utils' 2 | import { getAllFolders, SasFileType } from './' 3 | import path from 'path' 4 | 5 | export const identifySasFile = async ( 6 | target: Target, 7 | sourcePath: string 8 | ): Promise => { 9 | if (isTestFile(sourcePath.split(path.sep).pop() as string)) { 10 | return SasFileType.Test 11 | } 12 | 13 | const serviceFolders = await getAllFolders(target, SasFileType.Service) 14 | 15 | if (serviceFolders.find((folder) => sourcePath.includes(folder))) { 16 | return SasFileType.Service 17 | } 18 | 19 | const jobFolders = await getAllFolders(target, SasFileType.Job) 20 | 21 | if (jobFolders.find((folder) => sourcePath.includes(folder))) { 22 | return SasFileType.Job 23 | } 24 | 25 | throw `Unable to identify file as ${SasFileType.Service}, ${SasFileType.Job} or ${SasFileType.Test}` 26 | } 27 | -------------------------------------------------------------------------------- /src/utils/loadEnvVariables.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import dotenv from 'dotenv' 3 | import { fileExists } from '@sasjs/utils/file' 4 | 5 | export async function loadProjectEnvVariables() { 6 | await loadEnvVariables('.env') 7 | } 8 | 9 | export async function loadTargetEnvVariables(targetName: string) { 10 | await loadEnvVariables(`.env.${targetName}`) 11 | } 12 | 13 | async function loadEnvVariables(fileName: string) { 14 | const envFileExistsInCurrentPath = await fileExists( 15 | path.join(process.cwd(), fileName) 16 | ) 17 | const envFileExistsInParentPath = await fileExists( 18 | path.join(process.cwd(), '..', fileName) 19 | ) 20 | const envFilePath = envFileExistsInCurrentPath 21 | ? path.join(process.cwd(), fileName) 22 | : envFileExistsInParentPath 23 | ? path.join(process.cwd(), '..', fileName) 24 | : null 25 | if (envFilePath) { 26 | dotenv.config({ path: envFilePath }) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/commands/flow/internal/saveToCsv.ts: -------------------------------------------------------------------------------- 1 | import { updateCsv } from '@sasjs/utils' 2 | import { displayError } from '../../../utils/displayResult' 3 | 4 | export const saveToCsv = async ( 5 | csvFileRealPath: string, 6 | data: any[], 7 | columns: string[], 8 | prependId?: string 9 | ) => { 10 | return new Promise(async (resolve, reject) => { 11 | if (process.csvFileAbleToSave === undefined) 12 | process.csvFileAbleToSave = true 13 | 14 | const timerId = setInterval(async () => { 15 | if (process.csvFileAbleToSave) { 16 | process.csvFileAbleToSave = false 17 | clearInterval(timerId) 18 | 19 | try { 20 | await updateCsv(csvFileRealPath, data, columns, prependId) 21 | process.csvFileAbleToSave = true 22 | resolve(true) 23 | } catch (error) { 24 | displayError(error) 25 | reject(error) 26 | } 27 | } 28 | }, 100) 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /src/commands/db/spec/db.spec.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { createTestApp, removeTestApp, verifyStep } from '../../../utils/test' 3 | import { deleteFolder, generateTimestamp } from '@sasjs/utils' 4 | import { buildDB } from '../db' 5 | 6 | describe(`sasjs db`, () => { 7 | const timestamp = generateTimestamp() 8 | const appName = `test-app-DB-${timestamp}` 9 | 10 | beforeAll(async () => { 11 | await createTestApp(__dirname, appName) 12 | }) 13 | 14 | afterAll(async () => { 15 | await removeTestApp(__dirname, appName) 16 | }) 17 | 18 | it(`should create db folder`, async () => { 19 | await expect(buildDB()).toResolve() 20 | 21 | await verifyStep('db') 22 | }) 23 | 24 | it(`should throw an error when the db folder does not exist`, async () => { 25 | await deleteFolder(path.join(__dirname, appName, 'sasjs', 'db')) 26 | 27 | await expect(buildDB()).rejects.toThrow('no such file or directory') 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /src/commands/folder/list.ts: -------------------------------------------------------------------------------- 1 | import { displaySuccess, displayError } from '../../utils/displayResult' 2 | import chalk from 'chalk' 3 | import SASjs from '@sasjs/adapter/node' 4 | 5 | /** 6 | * Lists folder children 7 | * @param {string} path - folder path. 8 | * @param {object} sasjs - configuration object of SAS adapter. 9 | * @param {string} accessToken - an access token for an authorized user. 10 | */ 11 | export const list = async ( 12 | path: string, 13 | sasjs: SASjs, 14 | accessToken: string 15 | ): Promise => { 16 | const sourceFolder = path 17 | 18 | const folderList = await sasjs 19 | .listFolder(sourceFolder, accessToken, 10000) 20 | .catch((err: any) => { 21 | throw err 22 | }) 23 | 24 | if (folderList) { 25 | const folderFormattedList = folderList.join(' ') 26 | process.logger?.success(folderFormattedList) 27 | 28 | return Promise.resolve(folderFormattedList) 29 | } 30 | 31 | return Promise.reject() 32 | } 33 | -------------------------------------------------------------------------------- /src/utils/fileStructures/builtFiles.ts: -------------------------------------------------------------------------------- 1 | import { Folder } from '../../types' 2 | 3 | export const builtFiles = (buildFileName: string): Folder => { 4 | return { 5 | folderName: 'sasjsbuild', 6 | files: [ 7 | { fileName: `${buildFileName}.sas` }, 8 | { fileName: `${buildFileName}.json` }, 9 | { fileName: `${buildFileName}.json.zip` } 10 | ], 11 | subFolders: [ 12 | { 13 | folderName: 'services', 14 | files: [], 15 | subFolders: [ 16 | { 17 | folderName: 'admin', 18 | files: [{ fileName: 'dostuff.sas' }], 19 | subFolders: [] 20 | }, 21 | { 22 | folderName: 'common', 23 | files: [ 24 | { fileName: 'appinit.sas' }, 25 | { fileName: 'example.sas' }, 26 | { fileName: 'getdata.sas' } 27 | ], 28 | subFolders: [] 29 | } 30 | ] 31 | } 32 | ] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/commands/docs/spec/initDocs.spec.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | import { createTestApp, removeTestApp } from '../../../utils/test' 4 | import { folderExists, deleteFolder, generateTimestamp } from '@sasjs/utils' 5 | import { initDocs } from '../initDocs' 6 | 7 | describe('sasjs doc', () => { 8 | let appName: string 9 | 10 | afterEach(async () => { 11 | await removeTestApp(__dirname, appName) 12 | }) 13 | 14 | it( 15 | `should create 'doxy' folder in 'sasjs'`, 16 | async () => { 17 | appName = `test-app-doc-${generateTimestamp()}` 18 | const doxypath = path.join(__dirname, appName, 'sasjs', 'doxy') 19 | 20 | await createTestApp(__dirname, appName) 21 | 22 | await deleteFolder(doxypath) 23 | await expect(folderExists(doxypath)).resolves.toEqual(false) 24 | 25 | await expect(initDocs()).resolves.not.toThrow() 26 | 27 | await expect(folderExists(doxypath)).resolves.toEqual(true) 28 | }, 29 | 60 * 1000 30 | ) 31 | }) 32 | -------------------------------------------------------------------------------- /src/utils/parseSourceFile.ts: -------------------------------------------------------------------------------- 1 | import { fileExists, isMacroVars, MacroVars, readFile } from '@sasjs/utils' 2 | import path from 'path' 3 | import { isJsonFile } from './file' 4 | 5 | export async function parseSourceFile(source: string): Promise { 6 | let macroVars 7 | 8 | const currentDirPath = path.isAbsolute(source) ? '' : process.projectDir 9 | source = path.join(currentDirPath, source) 10 | 11 | if (!isJsonFile(source)) throw 'Source file has to be JSON.' 12 | 13 | await fileExists(source).catch((_) => { 14 | throw 'Error while checking if source file exists.' 15 | }) 16 | 17 | source = await readFile(source).catch((_) => { 18 | throw 'Error while reading source file.' 19 | }) 20 | 21 | macroVars = JSON.parse(source as string) as MacroVars 22 | 23 | if (!isMacroVars(macroVars)) { 24 | throw `Provided source is not valid. An example of valid source: 25 | { macroVars: { name1: 'value1', name2: 'value2' } }` 26 | } 27 | 28 | return macroVars 29 | } 30 | -------------------------------------------------------------------------------- /src/utils/fileStructures/builtFilesCustom1.ts: -------------------------------------------------------------------------------- 1 | import { Folder } from '../../types' 2 | 3 | export const builtFilesCustom1 = (buildFileName: string): Folder => { 4 | return { 5 | folderName: 'sasjsbuild', 6 | files: [ 7 | { fileName: `${buildFileName}.sas` }, 8 | { fileName: `${buildFileName}.json` }, 9 | { fileName: `${buildFileName}.json.zip` } 10 | ], 11 | subFolders: [ 12 | { 13 | folderName: 'services', 14 | files: [], 15 | subFolders: [ 16 | { 17 | folderName: 'admin', 18 | files: [{ fileName: 'dostuff.sas' }], 19 | subFolders: [] 20 | }, 21 | { 22 | folderName: 'common', 23 | files: [ 24 | { fileName: 'appinit.sas' }, 25 | { fileName: 'example.sas' }, 26 | { fileName: 'get.sasdata.sas' } 27 | ], 28 | subFolders: [] 29 | } 30 | ] 31 | } 32 | ] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.github/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | Since March 7th, 2020, the changelog is managed by github releases. See [https://github.com/sasjs/cli/releases](https://github.com/sasjs/cli/releases). 4 | 5 | ## Changes Prior to March 7th 2020 6 | 7 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 8 | 9 | 10 | ## [1.0.4](https://gitlab.com/macropeople/sasjs-cli/compare/v1.0.3...v1.0.4) (2020-03-07) 11 | 12 | 13 | ### Bug Fixes 14 | 15 | * redo comment stripping logic ([84a1a91](https://gitlab.com/macropeople/sasjs-cli/commit/84a1a91)) 16 | 17 | 18 | 19 | 20 | ## [1.0.3](https://gitlab.com/macropeople/sasjs-cli/compare/v1.0.2...v1.0.3) (2020-03-07) 21 | 22 | 23 | 24 | 25 | ## [1.0.2](https://gitlab.com/macropeople/sasjs-cli/compare/v1.0.1...v1.0.2) (2020-03-07) 26 | 27 | 28 | 29 | 30 | ## 1.0.1 (2020-03-07) 31 | -------------------------------------------------------------------------------- /src/doxy/new_footer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | 22 | 23 | 24 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 (Allan Bowe) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /src/commands/compile/internal/getDestinationPath.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | export const getDestinationServicePath = (inputPath: string): string => { 4 | if (!inputPath) { 5 | throw new Error( 6 | 'Cannot get leaf folder name: input path is empty, null or undefined.' 7 | ) 8 | } 9 | const { buildDestinationServicesFolder } = process.sasjsConstants 10 | 11 | const inputPathParts = inputPath.split(path.sep) 12 | const leafFolderName = inputPathParts.pop() as string 13 | return path.join(buildDestinationServicesFolder, leafFolderName) 14 | } 15 | 16 | export const getDestinationJobPath = (inputPath: string): string => { 17 | if (!inputPath) { 18 | throw new Error( 19 | 'Cannot get leaf folder name: input path is empty, null or undefined.' 20 | ) 21 | } 22 | const { buildDestinationJobsFolder } = process.sasjsConstants 23 | 24 | const inputPathParts = inputPath.split(path.sep) 25 | const leafFolderName = inputPathParts.pop() as string 26 | return path.join(buildDestinationJobsFolder, leafFolderName) 27 | } 28 | -------------------------------------------------------------------------------- /src/commands/version/versionCommand.ts: -------------------------------------------------------------------------------- 1 | import { CommandExample, ReturnCode } from '../../types/command' 2 | import { TargetCommand } from '../../types/command/targetCommand' 3 | import { displayError } from '../../utils' 4 | import { printVersion } from './version' 5 | 6 | const syntax = 'version' 7 | const aliases = ['v'] 8 | const usage = 'Usage: sasjs version' 9 | const description = 'displays currently installed version.' 10 | const examples: CommandExample[] = [ 11 | { command: 'sasjs version', description }, 12 | { command: 'sasjs v', description } 13 | ] 14 | 15 | export class VersionCommand extends TargetCommand { 16 | constructor(args: string[]) { 17 | super(args, { syntax, usage, description, examples, aliases }) 18 | } 19 | 20 | public async execute() { 21 | return await printVersion() 22 | .then(async () => { 23 | return ReturnCode.Success 24 | }) 25 | .catch((err) => { 26 | displayError(err, 'An error has occurred while checking version.') 27 | return ReturnCode.InternalError 28 | }) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/commands/web/internal/updateLinkTag.ts: -------------------------------------------------------------------------------- 1 | import { AssetPathMap } from './createAssetServices' 2 | 3 | export const updateLinkTag = ( 4 | linkTag: HTMLLinkElement | HTMLSourceElement | HTMLImageElement, 5 | assetPathMap: AssetPathMap[], 6 | tagType: 'link' | 'src' = 'link' 7 | ) => { 8 | const linkSourcePath = 9 | tagType === 'link' 10 | ? linkTag.getAttribute('href') 11 | : linkTag.getAttribute('src') 12 | 13 | if (!linkSourcePath) return 14 | 15 | const isUrl = 16 | linkSourcePath.startsWith('http') || linkSourcePath.startsWith('//') 17 | if (isUrl) return 18 | 19 | const assetPath = assetPathMap.find( 20 | (entry) => 21 | entry.source === linkSourcePath || 22 | `./${entry.source}` === linkSourcePath || 23 | `/${entry.source}` === linkSourcePath 24 | ) 25 | 26 | if (!assetPath?.target) { 27 | throw new Error(`Unable to find file: ${linkSourcePath}`) 28 | } 29 | 30 | tagType === 'link' 31 | ? linkTag.setAttribute('href', assetPath.target) 32 | : linkTag.setAttribute('src', assetPath.target) 33 | } 34 | -------------------------------------------------------------------------------- /src/commands/index.ts: -------------------------------------------------------------------------------- 1 | export { addTarget } from './add/addTarget' 2 | export { addCredential } from './add/addCredential' 3 | 4 | export { compile } from './compile/compile' 5 | export { compileSingleFile } from './compile/compileSingleFile' 6 | 7 | export { build, getBuildVars } from './build/build' 8 | 9 | export { init } from './init/init' 10 | 11 | export { create } from './create/create' 12 | 13 | export { generateDocs } from './docs/generateDocs' 14 | export { generateDot } from './docs/generateDot' 15 | export { initDocs } from './docs/initDocs' 16 | 17 | export { buildDB } from './db/db' 18 | 19 | export { deploy } from './deploy/deploy' 20 | 21 | export { printHelpText } from './help/help' 22 | 23 | export { runSasJob } from './request/request' 24 | 25 | export { runSasCode } from './run/run' 26 | 27 | export { printVersion } from './version/version' 28 | 29 | export { createWebAppServices } from './web/web' 30 | 31 | export { runTest } from './testing/test' 32 | 33 | export { processLint } from './lint/processLint' 34 | 35 | export { initLint } from './lint/initLint' 36 | -------------------------------------------------------------------------------- /src/commands/db/dbCommand.ts: -------------------------------------------------------------------------------- 1 | import { buildDB } from './db' 2 | import { CommandExample, ReturnCode } from '../../types/command' 3 | import { CommandBase } from '../../types' 4 | 5 | const syntax = 'db' 6 | const aliases = ['build-DB', 'build-db'] 7 | const usage = 'sasjs db' 8 | const description = 9 | 'Concatenates the DDL and SAS files within the `sasjs/db` directory.' 10 | const examples: CommandExample[] = [ 11 | { 12 | command: 'sasjs db', 13 | description: '' 14 | } 15 | ] 16 | 17 | export class DbCommand extends CommandBase { 18 | constructor(args: string[]) { 19 | super(args, { 20 | syntax, 21 | aliases, 22 | usage, 23 | description, 24 | examples 25 | }) 26 | } 27 | 28 | public async execute() { 29 | return await buildDB() 30 | .then(() => { 31 | process.logger?.success('DB build completed!') 32 | return ReturnCode.Success 33 | }) 34 | .catch((err) => { 35 | process.logger?.error('Error building DB: ', err) 36 | return ReturnCode.InternalError 37 | }) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | As an Enterprise tool, security is taken seriously by the SASjs team. In general we look to minimise third party dependencies, and we distinguish between production dependencies and development dependencies whenever possible. 4 | 5 | In addition we take the following steps: 6 | 7 | - Use of Dependabot for automated reporting of security issues. 8 | - Locking versions to prevent upgrades unless explicitly applied. 9 | - We run `npm run audit` as part of the CI build to ensure the dependency tree is clear from warnings. At the moment we ignore the reported Cross-Site Request Forgery vulnerability in Axios because it is mainly related to the browsers and it does not apply to @sasjs/cli. 10 | 11 | ## Supported Versions 12 | 13 | We support only the latest version with security updates. If you would like an earlier version supported, then do [get in touch](https://sasapps.io/contact-us). 14 | 15 | ## Reporting a Vulnerability 16 | 17 | We welcome reponsible disclosures and will act immediately. Please report [here](https://sasapps.io/contact-us) with full details of the vulnerability. 18 | -------------------------------------------------------------------------------- /src/commands/context/edit.ts: -------------------------------------------------------------------------------- 1 | import SASjs from '@sasjs/adapter/node' 2 | import { displayError, displaySuccess } from '../../utils/displayResult' 3 | 4 | /** 5 | * Edits existing compute context. 6 | * @param {string} configName - name of the config to edit. 7 | * @param {object} config - context configuration. 8 | * @param {object} sasjs - configuration object of SAS adapter. 9 | * @param {string} accessToken - an access token for an authorized user. 10 | */ 11 | export async function edit( 12 | configName: string | null, 13 | config: any, 14 | sasjs: SASjs, 15 | accessToken: string 16 | ) { 17 | const name = configName || config.name 18 | 19 | delete config.id 20 | 21 | const editedContext = await sasjs 22 | .editComputeContext(name, config, accessToken) 23 | .catch((err) => { 24 | process.logger?.error('Error editing context: ', err) 25 | throw err 26 | }) 27 | 28 | if (editedContext) { 29 | const editedContextName = editedContext.result.name || '' 30 | 31 | process.logger?.success( 32 | `Context '${editedContextName}' successfully updated!` 33 | ) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/commands/web/internal/sas9/generateAssetService.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { base64EncodeFile, createFile } from '@sasjs/utils' 3 | import { getWebServiceContent } from './' 4 | 5 | /** 6 | * Creates service file for SAS9 server only. 7 | * @param {string} sourcePath path of source file. 8 | * @param {string} destinationPath path of destination service file. 9 | * @returns {string} name of created service file. 10 | */ 11 | export const generateAssetService = async ( 12 | sourcePath: string, 13 | destinationPath: string 14 | ): Promise => { 15 | const fileExtension = path.extname(sourcePath) 16 | const fileType = fileExtension.replace('.', '').toUpperCase() 17 | const fileName = path 18 | .basename(sourcePath) 19 | .replace(new RegExp(fileExtension + '$'), fileExtension.replace('.', '-')) 20 | const base64string = await base64EncodeFile(sourcePath) 21 | 22 | const serviceContent = await getWebServiceContent(base64string, fileType) 23 | 24 | await createFile( 25 | path.join(destinationPath, `${fileName}.sas`), 26 | serviceContent 27 | ) 28 | 29 | return `${fileName}.sas` 30 | } 31 | -------------------------------------------------------------------------------- /src/commands/create/spec/minimalAppFiles.ts: -------------------------------------------------------------------------------- 1 | import { Folder } from '../../../types' 2 | 3 | export const minimalAppFiles: Folder = { 4 | folderName: '', 5 | files: [ 6 | { fileName: 'package.json' }, 7 | { fileName: 'package-lock.json' }, 8 | { fileName: 'README.md' } 9 | ], 10 | subFolders: [ 11 | { 12 | folderName: 'sasjs', 13 | files: [{ fileName: 'sasjsconfig.json' }], 14 | subFolders: [ 15 | { 16 | folderName: 'macros', 17 | files: [], 18 | subFolders: [] 19 | }, 20 | { 21 | folderName: 'services', 22 | files: [], 23 | subFolders: [ 24 | { 25 | folderName: 'common', 26 | files: [{ fileName: 'appinit.sas' }, { fileName: 'getdata.sas' }], 27 | subFolders: [] 28 | } 29 | ] 30 | } 31 | ] 32 | }, 33 | { 34 | folderName: 'src', 35 | files: [ 36 | { fileName: 'index.html' }, 37 | { fileName: 'scripts.js' }, 38 | { fileName: 'style.css' } 39 | ], 40 | subFolders: [] 41 | } 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /src/commands/folder/move.ts: -------------------------------------------------------------------------------- 1 | import SASjs from '@sasjs/adapter/node' 2 | import { displayError, displaySuccess } from '../../utils/displayResult' 3 | 4 | /** 5 | * Moves folder to a new location. 6 | * @param {string} paths - folder paths (source path and destination path separated by space). 7 | * @param {object} sasjs - configuration object of SAS adapter. 8 | * @param {string} accessToken - an access token for an authorized user. 9 | */ 10 | export const move = async ( 11 | sourcePath: string, 12 | destinationPath: string, 13 | sasjs: SASjs, 14 | accessToken: string 15 | ) => { 16 | const sourceFolder = sourcePath 17 | let targetFolder = destinationPath 18 | const targetFolderName = targetFolder.split('/').pop() as string 19 | 20 | const movedFolder = await sasjs 21 | .moveFolder(sourceFolder, targetFolder, targetFolderName, accessToken) 22 | .catch((err: any) => { 23 | displayError(err, `An error occurred when moving folder ${sourceFolder}.`) 24 | }) 25 | 26 | if (movedFolder) { 27 | displaySuccess( 28 | `Folder successfully moved from '${sourceFolder}' to '${targetFolder}'.` 29 | ) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/commands/init/spec/initFiles.ts: -------------------------------------------------------------------------------- 1 | import { Folder } from '../../../types' 2 | 3 | export const initFiles: Folder = { 4 | folderName: '', 5 | files: [ 6 | { fileName: 'package.json' }, 7 | { fileName: 'package-lock.json' }, 8 | { fileName: '.gitignore' } 9 | ], 10 | subFolders: [ 11 | { 12 | folderName: 'sasjs', 13 | files: [{ fileName: 'sasjsconfig.json' }], 14 | subFolders: [ 15 | { 16 | folderName: 'doxy', 17 | files: [ 18 | { fileName: 'Doxyfile' }, 19 | { fileName: 'DoxygenLayout.xml' }, 20 | { fileName: 'favicon.ico' }, 21 | { fileName: 'new_footer.html' }, 22 | { fileName: 'new_header.html' }, 23 | { fileName: 'new_stylesheet.css' }, 24 | { fileName: 'logo.png' } 25 | ], 26 | subFolders: [] 27 | } 28 | ] 29 | }, 30 | { 31 | folderName: 'node_modules', 32 | files: [], 33 | subFolders: [ 34 | { 35 | folderName: '@sasjs', 36 | files: [], 37 | subFolders: [] 38 | } 39 | ] 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /src/commands/request/spec/utils.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { readFile } from '@sasjs/utils' 3 | 4 | export const sampleDataJson = { 5 | table1: [ 6 | { col1: 'first col value1', col2: 'second col value1' }, 7 | { col1: 'first col value2', col2: 'second col value2' } 8 | ], 9 | table2: [{ col1: 'first col value' }] 10 | } 11 | export const expectedDataArr = { 12 | table1: [ 13 | ['first col value1', 'second col value1'], 14 | ['first col value2', 'second col value2'] 15 | ], 16 | table2: [['first col value']] 17 | } 18 | export const expectedDataObj = { 19 | table1: [ 20 | { 21 | COL1: 'first col value1', 22 | COL2: 'second col value1' 23 | }, 24 | { 25 | COL1: 'first col value2', 26 | COL2: 'second col value2' 27 | } 28 | ], 29 | table2: [ 30 | { 31 | COL1: 'first col value' 32 | } 33 | ] 34 | } 35 | 36 | export const getOutputJson = async (fileName: string) => 37 | JSON.parse( 38 | await readFile( 39 | path.join( 40 | process.sasjsConstants.buildDestinationResultsFolder, 41 | 'requests', 42 | fileName 43 | ) 44 | ) 45 | ) 46 | -------------------------------------------------------------------------------- /src/commands/docs/internal/getFileInputs.ts: -------------------------------------------------------------------------------- 1 | import { getList, DependencyHeader } from '@sasjs/utils' 2 | 3 | /** 4 | * Returns list of Inputs and populates Map of Nodes 5 | * @param {string} fileName- Name of the file from which Inputs need to extract 6 | * @param {string} fileContent- Contents of the file from which Inputs need to extract 7 | * @param {Map} paramNodes- Map for params(Inputs/Outputs) 8 | */ 9 | export function getFileInputs( 10 | fileName: string, 11 | fileContent: string, 12 | paramNodes: Map 13 | ) { 14 | const fileInputs = getList(DependencyHeader.DataInput, fileContent) 15 | .map((input) => input.toUpperCase()) 16 | .filter((input) => !input.endsWith('.DLL')) 17 | 18 | fileInputs.forEach((inputParam) => { 19 | inputParam = inputParam.toUpperCase() 20 | const paramNode = paramNodes.get(inputParam) 21 | if (paramNode) { 22 | paramNode.edges.push(fileName) 23 | paramNodes.set(inputParam, paramNode) 24 | } else 25 | paramNodes.set(inputParam, { 26 | edges: [fileName], 27 | label: inputParam 28 | }) 29 | }) 30 | 31 | return fileInputs 32 | } 33 | -------------------------------------------------------------------------------- /src/commands/flow/internal/checkPredecessorDeadlock.ts: -------------------------------------------------------------------------------- 1 | import { FlowWave } from '../../../types' 2 | 3 | interface PredecessorDeadlockChain { 4 | chain?: string[] 5 | present: boolean 6 | } 7 | 8 | export const checkPredecessorDeadlock = (flows: { 9 | [key: string]: FlowWave 10 | }): PredecessorDeadlockChain => { 11 | for (const flowName in flows) { 12 | const result = checkPredecessorDeadlockRecursive(flows, flowName, []) 13 | if (result.present) return result 14 | } 15 | 16 | return { present: false } 17 | } 18 | 19 | const checkPredecessorDeadlockRecursive = ( 20 | flows: { [key: string]: FlowWave }, 21 | flowName: string, 22 | chain: string[] 23 | ): PredecessorDeadlockChain => { 24 | if (chain.includes(flowName)) 25 | return { chain: [...chain, flowName], present: true } 26 | 27 | const predecessors = flows[flowName]?.predecessors 28 | if (predecessors) 29 | for (const predecessorName of predecessors) { 30 | const result = checkPredecessorDeadlockRecursive(flows, predecessorName, [ 31 | ...chain, 32 | flowName 33 | ]) 34 | if (result.present) return result 35 | } 36 | return { present: false } 37 | } 38 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | process_result() { 5 | success_message=$'\e[32mSuccess!' 6 | failure_message=$'\e[91mTest failed.' 7 | 8 | if [ $1 -eq 0 ] 9 | then 10 | echo $success_message 11 | else 12 | echo $failure_message 13 | exit 1 14 | fi 15 | } 16 | 17 | echo $'\e[34mSASjs Version' 18 | sasjs v 19 | process_result $? 20 | 21 | echo $'\e[34mSASjs Create' 22 | sasjs create test1 23 | process_result $? 24 | 25 | echo $'\e[34mSASjs Create Minimal App' 26 | sasjs create test2 -t minimal 27 | process_result $? 28 | 29 | echo $'\e[34mSASjs Create React App' 30 | sasjs create test3 -t react 31 | process_result $? 32 | 33 | echo $'\e[34mSASjs Create Angular App' 34 | sasjs create test4 -t angular 35 | process_result $? 36 | 37 | echo $'\e[34mSASjs Create SAS Only App' 38 | sasjs create test5 -t sasonly 39 | process_result $? 40 | 41 | # Turning off this test until the seed apps have been migrated to the new target format 42 | # echo $'\e[34mSASjs Compile Build' 43 | # cd test5 44 | # sasjs cb 45 | # process_result $? 46 | # cd - 47 | 48 | echo $'\e[34mCleaning up...' 49 | rm -rf test1 50 | rm -rf test2 51 | rm -rf test3 52 | rm -rf test4 53 | rm -rf test5 54 | -------------------------------------------------------------------------------- /src/commands/context/create.ts: -------------------------------------------------------------------------------- 1 | import SASjs from '@sasjs/adapter/node' 2 | 3 | /** 4 | * Creates compute context using provided config. 5 | * @param {object} config - context configuration. 6 | * @param {object} sasjs - configuration object of SAS adapter. 7 | * @param {string} accessToken - an access token for an authorized user. 8 | */ 9 | export async function create(config: any, sasjs: SASjs, accessToken: string) { 10 | const { name } = config 11 | const launchName = config.launchContext && config.launchContext.contextName 12 | const autoExecLines = config.environment && config.environment.autoExecLines 13 | const sharedAccountId = config.attributes && config.attributes.runServerAs 14 | 15 | const createdContext = await sasjs 16 | .createComputeContext( 17 | name, 18 | launchName, 19 | sharedAccountId, 20 | autoExecLines, 21 | accessToken 22 | ) 23 | .catch((err) => { 24 | process.logger?.error('Error creating context: ', err) 25 | throw err 26 | }) 27 | 28 | if (createdContext) { 29 | process.logger?.success( 30 | `Context '${name}' with id '${createdContext.id}' successfully created!` 31 | ) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/commands/docs/internal/populateParamNodeTypes.ts: -------------------------------------------------------------------------------- 1 | const CRAYONS = [ 2 | '#e6194b', // red 3 | '#3cb44b', // green 4 | '#4363d8', // blue 5 | '#f58231', // orange 6 | '#911eb4', // purple 7 | '#46f0f0', // cyan 8 | '#f032e6', // magenta 9 | '#bcf60c', // lime 10 | '#fabebe', // pink 11 | '#008080', // teal 12 | '#e6beff', // lavender 13 | '#9a6324', // brown 14 | '#fffac8', // beige 15 | '#800000', // maroon 16 | '#aaffc3', // mint 17 | '#808000', // olive 18 | '#ffd8b1', // apricot 19 | '#000075', // navy 20 | '#808080', // gray 21 | '#ffe119', // yellow 22 | '#ffffff' // white 23 | ] 24 | 25 | /** 26 | * Populates Types of Nodes Map for param (Inputs/Outputs) 27 | * @param {Map} paramNodeTypes- Map for param Nodes having colors 28 | * @param {Map} nodes- Map for params(Inputs/Outputs) Or files 29 | */ 30 | export function populateParamNodeTypes( 31 | paramNodeTypes: Map, 32 | nodes: Map 33 | ) { 34 | nodes.forEach((node, key) => { 35 | const librefFound = key.match(/^[A-Z]{2,5}\./) 36 | if (librefFound && !paramNodeTypes.has(librefFound[0])) 37 | paramNodeTypes.set(librefFound[0], CRAYONS[paramNodeTypes.size]) 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /src/commands/init/initCommand.ts: -------------------------------------------------------------------------------- 1 | import { init } from './init' 2 | import { CommandExample, ReturnCode } from '../../types/command' 3 | import { CommandBase } from '../../types' 4 | 5 | const syntax = 'init' 6 | const usage = 'sasjs init' 7 | const description = 8 | 'Adds SASjs to an existing project located in the current working directory.' 9 | const examples: CommandExample[] = [ 10 | { 11 | command: 'sasjs init', 12 | description: '' 13 | } 14 | ] 15 | 16 | export class InitCommand extends CommandBase { 17 | constructor(args: string[]) { 18 | super(args, { 19 | syntax, 20 | usage, 21 | description, 22 | examples 23 | }) 24 | } 25 | 26 | public async execute() { 27 | return await init() 28 | .then(() => { 29 | process.logger?.success( 30 | 'This project is now powered by SASjs!\nYou can use any sasjs command within the project.\n\nFor more information, type `sasjs help` or visit https://cli.sasjs.io/' 31 | ) 32 | return ReturnCode.Success 33 | }) 34 | .catch((err: any) => { 35 | process.logger?.error('Error initialising SASjs: ', err) 36 | return ReturnCode.InternalError 37 | }) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/commands/shared/fetchLogFileContent.ts: -------------------------------------------------------------------------------- 1 | import SASjs from '@sasjs/adapter/node' 2 | import { LogJson } from '../../types' 3 | 4 | /** 5 | * Fetches content of the log file 6 | * @param {object} sasjs - configuration object of SAS adapter. 7 | * @param {string} accessToken - an access token for an authorized user. 8 | * @param {string} logUrl - url of the log file. 9 | * @param {number} logCount- total number of log lines in file. 10 | * @returns an object containing log lines in 'items' array. 11 | */ 12 | export const fetchLogFileContent = async ( 13 | sasjs: SASjs, 14 | accessToken: string, 15 | logUrl: string, 16 | logCount: number 17 | ): Promise => { 18 | const logJson: LogJson = { items: [] } 19 | 20 | const loglimit = logCount < 10000 ? logCount : 10000 21 | let start = 0 22 | do { 23 | const logChunkJson = JSON.parse( 24 | (await sasjs.fetchLogFileContent( 25 | `${logUrl}?start=${start}&limit=${loglimit}`, 26 | accessToken 27 | )) as string 28 | ) 29 | 30 | if (logChunkJson.items.length === 0) break 31 | 32 | logJson.items = [...logJson.items, ...logChunkJson.items] 33 | 34 | start += loglimit 35 | } while (start < logCount) 36 | return logJson 37 | } 38 | -------------------------------------------------------------------------------- /src/commands/shared/createFileStructure.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { asyncForEach, fileExists } from '@sasjs/utils' 3 | import { getFolders } from '../../utils/config' 4 | import { createFolderStructure } from '../../utils/file' 5 | import { Folder } from '../../types' 6 | import { createConfigFile } from './createConfigFile' 7 | 8 | /** 9 | * Creates the folder structure specified in config.json 10 | * Also creates a SASjs configuration file, named 'sasjsconfig.json'. 11 | * @param {string} parentFolderName- the name of the project folder. 12 | */ 13 | export const createFileStructure = async (parentFolderName: string) => { 14 | const fileStructure = await getFolders() 15 | await asyncForEach(fileStructure, async (folder: Folder, index: number) => { 16 | const pathExists = await fileExists( 17 | path.join(process.projectDir, parentFolderName, folder.folderName) 18 | ) 19 | if (pathExists) { 20 | throw new Error( 21 | `The folder ${folder.folderName} already exists! Please remove any unnecessary files and try again.` 22 | ) 23 | } 24 | await createFolderStructure(folder, parentFolderName) 25 | if (index === 0) { 26 | await createConfigFile(parentFolderName) 27 | } 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /src/commands/help/spec/help.spec.ts: -------------------------------------------------------------------------------- 1 | import { printHelpText } from '../help' 2 | import { sanitizeSpecialChars } from '@sasjs/utils/formatter' 3 | import { getAllSupportedCommands } from '../../../types/command/commandFactory' 4 | import { getAllSupportedAliases } from '../../../types/command/commandAliases' 5 | 6 | describe('sasjs help', () => { 7 | describe('printHelpText', () => { 8 | it('should output information about all supported commands', async () => { 9 | const supportedCommands = getAllSupportedCommands() 10 | 11 | let { outputCommands } = await printHelpText() 12 | 13 | outputCommands = sanitizeSpecialChars(outputCommands) 14 | 15 | for (const command of supportedCommands) { 16 | expect(outputCommands.includes(`* ${command}`)).toEqual(true) 17 | } 18 | }) 19 | 20 | it('should output information about all supported aliases', async () => { 21 | const supportedAliases = getAllSupportedAliases() 22 | 23 | let { outputAliases } = await printHelpText() 24 | 25 | outputAliases = sanitizeSpecialChars(outputAliases) 26 | 27 | for (const command of supportedAliases) { 28 | expect(outputAliases.includes(`* ${command}`)).toEqual(true) 29 | } 30 | }) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /src/commands/docs/spec/getDocConfig.spec.ts: -------------------------------------------------------------------------------- 1 | import { Configuration, Target } from '@sasjs/utils' 2 | import { Constants } from '../../../constants' 3 | import { getDocConfig } from '../internal/getDocConfig' 4 | 5 | const UNCErrorMessage = `UNC paths are not supported. 6 | Please map to a network drive, or migrate the project to an existing path (with a drive letter).` 7 | 8 | describe('getDocConfig', () => { 9 | let sasjsConstants: Constants 10 | 11 | beforeAll(() => { 12 | ;({ sasjsConstants } = process) 13 | process.sasjsConstants = {} as Constants 14 | }) 15 | 16 | afterAll(() => { 17 | process.sasjsConstants = sasjsConstants 18 | }) 19 | 20 | it('should throw for having UNC path picked up from target', () => { 21 | const target = { 22 | docConfig: { doxyContent: { path: '//server1/unc/path' } } 23 | } as unknown as Target 24 | 25 | expect(() => getDocConfig(target)).toThrowError(UNCErrorMessage) 26 | }) 27 | 28 | it('should throw for having UNC path picked up from config', () => { 29 | const config = { 30 | docConfig: { doxyContent: { path: '//server1/unc/path' } } 31 | } as unknown as Configuration 32 | 33 | expect(() => getDocConfig(undefined, config)).toThrowError(UNCErrorMessage) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /src/commands/flow/internal/examples.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | command: `Command example: sasjs flow execute --source /local/flow.json --logFolder /local/log/folder --csvFile /local/some.csv --target targetName`, 3 | source: `Source file is not valid. Source file example: 4 | { 5 | "name": "myAmazingFlow", 6 | "flows": { 7 | "firstFlow": { 8 | "jobs": [ 9 | { 10 | "location": "/Projects/job1" 11 | }, 12 | { 13 | "location": "/Projects/job2" 14 | }, 15 | { 16 | "location": "/Projects/job3" 17 | } 18 | ], 19 | "predecessors": [] 20 | }, 21 | "secondFlow": { 22 | "jobs": [ 23 | { 24 | "location": "/Projects/job11" 25 | } 26 | ], 27 | "predecessors": [ 28 | "firstFlow" 29 | ] 30 | }, 31 | "anotherFlow": { 32 | "jobs": [ 33 | { 34 | "location": "/Public/job15" 35 | } 36 | ], 37 | "predecessors": [ 38 | "firstFlow", 39 | "secondFlow" 40 | ] 41 | }, 42 | "yetAnotherFlow": { 43 | "jobs": [ 44 | { 45 | "location": "/Public/job115" 46 | } 47 | ], 48 | "predecessors": [] 49 | } 50 | } 51 | } 52 | ` 53 | } 54 | -------------------------------------------------------------------------------- /src/utils/spec/file.spec.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { 3 | createFile, 4 | readFile, 5 | fileExists, 6 | deleteFile, 7 | deleteFolder, 8 | generateTimestamp 9 | } from '@sasjs/utils' 10 | 11 | describe('file utility', () => { 12 | describe('createFile', () => { 13 | const filename = `test-create-file-${generateTimestamp()}.txt` 14 | 15 | it('should create a file', async () => { 16 | const filePath = path.join(process.cwd(), filename) 17 | const content = 'test content' 18 | 19 | await createFile(filePath, content) 20 | 21 | await expect(fileExists(filePath)).resolves.toEqual(true) 22 | await expect(readFile(filePath)).resolves.toEqual(content) 23 | 24 | deleteFile(filePath) 25 | }) 26 | 27 | it('should create a file and parent folders', async () => { 28 | const filePath = path.join( 29 | process.cwd(), 30 | 'testFolder_1', 31 | 'testFolder_2', 32 | filename 33 | ) 34 | const content = 'test content' 35 | 36 | await createFile(filePath, content) 37 | 38 | await expect(fileExists(filePath)).resolves.toEqual(true) 39 | await expect(readFile(filePath)).resolves.toEqual(content) 40 | 41 | deleteFolder(path.join(process.cwd(), 'testFolder_1')) 42 | }) 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /src/commands/flow/internal/allFlowsCompleted.ts: -------------------------------------------------------------------------------- 1 | import { FlowWave, FlowWaveJob } from '../../../types' 2 | 3 | interface flowsCompletionStatus { 4 | completed: boolean 5 | completedWithAllSuccess: boolean 6 | } 7 | 8 | export const allFlowsCompleted = (flows: { 9 | [key: string]: FlowWave 10 | }): flowsCompletionStatus => { 11 | const flowNames = Object.keys(flows) 12 | 13 | const jobsCount = flowNames.reduce( 14 | (count: number, name: string) => count + flows[name].jobs.length, 15 | 0 16 | ) 17 | const jobsWithSuccessStatus = flowNames.reduce( 18 | (count: number, name: string) => 19 | count + 20 | flows[name].jobs.filter( 21 | (job: FlowWaveJob) => job.status && job.status === 'success' 22 | ).length, 23 | 0 24 | ) 25 | const jobsWithFailureStatus = flowNames.reduce( 26 | (count: number, name: string) => 27 | count + 28 | flows[name].jobs.filter( 29 | (job: FlowWaveJob) => job.status && job.status !== 'success' 30 | ).length, 31 | 0 32 | ) 33 | 34 | return { 35 | completed: 36 | jobsCount === jobsWithSuccessStatus || 37 | jobsCount === jobsWithFailureStatus || 38 | jobsCount === jobsWithSuccessStatus + jobsWithFailureStatus, 39 | completedWithAllSuccess: jobsCount === jobsWithSuccessStatus 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/commands/web/internal/spec/updateLinkTag.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | 5 | import { updateLinkTag } from '../updateLinkTag' 6 | 7 | describe('updateLinkTag', () => { 8 | it(`should update href of link tag`, () => { 9 | const linkTag: HTMLLinkElement = document.createElement('link') 10 | linkTag.setAttribute('href', './style.css') 11 | 12 | const assetPathMap = [] 13 | const sasProgramPath = 14 | '/SASJobExecution?_FILE=appLoc/services/streamWebFolder/style.css' 15 | 16 | assetPathMap.push({ 17 | source: 'style.css', 18 | target: sasProgramPath 19 | }) 20 | 21 | updateLinkTag(linkTag, assetPathMap) 22 | expect(linkTag.getAttribute('href')).toEqual(sasProgramPath) 23 | }) 24 | 25 | it(`should update src of image tag`, () => { 26 | const imgTag: HTMLImageElement = document.createElement('img') 27 | imgTag.setAttribute('src', './image.jpeg') 28 | 29 | const assetPathMap = [] 30 | const sasProgramPath = 31 | '/SASJobExecution?_FILE=appLoc/services/streamWebFolder/image.jpeg' 32 | 33 | assetPathMap.push({ 34 | source: 'image.jpeg', 35 | target: sasProgramPath 36 | }) 37 | 38 | updateLinkTag(imgTag, assetPathMap, 'src') 39 | expect(imgTag.getAttribute('src')).toEqual(sasProgramPath) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /src/commands/docs/generateDot.ts: -------------------------------------------------------------------------------- 1 | import { getFoldersForDocs } from './internal/getFoldersForDocs' 2 | import { createDotFiles } from './internal/createDotFiles' 3 | import { getDocConfig } from './internal/getDocConfig' 4 | import { Configuration, Target } from '@sasjs/utils' 5 | 6 | /** 7 | * Generates lineage in dot language 8 | * By default the dot files will be at 'sasjsbuild/docs' folder 9 | * Generates dot files only for the jobs / services in target (and the root). 10 | * If no target is supplied, first target from sasjsconfig will be used. 11 | * @param {string} target- the target for dot files. 12 | * @param {string} outDirectory- the name of the output folder, picks from sasjsconfig.docConfig if present. 13 | */ 14 | export async function generateDot( 15 | target?: Target, 16 | config?: Configuration, 17 | outDirectory?: string 18 | ): Promise<{ outDirectory: string }> { 19 | const { serverUrl, newOutDirectory } = getDocConfig( 20 | target, 21 | config, 22 | outDirectory 23 | ) 24 | 25 | const { service: serviceFolders, job: jobFolders } = getFoldersForDocs( 26 | target, 27 | config 28 | ) 29 | 30 | const folderList = [...new Set([...serviceFolders, ...jobFolders])] 31 | 32 | await createDotFiles(folderList, newOutDirectory, serverUrl) 33 | 34 | return { outDirectory: newOutDirectory } 35 | } 36 | -------------------------------------------------------------------------------- /src/commands/servicepack/deploy.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { displayError, displaySuccess } from '../../utils/displayResult' 3 | import { Target, getAbsolutePath } from '@sasjs/utils' 4 | import { deployToSasViyaWithServicePack } from '../shared/deployToSasViyaWithServicePack' 5 | 6 | export async function servicePackDeploy( 7 | target: Target, 8 | isLocal: boolean, 9 | jsonFilePath: string, 10 | isForced = false 11 | ) { 12 | if (path.extname(jsonFilePath) !== '.json') { 13 | throw new Error('Provided data file must be valid json.') 14 | } 15 | 16 | process.logger?.info( 17 | `Deploying service pack to ${target.serverUrl} at location ${target.appLoc} .` 18 | ) 19 | 20 | if (jsonFilePath) { 21 | jsonFilePath = getAbsolutePath(jsonFilePath, process.cwd()) 22 | } else { 23 | const { buildDestinationFolder } = process.sasjsConstants 24 | jsonFilePath = path.join(buildDestinationFolder, `${target.name}.json`) 25 | } 26 | 27 | let success 28 | await deployToSasViyaWithServicePack(jsonFilePath, target, isLocal, isForced) 29 | .then((_) => { 30 | displaySuccess('Servicepack successfully deployed!') 31 | 32 | success = true 33 | }) 34 | .catch((err) => { 35 | displayError(err, 'Servicepack deploy failed.') 36 | 37 | success = false 38 | }) 39 | 40 | return success 41 | } 42 | -------------------------------------------------------------------------------- /src/commands/deploy/deployCommand.ts: -------------------------------------------------------------------------------- 1 | import { CommandExample, ReturnCode } from '../../types/command' 2 | import { TargetCommand } from '../../types/command/targetCommand' 3 | import { deploy } from './deploy' 4 | 5 | const syntax = 'deploy' 6 | const aliases = ['d'] 7 | const usage = 'sasjs deploy [options]' 8 | const description = 9 | 'Deploys a built project to the server specified in the target.' 10 | const examples: CommandExample[] = [ 11 | { 12 | command: 'sasjs deploy -t myTarget', 13 | description: '' 14 | }, 15 | { 16 | command: 'sasjs d -t myTarget', 17 | description: '' 18 | } 19 | ] 20 | 21 | export class DeployCommand extends TargetCommand { 22 | constructor(args: string[]) { 23 | super(args, { syntax, usage, description, examples, aliases }) 24 | } 25 | 26 | public async execute() { 27 | const { target, isLocal } = await this.getTargetInfo() 28 | 29 | return await deploy(target, isLocal) 30 | .then(() => { 31 | process.logger?.success( 32 | `Services have been successfully deployed to ${target.serverUrl}.` 33 | ) 34 | return ReturnCode.Success 35 | }) 36 | .catch((err) => { 37 | process.logger?.error( 38 | `An error has occurred when deploying services: ${err}` 39 | ) 40 | return ReturnCode.InternalError 41 | }) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/commands/deploy/internal/executeNonSasScript.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { 3 | executeShellScript, 4 | executePowerShellScript 5 | } from '../../../utils/utils' 6 | import { isShellScript, isPowerShellScript } from '../../../utils/file' 7 | 8 | export async function executeNonSasScript(scriptPath: string) { 9 | const fileExtension = path.extname(scriptPath) 10 | 11 | const { buildDestinationResultsLogsFolder } = process.sasjsConstants 12 | 13 | const logPath = path.join( 14 | buildDestinationResultsLogsFolder, 15 | path.basename(scriptPath).replace(fileExtension, '.log') 16 | ) 17 | 18 | if (isShellScript(scriptPath)) { 19 | process.logger?.info(`Executing shell script ${scriptPath} ...`) 20 | 21 | await executeShellScript(scriptPath, logPath) 22 | 23 | process.logger?.success( 24 | `Shell script execution completed! Log is here: ${logPath}` 25 | ) 26 | 27 | return 28 | } 29 | 30 | if (isPowerShellScript(scriptPath)) { 31 | process.logger?.info(`Executing powershell script ${scriptPath} ...`) 32 | 33 | await executePowerShellScript(scriptPath, logPath) 34 | 35 | process.logger?.success( 36 | `PowerShell script execution completed! Log is here: ${logPath}` 37 | ) 38 | 39 | return 40 | } 41 | 42 | process.logger?.error(`Unable to process script located at ${scriptPath}`) 43 | } 44 | -------------------------------------------------------------------------------- /src/commands/compile/spec/extra-spacing-sas-programs.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @brief makedata1 job 4 | @details This is where the job is described (using markdown). The sections below contain 5 | more info. 6 | 7 | ## SAS Macros 8 | This section (in "h4" tags) is where you list project specific macros. You can also reference any 9 | of the @sasjs/core macros and they will be automatically included. 10 | 11 | ## SAS Programs 12 | Unlike macros (which are first compiled) SAS programs are executed immediately. 13 | To provide control, therefore, SAS programs are loaded into filerefs, where 14 | they can be called on demand. 15 | 16 | ## Data Inputs / Outputs 17 | This is where you can provide the library.table references of your input and 18 | output tables. In a future release it will be possible to use this information 19 | to diagram the data lineage. 20 | 21 | 22 |

SAS Macros

23 | @li example.sas 24 | 25 |

SAS Programs

26 | @li demotable1.ddl FREF1 27 | @li demotable1.sas FREF2 28 | 29 |

Data Outputs

30 | @li work.example 31 | 32 | **/ 33 | 34 | %example(MakeData1 is executing) 35 | 36 | /* these file refs are configurable above */ 37 | %inc FREF1; 38 | 39 | %inc FREF2; 40 | 41 | proc append base=&mylib..demotable1 data=work.append; 42 | run; 43 | 44 | data _null_; 45 | rc=sleep(10); 46 | run; -------------------------------------------------------------------------------- /src/commands/docs/internal/createDotFiles.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { graphviz } from 'node-graphviz' 3 | 4 | import { createFolder, createFile } from '@sasjs/utils' 5 | 6 | import { getDotFileContent } from './getDotFileContent' 7 | 8 | /** 9 | * Creates Dot files dot-code(data_lineage.dot) and diagram(data_lineage.svg) 10 | * @param {string[]} folderList- dot files will be generated against provided folderList 11 | * @param {string} outDirectory- the name of the output folder for dot files. 12 | * @param {string} serverUrl- prefixes with links to Libs(Inputs/Outputs) 13 | */ 14 | export async function createDotFiles( 15 | folderList: string[], 16 | outDirectory: string, 17 | serverUrl: string 18 | ) { 19 | const dotFilePath = path.join(outDirectory, 'data_lineage.dot') 20 | const dotGraphPath = path.join(outDirectory, 'data_lineage.svg') 21 | 22 | await createFolder(outDirectory) 23 | 24 | const dotFileContent = await getDotFileContent(folderList, serverUrl) 25 | 26 | await createFile(dotFilePath, dotFileContent) 27 | process.logger?.success(`File ${dotFilePath} has been created.`) 28 | 29 | try { 30 | const svg = await graphviz.dot(dotFileContent, 'svg') 31 | await createFile(dotGraphPath, svg) 32 | process.logger?.success(`File ${dotGraphPath} has been created.`) 33 | } catch (e) { 34 | throw 'Unable to generate graph from generated Dot file.\n' + e 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/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/commands/flow/spec/sourceFiles/testFlow_7.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "myAmazingFlow", 3 | "flows": { 4 | "firstFlow": { 5 | "jobs": [ 6 | { 7 | "location": "jobs/testJob/job" 8 | }, 9 | { 10 | "location": "jobs/testJob/job" 11 | } 12 | ], 13 | "predecessors": [] 14 | }, 15 | "secondFlow": { 16 | "jobs": [ 17 | { 18 | "location": "jobs/testJob/job" 19 | } 20 | ], 21 | "predecessors": ["firstFlow"] 22 | }, 23 | "thirdFlow": { 24 | "jobs": [ 25 | { 26 | "location": "jobs/testJob/job" 27 | }, 28 | { 29 | "location": "jobs/testJob/failingJob" 30 | } 31 | ], 32 | "predecessors": ["firstFlow", "secondFlow"] 33 | }, 34 | "fourthFlow": { 35 | "jobs": [ 36 | { 37 | "location": "jobs/testJob/job" 38 | } 39 | ], 40 | "predecessors": ["secondFlow"] 41 | }, 42 | "fifthFlow": { 43 | "jobs": [ 44 | { 45 | "location": "jobs/testJob/job" 46 | }, 47 | { 48 | "location": "jobs/testJob/failingJob" 49 | } 50 | ], 51 | "predecessors": ["thirdFlow"] 52 | }, 53 | "sixthFlow": { 54 | "jobs": [ 55 | { 56 | "location": "jobs/testJob/failingJob" 57 | } 58 | ], 59 | "predecessors": [] 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/commands/deploy/spec/getDeployScripts.spec.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { generateTimestamp, ServerType, Target } from '@sasjs/utils' 3 | import { removeFromGlobalConfig } from '../../../utils/config' 4 | import { 5 | createTestApp, 6 | generateTestTarget, 7 | removeTestApp 8 | } from '../../../utils/test' 9 | import { getDeployScripts } from '../internal/getDeployScripts' 10 | 11 | describe('getDeployScripts', () => { 12 | let target: Target 13 | 14 | beforeEach(async () => { 15 | const appName = `cli-tests-${generateTimestamp()}` 16 | await createTestApp(__dirname, appName) 17 | target = generateTestTarget( 18 | appName, 19 | `/Public/app/cli-tests/${appName}`, 20 | { 21 | serviceFolders: [path.join('sasjs', 'services')], 22 | initProgram: '', 23 | termProgram: '', 24 | macroVars: {} 25 | }, 26 | ServerType.Sas9 27 | ) 28 | }) 29 | 30 | afterEach(async () => { 31 | await removeTestApp(__dirname, target.name) 32 | await removeFromGlobalConfig(target.name) 33 | }) 34 | 35 | it('should return all deployScripts present at root level of configuration and in target', async () => { 36 | const scripts = ['script1.sh', 'script2.bat', 'script3.ps1'] 37 | target.deployConfig?.deployScripts.push(...scripts) 38 | const deployScripts = await getDeployScripts(target) 39 | 40 | expect(deployScripts).toEqual(['sasjs/build/deployscript.sh', ...scripts]) 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /src/commands/flow/internal/spec/failAllSuccessors.spec.ts: -------------------------------------------------------------------------------- 1 | import { failAllSuccessors } from '..' 2 | 3 | describe('failAllSuccessors', () => { 4 | it('should update all successors only, recursively', () => { 5 | const flows = { 6 | flow1: { 7 | jobs: [{ location: 'job' }], 8 | predecessors: ['flow4'] 9 | }, 10 | flow2: { 11 | jobs: [{ location: 'job' }], 12 | predecessors: ['flow5', 'flow4'] 13 | }, 14 | flow3: { 15 | jobs: [{ location: 'job' }], 16 | predecessors: [] 17 | }, 18 | flowIndependent: { 19 | jobs: [{ location: 'job' }], 20 | predecessors: [] 21 | }, 22 | flow4: { 23 | jobs: [{ location: 'job' }], 24 | predecessors: ['flow3'] 25 | }, 26 | flow5: { 27 | jobs: [{ location: 'job' }], 28 | predecessors: ['flow3'] 29 | } 30 | } 31 | 32 | failAllSuccessors(flows, 'flow3') 33 | 34 | expect(flows.flow1.jobs[0]).toEqual({ location: 'job', status: 'failure' }) 35 | expect(flows.flow2.jobs[0]).toEqual({ location: 'job', status: 'failure' }) 36 | expect(flows.flow3.jobs[0]).toEqual({ location: 'job', status: undefined }) 37 | expect(flows.flowIndependent.jobs[0]).toEqual({ 38 | location: 'job', 39 | status: undefined 40 | }) 41 | expect(flows.flow4.jobs[0]).toEqual({ location: 'job', status: 'failure' }) 42 | expect(flows.flow5.jobs[0]).toEqual({ location: 'job', status: 'failure' }) 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /src/commands/job/internal/execute/sasjs.ts: -------------------------------------------------------------------------------- 1 | import { AuthConfig, Target } from '@sasjs/utils' 2 | import { saveLog, saveOutput } from '../utils' 3 | import SASjs from '@sasjs/adapter/node' 4 | 5 | /** 6 | * Triggers existing job for execution on SASJS server. 7 | * @param {object} sasjs - configuration object of SAS adapter. 8 | * @param target - SASJS server configuration. 9 | * @param jobPath - location of the job. 10 | * @param logFile - flag indicating if CLI should fetch and save log to provided file path. If filepath wasn't provided, {job}.log file will be created in current folder. 11 | * @param output - flag indicating if CLI should save output to provided file path. 12 | * @returns - promise that resolves into an object with log and output. 13 | */ 14 | export async function executeJobSasjs( 15 | sasjs: SASjs, 16 | target: Target, 17 | jobPath: string, 18 | logFile?: string, 19 | output?: string, 20 | authConfig?: AuthConfig 21 | ) { 22 | const response = await sasjs.executeJob( 23 | { 24 | _program: jobPath 25 | }, 26 | target.appLoc, 27 | authConfig 28 | ) 29 | 30 | if (response) { 31 | // handle success 32 | process.logger?.success('Job executed successfully!') 33 | 34 | // save log if it is present 35 | if (!!logFile && response.log) await saveLog(response.log, logFile, jobPath) 36 | 37 | // save output if it is present 38 | if (!!output && response.result) await saveOutput(response.result, output) 39 | } 40 | 41 | return response 42 | } 43 | -------------------------------------------------------------------------------- /src/commands/run/spec/run.spec.server.sasjs.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { runSasCode } from '../..' 3 | import { copy, Target, generateTimestamp, ServerType } from '@sasjs/utils' 4 | import { 5 | createTestApp, 6 | generateTestTarget, 7 | removeTestApp, 8 | removeTestServerFolder 9 | } from '../../../utils/test' 10 | 11 | describe('sasjs run with server type sasjs', () => { 12 | let target: Target 13 | 14 | beforeEach(async () => { 15 | const appName = 'cli-tests-run-sasjs' + generateTimestamp() 16 | await createTestApp(__dirname, appName) 17 | target = generateTestTarget( 18 | appName, 19 | `/Public/app/cli-tests/${appName}`, 20 | { 21 | serviceFolders: ['sasjs/testServices'], 22 | initProgram: '', 23 | termProgram: '', 24 | macroVars: {} 25 | }, 26 | ServerType.Sasjs 27 | ) 28 | await copy( 29 | path.join(__dirname, 'testServices'), 30 | path.join(process.projectDir, 'sasjs', 'testServices') 31 | ) 32 | }) 33 | 34 | afterEach(async () => { 35 | await removeTestApp(__dirname, target.name) 36 | }) 37 | 38 | it('should run a file when a relative path is provided', async () => { 39 | await expect(runSasCode(target, 'sasjs/testServices/logJob.js')).toResolve() 40 | }) 41 | 42 | it('should run a file when an absolute path is provided', async () => { 43 | await expect( 44 | runSasCode(target, `${process.projectDir}/sasjs/testServices/logJob.js`) 45 | ).toResolve() 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /.github/workflows/npmpublish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: SASjs CLI Deploy 5 | 6 | on: 7 | push: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | release: 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | matrix: 17 | node-version: [lts/iron] 18 | 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v2 22 | 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v3 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | cache: npm 28 | 29 | - name: Install dependencies 30 | run: | 31 | npm config set registry https://registry.npmjs.org 32 | npm ci 33 | 34 | - name: Build project 35 | run: npm run build 36 | 37 | - name: Audit for vulnerabilities 38 | run: npm audit --omit=dev --audit-level=low 39 | 40 | - name: Set NPM Registry back to https 41 | run: npm config set registry https://registry.npmjs.org 42 | 43 | - name: Semantic Release 44 | uses: cycjimmy/semantic-release-action@v3 45 | with: 46 | node-version: ${{ matrix.node-version }} 47 | cache: npm 48 | env: 49 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 50 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 51 | -------------------------------------------------------------------------------- /src/commands/build/buildCommand.ts: -------------------------------------------------------------------------------- 1 | import { CommandExample, ReturnCode } from '../../types/command' 2 | import { TargetCommand } from '../../types/command/targetCommand' 3 | import { displayError } from '../../utils' 4 | import { build } from './build' 5 | 6 | const syntax = 'build' 7 | const aliases = ['b'] 8 | const usage = 'sasjs build [options]' 9 | const description = 10 | 'Collates all the compiled jobs and services in the project into a single .sas file and a .json file for deployment. Uses configuration from the specified target.' 11 | const examples: CommandExample[] = [ 12 | { 13 | command: 'sasjs build -t myTarget', 14 | description: '' 15 | }, 16 | { 17 | command: 'sasjs b -t myTarget', 18 | description: '' 19 | } 20 | ] 21 | 22 | export class BuildCommand extends TargetCommand { 23 | constructor(args: string[]) { 24 | super(args, { syntax, usage, description, examples, aliases }) 25 | } 26 | 27 | public async execute() { 28 | const { target } = await this.getTargetInfo() 29 | return await build(target) 30 | .then(async () => { 31 | const { buildDestinationFolder } = process.sasjsConstants 32 | process.logger?.success( 33 | `Services have been successfully built!\nThe build output is located in the ${buildDestinationFolder} directory.` 34 | ) 35 | return ReturnCode.Success 36 | }) 37 | .catch((err) => { 38 | displayError(err, 'An error has occurred when building services.') 39 | return ReturnCode.InternalError 40 | }) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/commands/web/webCommand.ts: -------------------------------------------------------------------------------- 1 | import { CommandExample, ReturnCode } from '../../types/command' 2 | import { TargetCommand } from '../../types/command/targetCommand' 3 | import { displayError, displaySuccess } from '../../utils' 4 | import { createWebAppServices } from './web' 5 | 6 | const syntax = 'web' 7 | const aliases = ['w'] 8 | const usage = 'Usage: sasjs web [options]' 9 | const description = 10 | 'compiles the web app service and place at webSourcePath specified in target.' 11 | const examples: CommandExample[] = [ 12 | { 13 | command: 'sasjs web -t myTarget', 14 | description: '' 15 | }, 16 | { 17 | command: 'sasjs w -t myTarget', 18 | description: '' 19 | } 20 | ] 21 | 22 | export class WebCommand extends TargetCommand { 23 | constructor(args: string[]) { 24 | super(args, { syntax, usage, description, examples, aliases }) 25 | } 26 | 27 | public async execute() { 28 | const { target } = await this.getTargetInfo() 29 | const { buildDestinationFolder } = process.sasjsConstants 30 | 31 | return await createWebAppServices(target) 32 | .then(async () => { 33 | displaySuccess( 34 | `Web app services have been successfully built!\nThe build output is located in the ${buildDestinationFolder} directory.` 35 | ) 36 | return ReturnCode.Success 37 | }) 38 | .catch((err) => { 39 | displayError( 40 | err, 41 | 'An error has occurred when building web app services.' 42 | ) 43 | return ReturnCode.InternalError 44 | }) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/commands/compile/internal/getAllFolders.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Configuration, 3 | getAbsolutePath, 4 | JobConfig, 5 | ServiceConfig, 6 | Target 7 | } from '@sasjs/utils' 8 | 9 | export enum SasFileType { 10 | Service = 'service', 11 | Job = 'job', 12 | Test = 'test' 13 | } 14 | 15 | export const getAllFolders = async ( 16 | target: Target, 17 | type: SasFileType.Service | SasFileType.Job, 18 | rootConfig?: Configuration 19 | ): Promise => { 20 | const configuration = rootConfig || process.sasjsConfig 21 | 22 | let allFolders: string[] | undefined 23 | let config: ServiceConfig | JobConfig | undefined 24 | let folders: string[] | undefined 25 | const addFolders = (folders: string[] | undefined) => { 26 | allFolders = [...(allFolders || []), ...(folders || [])] 27 | } 28 | 29 | if (type === SasFileType.Service) { 30 | config = configuration.serviceConfig 31 | folders = config?.serviceFolders 32 | 33 | addFolders(folders) 34 | 35 | config = target.serviceConfig 36 | folders = config?.serviceFolders 37 | } else { 38 | config = configuration.jobConfig 39 | folders = config?.jobFolders 40 | 41 | addFolders(folders) 42 | 43 | config = target.jobConfig 44 | folders = config?.jobFolders 45 | } 46 | 47 | addFolders(folders) 48 | 49 | if (!allFolders) return [] 50 | 51 | allFolders = allFolders.filter((p) => !!p) 52 | 53 | const { buildSourceFolder } = process.sasjsConstants 54 | 55 | allFolders = allFolders.map((sasFile) => 56 | getAbsolutePath(sasFile, buildSourceFolder) 57 | ) 58 | 59 | return [...new Set(allFolders)] 60 | } 61 | -------------------------------------------------------------------------------- /src/commands/build/internal/config.ts: -------------------------------------------------------------------------------- 1 | import { Target, readFile, getAbsolutePath } from '@sasjs/utils' 2 | 3 | export const getBuildInit = async (target: Target) => { 4 | const { buildSourceFolder } = process.sasjsConstants 5 | let buildInitContent = '' 6 | if (target?.buildConfig?.initProgram) { 7 | buildInitContent = await readFile( 8 | getAbsolutePath(target.buildConfig.initProgram, buildSourceFolder) 9 | ) 10 | } else { 11 | const configuration = process.sasjsConfig 12 | if (configuration?.buildConfig?.initProgram) { 13 | buildInitContent = await readFile( 14 | getAbsolutePath( 15 | configuration.buildConfig.initProgram, 16 | buildSourceFolder 17 | ) 18 | ) 19 | } 20 | } 21 | 22 | return buildInitContent 23 | ? `\n* BuildInit start;\n${buildInitContent}\n* BuildInit end;` 24 | : '' 25 | } 26 | 27 | export const getBuildTerm = async (target: Target) => { 28 | const { buildSourceFolder } = process.sasjsConstants 29 | let buildTermContent = '' 30 | if (target?.buildConfig?.termProgram) { 31 | buildTermContent = await readFile( 32 | getAbsolutePath(target.buildConfig.termProgram, buildSourceFolder) 33 | ) 34 | } else { 35 | const configuration = process.sasjsConfig 36 | if (configuration?.buildConfig?.termProgram) { 37 | buildTermContent = await readFile( 38 | getAbsolutePath( 39 | configuration.buildConfig.termProgram, 40 | buildSourceFolder 41 | ) 42 | ) 43 | } 44 | } 45 | 46 | return buildTermContent 47 | ? `\n* BuildTerm start;\n${buildTermContent}\n* BuildTerm end;` 48 | : '' 49 | } 50 | -------------------------------------------------------------------------------- /src/commands/web/internal/getTags.ts: -------------------------------------------------------------------------------- 1 | import { JSDOM } from 'jsdom' 2 | 3 | export const getSasjsTags = (parsedHtml: JSDOM) => 4 | Array.from(parsedHtml.window.document.getElementsByTagName('sasjs')) 5 | 6 | export const getScriptTags = (parsedHtml: JSDOM) => 7 | Array.from(parsedHtml.window.document.getElementsByTagName('script')) 8 | 9 | export const getModulePreload = (parsedHtml: JSDOM) => 10 | Array.from(parsedHtml.window.document.getElementsByTagName('link')).filter( 11 | (s) => s.getAttribute('rel') === 'modulepreload' 12 | ) 13 | 14 | export const getStyleInPageTags = (parsedHtml: JSDOM) => 15 | Array.from(parsedHtml.window.document.getElementsByTagName('style')) 16 | 17 | export const getStyleTags = (parsedHtml: JSDOM) => 18 | Array.from(parsedHtml.window.document.getElementsByTagName('link')).filter( 19 | (s) => s.getAttribute('rel') === 'stylesheet' 20 | ) 21 | 22 | export const getFaviconTags = (parsedHtml: JSDOM) => 23 | Array.from(parsedHtml.window.document.getElementsByTagName('link')).filter( 24 | (s) => s.getAttribute('rel')?.includes('icon') 25 | ) 26 | 27 | export const getImgTags = (parsedHtml: JSDOM) => 28 | Array.from(parsedHtml.window.document.getElementsByTagName('img')).filter( 29 | (s) => s.getAttribute('src') 30 | ) 31 | 32 | export const getSourceTags = (parsedHtml: JSDOM) => 33 | Array.from(parsedHtml.window.document.getElementsByTagName('source')).filter( 34 | (s) => s.getAttribute('src') 35 | ) 36 | 37 | export const getManifestTags = (parsedHtml: JSDOM) => 38 | Array.from(parsedHtml.window.document.getElementsByTagName('link')).filter( 39 | (s) => s.getAttribute('rel') === 'manifest' 40 | ) 41 | -------------------------------------------------------------------------------- /src/commands/help/spec/helpCommand.spec.ts: -------------------------------------------------------------------------------- 1 | import * as helpModule from '../help' 2 | import { HelpCommand } from '../helpCommand' 3 | import { Logger, LogLevel } from '@sasjs/utils' 4 | import { ReturnCode } from '../../../types/command' 5 | 6 | const defaultArgs = ['node', 'sasjs'] 7 | 8 | describe('HelpCommand', () => { 9 | beforeEach(() => { 10 | setupMocks() 11 | }) 12 | 13 | it('should parse a simple sasjs help command', () => { 14 | const args = [...defaultArgs, 'help'] 15 | 16 | const command = new HelpCommand(args) 17 | 18 | expect(command.name).toEqual('help') 19 | expect(command.subCommand).toEqual('') 20 | }) 21 | 22 | it('should return the success code when execution is successful', async () => { 23 | const args = [...defaultArgs, 'help'] 24 | 25 | const command = new HelpCommand(args) 26 | const returnCode = await command.execute() 27 | 28 | expect(returnCode).toEqual(ReturnCode.Success) 29 | }) 30 | 31 | it('should return the error code when execution is unsuccessful', async () => { 32 | const args = [...defaultArgs, 'help'] 33 | jest 34 | .spyOn(helpModule, 'printHelpText') 35 | .mockImplementation(() => Promise.reject(new Error('Test Error'))) 36 | 37 | const command = new HelpCommand(args) 38 | const returnCode = await command.execute() 39 | 40 | expect(returnCode).toEqual(ReturnCode.InternalError) 41 | }) 42 | }) 43 | 44 | const setupMocks = () => { 45 | jest.resetAllMocks() 46 | jest.mock('../help') 47 | jest 48 | .spyOn(helpModule, 'printHelpText') 49 | .mockImplementation(() => Promise.resolve({} as any)) 50 | 51 | process.logger = new Logger(LogLevel.Off) 52 | } 53 | -------------------------------------------------------------------------------- /src/commands/web/internal/sas9/createClickMeService.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { chunk, createFile } from '@sasjs/utils' 3 | import { sasjsout } from '../sasjsout' 4 | 5 | /** 6 | * Creates index service file for SAS9 server only. 7 | * @param {string} indexHtmlContent contents of index source file. 8 | * @param {string} fileName name of index service file. 9 | */ 10 | export const createClickMeService = async ( 11 | indexHtmlContent: string, 12 | fileName: string 13 | ) => { 14 | const lines = indexHtmlContent.replace(/\r\n/g, '\n').split('\n') 15 | let clickMeServiceContent = `${sasjsout()}\nfilename sasjs temp lrecl=99999999;\ndata _null_;\nfile sasjs;\n` 16 | 17 | lines.forEach((line) => { 18 | const chunkedLines = chunk(line) 19 | if (chunkedLines.length === 1) { 20 | if (chunkedLines[0].length == 0) chunkedLines[0] = ' ' 21 | 22 | clickMeServiceContent += `put '${chunkedLines[0] 23 | .split("'") 24 | .join("''")}';\n` 25 | } else { 26 | let combinedLines = '' 27 | chunkedLines.forEach((chunkedLine, index) => { 28 | let text = `put '${chunkedLine.split("'").join("''")}'` 29 | if (index !== chunkedLines.length - 1) { 30 | text += '@;\n' 31 | } else { 32 | text += ';\n' 33 | } 34 | combinedLines += text 35 | }) 36 | clickMeServiceContent += combinedLines 37 | } 38 | }) 39 | clickMeServiceContent += 'run;\n%sasjsout(HTML)' 40 | const { buildDestinationServicesFolder } = process.sasjsConstants 41 | await createFile( 42 | path.join(buildDestinationServicesFolder, `${fileName}.sas`), 43 | clickMeServiceContent 44 | ) 45 | } 46 | -------------------------------------------------------------------------------- /src/commands/compile/internal/compileFile.ts: -------------------------------------------------------------------------------- 1 | import { createFile, isTestFile, CompileTree } from '@sasjs/utils' 2 | import { Target, SASJsFileType } from '@sasjs/utils/types' 3 | import { loadDependencies } from './' 4 | import path from 'path' 5 | 6 | /** 7 | * Compiles file dependencies. 8 | * @param {Target} target - SAS server configuration. 9 | * @param {string} filePath - file path of the file to be compiled. 10 | * @param {string[]} macroFolders - macro folders paths. 11 | * @param {string[]} programFolders - program folders paths. 12 | * @param {string} programVar - program variable. 13 | * @param {object} compileTree - compilation tree that stores used compilation assets. 14 | * @param {SASJsFileType} fileType - sasjs file type. 15 | * @param {string} sourceFolder - folder path of the source folder. 16 | */ 17 | export async function compileFile( 18 | target: Target, 19 | filePath: string, 20 | macroFolders: string[], 21 | programFolders: string[], 22 | programVar: string = '', 23 | compileTree: CompileTree, 24 | fileType: SASJsFileType, 25 | sourceFolder: string 26 | ) { 27 | let dependencies = await loadDependencies( 28 | target, 29 | sourceFolder 30 | ? path.join(sourceFolder, filePath.split(path.sep).pop()!) 31 | : filePath, 32 | macroFolders, 33 | programFolders, 34 | isTestFile(filePath) ? SASJsFileType.test : fileType, 35 | compileTree 36 | ) 37 | 38 | if (fileType === SASJsFileType.service) { 39 | dependencies = `${programVar}\n${dependencies}` 40 | } else { 41 | dependencies = `${programVar ? programVar + '\n' : ''}${dependencies}` 42 | } 43 | 44 | await createFile(filePath, dependencies) 45 | } 46 | -------------------------------------------------------------------------------- /src/commands/web/internal/sas9/getWebServiceContent.ts: -------------------------------------------------------------------------------- 1 | import { chunk } from '@sasjs/utils' 2 | import { sasjsout } from '../sasjsout' 3 | 4 | /** 5 | * Prepares service code for SAS9 server only. 6 | * @param {string} contentBase64 source code in base64. 7 | * @param {string} type extension of source code's file in uppercase. 8 | * @returns {string} service contents of provided source code. 9 | */ 10 | export const getWebServiceContent = async ( 11 | contentBase64: string, 12 | type = 'JS' 13 | ) => { 14 | let lines = [contentBase64] 15 | 16 | // Encode to base64 *.svg, *.js and *.css files if target server type is SAS 9. 17 | const typesToEncode: { [key: string]: string } = { 18 | JS: 'JS64', 19 | CSS: 'CSS64', 20 | SVG: 'SVG64' 21 | } 22 | 23 | let serviceContent = `${sasjsout()}\nfilename sasjs temp lrecl=99999999; 24 | data _null_; 25 | file sasjs; 26 | ` 27 | 28 | lines.forEach((line) => { 29 | const chunkedLines = chunk(line) 30 | if (chunkedLines.length === 1) { 31 | serviceContent += `put '${chunkedLines[0].split("'").join("''")}';\n` 32 | } else { 33 | let combinedLines = '' 34 | chunkedLines.forEach((chunkedLine, index) => { 35 | let text = `put '${chunkedLine.split("'").join("''")}'` 36 | if (index !== chunkedLines.length - 1) { 37 | text += '@;\n' 38 | } else { 39 | text += ';\n' 40 | } 41 | combinedLines += text 42 | }) 43 | serviceContent += combinedLines 44 | } 45 | }) 46 | 47 | if (typesToEncode.hasOwnProperty(type)) { 48 | serviceContent += `\nrun;\n%sasjsout(${typesToEncode[type]})` 49 | } 50 | 51 | return serviceContent 52 | } 53 | -------------------------------------------------------------------------------- /src/commands/shared/deployToSasViyaWithServicePack.ts: -------------------------------------------------------------------------------- 1 | import { FileTree, MemberType, readFile, Target } from '@sasjs/utils' 2 | import { getAccessToken, getSASjs } from '../../utils' 3 | 4 | export async function deployToSasViyaWithServicePack( 5 | jsonFilePath: string, 6 | target: Target, 7 | isLocal: boolean, 8 | isForced: boolean = false 9 | ): Promise { 10 | const jsonContent = await readFile(jsonFilePath) 11 | 12 | let jsonObject: FileTree 13 | 14 | try { 15 | jsonObject = JSON.parse(jsonContent) 16 | } catch (err) { 17 | throw new Error('Provided data file must be valid json.') 18 | } 19 | 20 | populateCodeInServicePack(jsonObject) 21 | 22 | const access_token: string = await getAccessToken(target).catch((e) => '') 23 | 24 | if (!access_token) { 25 | throw new Error( 26 | `Deployment failed. Request is not authenticated.\nPlease add the following variables to your .env${ 27 | isLocal ? `.${target.name}` : '' 28 | } file:\nCLIENT, SECRET, ACCESS_TOKEN, REFRESH_TOKEN` 29 | ) 30 | } 31 | 32 | const sasjs = getSASjs(target) 33 | 34 | await sasjs 35 | .deployServicePack(jsonObject, undefined, undefined, access_token, isForced) 36 | .catch((err: any) => { 37 | process.logger.error('deployServicePack error', err) 38 | throw new Error('Deploy service pack error') 39 | }) 40 | 41 | return jsonObject 42 | } 43 | 44 | const populateCodeInServicePack = (json: any) => 45 | json?.members?.forEach((member: any) => { 46 | if (member.type === MemberType.file) 47 | member.code = Buffer.from(member.code!, 'base64') 48 | if (member.type === MemberType.folder) populateCodeInServicePack(member) 49 | }) 50 | -------------------------------------------------------------------------------- /src/commands/init/init.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | import { 4 | setupNpmProject, 5 | setupGitIgnore, 6 | setupDoxygen 7 | } from '../../utils/utils' 8 | 9 | import { fileExists, createFile } from '@sasjs/utils' 10 | import { createLintConfigFile } from '../shared/createLintConfigFile' 11 | 12 | export async function init() { 13 | process.logger?.info('Initialising SASjs...') 14 | const parentFolderName = '.' 15 | 16 | await setupNpmProject(parentFolderName) 17 | 18 | await setupGitIgnore(parentFolderName) 19 | 20 | await setupDoxygen(parentFolderName) 21 | 22 | await createConfigFile(parentFolderName) 23 | 24 | const lintConfigPath = path.join( 25 | process.projectDir, 26 | parentFolderName, 27 | '.sasjslint' 28 | ) 29 | if (!(await fileExists(lintConfigPath))) 30 | await createLintConfigFile(parentFolderName) 31 | } 32 | 33 | /** 34 | * Creates a SASjs configuration file. 35 | * Its name will be of the form 'sasjsconfig.json' 36 | * @param {string} parentFolderName- the name of the project folder. 37 | */ 38 | export const createConfigFile = async (parentFolderName: string) => { 39 | const config = { 40 | $schema: 'https://cli.sasjs.io/sasjsconfig-schema.json', 41 | macroFolders: ['sasjs/macros'], 42 | defaultTarget: 'mytarget', 43 | targets: [ 44 | { 45 | name: 'mytarget', 46 | serverType: 'SASJS', 47 | serverUrl: ' ', 48 | appLoc: '/Public/apps/myapp' 49 | } 50 | ] 51 | } 52 | const configDestinationPath = path.join( 53 | process.projectDir, 54 | parentFolderName, 55 | 'sasjs', 56 | 'sasjsconfig.json' 57 | ) 58 | await createFile(configDestinationPath, JSON.stringify(config, null, 2)) 59 | } 60 | -------------------------------------------------------------------------------- /src/commands/folder/create.ts: -------------------------------------------------------------------------------- 1 | import SASjs from '@sasjs/adapter/node' 2 | import { displayError, displaySuccess } from '../../utils/displayResult' 3 | 4 | /** 5 | * Creates folder. 6 | * @param {string} path - folder path. 7 | * @param {object} sasjs - configuration object of SAS adapter. 8 | * @param {string} accessToken - an access token for an authorized user. 9 | * @param {boolean} isForced - forced flag indicates if target folder already exists, its content and all subfolders will be deleted. 10 | */ 11 | export const create = async ( 12 | path: string, 13 | sasjs: SASjs, 14 | accessToken: string, 15 | isForced: boolean 16 | ) => { 17 | const pathMap = path.split('/') 18 | const folder = sanitize(pathMap.pop() || '') 19 | let parentFolderPath = pathMap.join('/') 20 | 21 | const createdFolder = await sasjs 22 | .createFolder( 23 | folder, 24 | parentFolderPath, 25 | undefined, 26 | accessToken, 27 | undefined, 28 | isForced 29 | ) 30 | .catch((err) => { 31 | displayError( 32 | err, 33 | `An error has occurred when creating the folder ${folder}` 34 | ) 35 | 36 | if (err.status && err.status === 409) { 37 | process.logger?.error( 38 | `Consider using '-f' or '--force' flag, eg 'sasjs folder create /Public/folderToCreate -f'.\nWARNING: When using force, any existing content and subfolders of the target folder will be deleted.` 39 | ) 40 | } 41 | }) 42 | 43 | if (createdFolder) { 44 | displaySuccess( 45 | `Folder '${ 46 | parentFolderPath + '/' + folder 47 | }' has been successfully created.` 48 | ) 49 | } 50 | } 51 | 52 | const sanitize = (path: string) => path.replace(/[^0-9a-zA-Z_\-. ]/g, '_') 53 | -------------------------------------------------------------------------------- /src/utils/saveLog.ts: -------------------------------------------------------------------------------- 1 | import { folderExists, createFolder, createFile } from '@sasjs/utils' 2 | import path from 'path' 3 | import { parseLogLines, displaySuccess } from '.' 4 | import { LogJson } from '../types' 5 | 6 | /** 7 | * Saves log to the log file. 8 | * @param logData - log content. 9 | * @param logFile - file path to log file. 10 | * @param jobPath - file path to job, job name will be used as a fallback name for log file. 11 | * @param silent - boolean indicating if additional info should be logged. 12 | */ 13 | export const saveLog = async ( 14 | logData: LogJson | string, 15 | logFile: string | undefined, 16 | jobPath: string, 17 | silent: boolean = false 18 | ) => { 19 | // throw an error if log is an object containing error property 20 | if (typeof logData !== 'string') { 21 | const { error } = logData 22 | 23 | if (error) throw JSON.stringify(error, null, 2) 24 | } 25 | 26 | // get absolute file path to log file 27 | const logPath = 28 | logFile || 29 | path.join( 30 | process.projectDir, 31 | `${jobPath.split(path.sep).slice(-1).pop()}.log` 32 | ) 33 | 34 | const folderPath = logPath.split(path.sep) 35 | folderPath.pop() 36 | 37 | const parentFolderPath = folderPath.join(path.sep) 38 | 39 | // create parent folder of the log file if it doesn't exist 40 | if (!(await folderExists(parentFolderPath))) { 41 | await createFolder(parentFolderPath) 42 | } 43 | 44 | // try to parse log 45 | const logLines = 46 | typeof logData === 'object' ? parseLogLines(logData) : logData 47 | 48 | if (!silent) process.logger?.info(`Creating log file at ${logPath} .`) 49 | 50 | await createFile(logPath, logLines) 51 | 52 | if (!silent) displaySuccess(`Log saved to ${logPath}`) 53 | } 54 | -------------------------------------------------------------------------------- /src/commands/flow/internal/saveLog.ts: -------------------------------------------------------------------------------- 1 | import SASjs from '@sasjs/adapter/node' 2 | import { createFile, fileExists, generateTimestamp } from '@sasjs/utils' 3 | import path from 'path' 4 | import { displayError } from '../../../utils/displayResult' 5 | import { parseLogLines } from '../../../utils/utils' 6 | import { fetchLogFileContent } from '../../shared/fetchLogFileContent' 7 | import { generateFileName } from './generateFileName' 8 | 9 | // REFACTOR: move to utility 10 | export const saveLog = async ( 11 | links: any[], 12 | flowName: string, 13 | jobLocation: string, 14 | logFolder: string, 15 | sasjs: SASjs, 16 | serverUrl: string, 17 | access_token: string, 18 | lineCount: number = 1000000 19 | ) => { 20 | if (!logFolder) throw 'No log folder provided' 21 | if (!links) throw 'No links provided' 22 | 23 | const logObj = links.find( 24 | (link: any) => link.rel === 'log' && link.method === 'GET' 25 | ) 26 | 27 | if (logObj) { 28 | const logUrl = serverUrl + logObj.href 29 | 30 | const logJson = await fetchLogFileContent( 31 | sasjs, 32 | access_token, 33 | logUrl, 34 | lineCount 35 | ) 36 | 37 | const logParsed = parseLogLines(logJson) 38 | 39 | let logName = generateFileName(flowName, jobLocation) 40 | 41 | while ( 42 | await fileExists(path.join(logFolder, logName)).catch((err) => 43 | displayError(err, 'Error while checking if log file exists.') 44 | ) 45 | ) { 46 | logName = generateFileName(flowName, jobLocation) 47 | } 48 | 49 | await createFile(path.join(logFolder, logName), logParsed).catch((err) => 50 | displayError(err, 'Error while creating log file.') 51 | ) 52 | 53 | return logName 54 | } 55 | 56 | return null 57 | } 58 | -------------------------------------------------------------------------------- /src/commands/docs/internal/getDocConfig.ts: -------------------------------------------------------------------------------- 1 | import { Target, Configuration } from '@sasjs/utils/types' 2 | 3 | /** 4 | * Returns doc related config from root-level and Target-specfic(having precedence) 5 | * @param {Configuration} config- from which doc related config will be extracted 6 | * @param {Target} target- the target for doc config. 7 | * @param {string} outDirectory- the name of the output folder, provided using command. 8 | */ 9 | export function getDocConfig( 10 | target?: Target, 11 | config?: Configuration, 12 | outDirectory?: string 13 | ) { 14 | const { buildDestinationDocsFolder } = process.sasjsConstants 15 | 16 | if (!outDirectory) { 17 | outDirectory = 18 | target?.docConfig?.outDirectory || 19 | config?.docConfig?.outDirectory || 20 | buildDestinationDocsFolder 21 | } 22 | 23 | let serverUrl = '' 24 | serverUrl = config?.docConfig?.dataControllerUrl 25 | ? config.docConfig.dataControllerUrl.split('#')[0] + '#/view/viewer/' 26 | : '' 27 | serverUrl = target?.docConfig?.dataControllerUrl 28 | ? target.docConfig.dataControllerUrl.split('#')[0] + '#/view/viewer/' 29 | : serverUrl 30 | 31 | const enableLineage: boolean = 32 | target?.docConfig?.enableLineage ?? config?.docConfig?.enableLineage ?? true 33 | 34 | const doxyContent = { 35 | ...config?.docConfig?.doxyContent, 36 | ...target?.docConfig?.doxyContent 37 | } 38 | 39 | if (doxyContent.path?.startsWith('//')) { 40 | throw new Error( 41 | 'UNC paths are not supported.\nPlease map to a network drive, or migrate the project to an existing path (with a drive letter).' 42 | ) 43 | } 44 | 45 | return { 46 | target, 47 | serverUrl, 48 | newOutDirectory: outDirectory, 49 | enableLineage, 50 | doxyContent 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/commands/web/internal/spec/updateStyleTag.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | 5 | import path from 'path' 6 | import { 7 | createFolder, 8 | deleteFolder, 9 | generateTimestamp, 10 | readFile, 11 | ServerType, 12 | Target 13 | } from '@sasjs/utils' 14 | import { updateStyleTag } from '../updateStyleTag' 15 | import { setConstants } from '../../../../utils' 16 | 17 | describe('updateStyleTag', () => { 18 | let destinationPath: string 19 | 20 | beforeAll(async () => { 21 | destinationPath = path.join( 22 | __dirname, 23 | `cli-test-web-updateStyleTag-${generateTimestamp()}` 24 | ) 25 | await createFolder(destinationPath) 26 | process.projectDir = destinationPath 27 | await setConstants() 28 | }) 29 | 30 | afterAll(async () => { 31 | await deleteFolder(destinationPath) 32 | }) 33 | 34 | it(`should update links in css script`, async () => { 35 | const sourcePath = path.join(__dirname, 'testFiles') 36 | const styleFilePath = path.join(sourcePath, 'style1.css') 37 | 38 | const target = { serverType: ServerType.SasViya } as any as Target 39 | 40 | const assetPathMap = [...assetLinks] 41 | 42 | const scriptTag: HTMLStyleElement = document.createElement('style') 43 | scriptTag.innerHTML = await readFile(styleFilePath) 44 | 45 | await updateStyleTag( 46 | scriptTag, 47 | sourcePath, 48 | destinationPath, 49 | target.serverType, 50 | assetPathMap 51 | ) 52 | 53 | const updatedContent = scriptTag.innerHTML 54 | 55 | assetLinks.forEach((link) => expect(updatedContent).toContain(link.target)) 56 | }) 57 | }) 58 | 59 | const assetLinks = [ 60 | { 61 | source: 'assets/fa-solid-900.ttf', 62 | target: 'link-to-compiled-webv/assets/fa-solid-900.ttf' 63 | } 64 | ] 65 | -------------------------------------------------------------------------------- /src/utils/spec/saveLog.spec.ts: -------------------------------------------------------------------------------- 1 | import { saveLog } from '../saveLog' 2 | import { LogJson } from '../../types' 3 | 4 | describe('saveLog', () => { 5 | it('should throw an error if there is an error in logData', async () => { 6 | const errorObject = { 7 | errorCode: 5737, 8 | message: 'This is the root node of a diagnostic error.', 9 | details: ['Details are available from the nested error objects.'], 10 | errors: [ 11 | { 12 | errorCode: 5738, 13 | message: 14 | 'The Compute provider for the Job Execution service failed to create job log file "4A3C0C80-87F3-4B44-A94A-68AF37E32521.log".', 15 | details: [ 16 | 'Log or listing output could not be streamed to the Files service.' 17 | ], 18 | errors: [ 19 | { 20 | errorCode: 5208, 21 | message: 22 | 'Log or listing output could not be streamed to the Files service.', 23 | details: [ 24 | 'The file "FileResource1684161631532" cannot be uploaded because it is larger than 100 megabytes.', 25 | 'Files service errorCode=124009 httpStatusCode=400.' 26 | ], 27 | links: [], 28 | version: 2, 29 | httpStatusCode: 500 30 | } 31 | ], 32 | links: [], 33 | version: 2, 34 | httpStatusCode: 500 35 | } 36 | ], 37 | links: [], 38 | version: 2, 39 | httpStatusCode: 207 40 | } 41 | const expectedError = JSON.stringify(errorObject, null, 2) 42 | 43 | const logData: LogJson = { items: [], error: errorObject } 44 | 45 | await expect(() => saveLog(logData, undefined, '', true)).rejects.toEqual( 46 | expectedError 47 | ) 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /src/utils/displayResult.ts: -------------------------------------------------------------------------------- 1 | import { LogLevel } from '@sasjs/utils' 2 | 3 | export function displaySuccess(message: string) { 4 | process.logger?.success(message) 5 | } 6 | 7 | export function displayError(err: any, errorMessage: string = '') { 8 | if (errorMessage) errorMessage = `${errorMessage}\n` 9 | 10 | if (err) { 11 | let failureDetails = '' 12 | 13 | if (err.hasOwnProperty('error')) { 14 | let body = err.error || null 15 | 16 | if (body) { 17 | const message = body.message || '' 18 | let details = body.details || '' 19 | let raw = body.raw || '' 20 | 21 | if (typeof details === 'object') details = JSON.stringify(details) 22 | if (typeof raw === 'object') raw = JSON.stringify(raw) 23 | 24 | failureDetails = `${message}${details ? '\n' + details : ''}${ 25 | raw ? '\n' + raw : '' 26 | }` 27 | 28 | process.logger?.error(errorMessage, failureDetails) 29 | return `${errorMessage}${failureDetails}` 30 | } 31 | } else if (err.hasOwnProperty('message')) { 32 | failureDetails = err.message 33 | } else if ( 34 | err.hasOwnProperty('body') && 35 | err.body.hasOwnProperty('message') 36 | ) { 37 | if (err.body.hasOwnProperty('details')) failureDetails = err.body.details 38 | } else { 39 | failureDetails = typeof err === 'object' ? JSON.stringify(err) : err 40 | failureDetails = failureDetails !== '{}' ? failureDetails : '' 41 | } 42 | 43 | process.logger?.error(errorMessage, failureDetails) 44 | if (err instanceof Error && process.env.LOG_LEVEL === LogLevel.Debug) { 45 | process.logger?.error(err.stack || '') 46 | } 47 | return `${errorMessage}${failureDetails}` 48 | } else { 49 | if (errorMessage) process.logger?.error(errorMessage) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/commands/create/create.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | import { 4 | setupNpmProject, 5 | setupGitIgnore, 6 | setupDoxygen, 7 | createReactApp, 8 | createAngularApp, 9 | createMinimalApp, 10 | createTemplateApp 11 | } from '../../utils/utils' 12 | 13 | import { createFolder, fileExists } from '@sasjs/utils' 14 | import { createReadme } from './internal/createReadme' 15 | import { createFileStructure } from '../shared/createFileStructure' 16 | import { createLintConfigFile } from '../shared/createLintConfigFile' 17 | 18 | export async function create(parentFolderName: string, appType: string) { 19 | process.logger?.info('Creating folders and files...') 20 | if (parentFolderName !== '.') { 21 | await createFolder(path.join(process.projectDir, parentFolderName)) 22 | } 23 | 24 | if (appType === 'react') { 25 | await createReactApp(path.join(process.projectDir, parentFolderName)) 26 | } else if (appType === 'angular') { 27 | await createAngularApp(path.join(process.projectDir, parentFolderName)) 28 | } else if (appType === 'minimal') { 29 | await createMinimalApp(path.join(process.projectDir, parentFolderName)) 30 | } else if (appType) { 31 | await createTemplateApp( 32 | path.join(process.projectDir, parentFolderName), 33 | appType 34 | ) 35 | } else { 36 | await createFileStructure(parentFolderName) 37 | } 38 | 39 | if (!appType) { 40 | await setupNpmProject(parentFolderName) 41 | } 42 | 43 | await setupGitIgnore(parentFolderName) 44 | await setupDoxygen(parentFolderName) 45 | await createReadme(parentFolderName) 46 | 47 | const lintConfigPath = path.join( 48 | process.projectDir, 49 | parentFolderName, 50 | '.sasjslint' 51 | ) 52 | 53 | if (!(await fileExists(lintConfigPath))) { 54 | await createLintConfigFile(parentFolderName) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/commands/db/spec/dbCommand.spec.ts: -------------------------------------------------------------------------------- 1 | import { Logger, LogLevel } from '@sasjs/utils/logger' 2 | import { ReturnCode } from '../../../types/command' 3 | import * as dbModule from '../db' 4 | import { DbCommand } from '../dbCommand' 5 | 6 | describe('DbCommand', () => { 7 | const defaultArgs = ['node', 'sasjs'] 8 | 9 | beforeEach(() => { 10 | setupMocks() 11 | }) 12 | 13 | it('should parse a sasjs db command', () => { 14 | const args = [...defaultArgs, 'db'] 15 | 16 | const command = new DbCommand(args) 17 | 18 | expect(command.name).toEqual('db') 19 | }) 20 | 21 | it('should call the db handler when executed', async () => { 22 | const args = [...defaultArgs, 'db'] 23 | const command = new DbCommand(args) 24 | 25 | const returnCode = await command.execute() 26 | 27 | expect(dbModule.buildDB).toHaveBeenCalled() 28 | expect(returnCode).toEqual(ReturnCode.Success) 29 | expect(process.logger.success).toHaveBeenCalledWith('DB build completed!') 30 | }) 31 | 32 | it('should return an error code when the execution errors out', async () => { 33 | const args = [...defaultArgs, 'db'] 34 | jest 35 | .spyOn(dbModule, 'buildDB') 36 | .mockImplementation(() => Promise.reject(new Error('Test Error'))) 37 | const command = new DbCommand(args) 38 | 39 | const returnCode = await command.execute() 40 | 41 | expect(returnCode).toEqual(ReturnCode.InternalError) 42 | expect(process.logger.error).toHaveBeenCalledWith( 43 | 'Error building DB: ', 44 | new Error('Test Error') 45 | ) 46 | }) 47 | }) 48 | 49 | const setupMocks = () => { 50 | jest.resetAllMocks() 51 | jest.mock('../db') 52 | jest.spyOn(dbModule, 'buildDB').mockImplementation(() => Promise.resolve()) 53 | 54 | process.logger = new Logger(LogLevel.Off) 55 | jest.spyOn(process.logger, 'success') 56 | jest.spyOn(process.logger, 'error') 57 | } 58 | -------------------------------------------------------------------------------- /src/types/testing.ts: -------------------------------------------------------------------------------- 1 | import { ServerType } from '@sasjs/utils' 2 | 3 | export interface TestFlow { 4 | testSetUp?: string 5 | testTearDown?: string 6 | tests: string[] 7 | } 8 | 9 | export enum CoverageType { 10 | service = 'service', 11 | job = 'job', 12 | macro = 'macro', 13 | test = 'test' 14 | } 15 | 16 | export enum CoverageState { 17 | notCovered = 'not covered', 18 | standalone = 'standalone' 19 | } 20 | 21 | export interface Coverage { 22 | [key: string]: { 23 | Type: CoverageType 24 | Coverage: CoverageState 25 | } 26 | } 27 | 28 | export interface TestResultDescription { 29 | TEST_DESCRIPTION: string 30 | TEST_RESULT: TestResultStatus.pass | TestResultStatus.fail 31 | TEST_COMMENT?: string 32 | } 33 | 34 | export enum TestResultStatus { 35 | pass = 'PASS', 36 | fail = 'FAIL', 37 | notProvided = 'not provided' 38 | } 39 | 40 | export interface TestDescription { 41 | test_target: string 42 | results: TestResult[] 43 | } 44 | export interface TestResult { 45 | test_loc: string 46 | sasjs_test_id: string 47 | result: TestResultStatus.notProvided | TestResultDescription[] 48 | test_url: string 49 | } 50 | export interface TestResults { 51 | sasjs_test_meta: TestDescription[] 52 | csv_result_path?: string 53 | xml_result_path?: string 54 | coverage_report_path?: string 55 | failed_to_complete?: number 56 | completed_with_failures?: number 57 | tests_with_results?: string 58 | tests_that_pass?: string 59 | target_name?: string 60 | target_server_url?: string 61 | target_server_type?: ServerType 62 | local_date_time?: string 63 | local_user_id?: string 64 | local_machine_name?: string 65 | } 66 | 67 | export interface TestResultCsv { 68 | test_target: string 69 | test_loc: string 70 | sasjs_test_id: string 71 | test_suite_result: TestResultStatus.pass | TestResultStatus.fail 72 | test_description: string 73 | test_url: string 74 | } 75 | -------------------------------------------------------------------------------- /src/commands/fs/internal/executeCode.ts: -------------------------------------------------------------------------------- 1 | import { Target, ServerType, decodeFromBase64 } from '@sasjs/utils' 2 | import { 3 | isSasJsServerInServerMode, 4 | getAuthConfig, 5 | getSASjsAndAuthConfig, 6 | getSASjs 7 | } from '../../../utils/' 8 | 9 | export const executeCode = async (target: Target, code: string) => { 10 | if (target.serverType === ServerType.SasViya) 11 | return await executeOnSasViya(target, code) 12 | 13 | if (target.serverType === ServerType.Sas9) 14 | return await executeOnSas9(target, code) 15 | 16 | return await executeOnSasJS(target, code) 17 | } 18 | 19 | const executeOnSasViya = async (target: Target, code: string) => { 20 | const { sasjs, authConfig } = await getSASjsAndAuthConfig(target) 21 | 22 | const contextName = target.contextName ?? sasjs.getSasjsConfig().contextName 23 | 24 | const { log } = await sasjs.executeScript({ 25 | fileName: 'program.sas', 26 | linesOfCode: code.split('\n'), 27 | contextName, 28 | authConfig 29 | }) 30 | 31 | return { log } 32 | } 33 | 34 | const executeOnSas9 = async (target: Target, code: string) => { 35 | const { sasjs, authConfigSas9 } = await getSASjsAndAuthConfig(target) 36 | const userName = authConfigSas9!.userName 37 | const password = decodeFromBase64(authConfigSas9!.password) 38 | 39 | const executionResult = await sasjs.executeScript({ 40 | linesOfCode: code.split('\n'), 41 | authConfigSas9: { userName, password } 42 | }) 43 | 44 | return { log: executionResult } 45 | } 46 | 47 | const executeOnSasJS = async (target: Target, code: string) => { 48 | const authConfig = (await isSasJsServerInServerMode(target)) 49 | ? await getAuthConfig(target) 50 | : undefined 51 | const sasjs = getSASjs(target) 52 | 53 | const executionResult = await sasjs.executeScript({ 54 | linesOfCode: code.split('\n'), 55 | runTime: 'sas', 56 | authConfig 57 | }) 58 | 59 | return { log: executionResult } 60 | } 61 | -------------------------------------------------------------------------------- /src/commands/version/spec/versionCommand.spec.ts: -------------------------------------------------------------------------------- 1 | import { Logger, LogLevel } from '@sasjs/utils/logger' 2 | import { ReturnCode } from '../../../types/command' 3 | import * as versionModule from '../version' 4 | import { VersionCommand } from '../versionCommand' 5 | 6 | describe('VersionCommand', () => { 7 | const defaultArgs = ['node', 'sasjs'] 8 | 9 | beforeEach(() => { 10 | setupMocks() 11 | }) 12 | 13 | it('should parse a sasjs version command', () => { 14 | const args = [...defaultArgs, 'version'] 15 | 16 | const command = new VersionCommand(args) 17 | 18 | expect(command.name).toEqual('version') 19 | }) 20 | 21 | it('should call the version handler when executed', async () => { 22 | const args = [...defaultArgs, 'version'] 23 | const command = new VersionCommand(args) 24 | 25 | const returnCode = await command.execute() 26 | 27 | expect(versionModule.printVersion).toHaveBeenCalled() 28 | expect(returnCode).toEqual(ReturnCode.Success) 29 | }) 30 | 31 | it('should return an error code when the execution errors out', async () => { 32 | const args = [...defaultArgs, 'version'] 33 | jest 34 | .spyOn(versionModule, 'printVersion') 35 | .mockImplementation(() => Promise.reject(new Error('Test Error'))) 36 | const command = new VersionCommand(args) 37 | 38 | const returnCode = await command.execute() 39 | 40 | expect(returnCode).toEqual(ReturnCode.InternalError) 41 | expect(process.logger.error).toHaveBeenCalledWith( 42 | 'An error has occurred while checking version.\n', 43 | 'Test Error' 44 | ) 45 | }) 46 | }) 47 | 48 | const setupMocks = () => { 49 | jest.resetAllMocks() 50 | jest.mock('../version') 51 | jest 52 | .spyOn(versionModule, 'printVersion') 53 | .mockImplementation(() => Promise.resolve('')) 54 | 55 | process.logger = new Logger(LogLevel.Off) 56 | jest.spyOn(process.logger, 'success') 57 | jest.spyOn(process.logger, 'error') 58 | } 59 | -------------------------------------------------------------------------------- /src/commands/build/internal/getLaunchPageCode.ts: -------------------------------------------------------------------------------- 1 | import { ServerTypeError } from '@sasjs/utils/error' 2 | import { ServerType } from '@sasjs/utils/types' 3 | 4 | const SAS9Code = (streamServiceName: string) => ` 5 | options notes; 6 | data _null_; 7 | format url $256.; 8 | rc=METADATA_GETURI("Stored Process Web App",url); 9 | url=coalescec(url,"localhost/SASStoredProcess"); 10 | urlEscaped = tranwrd(trim(url)," ","%20"); 11 | putlog "NOTE: SASjs Streaming App Created! Check it out here:" ; 12 | putlog "NOTE- ";putlog "NOTE- ";putlog "NOTE- ";putlog "NOTE- "; 13 | putlog "NOTE- " urlEscaped +(-1) "?_program=&appLoc/services/${streamServiceName}" ; 14 | putlog "NOTE- ";putlog "NOTE- ";putlog "NOTE- ";putlog "NOTE- "; 15 | run; 16 | ` 17 | const SASViyaCode = (streamServiceName: string) => ` 18 | /* Tell the user where the app was deployed so they can open it */ 19 | options notes; 20 | data _null_; 21 | if symexist('_baseurl') then do; 22 | url=symget('_baseurl'); 23 | if subpad(url,length(url)-9,9)='SASStudio' 24 | then url=substr(url,1,length(url)-11); 25 | else url="&systcpiphostname"; 26 | end; 27 | else url="&systcpiphostname"; 28 | url=cats(url,"/SASJobExecution?_FILE=&appLoc/services/"); 29 | urlEscaped = tranwrd(trim(url)," ","%20"); 30 | putlog "NOTE: SASjs Streaming App Created! Check it out here:" ; 31 | putlog "NOTE- ";putlog "NOTE- ";putlog "NOTE- ";putlog "NOTE- "; 32 | putlog "NOTE- " urlEscaped +(-1) '${streamServiceName}.html' ; 33 | putlog "NOTE- ";putlog "NOTE- ";putlog "NOTE- ";putlog "NOTE- "; 34 | run; 35 | ` 36 | 37 | export const getLaunchPageCode = ( 38 | serverType: ServerType, 39 | streamServiceName: string 40 | ): string => { 41 | switch (serverType) { 42 | case ServerType.SasViya: 43 | return SASViyaCode(streamServiceName) 44 | 45 | case ServerType.Sas9: 46 | return SAS9Code(streamServiceName) 47 | 48 | default: 49 | throw new ServerTypeError([ServerType.SasViya, ServerType.Sas9]) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/commands/create/createCommand.ts: -------------------------------------------------------------------------------- 1 | import { create } from './create' 2 | import { CommandExample, ReturnCode } from '../../types/command' 3 | import { CreateTemplate } from './createTemplate' 4 | import { CommandBase } from '../../types' 5 | 6 | const syntax = 'create [folderName]' 7 | const usage = 'sasjs create [folder-name] [options]' 8 | const description = 9 | 'Create a SASjs app in the specified folder. You can also specify a template to scaffold your app with.' 10 | const examples: CommandExample[] = [ 11 | { 12 | command: 'sasjs create my-app --template react', 13 | description: '' 14 | } 15 | ] 16 | 17 | export class CreateCommand extends CommandBase { 18 | constructor(args: string[]) { 19 | const parseOptions: { [key: string]: Object } = { 20 | template: { 21 | alias: 't', 22 | choices: [ 23 | CreateTemplate.Angular, 24 | CreateTemplate.React, 25 | CreateTemplate.Minimal, 26 | CreateTemplate.Jobs, 27 | CreateTemplate.SasOnly 28 | ] 29 | } 30 | } 31 | super(args, { 32 | parseOptions, 33 | syntax, 34 | usage, 35 | description, 36 | examples 37 | }) 38 | } 39 | 40 | public get template(): CreateTemplate { 41 | return this.parsed.template as CreateTemplate 42 | } 43 | 44 | public get folderName() { 45 | return (this.parsed.folderName as string) || '.' 46 | } 47 | 48 | public async execute() { 49 | return await create(this.folderName, this.template) 50 | .then(() => { 51 | process.logger?.success( 52 | `Project${ 53 | this.folderName ? ` ${this.folderName} created` : ` updated` 54 | } successfully.\nGet ready to unleash your SAS!` 55 | ) 56 | return ReturnCode.Success 57 | }) 58 | .catch((err: any) => { 59 | process.logger?.error('Error while creating your project: ', err) 60 | return ReturnCode.InternalError 61 | }) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/types/command/targetCommand.ts: -------------------------------------------------------------------------------- 1 | import { Target } from '@sasjs/utils' 2 | import { 3 | findTargetInConfiguration, 4 | loadTargetEnvVariables, 5 | validateTargetName, 6 | getLocalConfig, 7 | getGlobalRcFile, 8 | setConstants 9 | } from '../../utils' 10 | import { CommandBase, CommandOptions } from './commandBase' 11 | import { ReturnCode } from './returnCode' 12 | 13 | export class TargetCommand extends CommandBase { 14 | protected _targetInfo?: { target: Target; isLocal: boolean } 15 | 16 | constructor(args: string[], options: CommandOptions) { 17 | const parseOptions = { 18 | ...(options.parseOptions || {}), 19 | target: { 20 | type: 'string', 21 | alias: 't', 22 | description: 'The target to execute this command against.' 23 | } 24 | } 25 | 26 | super(args, { ...options, parseOptions }) 27 | } 28 | 29 | public async getTargetInfo(): Promise<{ target: Target; isLocal: boolean }> { 30 | if (this._targetInfo) { 31 | return Promise.resolve(this._targetInfo) 32 | } 33 | 34 | const targetName = validateTargetName(this.parsed.target as string) 35 | 36 | await loadTargetEnvVariables(targetName).catch((err) => { 37 | process.logger?.error( 38 | `Error loading environment variables for target ${targetName}: `, 39 | err 40 | ) 41 | process.exit(ReturnCode.InternalError) 42 | }) 43 | 44 | return await findTargetInConfiguration(targetName) 45 | .then(async (res) => { 46 | this._targetInfo = res 47 | 48 | const configuration = res.isLocal 49 | ? await getLocalConfig() 50 | : await getGlobalRcFile() 51 | 52 | await setConstants(res.isLocal, res.target, configuration) 53 | process.sasjsConfig = configuration 54 | 55 | return res 56 | }) 57 | .catch((err) => { 58 | process.logger?.error('Error reading target from configuration: ', err) 59 | process.exit(ReturnCode.InternalError) 60 | }) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/commands/run/spec/run.spec.server.sas9.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv' 2 | import path from 'path' 3 | import { runSasCode } from '../..' 4 | import { copy, Target, generateTimestamp, ServerType } from '@sasjs/utils' 5 | import { 6 | createTestApp, 7 | generateTestTarget, 8 | removeTestApp, 9 | removeTestServerFolder 10 | } from '../../../utils/test' 11 | 12 | describe('sasjs run with SAS9', () => { 13 | let target: Target 14 | 15 | beforeEach(async () => { 16 | const appName = 'cli-tests-run-sas9-' + generateTimestamp() 17 | await createTestApp(__dirname, appName) 18 | target = generateTestTarget( 19 | appName, 20 | `/Public/app/cli-tests/${appName}`, 21 | { 22 | serviceFolders: ['sasjs/testServices'], 23 | initProgram: '', 24 | termProgram: '', 25 | macroVars: {} 26 | }, 27 | ServerType.Sas9 28 | ) 29 | await copy( 30 | path.join(__dirname, 'testServices'), 31 | path.join(process.projectDir, 'sasjs', 'testServices') 32 | ) 33 | }) 34 | 35 | afterEach(async () => { 36 | await removeTestServerFolder(target.appLoc, target) 37 | await removeTestApp(__dirname, target.name) 38 | }) 39 | 40 | it('should run a file when a relative path is provided', async () => { 41 | const logParts = ['data;', 'do x=1 to 100;', 'output;', 'end;', 'run;'] 42 | 43 | const result: any = await runSasCode( 44 | target, 45 | 'sasjs/testServices/logJob.sas' 46 | ) 47 | 48 | logParts.forEach((logPart) => { 49 | expect(result.log.includes(logPart)).toBeTruthy() 50 | }) 51 | }) 52 | 53 | it('should run a file when an absolute path is provided', async () => { 54 | const logParts = ['data;', 'do x=1 to 100;', 'output;', 'end;', 'run;'] 55 | 56 | const result: any = await runSasCode( 57 | target, 58 | `${process.projectDir}/sasjs/testServices/logJob.sas` 59 | ) 60 | 61 | logParts.forEach((logPart) => { 62 | expect(result.log.includes(logPart)).toBeTruthy() 63 | }) 64 | }) 65 | }) 66 | -------------------------------------------------------------------------------- /src/commands/context/export.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { createFile } from '@sasjs/utils' 3 | import { sanitizeFileName } from '../../utils/file' 4 | import SASjs from '@sasjs/adapter/node' 5 | 6 | /** 7 | * Export compute context to json file in current folder. 8 | * @param {string} contextName - name of the context to export. 9 | * @param {object} sasjs - configuration object of SAS adapter. 10 | * @param {string} accessToken - an access token for an authorized user. 11 | */ 12 | export async function exportContext( 13 | contextName: string, 14 | sasjs: SASjs, 15 | accessToken: string 16 | ) { 17 | const context = await sasjs 18 | .getComputeContextByName(contextName, accessToken) 19 | .catch((err) => { 20 | process.logger?.error( 21 | `An error has occurred while fetching context ${contextName}: `, 22 | err 23 | ) 24 | throw err 25 | }) 26 | 27 | if (context && context.id) { 28 | const contextAllAttributes = await sasjs 29 | .getComputeContextById(context.id, accessToken) 30 | .catch((err) => { 31 | process.logger?.error( 32 | `An error has occurred while fetching context ${contextName}: `, 33 | err 34 | ) 35 | throw err 36 | }) 37 | 38 | if (contextAllAttributes) { 39 | delete (contextAllAttributes as any).links 40 | 41 | let output 42 | 43 | try { 44 | output = JSON.stringify(contextAllAttributes, null, 2) 45 | } catch (error) { 46 | process.logger?.error('Error stringifying context JSON: ', error) 47 | throw error 48 | } 49 | 50 | const outputFileName = sanitizeFileName(contextName) + '.json' 51 | const outputPath = path.join(process.cwd(), outputFileName) 52 | 53 | await createFile(outputPath, output).catch((err) => { 54 | process.logger?.error('Error creating context JSON file: ', err) 55 | throw err 56 | }) 57 | 58 | process.logger?.success( 59 | `Context successfully exported to '${outputPath}'.` 60 | ) 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/commands/snippets/snippetsCommand.ts: -------------------------------------------------------------------------------- 1 | import { generateSnippets } from './snippets' 2 | import { CommandExample, ReturnCode } from '../../types/command' 3 | import { TargetCommand } from '../../types/command/targetCommand' 4 | import { getMacroFolders } from '../../utils' 5 | 6 | const syntax = 'snippets' 7 | const usage = 'sasjs snippets' 8 | const description = `Generates VS Code snippets from the Doxygen headers in the SAS Macros.` 9 | const examples: CommandExample[] = [ 10 | { 11 | command: 'sasjs snippets', 12 | description: description 13 | } 14 | ] 15 | 16 | const parseOptions = { 17 | outDirectory: { 18 | type: 'string', 19 | alias: 'o', 20 | description: 21 | 'Path to the directory where the VS Code snippets output will be generated.' 22 | } 23 | } 24 | 25 | /** 26 | * 'snippets' command class. 27 | */ 28 | export class SnippetsCommand extends TargetCommand { 29 | constructor(args: string[]) { 30 | super(args, { syntax, usage, description, examples, parseOptions }) 31 | } 32 | 33 | /** 34 | * Command execution method. 35 | * @returns promise that results into return code. 36 | */ 37 | public async execute() { 38 | const { target } = await this.getTargetInfo() 39 | const macroFolders = await getMacroFolders(target) 40 | const { outDirectory } = this.parsed 41 | 42 | // Generate snippets 43 | return await generateSnippets(macroFolders, outDirectory as string) 44 | .then((filePath) => { 45 | // handle command execution success 46 | process.logger?.success( 47 | `VS Code snippets generated! File location: ${filePath}` 48 | ) 49 | process.logger?.info( 50 | `Follow these instructions https://cli.sasjs.io/snippets/#import-snippets-to-vs-code to import generated VS Code snippets into your VS Code.` 51 | ) 52 | 53 | return ReturnCode.Success 54 | }) 55 | .catch((err) => { 56 | // handle command execution failure 57 | process.logger?.error('Error generating VS Code snippets: ', err) 58 | 59 | return ReturnCode.InternalError 60 | }) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/commands/docs/internal/getFoldersForDocs.ts: -------------------------------------------------------------------------------- 1 | import { Target, Configuration, getAbsolutePath } from '@sasjs/utils' 2 | 3 | /** 4 | * Returns list of folders for documentation( macroCore / macros / SAS programs/ services / jobs ) 5 | * @param {Target} target- target for docs. 6 | * @param {Configuration} config- sasjsconfig.json 7 | */ 8 | export function getFoldersForDocs(target?: Target, config?: Configuration) { 9 | let macroCore = [] 10 | 11 | const rootFolders = extractFoldersForDocs(config) 12 | macroCore = rootFolders.macroCore 13 | 14 | const targetFolders = extractFoldersForDocs(target) 15 | if (target?.docConfig?.displayMacroCore !== undefined) 16 | macroCore = targetFolders.macroCore 17 | 18 | return { 19 | macroCore, 20 | macro: rootFolders.macro.concat(targetFolders.macro), 21 | program: rootFolders.program.concat(targetFolders.program), 22 | service: rootFolders.service.concat(targetFolders.service), 23 | job: rootFolders.job.concat(targetFolders.job) 24 | } 25 | } 26 | 27 | function extractFoldersForDocs(config?: Target | Configuration) { 28 | const { buildSourceFolder } = process.sasjsConstants 29 | 30 | const macroCoreFolders = 31 | config?.docConfig?.displayMacroCore === false 32 | ? [] 33 | : [process.sasjsConstants.macroCorePath] 34 | 35 | const macroFolders = config?.macroFolders 36 | ? config.macroFolders.map((f) => getAbsolutePath(f, buildSourceFolder)) 37 | : [] 38 | const programFolders = config?.programFolders 39 | ? config.programFolders.map((f) => getAbsolutePath(f, buildSourceFolder)) 40 | : [] 41 | const serviceFolders = config?.serviceConfig?.serviceFolders 42 | ? config.serviceConfig.serviceFolders.map((f) => 43 | getAbsolutePath(f, buildSourceFolder) 44 | ) 45 | : [] 46 | const jobFolders = config?.jobConfig?.jobFolders 47 | ? config.jobConfig.jobFolders.map((f) => 48 | getAbsolutePath(f, buildSourceFolder) 49 | ) 50 | : [] 51 | 52 | return { 53 | macroCore: macroCoreFolders, 54 | macro: macroFolders, 55 | program: programFolders, 56 | service: serviceFolders, 57 | job: jobFolders 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/commands/init/spec/initCommand.spec.ts: -------------------------------------------------------------------------------- 1 | import { Logger, LogLevel } from '@sasjs/utils/logger' 2 | import { ReturnCode } from '../../../types/command' 3 | import * as initModule from '../init' 4 | import { InitCommand } from '../initCommand' 5 | 6 | describe('InitCommand', () => { 7 | const defaultArgs = ['node', 'sasjs'] 8 | 9 | beforeEach(() => { 10 | setupMocks() 11 | }) 12 | 13 | it('should parse a simple sasjs init command', () => { 14 | const args = [...defaultArgs, 'init'] 15 | 16 | const command = new InitCommand(args) 17 | 18 | expect(command.name).toEqual('init') 19 | expect(command.value).toEqual('') 20 | }) 21 | 22 | it('should call the init handler when executed', async () => { 23 | const args = [...defaultArgs, 'init'] 24 | const command = new InitCommand(args) 25 | 26 | const returnCode = await command.execute() 27 | 28 | expect(initModule.init).toHaveBeenCalled() 29 | expect(returnCode).toEqual(ReturnCode.Success) 30 | expect(process.logger.success).toHaveBeenCalledWith( 31 | 'This project is now powered by SASjs!\nYou can use any sasjs command within the project.\n\nFor more information, type `sasjs help` or visit https://cli.sasjs.io/' 32 | ) 33 | }) 34 | 35 | it('should return an error code when the execution errors out', async () => { 36 | const args = [...defaultArgs, 'init'] 37 | jest 38 | .spyOn(initModule, 'init') 39 | .mockImplementation(() => Promise.reject(new Error('Test Error'))) 40 | const command = new InitCommand(args) 41 | 42 | const returnCode = await command.execute() 43 | 44 | expect(initModule.init).toHaveBeenCalled() 45 | expect(returnCode).toEqual(ReturnCode.InternalError) 46 | expect(process.logger.error).toHaveBeenCalledWith( 47 | 'Error initialising SASjs: ', 48 | new Error('Test Error') 49 | ) 50 | }) 51 | }) 52 | 53 | const setupMocks = () => { 54 | jest.resetAllMocks() 55 | jest.mock('../init') 56 | jest.spyOn(initModule, 'init').mockImplementation(() => Promise.resolve()) 57 | 58 | process.logger = new Logger(LogLevel.Off) 59 | jest.spyOn(process.logger, 'success') 60 | jest.spyOn(process.logger, 'error') 61 | } 62 | -------------------------------------------------------------------------------- /src/commands/create/internal/createReadme.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | import { createFile, fileExists } from '@sasjs/utils' 4 | 5 | const contentReadMe = ` 6 | # SASjs Project Repo 7 | 8 | ## Contributing 9 | 10 | Contributions are warmly welcomed! To avoid any misunderstandings, do please first discuss the change you wish to make via issue, email, or any other method with the owners of this repository before submitting a PR. 11 | 12 | Please note we have a [code of conduct](https://www.contributor-covenant.org/version/2/0/code_of_conduct/), please follow it in all your interactions with the project. 13 | 14 | ## Environment Setup 15 | 16 | This repository makes use of the [SASjs](https://sasjs.io) framework for code organisation, compilation, documentation, and deployment. The following tools are highly recommended: 17 | 18 | - [NPM](https://sasjs.io/windows/#npm) - the runtime and dependency manager for [SASjs CLI](https://cli.sasjs.io) (batteries included) 19 | - [VSCode](https://sasjs.io/windows/#vscode) - feature packed IDE for code editing (warning - highly effective!) 20 | - [GIT](https://sasjs.io/windows/#git) - a safety net you cannot (and should not) do without. 21 | 22 | For generating the documentation (\`sasjs doc\`) it is also necessary to install [doxygen](https://www.doxygen.nl/manual/install.html). 23 | 24 | To get configured: 25 | 26 | 1. Clone the repository 27 | 2. Install local dependencies (\`npm install\`) 28 | 3. Install the SASjs CLI globally (\`npm install -g @sasjs/cli\`) 29 | 4. Add a target, and authentication (\`npm add\`). See [docs](https://cli.sasjs.io/add/). 30 | 31 | To contribute: 32 | 33 | 1. Create your feature branch (\`git checkout -b myfeature\`) 34 | 2. Make your change 35 | 3. Commit the change, using the [conventional commit](https://www.conventionalcommits.org/en/v1.0.0) standard 36 | 4. Push and make a PR 37 | ` 38 | 39 | export async function createReadme(folderPath: string) { 40 | const readMeDestinationPath = path.join( 41 | process.projectDir, 42 | folderPath, 43 | 'README.md' 44 | ) 45 | const pathExists = await fileExists(readMeDestinationPath) 46 | if (!pathExists) await createFile(readMeDestinationPath, contentReadMe) 47 | } 48 | -------------------------------------------------------------------------------- /src/commands/servicepack/spec/servicepack.spec.server.viya.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv' 2 | import path from 'path' 3 | import { ServerType, Target, generateTimestamp } from '@sasjs/utils' 4 | import { contextName, setConstants } from '../../../utils' 5 | import { servicePackDeploy } from '../deploy' 6 | import { removeTestServerFolder } from '../../../utils/test' 7 | 8 | describe('sasjs servicepack with Viya', () => { 9 | let target: Target 10 | 11 | beforeAll(async () => { 12 | dotenv.config() 13 | 14 | await setConstants() 15 | 16 | const targetName = 'cli-tests-servicepack-' + generateTimestamp() 17 | target = new Target({ 18 | name: targetName, 19 | serverType: ServerType.SasViya, 20 | serverUrl: process.env.VIYA_SERVER_URL as string, 21 | appLoc: `/Public/app/cli-tests/${targetName}`, 22 | contextName, 23 | authConfig: { 24 | client: process.env.CLIENT as string, 25 | secret: process.env.SECRET as string, 26 | access_token: process.env.ACCESS_TOKEN as string, 27 | refresh_token: process.env.REFRESH_TOKEN as string 28 | }, 29 | macroFolders: [], 30 | programFolders: [] 31 | }) 32 | 33 | process.projectDir = path.join(process.cwd()) 34 | process.currentDir = process.projectDir 35 | }) 36 | 37 | describe('processServicepack', () => { 38 | it( 39 | 'should deploy servicepack', 40 | async () => { 41 | await expect( 42 | servicePackDeploy( 43 | target, 44 | false, 45 | 'src/commands/servicepack/spec/testServicepack.json', 46 | true 47 | ) 48 | ).resolves.toEqual(true) 49 | }, 50 | 60 * 1000 51 | ) 52 | 53 | it( 54 | 'should fail because servicepack already been deployed', 55 | async () => { 56 | await expect( 57 | servicePackDeploy( 58 | target, 59 | false, 60 | 'src/commands/servicepack/spec/testServicepack.json' 61 | ) 62 | ).resolves.toEqual(false) 63 | }, 64 | 60 * 1000 65 | ) 66 | }) 67 | 68 | afterAll(async () => { 69 | await removeTestServerFolder(target.appLoc, target) 70 | }, 60 * 1000) 71 | }) 72 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | The purpose of this repo is to create the `sasjs` command line utility. The build and release happens automatically when merging with master. 4 | 5 | ## Prerequisites 6 | 7 | * NPM (v7) 8 | * Doxygen (v1.9.1) - https://www.doxygen.nl/download.html 9 | * Jest (`npm i -g jest`) - not needed for build, but useful for debugging 10 | 11 | ## Build Process 12 | For development, the following steps are needed to build: 13 | 14 | ``` 15 | git clone git@github.com:sasjs/cli.git 16 | cd cli 17 | npm ci 18 | npm run build 19 | npm link 20 | npm start 21 | ``` 22 | 23 | The `npm start` script watches for changes in the source files and automatically rebuilds them. Once done, you can use `npm unlink` from the repository to unlink it. If this doesn't work, you can try: `npm rm -g @sasjs/cli`. 24 | 25 | ## Development Notes 26 | 27 | If you want to run `npm run test:server` or anything remote on Viya, you will need to provide credentials - you can rename `.env.example` as `.env` and provide your CLIENT/SECRET/ACCESS_TOKEN/REFRESH_TOKEN. To make this process easier, you can deploy the [Viya Token Generator](https://sasjs.io/apps/#viya-client-token-generator). 28 | 29 | All server-side tests should be written in `name.spec.server.viya.ts`, `name.spec.server.sas9.ts` or `name.spec.server.sasjs.ts` to be conducted with `Viya`, `SAS9` or [SASjs](https://server.sasjs.io) accordingly. There is a way to filter what kind of server types will be used to execute server-side tests as part of `test:server` npm script. To provide server types a comma-separated string containing server types (`viya`, `sas9` or `sasjs`) should be set as Github secret `TEST_SERVER_TYPES`. If Github secret does not exist (local development environment), a comma-separated string containing server type should be provided as `testServerTypes` entry in `package.json`. 30 | 31 | All code should be written in TypeScript. See the checks in the PULL_REQUEST_TEMPLATE.md that must be completed before review. If you can, please test in a Windows environment as 99% of our customers will use the CLI there. 32 | 33 | For support, you can contact team members in our matrix channel. Just reach out to us [here](https://matrix.to/#/%23sasjs:4gl.io) and we'll add you. 34 | -------------------------------------------------------------------------------- /src/commands/flow/internal/spec/checkPredecessorDeadlock.spec.ts: -------------------------------------------------------------------------------- 1 | import { checkPredecessorDeadlock } from '..' 2 | 3 | describe('checkPredecessorDeadlock', () => { 4 | it('should return true, if predecessorDeadlock is present and it is pointing to itself', () => { 5 | const flows = { 6 | flow1: { 7 | jobs: [{ location: 'job' }], 8 | predecessors: ['flow1'] 9 | } 10 | } 11 | 12 | expect(checkPredecessorDeadlock(flows)).toEqual({ 13 | present: true, 14 | chain: ['flow1', 'flow1'] 15 | }) 16 | }) 17 | 18 | it('should return true, if predecessorDeadlock is present and pointing to each other', () => { 19 | const flows = { 20 | flow1: { 21 | jobs: [{ location: 'job' }], 22 | predecessors: ['flow2'] 23 | }, 24 | flow2: { 25 | jobs: [{ location: 'job' }], 26 | predecessors: ['flow1'] 27 | } 28 | } 29 | 30 | expect(checkPredecessorDeadlock(flows)).toEqual({ 31 | present: true, 32 | chain: ['flow1', 'flow2', 'flow1'] 33 | }) 34 | }) 35 | 36 | it('should return true, if predecessorDeadlock is present and pointing to each other indirectly', () => { 37 | const flows = { 38 | flow1: { 39 | jobs: [{ location: 'job' }], 40 | predecessors: ['flow2'] 41 | }, 42 | flow2: { 43 | jobs: [{ location: 'job' }], 44 | predecessors: ['flow3'] 45 | }, 46 | flow3: { 47 | jobs: [{ location: 'job' }], 48 | predecessors: ['flow1'] 49 | } 50 | } 51 | 52 | expect(checkPredecessorDeadlock(flows)).toEqual({ 53 | present: true, 54 | chain: ['flow1', 'flow2', 'flow3', 'flow1'] 55 | }) 56 | }) 57 | 58 | it('should return false, if predecessorDeadlock is not present', () => { 59 | const flows = { 60 | flow1: { 61 | jobs: [{ location: 'job' }], 62 | predecessors: ['flow2'] 63 | }, 64 | flow2: { 65 | jobs: [{ location: 'job' }], 66 | predecessors: [] 67 | }, 68 | flow3: { 69 | jobs: [{ location: 'job' }], 70 | predecessors: ['flow1'] 71 | } 72 | } 73 | 74 | expect(checkPredecessorDeadlock(flows)).toEqual({ present: false }) 75 | }) 76 | }) 77 | --------------------------------------------------------------------------------