diff --git a/.all-contributorsrc b/.all-contributorsrc
new file mode 100644
index 0000000..ca85c07
--- /dev/null
+++ b/.all-contributorsrc
@@ -0,0 +1,84 @@
+{
+ "projectName": "server",
+ "projectOwner": "sasjs",
+ "repoType": "github",
+ "repoHost": "https://github.com",
+ "files": [
+ "README.md"
+ ],
+ "imageSize": 100,
+ "commit": true,
+ "commitConvention": "angular",
+ "contributors": [
+ {
+ "login": "saadjutt01",
+ "name": "Saad Jutt",
+ "avatar_url": "https://avatars.githubusercontent.com/u/8914650?v=4",
+ "profile": "https://github.com/saadjutt01",
+ "contributions": [
+ "code",
+ "test"
+ ]
+ },
+ {
+ "login": "sabhas",
+ "name": "Sabir Hassan",
+ "avatar_url": "https://avatars.githubusercontent.com/u/82647447?v=4",
+ "profile": "https://github.com/sabhas",
+ "contributions": [
+ "code",
+ "test"
+ ]
+ },
+ {
+ "login": "YuryShkoda",
+ "name": "Yury Shkoda",
+ "avatar_url": "https://avatars.githubusercontent.com/u/25773492?v=4",
+ "profile": "https://www.erudicat.com/",
+ "contributions": [
+ "code",
+ "test"
+ ]
+ },
+ {
+ "login": "medjedovicm",
+ "name": "Mihajlo Medjedovic",
+ "avatar_url": "https://avatars.githubusercontent.com/u/18329105?v=4",
+ "profile": "https://github.com/medjedovicm",
+ "contributions": [
+ "code",
+ "test"
+ ]
+ },
+ {
+ "login": "allanbowe",
+ "name": "Allan Bowe",
+ "avatar_url": "https://avatars.githubusercontent.com/u/4420615?v=4",
+ "profile": "https://4gl.io/",
+ "contributions": [
+ "code",
+ "doc"
+ ]
+ },
+ {
+ "login": "VladislavParhomchik",
+ "name": "Vladislav Parhomchik",
+ "avatar_url": "https://avatars.githubusercontent.com/u/83717836?v=4",
+ "profile": "https://github.com/VladislavParhomchik",
+ "contributions": [
+ "test"
+ ]
+ },
+ {
+ "login": "kknapen",
+ "name": "Koen Knapen",
+ "avatar_url": "https://avatars.githubusercontent.com/u/78609432?v=4",
+ "profile": "https://github.com/kknapen",
+ "contributions": [
+ "userTesting"
+ ]
+ }
+ ],
+ "contributorsPerLine": 7,
+ "skipCi": true
+}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f3a72b6..893bae2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,36 @@
+## [0.6.1](https://github.com/sasjs/server/compare/v0.6.0...v0.6.1) (2022-06-17)
+
+
+### Bug Fixes
+
+* home page wording. Using fix to force previous change through.. ([8702a4e](https://github.com/sasjs/server/commit/8702a4e8fd1bbfaf4f426b75e8b85a87ede0e0b0))
+
+# [0.6.0](https://github.com/sasjs/server/compare/v0.5.0...v0.6.0) (2022-06-16)
+
+
+### Features
+
+* get group by group name ([6b0b94a](https://github.com/sasjs/server/commit/6b0b94ad38215ae58e62279a4f73ac3ed2d9d0e8))
+
+# [0.5.0](https://github.com/sasjs/server/compare/v0.4.2...v0.5.0) (2022-06-16)
+
+
+### Bug Fixes
+
+* npm audit fix to avoid warnings on npm i ([28a6a36](https://github.com/sasjs/server/commit/28a6a36bb708b93fb5c2b74d587e9b2e055582be))
+
+
+### Features
+
+* **api:** deployment through zipped/compressed file ([b81d742](https://github.com/sasjs/server/commit/b81d742c6c70d4cf1cab365b0e3efc087441db00))
+
+## [0.4.2](https://github.com/sasjs/server/compare/v0.4.1...v0.4.2) (2022-06-15)
+
+
+### Bug Fixes
+
+* appStream redesign ([73792fb](https://github.com/sasjs/server/commit/73792fb574c90bd280c4324e0b41c6fee7d572b6))
+
## [0.4.1](https://github.com/sasjs/server/compare/v0.4.0...v0.4.1) (2022-06-15)
diff --git a/README.md b/README.md
index d3d209a..d2dec6a 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,7 @@
# SASjs Server
+
+[](#contributors-)
+
SASjs Server provides a NodeJS wrapper for calling the SAS binary executable. It can be installed on an actual SAS server, or locally on your desktop. It provides:
@@ -196,3 +199,29 @@ The following credentials can be used for the initial connection to SASjs/server
- CLIENTID: `clientID1`
- USERNAME: `secretuser`
- PASSWORD: `secretpassword`
+
+## Contributors ✨
+
+Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
+
+
+
+
+
+
+
+
+
+
+
+This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
\ No newline at end of file
diff --git a/api/package-lock.json b/api/package-lock.json
index 9da3f1d..6be30ee 100644
--- a/api/package-lock.json
+++ b/api/package-lock.json
@@ -25,12 +25,14 @@
"morgan": "^1.10.0",
"multer": "^1.4.3",
"swagger-ui-express": "4.3.0",
+ "unzipper": "^0.10.11",
"url": "^0.10.3"
},
"bin": {
"api": "build/src/server.js"
},
"devDependencies": {
+ "@types/adm-zip": "^0.5.0",
"@types/bcryptjs": "^2.4.2",
"@types/cookie-parser": "^1.4.2",
"@types/cors": "^2.8.12",
@@ -45,6 +47,8 @@
"@types/node": "^15.12.2",
"@types/supertest": "^2.0.11",
"@types/swagger-ui-express": "^4.1.3",
+ "@types/unzipper": "^0.10.5",
+ "adm-zip": "^0.5.9",
"dotenv": "^10.0.0",
"http-headers-validation": "^0.0.1",
"jest": "^27.0.6",
@@ -1753,6 +1757,15 @@
"yarn": ">=1.9.4"
}
},
+ "node_modules/@types/adm-zip": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.5.0.tgz",
+ "integrity": "sha512-FCJBJq9ODsQZUNURo5ILAQueuA8WJhRvuihS3ke2iI25mJlfV2LK8jG2Qj2z2AWg8U0FtWWqBHVRetceLskSaw==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/babel__core": {
"version": "7.1.15",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.15.tgz",
@@ -2176,6 +2189,15 @@
"integrity": "sha512-MhSa0yylXtVMsyT8qFpHA1DLHj4DvQGH5ntxrhHSh8PxUVNi35Wk+P5hVgqbO2qZqOotqr9jaoPRL+iRjWYm/A==",
"dev": true
},
+ "node_modules/@types/unzipper": {
+ "version": "0.10.5",
+ "resolved": "https://registry.npmjs.org/@types/unzipper/-/unzipper-0.10.5.tgz",
+ "integrity": "sha512-NrLJb29AdnBARpg9S/4ktfPEisbJ0AvaaAr3j7Q1tg8AgcEUsq2HqbNzvgLRoWyRtjzeLEv7vuL39u1mrNIyNA==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/webidl-conversions": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-6.1.1.tgz",
@@ -2272,6 +2294,15 @@
"node": ">=0.4.0"
}
},
+ "node_modules/adm-zip": {
+ "version": "0.5.9",
+ "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.9.tgz",
+ "integrity": "sha512-s+3fXLkeeLjZ2kLjCBwQufpI5fuN+kIGBxu6530nVQZGVol0d7Y/M88/xw9HGGUcJjKf8LutN3VPRUBq6N7Ajg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.0"
+ }
+ },
"node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
@@ -2684,6 +2715,26 @@
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
"integrity": "sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms="
},
+ "node_modules/big-integer": {
+ "version": "1.6.51",
+ "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz",
+ "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/binary": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz",
+ "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==",
+ "dependencies": {
+ "buffers": "~0.1.1",
+ "chainsaw": "~0.1.0"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
@@ -2710,6 +2761,11 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
},
+ "node_modules/bluebird": {
+ "version": "3.4.7",
+ "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz",
+ "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA=="
+ },
"node_modules/bn.js": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
@@ -2881,6 +2937,22 @@
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A=="
},
+ "node_modules/buffer-indexof-polyfill": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz",
+ "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==",
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/buffers": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz",
+ "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==",
+ "engines": {
+ "node": ">=0.2.0"
+ }
+ },
"node_modules/busboy": {
"version": "0.2.14",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz",
@@ -3011,6 +3083,17 @@
}
]
},
+ "node_modules/chainsaw": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz",
+ "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==",
+ "dependencies": {
+ "traverse": ">=0.3.0 <0.4"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/chalk": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
@@ -3780,6 +3863,36 @@
"node": ">=10"
}
},
+ "node_modules/duplexer2": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
+ "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==",
+ "dependencies": {
+ "readable-stream": "^2.0.2"
+ }
+ },
+ "node_modules/duplexer2/node_modules/readable-stream": {
+ "version": "2.3.7",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
+ "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/duplexer2/node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
"node_modules/duplexer3": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",
@@ -4450,6 +4563,42 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
+ "node_modules/fstream": {
+ "version": "1.0.12",
+ "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz",
+ "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==",
+ "dependencies": {
+ "graceful-fs": "^4.1.2",
+ "inherits": "~2.0.0",
+ "mkdirp": ">=0.5 0",
+ "rimraf": "2"
+ },
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/fstream/node_modules/mkdirp": {
+ "version": "0.5.6",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
+ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
+ "dependencies": {
+ "minimist": "^1.2.6"
+ },
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ }
+ },
+ "node_modules/fstream/node_modules/rimraf": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
+ "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ }
+ },
"node_modules/function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
@@ -7064,6 +7213,11 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/listenercount": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz",
+ "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ=="
+ },
"node_modules/locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
@@ -8910,6 +9064,11 @@
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
"dev": true
},
+ "node_modules/setimmediate": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
+ "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
+ },
"node_modules/setprototypeof": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
@@ -9648,6 +9807,14 @@
"node": ">=8"
}
},
+ "node_modules/traverse": {
+ "version": "0.3.9",
+ "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz",
+ "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==",
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/traverse-chain": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/traverse-chain/-/traverse-chain-0.1.0.tgz",
@@ -9926,6 +10093,45 @@
"node": ">= 0.8"
}
},
+ "node_modules/unzipper": {
+ "version": "0.10.11",
+ "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz",
+ "integrity": "sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw==",
+ "dependencies": {
+ "big-integer": "^1.6.17",
+ "binary": "~0.3.0",
+ "bluebird": "~3.4.1",
+ "buffer-indexof-polyfill": "~1.0.0",
+ "duplexer2": "~0.1.4",
+ "fstream": "^1.0.12",
+ "graceful-fs": "^4.2.2",
+ "listenercount": "~1.0.1",
+ "readable-stream": "~2.3.6",
+ "setimmediate": "~1.0.4"
+ }
+ },
+ "node_modules/unzipper/node_modules/readable-stream": {
+ "version": "2.3.7",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
+ "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/unzipper/node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
"node_modules/update-notifier": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.3.tgz",
@@ -11700,6 +11906,15 @@
"validator": "^13.6.0"
}
},
+ "@types/adm-zip": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.5.0.tgz",
+ "integrity": "sha512-FCJBJq9ODsQZUNURo5ILAQueuA8WJhRvuihS3ke2iI25mJlfV2LK8jG2Qj2z2AWg8U0FtWWqBHVRetceLskSaw==",
+ "dev": true,
+ "requires": {
+ "@types/node": "*"
+ }
+ },
"@types/babel__core": {
"version": "7.1.15",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.15.tgz",
@@ -12097,6 +12312,15 @@
"integrity": "sha512-MhSa0yylXtVMsyT8qFpHA1DLHj4DvQGH5ntxrhHSh8PxUVNi35Wk+P5hVgqbO2qZqOotqr9jaoPRL+iRjWYm/A==",
"dev": true
},
+ "@types/unzipper": {
+ "version": "0.10.5",
+ "resolved": "https://registry.npmjs.org/@types/unzipper/-/unzipper-0.10.5.tgz",
+ "integrity": "sha512-NrLJb29AdnBARpg9S/4ktfPEisbJ0AvaaAr3j7Q1tg8AgcEUsq2HqbNzvgLRoWyRtjzeLEv7vuL39u1mrNIyNA==",
+ "dev": true,
+ "requires": {
+ "@types/node": "*"
+ }
+ },
"@types/webidl-conversions": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-6.1.1.tgz",
@@ -12177,6 +12401,12 @@
"integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==",
"dev": true
},
+ "adm-zip": {
+ "version": "0.5.9",
+ "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.9.tgz",
+ "integrity": "sha512-s+3fXLkeeLjZ2kLjCBwQufpI5fuN+kIGBxu6530nVQZGVol0d7Y/M88/xw9HGGUcJjKf8LutN3VPRUBq6N7Ajg==",
+ "dev": true
+ },
"agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
@@ -12500,6 +12730,20 @@
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
"integrity": "sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms="
},
+ "big-integer": {
+ "version": "1.6.51",
+ "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz",
+ "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg=="
+ },
+ "binary": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz",
+ "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==",
+ "requires": {
+ "buffers": "~0.1.1",
+ "chainsaw": "~0.1.0"
+ }
+ },
"binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
@@ -12525,6 +12769,11 @@
}
}
},
+ "bluebird": {
+ "version": "3.4.7",
+ "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz",
+ "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA=="
+ },
"bn.js": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
@@ -12651,6 +12900,16 @@
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A=="
},
+ "buffer-indexof-polyfill": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz",
+ "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A=="
+ },
+ "buffers": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz",
+ "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ=="
+ },
"busboy": {
"version": "0.2.14",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz",
@@ -12748,6 +13007,14 @@
"integrity": "sha512-jUNz+a9blQTQVu4uFcn17uAD8IDizPzQkIKh3LCJfg9BkyIqExYYdyc/ZSlWUSKb8iYiXxKsxbv4zYSvkqjrxw==",
"dev": true
},
+ "chainsaw": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz",
+ "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==",
+ "requires": {
+ "traverse": ">=0.3.0 <0.4"
+ }
+ },
"chalk": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
@@ -13374,6 +13641,38 @@
"integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==",
"dev": true
},
+ "duplexer2": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
+ "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==",
+ "requires": {
+ "readable-stream": "^2.0.2"
+ },
+ "dependencies": {
+ "readable-stream": {
+ "version": "2.3.7",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
+ "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "requires": {
+ "safe-buffer": "~5.1.0"
+ }
+ }
+ }
+ },
"duplexer3": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",
@@ -13896,6 +14195,35 @@
"dev": true,
"optional": true
},
+ "fstream": {
+ "version": "1.0.12",
+ "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz",
+ "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==",
+ "requires": {
+ "graceful-fs": "^4.1.2",
+ "inherits": "~2.0.0",
+ "mkdirp": ">=0.5 0",
+ "rimraf": "2"
+ },
+ "dependencies": {
+ "mkdirp": {
+ "version": "0.5.6",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
+ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
+ "requires": {
+ "minimist": "^1.2.6"
+ }
+ },
+ "rimraf": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
+ "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
+ "requires": {
+ "glob": "^7.1.3"
+ }
+ }
+ }
+ },
"function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
@@ -15839,6 +16167,11 @@
"type-check": "~0.3.2"
}
},
+ "listenercount": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz",
+ "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ=="
+ },
"locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
@@ -17219,6 +17552,11 @@
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
"dev": true
},
+ "setimmediate": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
+ "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
+ },
"setprototypeof": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
@@ -17785,6 +18123,11 @@
"punycode": "^2.1.1"
}
},
+ "traverse": {
+ "version": "0.3.9",
+ "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz",
+ "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ=="
+ },
"traverse-chain": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/traverse-chain/-/traverse-chain-0.1.0.tgz",
@@ -17972,6 +18315,47 @@
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
},
+ "unzipper": {
+ "version": "0.10.11",
+ "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz",
+ "integrity": "sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw==",
+ "requires": {
+ "big-integer": "^1.6.17",
+ "binary": "~0.3.0",
+ "bluebird": "~3.4.1",
+ "buffer-indexof-polyfill": "~1.0.0",
+ "duplexer2": "~0.1.4",
+ "fstream": "^1.0.12",
+ "graceful-fs": "^4.2.2",
+ "listenercount": "~1.0.1",
+ "readable-stream": "~2.3.6",
+ "setimmediate": "~1.0.4"
+ },
+ "dependencies": {
+ "readable-stream": {
+ "version": "2.3.7",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
+ "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "requires": {
+ "safe-buffer": "~5.1.0"
+ }
+ }
+ }
+ },
"update-notifier": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.3.tgz",
diff --git a/api/package.json b/api/package.json
index c3a0292..bbd4bbb 100644
--- a/api/package.json
+++ b/api/package.json
@@ -64,9 +64,11 @@
"morgan": "^1.10.0",
"multer": "^1.4.3",
"swagger-ui-express": "4.3.0",
+ "unzipper": "^0.10.11",
"url": "^0.10.3"
},
"devDependencies": {
+ "@types/adm-zip": "^0.5.0",
"@types/bcryptjs": "^2.4.2",
"@types/cookie-parser": "^1.4.2",
"@types/cors": "^2.8.12",
@@ -81,6 +83,8 @@
"@types/node": "^15.12.2",
"@types/supertest": "^2.0.11",
"@types/swagger-ui-express": "^4.1.3",
+ "@types/unzipper": "^0.10.5",
+ "adm-zip": "^0.5.9",
"dotenv": "^10.0.0",
"http-headers-validation": "^0.0.1",
"jest": "^27.0.6",
diff --git a/api/public/swagger.yaml b/api/public/swagger.yaml
index d95d7ab..0ab1c0e 100644
--- a/api/public/swagger.yaml
+++ b/api/public/swagger.yaml
@@ -435,6 +435,27 @@ components:
- description
type: object
additionalProperties: false
+ _LeanDocument__LeanDocument_T__:
+ properties: {}
+ type: object
+ Pick__LeanDocument_T_.Exclude_keyof_LeanDocument_T_.Exclude_keyofDocument._id-or-id-or-__v_-or-%24isSingleNested__:
+ properties:
+ id:
+ description: 'The string version of this documents _id.'
+ _id:
+ $ref: '#/components/schemas/_LeanDocument__LeanDocument_T__'
+ description: 'This documents _id.'
+ __v:
+ description: 'This documents __v.'
+ type: object
+ description: 'From T, pick a set of properties whose keys are in the union K'
+ Omit__LeanDocument_this_.Exclude_keyofDocument._id-or-id-or-__v_-or-%24isSingleNested_:
+ $ref: '#/components/schemas/Pick__LeanDocument_T_.Exclude_keyof_LeanDocument_T_.Exclude_keyofDocument._id-or-id-or-__v_-or-%24isSingleNested__'
+ description: 'Construct a type with the properties of T except for those in type K.'
+ LeanDocument_this_:
+ $ref: '#/components/schemas/Omit__LeanDocument_this_.Exclude_keyofDocument._id-or-id-or-__v_-or-%24isSingleNested_'
+ IGroup:
+ $ref: '#/components/schemas/LeanDocument_this_'
InfoResponse:
properties:
mode:
@@ -742,7 +763,8 @@ paths:
examples:
'Example 1':
value: {status: failure, message: 'Deployment failed!'}
- summary: 'Creates/updates files within SASjs Drive using uploaded JSON file.'
+ description: "Accepts JSON file and zipped compressed JSON file as well.\nCompressed file should only contain one JSON file and should have same name\nas of compressed file e.g. deploy.JSON should be compressed to deploy.JSON.zip\nAny other file or JSON file in zipped will be ignored!"
+ summary: 'Creates/updates files within SASjs Drive using uploaded JSON/compressed JSON file.'
tags:
- Drive
security:
@@ -1230,6 +1252,30 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/GroupPayload'
+ '/SASjsApi/group/by/groupname/{name}':
+ get:
+ operationId: GetGroupByGroupName
+ responses:
+ '200':
+ description: Ok
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GroupDetailsResponse'
+ summary: 'Get list of members of a group (userName). All users can request this.'
+ tags:
+ - Group
+ security:
+ -
+ bearerAuth: []
+ parameters:
+ -
+ description: 'The group''s name'
+ in: path
+ name: name
+ required: true
+ schema:
+ type: string
'/SASjsApi/group/{groupId}':
get:
operationId: GetGroup
@@ -1259,8 +1305,14 @@ paths:
delete:
operationId: DeleteGroup
responses:
- '204':
- description: 'No content'
+ '200':
+ description: Ok
+ content:
+ application/json:
+ schema:
+ allOf:
+ - {$ref: '#/components/schemas/IGroup'}
+ - {properties: {_id: {}}, required: [_id], type: object}
summary: 'Delete a group. Admin task only.'
tags:
- Group
diff --git a/api/src/controllers/drive.ts b/api/src/controllers/drive.ts
index d4eee88..3562311 100644
--- a/api/src/controllers/drive.ts
+++ b/api/src/controllers/drive.ts
@@ -96,7 +96,12 @@ export class DriveController {
}
/**
- * @summary Creates/updates files within SASjs Drive using uploaded JSON file.
+ * Accepts JSON file and zipped compressed JSON file as well.
+ * Compressed file should only contain one JSON file and should have same name
+ * as of compressed file e.g. deploy.JSON should be compressed to deploy.JSON.zip
+ * Any other file or JSON file in zipped will be ignored!
+ *
+ * @summary Creates/updates files within SASjs Drive using uploaded JSON/compressed JSON file.
*
*/
@Example(successDeployResponse)
diff --git a/api/src/controllers/group.ts b/api/src/controllers/group.ts
index b3cbb0f..3a6ff66 100644
--- a/api/src/controllers/group.ts
+++ b/api/src/controllers/group.ts
@@ -28,6 +28,11 @@ interface GroupDetailsResponse {
users: UserResponse[]
}
+interface GetGroupBy {
+ groupId?: number
+ name?: string
+}
+
@Security('bearerAuth')
@Route('SASjsApi/group')
@Tags('Group')
@@ -66,6 +71,18 @@ export class GroupController {
return createGroup(body)
}
+ /**
+ * @summary Get list of members of a group (userName). All users can request this.
+ * @param name The group's name
+ * @example dcgroup
+ */
+ @Get('by/groupname/{name}')
+ public async getGroupByGroupName(
+ @Path() name: string
+ ): Promise {
+ return getGroup({ name })
+ }
+
/**
* @summary Get list of members of a group (userName). All users can request this.
* @param groupId The group's identifier
@@ -75,7 +92,7 @@ export class GroupController {
public async getGroup(
@Path() groupId: number
): Promise {
- return getGroup(groupId)
+ return getGroup({ groupId })
}
/**
@@ -129,9 +146,13 @@ export class GroupController {
*/
@Delete('{groupId}')
public async deleteGroup(@Path() groupId: number) {
- const { deletedCount } = await Group.deleteOne({ groupId })
- if (deletedCount) return
- throw new Error('No Group deleted!')
+ const group = await Group.findOne({ groupId })
+ if (group) return await group.remove()
+ throw {
+ code: 404,
+ status: 'Not Found',
+ message: 'Group not found.'
+ }
}
}
@@ -145,6 +166,15 @@ const createGroup = async ({
description,
isActive
}: GroupPayload): Promise => {
+ // Checking if user is already in the database
+ const groupnameExist = await Group.findOne({ name })
+ if (groupnameExist)
+ throw {
+ code: 409,
+ status: 'Conflict',
+ message: 'Group name already exists.'
+ }
+
const group = new Group({
name,
description,
@@ -162,15 +192,20 @@ const createGroup = async ({
}
}
-const getGroup = async (groupId: number): Promise => {
+const getGroup = async (findBy: GetGroupBy): Promise => {
const group = (await Group.findOne(
- { groupId },
+ findBy,
'groupId name description isActive users -_id'
).populate(
'users',
'id username displayName -_id'
)) as unknown as GroupDetailsResponse
- if (!group) throw new Error('Group not found.')
+ if (!group)
+ throw {
+ code: 404,
+ status: 'Not Found',
+ message: 'Group not found.'
+ }
return {
groupId: group.groupId,
@@ -199,16 +234,31 @@ const updateUsersListInGroup = async (
action: 'addUser' | 'removeUser'
): Promise => {
const group = await Group.findOne({ groupId })
- if (!group) throw new Error('Group not found.')
+ if (!group)
+ throw {
+ code: 404,
+ status: 'Not Found',
+ message: 'Group not found.'
+ }
const user = await User.findOne({ id: userId })
- if (!user) throw new Error('User not found.')
+ if (!user)
+ throw {
+ code: 404,
+ status: 'Not Found',
+ message: 'User not found.'
+ }
const updatedGroup = (action === 'addUser'
? await group.addUser(user._id)
: await group.removeUser(user._id)) as unknown as GroupDetailsResponse
- if (!updatedGroup) throw new Error('Unable to update group')
+ if (!updatedGroup)
+ throw {
+ code: 400,
+ status: 'Bad Request',
+ message: 'Unable to update group.'
+ }
if (action === 'addUser') user.addGroup(group._id)
else user.removeGroup(group._id)
diff --git a/api/src/controllers/internal/Session.ts b/api/src/controllers/internal/Session.ts
index 3f69a95..623ec85 100644
--- a/api/src/controllers/internal/Session.ts
+++ b/api/src/controllers/internal/Session.ts
@@ -86,6 +86,8 @@ ${autoExecContent}`
// however we also need a promise so that we can update the
// session array to say that it has (eventually) finished.
+ // Additional windows specific options to avoid the desktop popups.
+
execFilePromise(process.sasLoc, [
'-SYSIN',
codePath,
@@ -99,7 +101,9 @@ ${autoExecContent}`
autoExecPath,
'-ENCODING',
'UTF-8',
- process.platform === 'win32' ? '-nosplash' : ''
+ process.platform === 'win32' ? '-nosplash' : '',
+ process.platform === 'win32' ? '-icon' : '',
+ process.platform === 'win32' ? '-nologo' : ''
])
.then(() => {
session.completed = true
diff --git a/api/src/model/Group.ts b/api/src/model/Group.ts
index 8ff04ff..36b7842 100644
--- a/api/src/model/Group.ts
+++ b/api/src/model/Group.ts
@@ -1,4 +1,5 @@
import mongoose, { Schema, model, Document, Model } from 'mongoose'
+import User from './User'
const AutoIncrement = require('mongoose-sequence')(mongoose)
export interface GroupPayload {
@@ -34,7 +35,8 @@ interface IGroupModel extends Model {}
const groupSchema = new Schema({
name: {
type: String,
- required: true
+ required: true,
+ unique: true
},
description: {
type: String,
@@ -46,6 +48,7 @@ const groupSchema = new Schema({
},
users: [{ type: Schema.Types.ObjectId, ref: 'User' }]
})
+
groupSchema.plugin(AutoIncrement, { inc_field: 'groupId' })
// Hooks
@@ -55,6 +58,17 @@ groupSchema.post('save', function (group: IGroup, next: Function) {
})
})
+// pre remove hook to remove all references of group from users
+groupSchema.pre('remove', async function () {
+ const userIds = this.users
+ await Promise.all(
+ userIds.map(async (userId) => {
+ const user = await User.findById(userId)
+ user?.removeGroup(this._id)
+ })
+ )
+})
+
// Instance Methods
groupSchema.method(
'addUser',
diff --git a/api/src/routes/api/drive.ts b/api/src/routes/api/drive.ts
index 71ffa4a..6126946 100644
--- a/api/src/routes/api/drive.ts
+++ b/api/src/routes/api/drive.ts
@@ -7,9 +7,12 @@ import { multerSingle } from '../../middlewares/multer'
import { DriveController } from '../../controllers/'
import {
deployValidation,
+ extractJSONFromZip,
+ extractName,
fileBodyValidation,
fileParamValidation,
- folderParamValidation
+ folderParamValidation,
+ isZipFile
} from '../../utils'
const controller = new DriveController()
@@ -49,7 +52,24 @@ driveRouter.post(
async (req, res) => {
if (!req.file) return res.status(400).send('"file" is not present.')
- const fileContent = await readFile(req.file.path)
+ let fileContent: string = ''
+
+ const { value: zipFile } = isZipFile(req.file)
+ if (zipFile) {
+ fileContent = await extractJSONFromZip(zipFile)
+ const fileInZip = extractName(zipFile.originalname)
+
+ if (!fileContent) {
+ deleteFile(req.file.path)
+ return res
+ .status(400)
+ .send(
+ `No content present in ${fileInZip} of compressed file ${zipFile.originalname}`
+ )
+ }
+ } else {
+ fileContent = await readFile(req.file.path)
+ }
let jsonContent
try {
diff --git a/api/src/routes/api/group.ts b/api/src/routes/api/group.ts
index 8164262..930b817 100644
--- a/api/src/routes/api/group.ts
+++ b/api/src/routes/api/group.ts
@@ -1,7 +1,7 @@
import express from 'express'
import { GroupController } from '../../controllers/'
import { authenticateAccessToken, verifyAdmin } from '../../middlewares'
-import { registerGroupValidation } from '../../utils'
+import { getGroupValidation, registerGroupValidation } from '../../utils'
const groupRouter = express.Router()
@@ -18,7 +18,11 @@ groupRouter.post(
const response = await controller.createGroup(body)
res.send(response)
} catch (err: any) {
- res.status(403).send(err.toString())
+ const statusCode = err.code
+
+ delete err.code
+
+ res.status(statusCode).send(err.message)
}
}
)
@@ -29,7 +33,11 @@ groupRouter.get('/', authenticateAccessToken, async (req, res) => {
const response = await controller.getAllGroups()
res.send(response)
} catch (err: any) {
- res.status(403).send(err.toString())
+ const statusCode = err.code
+
+ delete err.code
+
+ res.status(statusCode).send(err.message)
}
})
@@ -41,10 +49,37 @@ groupRouter.get('/:groupId', authenticateAccessToken, async (req, res) => {
const response = await controller.getGroup(parseInt(groupId))
res.send(response)
} catch (err: any) {
- res.status(403).send(err.toString())
+ const statusCode = err.code
+
+ delete err.code
+
+ res.status(statusCode).send(err.message)
}
})
+groupRouter.get(
+ '/by/groupname/:name',
+ authenticateAccessToken,
+ async (req, res) => {
+ const { error, value: params } = getGroupValidation(req.params)
+ if (error) return res.status(400).send(error.details[0].message)
+
+ const { name } = params
+
+ const controller = new GroupController()
+ try {
+ const response = await controller.getGroupByGroupName(name)
+ res.send(response)
+ } catch (err: any) {
+ const statusCode = err.code
+
+ delete err.code
+
+ res.status(statusCode).send(err.message)
+ }
+ }
+)
+
groupRouter.post(
'/:groupId/:userId',
authenticateAccessToken,
@@ -60,7 +95,11 @@ groupRouter.post(
)
res.send(response)
} catch (err: any) {
- res.status(403).send(err.toString())
+ const statusCode = err.code
+
+ delete err.code
+
+ res.status(statusCode).send(err.message)
}
}
)
@@ -80,7 +119,11 @@ groupRouter.delete(
)
res.send(response)
} catch (err: any) {
- res.status(403).send(err.toString())
+ const statusCode = err.code
+
+ delete err.code
+
+ res.status(statusCode).send(err.message)
}
}
)
@@ -97,7 +140,11 @@ groupRouter.delete(
await controller.deleteGroup(parseInt(groupId))
res.status(200).send('Group Deleted!')
} catch (err: any) {
- res.status(403).send(err.toString())
+ const statusCode = err.code
+
+ delete err.code
+
+ res.status(statusCode).send(err.message)
}
}
)
diff --git a/api/src/routes/api/spec/drive.spec.ts b/api/src/routes/api/spec/drive.spec.ts
index 341bd02..e9e6e8a 100644
--- a/api/src/routes/api/spec/drive.spec.ts
+++ b/api/src/routes/api/spec/drive.spec.ts
@@ -3,6 +3,7 @@ import { Express } from 'express'
import mongoose, { Mongoose } from 'mongoose'
import { MongoMemoryServer } from 'mongodb-memory-server'
import request from 'supertest'
+import AdmZip from 'adm-zip'
import {
folderExists,
@@ -72,11 +73,52 @@ describe('drive', () => {
})
describe('deploy', () => {
- const shouldFailAssertion = async (payload: any) => {
- const res = await request(app)
- .post('/SASjsApi/drive/deploy')
- .auth(accessToken, { type: 'bearer' })
- .send({ appLoc: '/Public', fileTree: payload })
+ const makeRequest = async (payload: any, type: string = 'payload') => {
+ const requestUrl =
+ type === 'payload'
+ ? '/SASjsApi/drive/deploy'
+ : '/SASjsApi/drive/deploy/upload'
+
+ if (type === 'payload') {
+ return await request(app)
+ .post(requestUrl)
+ .auth(accessToken, { type: 'bearer' })
+ .send({ appLoc: '/Public', fileTree: payload })
+ }
+ if (type === 'file') {
+ const deployContents = JSON.stringify({
+ appLoc: '/Public',
+ fileTree: payload
+ })
+ return await request(app)
+ .post(requestUrl)
+ .auth(accessToken, { type: 'bearer' })
+ .attach('file', Buffer.from(deployContents), 'deploy.json')
+ } else {
+ const deployContents = JSON.stringify({
+ appLoc: '/Public',
+ fileTree: payload
+ })
+ const zip = new AdmZip()
+ // add file directly
+ zip.addFile(
+ 'deploy.json',
+ Buffer.from(deployContents, 'utf8'),
+ 'entry comment goes here'
+ )
+
+ return await request(app)
+ .post(requestUrl)
+ .auth(accessToken, { type: 'bearer' })
+ .attach('file', zip.toBuffer(), 'deploy.json.zip')
+ }
+ }
+
+ const shouldFailAssertion = async (
+ payload: any,
+ type: string = 'payload'
+ ) => {
+ const res = await makeRequest(payload, type)
expect(res.statusCode).toEqual(400)
@@ -176,6 +218,240 @@ describe('drive', () => {
await deleteFolder(path.join(getFilesFolder(), 'public'))
})
+
+ describe('upload', () => {
+ it('should respond with payload example if valid JSON file was not provided', async () => {
+ await shouldFailAssertion(null, 'file')
+ await shouldFailAssertion(undefined, 'file')
+ await shouldFailAssertion('data', 'file')
+ await shouldFailAssertion({}, 'file')
+ await shouldFailAssertion(
+ {
+ userId: 1,
+ title: 'test is cool'
+ },
+ 'file'
+ )
+ await shouldFailAssertion(
+ {
+ membersWRONG: []
+ },
+ 'file'
+ )
+ await shouldFailAssertion(
+ {
+ members: {}
+ },
+ 'file'
+ )
+ await shouldFailAssertion(
+ {
+ members: [
+ {
+ nameWRONG: 'jobs',
+ type: 'folder',
+ members: []
+ }
+ ]
+ },
+ 'file'
+ )
+ await shouldFailAssertion(
+ {
+ members: [
+ {
+ name: 'jobs',
+ type: 'WRONG',
+ members: []
+ }
+ ]
+ },
+ 'file'
+ )
+ await shouldFailAssertion(
+ {
+ members: [
+ {
+ name: 'jobs',
+ type: 'folder',
+ members: [
+ {
+ name: 'extract',
+ type: 'folder',
+ members: [
+ {
+ name: 'makedata1',
+ type: 'service',
+ codeWRONG: '%put Hello World!;'
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ 'file'
+ )
+ })
+
+ it('should successfully deploy if valid JSON file was provided', async () => {
+ const deployContents = JSON.stringify({
+ appLoc: '/public',
+ fileTree: getTreeExample()
+ })
+ const res = await request(app)
+ .post('/SASjsApi/drive/deploy/upload')
+ .auth(accessToken, { type: 'bearer' })
+ .attach('file', Buffer.from(deployContents), 'deploy.json')
+
+ expect(res.statusCode).toEqual(200)
+ expect(res.text).toEqual(
+ '{"status":"success","message":"Files deployed successfully to @sasjs/server."}'
+ )
+ await expect(folderExists(getFilesFolder())).resolves.toEqual(true)
+
+ const testJobFolder = path.join(
+ getFilesFolder(),
+ 'public',
+ 'jobs',
+ 'extract'
+ )
+ await expect(folderExists(testJobFolder)).resolves.toEqual(true)
+
+ const exampleService = getExampleService()
+ const testJobFile =
+ path.join(testJobFolder, exampleService.name) + '.sas'
+
+ await expect(fileExists(testJobFile)).resolves.toEqual(true)
+
+ await expect(readFile(testJobFile)).resolves.toEqual(
+ exampleService.code
+ )
+
+ await deleteFolder(path.join(getFilesFolder(), 'public'))
+ })
+ })
+
+ describe('upload - zipped', () => {
+ it('should respond with payload example if valid Zipped file was not provided', async () => {
+ await shouldFailAssertion(null, 'zip')
+ await shouldFailAssertion(undefined, 'zip')
+ await shouldFailAssertion('data', 'zip')
+ await shouldFailAssertion({}, 'zip')
+ await shouldFailAssertion(
+ {
+ userId: 1,
+ title: 'test is cool'
+ },
+ 'zip'
+ )
+ await shouldFailAssertion(
+ {
+ membersWRONG: []
+ },
+ 'zip'
+ )
+ await shouldFailAssertion(
+ {
+ members: {}
+ },
+ 'zip'
+ )
+ await shouldFailAssertion(
+ {
+ members: [
+ {
+ nameWRONG: 'jobs',
+ type: 'folder',
+ members: []
+ }
+ ]
+ },
+ 'zip'
+ )
+ await shouldFailAssertion(
+ {
+ members: [
+ {
+ name: 'jobs',
+ type: 'WRONG',
+ members: []
+ }
+ ]
+ },
+ 'zip'
+ )
+ await shouldFailAssertion(
+ {
+ members: [
+ {
+ name: 'jobs',
+ type: 'folder',
+ members: [
+ {
+ name: 'extract',
+ type: 'folder',
+ members: [
+ {
+ name: 'makedata1',
+ type: 'service',
+ codeWRONG: '%put Hello World!;'
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ 'zip'
+ )
+ })
+
+ it('should successfully deploy if valid Zipped file was provided', async () => {
+ const deployContents = JSON.stringify({
+ appLoc: '/public',
+ fileTree: getTreeExample()
+ })
+
+ const zip = new AdmZip()
+ // add file directly
+ zip.addFile(
+ 'deploy.json',
+ Buffer.from(deployContents, 'utf8'),
+ 'entry comment goes here'
+ )
+ const res = await request(app)
+ .post('/SASjsApi/drive/deploy/upload')
+ .auth(accessToken, { type: 'bearer' })
+ .attach('file', zip.toBuffer(), 'deploy.json.zip')
+
+ expect(res.statusCode).toEqual(200)
+ expect(res.text).toEqual(
+ '{"status":"success","message":"Files deployed successfully to @sasjs/server."}'
+ )
+ await expect(folderExists(getFilesFolder())).resolves.toEqual(true)
+
+ const testJobFolder = path.join(
+ getFilesFolder(),
+ 'public',
+ 'jobs',
+ 'extract'
+ )
+ await expect(folderExists(testJobFolder)).resolves.toEqual(true)
+
+ const exampleService = getExampleService()
+ const testJobFile =
+ path.join(testJobFolder, exampleService.name) + '.sas'
+
+ await expect(fileExists(testJobFile)).resolves.toEqual(true)
+
+ await expect(readFile(testJobFile)).resolves.toEqual(
+ exampleService.code
+ )
+
+ await deleteFolder(path.join(getFilesFolder(), 'public'))
+ })
+ })
})
describe('folder', () => {
diff --git a/api/src/routes/api/spec/group.spec.ts b/api/src/routes/api/spec/group.spec.ts
index b48bad8..86196ce 100644
--- a/api/src/routes/api/spec/group.spec.ts
+++ b/api/src/routes/api/spec/group.spec.ts
@@ -23,7 +23,7 @@ const user = {
}
const group = {
- name: 'DCGroup1',
+ name: 'dcgroup1',
description: 'DC group for testing purposes.'
}
@@ -70,6 +70,32 @@ describe('group', () => {
expect(res.body.users).toEqual([])
})
+ it('should respond with Conflict when group already exists with same name', async () => {
+ await groupController.createGroup(group)
+
+ const res = await request(app)
+ .post('/SASjsApi/group')
+ .auth(adminAccessToken, { type: 'bearer' })
+ .send(group)
+ .expect(409)
+
+ expect(res.text).toEqual('Group name already exists.')
+ expect(res.body).toEqual({})
+ })
+
+ it('should respond with Bad Request when group name does not match the group name schema', async () => {
+ const res = await request(app)
+ .post('/SASjsApi/group')
+ .auth(adminAccessToken, { type: 'bearer' })
+ .send({ ...group, name: 'Wrong Group Name' })
+ .expect(400)
+
+ expect(res.text).toEqual(
+ '"name" must only contain alpha-numeric characters'
+ )
+ expect(res.body).toEqual({})
+ })
+
it('should respond with Unauthorized if access token is not present', async () => {
const res = await request(app).post('/SASjsApi/group').send().expect(401)
@@ -125,14 +151,51 @@ describe('group', () => {
expect(res.body).toEqual({})
})
- it('should respond with Forbidden if groupId is incorrect', async () => {
+ it(`should delete group's reference from users' groups array`, async () => {
+ const dbGroup = await groupController.createGroup(group)
+ const dbUser1 = await userController.createUser({
+ ...user,
+ username: 'deletegroup1'
+ })
+ const dbUser2 = await userController.createUser({
+ ...user,
+ username: 'deletegroup2'
+ })
+
+ await groupController.addUserToGroup(dbGroup.groupId, dbUser1.id)
+ await groupController.addUserToGroup(dbGroup.groupId, dbUser2.id)
+
+ await request(app)
+ .delete(`/SASjsApi/group/${dbGroup.groupId}`)
+ .auth(adminAccessToken, { type: 'bearer' })
+ .send()
+ .expect(200)
+
+ const res1 = await request(app)
+ .get(`/SASjsApi/user/${dbUser1.id}`)
+ .auth(adminAccessToken, { type: 'bearer' })
+ .send()
+ .expect(200)
+
+ expect(res1.body.groups).toEqual([])
+
+ const res2 = await request(app)
+ .get(`/SASjsApi/user/${dbUser2.id}`)
+ .auth(adminAccessToken, { type: 'bearer' })
+ .send()
+ .expect(200)
+
+ expect(res2.body.groups).toEqual([])
+ })
+
+ it('should respond with Not Found if groupId is incorrect', async () => {
const res = await request(app)
.delete(`/SASjsApi/group/1234`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
- .expect(403)
+ .expect(404)
- expect(res.text).toEqual('Error: No Group deleted!')
+ expect(res.text).toEqual('Group not found.')
expect(res.body).toEqual({})
})
@@ -216,16 +279,76 @@ describe('group', () => {
expect(res.body).toEqual({})
})
- it('should respond with Forbidden if groupId is incorrect', async () => {
+ it('should respond with Not Found if groupId is incorrect', async () => {
const res = await request(app)
.get('/SASjsApi/group/1234')
.auth(adminAccessToken, { type: 'bearer' })
.send()
- .expect(403)
+ .expect(404)
- expect(res.text).toEqual('Error: Group not found.')
+ expect(res.text).toEqual('Group not found.')
expect(res.body).toEqual({})
})
+
+ describe('by group name', () => {
+ it('should respond with group', async () => {
+ const { name } = await groupController.createGroup(group)
+
+ const res = await request(app)
+ .get(`/SASjsApi/group/by/groupname/${name}`)
+ .auth(adminAccessToken, { type: 'bearer' })
+ .send()
+ .expect(200)
+
+ expect(res.body.groupId).toBeTruthy()
+ expect(res.body.name).toEqual(group.name)
+ expect(res.body.description).toEqual(group.description)
+ expect(res.body.isActive).toEqual(true)
+ expect(res.body.users).toEqual([])
+ })
+
+ it('should respond with group when access token is not of an admin account', async () => {
+ const accessToken = await generateSaveTokenAndCreateUser({
+ ...user,
+ username: 'getbyname' + user.username
+ })
+
+ const { name } = await groupController.createGroup(group)
+
+ const res = await request(app)
+ .get(`/SASjsApi/group/by/groupname/${name}`)
+ .auth(accessToken, { type: 'bearer' })
+ .send()
+ .expect(200)
+
+ expect(res.body.groupId).toBeTruthy()
+ expect(res.body.name).toEqual(group.name)
+ expect(res.body.description).toEqual(group.description)
+ expect(res.body.isActive).toEqual(true)
+ expect(res.body.users).toEqual([])
+ })
+
+ it('should respond with Unauthorized if access token is not present', async () => {
+ const res = await request(app)
+ .get('/SASjsApi/group/by/groupname/dcgroup')
+ .send()
+ .expect(401)
+
+ expect(res.text).toEqual('Unauthorized')
+ expect(res.body).toEqual({})
+ })
+
+ it('should respond with Not Found if groupname is incorrect', async () => {
+ const res = await request(app)
+ .get('/SASjsApi/group/by/groupname/randomCharacters')
+ .auth(adminAccessToken, { type: 'bearer' })
+ .send()
+ .expect(404)
+
+ expect(res.text).toEqual('Group not found.')
+ expect(res.body).toEqual({})
+ })
+ })
})
describe('getAll', () => {
@@ -245,8 +368,8 @@ describe('group', () => {
expect(res.body).toEqual([
{
groupId: expect.anything(),
- name: 'DCGroup1',
- description: 'DC group for testing purposes.'
+ name: group.name,
+ description: group.description
}
])
})
@@ -267,8 +390,8 @@ describe('group', () => {
expect(res.body).toEqual([
{
groupId: expect.anything(),
- name: 'DCGroup1',
- description: 'DC group for testing purposes.'
+ name: group.name,
+ description: group.description
}
])
})
@@ -309,6 +432,34 @@ describe('group', () => {
])
})
+ it(`should add group to user's groups array`, async () => {
+ const dbGroup = await groupController.createGroup(group)
+ const dbUser = await userController.createUser({
+ ...user,
+ username: 'addUserToGroup'
+ })
+
+ await request(app)
+ .post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
+ .auth(adminAccessToken, { type: 'bearer' })
+ .send()
+ .expect(200)
+
+ const res = await request(app)
+ .get(`/SASjsApi/user/${dbUser.id}`)
+ .auth(adminAccessToken, { type: 'bearer' })
+ .send()
+ .expect(200)
+
+ expect(res.body.groups).toEqual([
+ {
+ groupId: expect.anything(),
+ name: group.name,
+ description: group.description
+ }
+ ])
+ })
+
it('should respond with group without duplicating user', async () => {
const dbGroup = await groupController.createGroup(group)
const dbUser = await userController.createUser({
@@ -362,26 +513,26 @@ describe('group', () => {
expect(res.body).toEqual({})
})
- it('should respond with Forbidden if groupId is incorrect', async () => {
+ it('should respond with Not Found if groupId is incorrect', async () => {
const res = await request(app)
.post('/SASjsApi/group/123/123')
.auth(adminAccessToken, { type: 'bearer' })
.send()
- .expect(403)
+ .expect(404)
- expect(res.text).toEqual('Error: Group not found.')
+ expect(res.text).toEqual('Group not found.')
expect(res.body).toEqual({})
})
- it('should respond with Forbidden if userId is incorrect', async () => {
+ it('should respond with Not Found if userId is incorrect', async () => {
const dbGroup = await groupController.createGroup(group)
const res = await request(app)
.post(`/SASjsApi/group/${dbGroup.groupId}/123`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
- .expect(403)
+ .expect(404)
- expect(res.text).toEqual('Error: User not found.')
+ expect(res.text).toEqual('User not found.')
expect(res.body).toEqual({})
})
})
@@ -412,6 +563,29 @@ describe('group', () => {
expect(res.body.users).toEqual([])
})
+ it(`should remove group from user's groups array`, async () => {
+ const dbGroup = await groupController.createGroup(group)
+ const dbUser = await userController.createUser({
+ ...user,
+ username: 'removeGroupFromUser'
+ })
+ await groupController.addUserToGroup(dbGroup.groupId, dbUser.id)
+
+ await request(app)
+ .delete(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
+ .auth(adminAccessToken, { type: 'bearer' })
+ .send()
+ .expect(200)
+
+ const res = await request(app)
+ .get(`/SASjsApi/user/${dbUser.id}`)
+ .auth(adminAccessToken, { type: 'bearer' })
+ .send()
+ .expect(200)
+
+ expect(res.body.groups).toEqual([])
+ })
+
it('should respond with Unauthorized if access token is not present', async () => {
const res = await request(app)
.delete('/SASjsApi/group/123/123')
@@ -438,26 +612,26 @@ describe('group', () => {
expect(res.body).toEqual({})
})
- it('should respond with Forbidden if groupId is incorrect', async () => {
+ it('should respond with Not Found if groupId is incorrect', async () => {
const res = await request(app)
.delete('/SASjsApi/group/123/123')
.auth(adminAccessToken, { type: 'bearer' })
.send()
- .expect(403)
+ .expect(404)
- expect(res.text).toEqual('Error: Group not found.')
+ expect(res.text).toEqual('Group not found.')
expect(res.body).toEqual({})
})
- it('should respond with Forbidden if userId is incorrect', async () => {
+ it('should respond with Not Found if userId is incorrect', async () => {
const dbGroup = await groupController.createGroup(group)
const res = await request(app)
.delete(`/SASjsApi/group/${dbGroup.groupId}/123`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
- .expect(403)
+ .expect(404)
- expect(res.text).toEqual('Error: User not found.')
+ expect(res.text).toEqual('User not found.')
expect(res.body).toEqual({})
})
})
diff --git a/api/src/routes/appStream/appStreamHtml.ts b/api/src/routes/appStream/appStreamHtml.ts
index 2635729..b0512ca 100644
--- a/api/src/routes/appStream/appStreamHtml.ts
+++ b/api/src/routes/appStream/appStreamHtml.ts
@@ -23,13 +23,21 @@ export const appStreamHtml = (appStreamConfig: AppStreamConfig) => `
${style}
- App Stream
+
+
+ App Stream
+
- ${Object.entries(appStreamConfig)
- .map(([streamServiceName, entry]) =>
- singleAppStreamHtml(streamServiceName, entry.appLoc, entry.streamLogo)
- )
- .join('')}
+ ${Object.entries(appStreamConfig)
+ .map(([streamServiceName, entry]) =>
+ singleAppStreamHtml(
+ streamServiceName,
+ entry.appLoc,
+ entry.streamLogo
+ )
+ )
+ .join('')}
+