mirror of
https://github.com/sasjs/adapter.git
synced 2025-12-11 01:14:36 +00:00
Compare commits
1275 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72ddd424a5 | ||
| 85f771d1ed | |||
|
|
1a781c3a56 | ||
|
|
296d4efdfb | ||
| 8df09d01de | |||
|
|
2d4a9d6dee | ||
|
|
38c30f6342 | ||
|
|
dd72304bc7 | ||
|
|
296a543b2d | ||
| 70b31dcb8f | |||
| b0c2a81989 | |||
| 53e167b17d | |||
|
|
5159318d0d | ||
|
|
6842ee13e4 | ||
|
|
2ce0395a2e | ||
|
|
63440ddfd2 | ||
|
|
eb6729a9c7 | ||
|
|
9cd9dc83f3 | ||
|
|
7608887a0e | ||
|
|
b453717fbc | ||
|
|
ecc896e52a | ||
|
|
e881e21f80 | ||
|
|
44d6cfd544 | ||
| b0c9ca3f9f | |||
|
|
e6f7781648 | ||
| b07770eb7b | |||
|
|
c498783d76 | ||
| 7b3b1d7d7d | |||
|
|
60d3ff2d39 | ||
|
|
5217198ef6 | ||
|
|
3a16c83697 | ||
|
|
e65b19b85e | ||
|
|
a1c09ec802 | ||
| 2cbba38af5 | |||
| 61fdf5573b | |||
| 5259cf3942 | |||
| 5f17f77018 | |||
| 84b46d02c9 | |||
| 4ec57d93dd | |||
| 8e73f543c3 | |||
| a45edbc066 | |||
| 9258817887 | |||
| 1f35e01f22 | |||
| 59af907202 | |||
| cd00c483c5 | |||
| a01eb948b5 | |||
|
|
d746d21dee | ||
| d7330617df | |||
| b72cee099c | |||
| 9871ce4fb3 | |||
|
|
16185eba4d | ||
|
|
eae14df683 | ||
|
|
d989f74112 | ||
|
|
64d72ea0c2 | ||
|
|
325202c8e1 | ||
|
|
e9422898b6 | ||
| 2b7281e70c | |||
|
|
c1e3a9f5bb | ||
|
|
d249295b49 | ||
| 010fd063df | |||
|
|
c6bbf1ff34 | ||
|
|
f1df27fdf1 | ||
| eb739a83a4 | |||
| d8b686dd7e | |||
| 3d8eb762d0 | |||
| c551cd0311 | |||
| 4a319f1aef | |||
|
|
a0b8316d7c | ||
|
|
3b53d5b3ae | ||
|
|
dfebab5abc | ||
|
|
8d5ee0d6e1 | ||
|
|
1ed7a11fc3 | ||
|
|
abe95f5432 | ||
|
|
92be5a2dca | ||
|
|
f58f2eba97 | ||
|
|
e37bb182c3 | ||
|
|
504777603c | ||
| 706cbe5513 | |||
| 88eadd27aa | |||
| 4ed9f87434 | |||
|
|
f0f80a1c1f | ||
| d0d8d58945 | |||
|
|
657721d7a3 | ||
|
|
a39faa0f4b | ||
|
|
7b8fb774cc | ||
|
|
982c4c329c | ||
| 8617e2dc57 | |||
|
|
d3d62f6888 | ||
|
|
bf35e52962 | ||
| 22eca50e3f | |||
|
|
eb83101dbf | ||
|
|
56d84e1940 | ||
|
|
283800dfa6 | ||
|
|
c073d72dd4 | ||
|
|
f5d40eaaf7 | ||
|
|
8e9cf98985 | ||
|
|
79ba044dea | ||
|
|
9329dc848a | ||
|
|
98c492e85e | ||
| d1fcc2ca0a | |||
|
|
122f302bae | ||
|
|
c3a0ad1f41 | ||
|
|
a28b48f815 | ||
|
|
9b6a42e412 | ||
|
|
db60962c1e | ||
|
|
1eae59ad3b | ||
|
|
d485023d65 | ||
|
|
c2f21babb4 | ||
|
|
dd788ae423 | ||
|
|
a113c95441 | ||
|
|
489947bcae | ||
|
|
1596173dda | ||
|
|
bb1b2ddcb2 | ||
| a3cc274ef1 | |||
| 451d0906fa | |||
| dd6b89b0d0 | |||
|
|
f602d5baf0 | ||
|
|
4744dbf196 | ||
|
|
f0525c5796 | ||
|
|
01bcfe176a | ||
| 076ed1cc7a | |||
| 0a3289b577 | |||
|
|
cbb55ff426 | ||
| 6d47174a5e | |||
|
|
ae13ca523c | ||
| 843cee4dbe | |||
|
|
1ac88ae102 | ||
|
|
a79766c00c | ||
|
|
3bc70b91b8 | ||
|
|
b0b1c32180 | ||
|
|
5b81e0bf4a | ||
|
|
54a33ac98a | ||
|
|
dd237ceeec | ||
| 552540fb88 | |||
| 6d50d4030e | |||
|
|
8856c3b6ec | ||
|
|
b8ea618f1e | ||
|
|
10c72e6483 | ||
|
|
9d03b54fba | ||
|
|
38ad9abfbc | ||
| 42d9a85cfa | |||
| 39f14cc5a0 | |||
| 29a65052dc | |||
| 63328163ab | |||
|
|
aebf4ea8d8 | ||
|
|
59e3edf4b3 | ||
| 8f309143e9 | |||
| a65d4257a5 | |||
| 5d2f1d306a | |||
| f7bd63ee7f | |||
| a4cd320272 | |||
| c243f25477 | |||
| 382a19ccfc | |||
| acc56e3a53 | |||
| 1d0fb1774a | |||
| b67340cc70 | |||
| 831023fe1c | |||
| 04f7cdf8ff | |||
| d01a6a60ad | |||
| 7c17f0f584 | |||
| 092b995d14 | |||
| eb296bf93c | |||
| e2e15ce8e1 | |||
| 3c59db91cd | |||
| a97b2f43ca | |||
| fdd3a261e5 | |||
| f69e5afaf0 | |||
| 7b78f65c4a | |||
|
|
72ed5e3fab | ||
|
|
6bfd7024ce | ||
| aabe473ef6 | |||
| 8a7d08c3b9 | |||
| fdc3e1cce8 | |||
| fc47222830 | |||
|
|
0a5de21386 | ||
|
|
1cbe57d512 | ||
| 936e4f8c0a | |||
|
|
4ebf949912 | ||
| c00c8007e5 | |||
| 54516665bf | |||
| ecec2e77c0 | |||
|
|
102898ac33 | ||
| 7370a2be4c | |||
| 135d019026 | |||
| e2651344d7 | |||
| 9bf3885868 | |||
| caa5aa47dc | |||
|
|
7a42bc1b88 | ||
|
|
6c02ee4cd6 | ||
| 73ee214b61 | |||
|
|
77487bfa35 | ||
|
|
9cf0165cf7 | ||
|
|
e4d4b3142f | ||
|
|
a87be39b44 | ||
|
|
8ea621ac98 | ||
| ea61119919 | |||
|
|
01235616a0 | ||
|
|
10051cb7d1 | ||
|
|
7b0ad2d60d | ||
|
|
fc0a450e94 | ||
| 6cab245cde | |||
| ed90cd8036 | |||
|
|
784bab4522 | ||
|
|
ee97e8211e | ||
| b0eb8b07a8 | |||
| 1d1ef7179e | |||
| d0eb1a7bfb | |||
|
|
256e4ef314 | ||
|
|
6a6dfc5e9d | ||
|
|
5140848039 | ||
|
|
31baf01d3e | ||
|
|
804e78cf0c | ||
| f6a621fe46 | |||
| c47d0c9789 | |||
|
|
1ddc71b017 | ||
|
|
3e507885ab | ||
| e92d0d73b5 | |||
| 00a99e752c | |||
|
|
b13f3d2fcb | ||
| d2ea67e5d6 | |||
| b0df4cb7ee | |||
|
|
16f034766f | ||
| 31004da20d | |||
| a607c56bb3 | |||
|
|
f372adca5b | ||
|
|
76a51a3e9c | ||
| 6adf8a2e78 | |||
| 5a92d96ba2 | |||
|
|
827c93886a | ||
|
|
f6abb61c69 | ||
|
|
4f3478c215 | ||
|
|
5927910a52 | ||
| 88f1c2f843 | |||
|
|
ff5dc5f196 | ||
|
|
f0f33cee52 | ||
|
|
495e4b9069 | ||
|
|
2e843e3f36 | ||
| 1ee69cd6dc | |||
| 6b9e0ed2cf | |||
| 36d14bc75e | |||
|
|
82e061a09c | ||
| 8de08bfc39 | |||
| e317393132 | |||
| 888a2b9bd3 | |||
|
|
b9defdd1dc | ||
| 6e8b19eda1 | |||
|
|
83350b5dd8 | ||
|
|
2e14d4f28c | ||
|
|
96cb77da45 | ||
|
|
1ee07eeecf | ||
|
|
b4c0946883 | ||
|
|
efcf3b273c | ||
|
|
761bf8de38 | ||
|
|
5ccfc18a35 | ||
|
|
92434e48ad | ||
|
|
d3c91e143a | ||
|
|
872e73b5f0 | ||
|
|
af4ad3a7af | ||
|
|
1ff67ed93c | ||
|
|
d2739d1791 | ||
|
|
487cb489f3 | ||
|
|
d9cb2db61f | ||
|
|
35f37ac796 | ||
|
|
d7ad0288b9 | ||
|
|
9c98cabe6c | ||
|
|
a6f6897543 | ||
|
|
2ea3925977 | ||
| 489df78391 | |||
|
|
842c7f9b23 | ||
| fe3f6d6287 | |||
|
|
9728ebd98d | ||
|
|
8e116d81d7 | ||
|
|
fbca91144d | ||
|
|
c392390147 | ||
|
|
9b239abda0 | ||
|
|
78945d9f45 | ||
|
|
1e0365de1c | ||
|
|
c8d71b0267 | ||
|
|
bfc534da15 | ||
|
|
d3dff44918 | ||
|
|
4026b03005 | ||
|
|
eab19a0e6e | ||
|
|
1d7b7d654d | ||
| 2c4152a593 | |||
|
|
de51946850 | ||
|
|
ebd55c5b02 | ||
|
|
f48089cb8c | ||
|
|
30a99f9cc5 | ||
|
|
b1979f63ef | ||
|
|
97c3cfd574 | ||
|
|
56df578ab2 | ||
|
|
556ab608c5 | ||
|
|
0633a6de84 | ||
|
|
a39f9bb7e8 | ||
|
|
a0172337ea | ||
|
|
56683bcee2 | ||
|
|
8d99612695 | ||
|
|
f232fe158c | ||
|
|
11b218702b | ||
|
|
a957e1d359 | ||
|
|
c6b25d2d49 | ||
|
|
ca5eb1dbbe | ||
|
|
c3bc3c051b | ||
|
|
dbb95b7763 | ||
|
|
6ca887ba56 | ||
|
|
70e26c57d9 | ||
|
|
1a5c84cd0f | ||
|
|
fbce35b272 | ||
|
|
84ed3e7d03 | ||
|
|
0f7f3e0a11 | ||
|
|
437bbe114b | ||
|
|
9f7870b804 | ||
|
|
9f00cd646e | ||
|
|
4e125ce38f | ||
|
|
4a963ffbf5 | ||
|
|
f25d9ec09d | ||
|
|
4197ad5aa8 | ||
|
|
2ebd6e11ba | ||
|
|
098e7f8590 | ||
|
|
42aec96410 | ||
|
|
f2905ee169 | ||
|
|
1ba9291746 | ||
|
|
c44766ea14 | ||
|
|
2c10b9c65c | ||
|
|
c56874fe00 | ||
|
|
ebe9c2ffeb | ||
|
|
b645d1495b | ||
| 3a4a4c3460 | |||
|
|
182de51f9b | ||
|
|
712d1549c7 | ||
|
|
5a478c8936 | ||
| c9ecc1dde4 | |||
| bdf9e2fd5b | |||
|
|
0b795b26c0 | ||
| 96aac0cfa2 | |||
|
|
a82e1f33e3 | ||
|
|
058c887cd3 | ||
|
|
81d959c7c1 | ||
| 7c5adeabb5 | |||
|
|
cb88376bda | ||
|
|
4c8ddeca25 | ||
|
|
d264a3f239 | ||
|
|
840b1aa1bf | ||
|
|
e26fd307c8 | ||
|
|
62deaf9f03 | ||
|
|
865bf71f7d | ||
|
|
734c5bccaa | ||
|
|
bc82cb5f5e | ||
|
|
f25c76fdfd | ||
|
|
e649b41e9e | ||
|
|
a962979765 | ||
|
|
01d76fa66f | ||
|
|
49cfde9f7d | ||
|
|
ce04ffea05 | ||
| 9dc0499f66 | |||
|
|
bc1a7dc54f | ||
|
|
93c267fd4e | ||
|
|
0457eb6663 | ||
|
|
519494718b | ||
|
|
de5c38f0fb | ||
|
|
208470e7d9 | ||
|
|
0321f77451 | ||
|
|
6c5fdc01eb | ||
|
|
2aa0cd8d7a | ||
|
|
397bc4524f | ||
|
|
8dce9f3e48 | ||
| 8e9f1df1ce | |||
|
|
ff4915f7f3 | ||
|
|
6ff8eece7b | ||
|
|
2849e6ed07 | ||
|
|
90b11fe3fa | ||
|
|
147609842d | ||
|
|
dd6f9cd617 | ||
|
|
7f590c35da | ||
|
|
a38de108e3 | ||
|
|
e975e7de97 | ||
|
|
d418a7e971 | ||
|
|
a5b5052a5f | ||
|
|
7638595523 | ||
|
|
70d64f6eec | ||
|
|
f0ecfa57e5 | ||
|
|
5f3416ecd7 | ||
|
|
d8b1a72da2 | ||
|
|
7e64819eb2 | ||
|
|
2f1d403af4 | ||
|
|
075d410f7d | ||
|
|
f964bcef9e | ||
|
|
5784232d4e | ||
|
|
70ecc8b50e | ||
|
|
369a035e8a | ||
|
|
e5655033c1 | ||
|
|
c7af30bfa3 | ||
|
|
c8da3a54cf | ||
|
|
100da16803 | ||
|
|
dc91679040 | ||
| 28c8ebfc65 | |||
|
|
0c4d30afe3 | ||
|
|
bc015b72b6 | ||
| 085a3f84e9 | |||
|
|
f241d75f0a | ||
|
|
8a883c09f6 | ||
|
|
42d01b4044 | ||
|
|
15ff90025a | ||
|
|
10cf4998f5 | ||
|
|
f714f20f29 | ||
|
|
19adcc3115 | ||
|
|
bb6b25bac7 | ||
|
|
ec9dbd7ad6 | ||
|
|
2cfba99bda | ||
|
|
a181914c36 | ||
|
|
539405e249 | ||
|
|
d9c27efa8d | ||
|
|
2ccc7b5499 | ||
|
|
4623b9665b | ||
| 9c099b899c | |||
|
|
3ae0809ee5 | ||
| d52c5b26a0 | |||
|
|
0ea6e839ac | ||
| 46ef7b19f6 | |||
|
|
a00bf5ba67 | ||
|
|
e0b09adbba | ||
|
|
19a57dbf6e | ||
|
|
cd2b32f2f4 | ||
|
|
a1f5355d6a | ||
|
|
0972c0deaa | ||
|
|
e1a5cc9e45 | ||
| 73f50c0435 | |||
|
|
351a22cb3c | ||
|
|
3ccd35a4e2 | ||
|
|
e4956cc1d4 | ||
|
|
291ba51b07 | ||
| 5ee57f3d07 | |||
|
|
ed72c5c48c | ||
| 146b0715bf | |||
| dfc1d567a5 | |||
| 779200f5fc | |||
| cf4c4cfca9 | |||
|
|
ad4eead4ca | ||
|
|
f40a86f0f6 | ||
|
|
aa9383a483 | ||
|
|
867422f4cc | ||
|
|
2a6e29b5b8 | ||
|
|
ba105f609c | ||
|
|
e4d669f9b6 | ||
|
|
5edf09e0a7 | ||
|
|
5a695f495c | ||
|
|
f231edb4a6 | ||
|
|
389ef94cd5 | ||
|
|
4c90f66dbc | ||
| ab8643a89a | |||
|
|
b831b93133 | ||
|
|
0c3aab673a | ||
|
|
83353326fb | ||
|
|
db7a5d601e | ||
|
|
ee977f4fab | ||
| 33e7564e8f | |||
|
|
1a59f95be7 | ||
| 77c4c473c1 | |||
| 47ff1a2293 | |||
|
|
97918f301b | ||
|
|
830a907bd1 | ||
|
|
ffae344476 | ||
|
|
4f62cd0148 | ||
| bd92c1925e | |||
|
|
6c29d7823b | ||
| 3c9f133374 | |||
| e72195ca5d | |||
| 3e7ddf59b4 | |||
| cd67fb38dc | |||
| 78149e6c54 | |||
| 63e220c5be | |||
| 8464e506e0 | |||
| 0bc69401e5 | |||
| 47fe7686cb | |||
| 1e5e803c92 | |||
|
|
dd2b3671fd | ||
| ad34a9a4db | |||
| 11e006741f | |||
| bd03b2b06d | |||
|
|
2b2b8e6429 | ||
|
|
5375d0a208 | ||
|
|
f2da84829e | ||
|
|
fc1c93957c | ||
|
|
f172ad66bc | ||
|
|
046c58bb80 | ||
|
|
bf825a4f65 | ||
|
|
d58cff9081 | ||
|
|
7ab1964746 | ||
| 0bf385d1e0 | |||
|
|
b118280a77 | ||
|
|
5317c14d54 | ||
| b217499b59 | |||
|
|
85fed5cd76 | ||
|
|
6f9196c690 | ||
|
|
2d0a73e74d | ||
|
|
ac8821baec | ||
|
|
0b9284e481 | ||
| d123296359 | |||
| 50ab866652 | |||
| f1252537a6 | |||
| b1682b6f32 | |||
| ded7990096 | |||
| 52e95e3455 | |||
| 1d13252045 | |||
| f4630309de | |||
|
|
7b7a80c502 | ||
|
|
1ace15a308 | ||
| 27b6e46973 | |||
| e5262a18d4 | |||
|
|
e1b3ef7c8c | ||
| cf10e83e91 | |||
| 0bd156141c | |||
| 890608a3e8 | |||
| 710056bded | |||
|
|
fb7a0f43e1 | ||
|
|
6c901f1c21 | ||
| 26f008d527 | |||
| 56ebc7be3b | |||
|
|
0ea66f6d37 | ||
|
|
cb30ed2b98 | ||
|
|
dfbe2d8f94 | ||
| a615c5fdb6 | |||
| ca7ee83f7f | |||
|
|
eac9da22bf | ||
|
|
626fc2e15f | ||
|
|
87e2edbd6c | ||
|
|
7cf681bea3 | ||
|
|
281a145bef | ||
|
|
15d5f9ec91 | ||
|
|
0a6c5a0ec4 | ||
|
|
2a9526d056 | ||
|
|
c2ff28c323 | ||
|
|
fbaa2327c6 | ||
|
|
50710ee1df | ||
|
|
062ba91c17 | ||
| 97a530cc66 | |||
| 317c8c81a0 | |||
| c87776ca1b | |||
| 04032831c3 | |||
| 6dd1d47bb2 | |||
| e70a9645ef | |||
| aeabc29e55 | |||
|
|
9600fa2512 | ||
|
|
7951817480 | ||
|
|
405eea1d6c | ||
|
|
e3f189eed4 | ||
|
|
0bb42c5e3c | ||
|
|
c02eac196e | ||
|
|
3fb0d863e9 | ||
|
|
6d573d3897 | ||
|
|
33280d7a5b | ||
|
|
507722da0d | ||
|
|
c8e029cff4 | ||
|
|
7bd2e31f3b | ||
|
|
cfa0c8b9af | ||
| c8fb141048 | |||
|
|
df9c1c643f | ||
|
|
5c8d311ae8 | ||
| e1a76bc45a | |||
| 85e5ade93a | |||
| 4a61fb8f7f | |||
| 5347aeba09 | |||
|
|
7ac7c5e52b | ||
| 5098342dfe | |||
| c69be8ffc3 | |||
| 69999d8e8b | |||
|
|
bec4180dcf | ||
| a429581089 | |||
| dd05258dd2 | |||
| c0e5327e49 | |||
| ffba2acad0 | |||
| 64c624bf2e | |||
| 2506d28dd4 | |||
| b6a438d222 | |||
| 6f24a55a04 | |||
| 55045eb101 | |||
| f5e367a645 | |||
| 017dc3a8b5 | |||
| 7285a3f40b | |||
| 438b39ceca | |||
| a39b9ea38f | |||
| 836c3ba518 | |||
| 2d2b4660dc | |||
|
|
1bb7807c25 | ||
|
|
816f1d19d4 | ||
| f25035ae1d | |||
| ff32f648da | |||
| d38d032309 | |||
| d2a90c77fd | |||
| 8a0f14b780 | |||
| f6cb2c4fac | |||
|
|
1594f0c7db | ||
|
|
7cb2a43f95 | ||
|
|
6e85c7a588 | ||
|
|
a68f6962fd | ||
|
|
a650ba15dd | ||
|
|
6ca1b489fc | ||
|
|
a5c9f11c75 | ||
|
|
1ff3937d11 | ||
|
|
d4725d2e54 | ||
|
|
db578564ba | ||
|
|
d4ebef4290 | ||
|
|
b9f368193d | ||
|
|
4257ec78aa | ||
|
|
a0fbe1a740 | ||
|
|
123b9fb535 | ||
|
|
f57c7b8f7d | ||
| 89590f9a37 | |||
| 5d61bebc9e | |||
| 99afa6e7e4 | |||
| b590a9f41b | |||
| 4466ee30d2 | |||
| db372950b4 | |||
| 46f5e07f11 | |||
|
|
1c90f4f455 | ||
|
|
0114a80e38 | ||
|
|
13be2f9c70 | ||
|
|
e396091aa7 | ||
|
|
a00cb1ebec | ||
|
|
7b1264d140 | ||
|
|
04ccbf6843 | ||
|
|
369b9fb023 | ||
|
|
76487b00e9 | ||
|
|
2d0515e25b | ||
|
|
b132b99586 | ||
|
|
5a7b4a1de4 | ||
|
|
6cac008b61 | ||
|
|
929ec6eb1c | ||
|
|
5a35237de5 | ||
|
|
5d77bbba8b | ||
|
|
eda021b6a5 | ||
|
|
259c479ef0 | ||
|
|
a962b8e7cf | ||
|
|
eb0e7247a6 | ||
| ccc77cb9d1 | |||
|
|
5cb5bbdb55 | ||
|
|
ac6cd7be82 | ||
|
|
63f5f4d03d | ||
|
|
a164fb7df9 | ||
|
|
336ba207cf | ||
|
|
3cfd45cc62 | ||
|
|
f7fb917282 | ||
|
|
a182037883 | ||
|
|
f9e79fb756 | ||
|
|
aaf0eef62b | ||
|
|
fafa0c3567 | ||
|
|
4a6845ad6a | ||
|
|
61d66c6f82 | ||
| 123fbc7235 | |||
|
|
eae8694a29 | ||
|
|
2b16be3aef | ||
|
|
d8d4da9c9a | ||
|
|
0b755b7304 | ||
|
|
0816b7b1f9 | ||
|
|
97d45e87ec | ||
|
|
57ef0647b5 | ||
|
|
a34eebba44 | ||
| 857e39eb33 | |||
| 9bd7d84975 | |||
| 731e38bce3 | |||
|
|
2ee6c45d16 | ||
|
|
b80283f8af | ||
|
|
291e23e40a | ||
| d53d1e1e6a | |||
| 8cf249e8fd | |||
| 5d7cfe1e6c | |||
| abc15fb3ab | |||
| 8cc4270e48 | |||
|
|
56b2ba026a | ||
|
|
8beda1ad6c | ||
|
|
b18b471549 | ||
| 93c9a34591 | |||
|
|
9493492dea | ||
|
|
9b976d48ca | ||
|
|
00b19de497 | ||
|
|
f4cdd2d607 | ||
|
|
cdc0c12ec4 | ||
|
|
bc6f109c48 | ||
|
|
cfab64cfa0 | ||
|
|
d4c8c58552 | ||
|
|
2b8cb51a50 | ||
|
|
e068d3263c | ||
| 630f2e9c37 | |||
| 51ac6b052b | |||
| c32258eb3c | |||
|
|
88f50e3c74 | ||
|
|
bfe5ac0ff7 | ||
|
|
d50f5a030a | ||
|
|
c320caec99 | ||
|
|
16a5b2b012 | ||
|
|
2951e0cc2d | ||
|
|
6bb4a7ea18 | ||
|
|
2827978fe5 | ||
|
|
541c19c1a4 | ||
|
|
c5e995f8d6 | ||
|
|
8bf36da566 | ||
| ccb4ec6e03 | |||
| 06ebb52bc9 | |||
|
|
6e23a0362f | ||
| a59d78bcf7 | |||
| 33d4ee92a7 | |||
| dadce3d4c9 | |||
|
|
b61cf34723 | ||
|
|
22445d1268 | ||
|
|
cba9dacb37 | ||
|
|
a055b36c5c | ||
| 06895cc9f8 | |||
| 24496a997a | |||
| 6419686269 | |||
|
|
4554c9100c | ||
| 919c83c143 | |||
| 00ba2957fb | |||
| 5beda6547a | |||
| bd49b3757a | |||
|
|
b32352a369 | ||
| b306f11148 | |||
|
|
8c4955cb65 | ||
|
|
155f2bb0e8 | ||
| 3ca971134a | |||
| 488d8b9316 | |||
| c20bdba4ae | |||
| 0be2d69aee | |||
| a6e67c3478 | |||
| 5968988984 | |||
| 31cd01610a | |||
| a67824762c | |||
|
|
0336541d40 | ||
|
|
01de3836d7 | ||
|
|
c571bb8490 | ||
|
|
5b4d354ea2 | ||
|
|
b0ce0dc40a | ||
| 88f70a7966 | |||
| 89ff323206 | |||
| d4357d939e | |||
|
|
6cb76f0b5c | ||
|
|
ba2baa36c0 | ||
|
|
e36cd785e8 | ||
| 2fa3a353fa | |||
|
|
bdb1ffb2ef | ||
|
|
84090661cf | ||
|
|
68e14bbf05 | ||
|
|
e4f23334d3 | ||
|
|
5593963b89 | ||
|
|
81c9138b93 | ||
|
|
83fa82108b | ||
|
|
76039c3ec7 | ||
|
|
9b57c9ca1c | ||
|
|
4018cf95ba | ||
|
|
173b6e3e8d | ||
|
|
0ed5447aff | ||
|
|
6344a906d8 | ||
|
|
b2c135ae61 | ||
|
|
2032aacba3 | ||
|
|
fadccfc94c | ||
|
|
551e4e43c1 | ||
|
|
1867658cde | ||
| 3fff4f9c4d | |||
|
|
3f119432db | ||
| 0b18fddc3e | |||
| 19503e0b31 | |||
| d8bdc02f09 | |||
| 2d0833061f | |||
|
|
5dfc4e4086 | ||
|
|
c5824a8a8d | ||
|
|
2147c59314 | ||
|
|
56a1960fff | ||
| b8c9522a55 | |||
| b461cff731 | |||
| 728167fd71 | |||
| 460575b462 | |||
|
|
b247da249a | ||
|
|
e79089b880 | ||
|
|
fe907e1c43 | ||
|
|
e95e894365 | ||
|
|
82414d8b8b | ||
|
|
456fa68f0f | ||
|
|
076adc1f6a | ||
|
|
9676488ff2 | ||
|
|
e9affb862d | ||
|
|
e04371510e | ||
|
|
19657a1c12 | ||
|
|
6424c82ac9 | ||
|
|
fcab18191f | ||
|
|
f157612a0e | ||
|
|
b8cb7d52e7 | ||
|
|
d8d1968162 | ||
|
|
0e1d1f1d99 | ||
|
|
0b055dd05f | ||
|
|
ba91c29ba8 | ||
|
|
bd19457c2a | ||
|
|
b0570e1cd9 | ||
|
|
a5f1b59f7b | ||
|
|
01ca29fc01 | ||
|
|
ed9648fdf9 | ||
|
|
7e17aa6eb3 | ||
|
|
9caee9941a | ||
|
|
e309e7a4f4 | ||
|
|
c47441d6d4 | ||
|
|
1844bc48ac | ||
|
|
7a5adebdb5 | ||
|
|
b39f0c577b | ||
|
|
15f4065cd8 | ||
|
|
4c67665b4d | ||
|
|
55e64ae9d6 | ||
|
|
76d0b82b4c | ||
|
|
95d65d270d | ||
|
|
4e5c9c1ccd | ||
|
|
3267af0724 | ||
|
|
75120424d0 | ||
|
|
f13c7e5cf1 | ||
|
|
53a7b1c9e6 | ||
|
|
8c30cbff13 | ||
|
|
8f3a7f33f8 | ||
|
|
67ec27bab7 | ||
|
|
c1b200b0d8 | ||
|
|
e03ec996d6 | ||
|
|
ad8dbfd4ec | ||
|
|
15a774ff81 | ||
|
|
98114c5591 | ||
|
|
f8c6318a88 | ||
|
|
dffcb66d54 | ||
|
|
67c7147e62 | ||
|
|
50d1b4d824 | ||
|
|
dc98ce3b0b | ||
|
|
cf1e3f3835 | ||
|
|
2f913e9363 | ||
|
|
05a9864df8 | ||
|
|
3a0d764dfa | ||
|
|
310087b895 | ||
|
|
dc39ecd4a8 | ||
|
|
99e192c5de | ||
|
|
b86658ef9b | ||
|
|
88f08e8864 | ||
|
|
80e5de5d65 | ||
|
|
665734b168 | ||
|
|
5543f467e6 | ||
|
|
a32c0879b3 | ||
|
|
bb2ad5bb9a | ||
|
|
6f2f11d112 | ||
|
|
9b32b28aa7 | ||
|
|
fef65bbfd2 | ||
|
|
efeba71612 | ||
|
|
8f54002b1e | ||
|
|
9d6882799d | ||
|
|
73a3acee68 | ||
|
|
0a88220e04 | ||
|
|
c8e1779272 | ||
|
|
8bd3580e23 | ||
|
|
f732b32873 | ||
|
|
65b18f9148 | ||
|
|
10b1676a35 | ||
|
|
b9bd09d3e8 | ||
|
|
537f687b94 | ||
|
|
bfd532f813 | ||
|
|
4f2b4f46a8 | ||
|
|
077cc9458d | ||
|
|
0a7ab394a4 | ||
|
|
f873febfde | ||
|
|
55e8ce359b | ||
|
|
99d7c8f119 | ||
|
|
b3c90f09d6 | ||
|
|
2401962c53 | ||
|
|
362b4d4db3 | ||
|
|
8aea325139 | ||
|
|
bb370061a2 | ||
|
|
48442f7769 | ||
|
|
e67a8531ce | ||
|
|
ef4f020e2a | ||
|
|
2feceeb2f9 | ||
|
|
eaec922fea | ||
|
|
de94777fff | ||
|
|
0aa0ae65e0 | ||
|
|
4b0d62d59b | ||
|
|
b3ef50e9eb | ||
|
|
d30a1890a1 | ||
|
|
f1c2569de3 | ||
|
|
4826388cdd | ||
| e88736056a | |||
| 9da2a29a72 | |||
| dce8a08a86 | |||
| 1fabb9e610 | |||
| 23db0ac80d | |||
| 28370341d8 | |||
|
|
a023ffe850 | ||
|
|
a4e77ecf6e | ||
|
|
7efc0a1fb2 | ||
|
|
c3e2b2ce70 | ||
|
|
dde1228b1d | ||
|
|
b3474b6dfb | ||
|
|
179a04ae31 | ||
|
|
2bdcbda54c | ||
|
|
a123392c56 | ||
|
|
719135e366 | ||
|
|
ba619554b7 | ||
|
|
6a3ab7032f | ||
|
|
d818d14cb4 | ||
|
|
599c130395 | ||
|
|
9ef2759e27 | ||
|
|
43355c88d4 | ||
|
|
15e1acaf4f | ||
|
|
ec77ffdd88 | ||
|
|
9797c1ca84 | ||
|
|
bbe9633dc8 | ||
|
|
6f60ac5cc7 | ||
|
|
e7ba09793c | ||
|
|
c0c0800e61 | ||
|
|
0bd9d8f93f | ||
|
|
214fc2d5cd | ||
|
|
55b0e2f934 | ||
|
|
609cd4ed6d | ||
|
|
2b20bbdcc8 | ||
|
|
946a95bea1 | ||
|
|
7ec1c152e3 | ||
|
|
0cf1110018 | ||
|
|
9c5ada6fa1 | ||
|
|
0f5702e21b | ||
|
|
f10d6ec80a | ||
|
|
490215df90 | ||
|
|
9d27451813 | ||
|
|
a977f59675 | ||
|
|
51a09d049c | ||
|
|
47be5e77e5 | ||
|
|
d517e79ec0 | ||
|
|
8714ecdde8 | ||
|
|
2cffc57209 | ||
|
|
4e43687de2 | ||
|
|
f8dab83e37 | ||
|
|
655af03cf3 | ||
|
|
0a4dd00edb | ||
|
|
55b4929c54 | ||
|
|
8eb73a6b3c | ||
|
|
0aeb201625 | ||
|
|
e0140a23c2 | ||
|
|
ff6698a9d1 | ||
|
|
843d498b72 | ||
|
|
349612a065 | ||
|
|
e48b22128d | ||
|
|
14dfe4ec51 | ||
|
|
f679b17cbe | ||
|
|
36a0f0e743 | ||
|
|
ad563b9bc8 | ||
|
|
e0051bf276 | ||
|
|
7b72998e1c | ||
|
|
893cce7f21 | ||
|
|
bf7e8fd0e6 | ||
|
|
66d02cf1d1 | ||
|
|
f2c8e40430 | ||
|
|
4b28ee8e73 | ||
|
|
c7e54cfe9f | ||
|
|
50be3acc78 | ||
|
|
66c12a69bb | ||
|
|
e2058f0c71 | ||
|
|
75a74152a5 | ||
|
|
9344c4fd35 | ||
|
|
be0fe00076 | ||
|
|
a2069ee48b | ||
|
|
907885cf85 | ||
|
|
f10ed3236e | ||
|
|
a852a0af7c | ||
|
|
03f3550774 | ||
|
|
2e8d06f9e1 | ||
|
|
1d31ec4dcc | ||
|
|
4407ed68ae | ||
|
|
b3359f2138 | ||
|
|
cff9104adf | ||
|
|
b73f821c19 | ||
|
|
dd3c1a7375 | ||
|
|
c10b5368af | ||
|
|
2df66765c0 | ||
|
|
ed65545571 | ||
|
|
b4ae486520 | ||
|
|
bb7b3d0b84 | ||
|
|
65b34aa015 | ||
|
|
b3c9303def | ||
|
|
19dfda9b6a | ||
|
|
ce2abb448f | ||
|
|
413c3b8098 | ||
|
|
269514a44f | ||
|
|
5fc334dd8b | ||
|
|
1b251f1cea | ||
|
|
70d3e25c7f | ||
|
|
5e9b33e346 | ||
|
|
eb52ec7532 | ||
|
|
f46c4bf3ca | ||
|
|
d8176912cf | ||
|
|
4c54ade2d3 | ||
|
|
52c41dfb3a | ||
|
|
36f0aa7411 | ||
|
|
a10e4ec264 | ||
|
|
851b6bc273 | ||
|
|
23f8d31b1b | ||
|
|
a5683bcd07 | ||
|
|
c7be71c781 | ||
|
|
594f274323 | ||
|
|
ba1ed5e732 | ||
|
|
613cc6b9ef | ||
|
|
8edb00f869 | ||
|
|
60a1f84604 | ||
|
|
36cfaee5db | ||
|
|
cb607c93ca | ||
|
|
03b5e1d824 | ||
|
|
85a530ae5a | ||
|
|
2ea49a425f | ||
|
|
3c894c4147 | ||
|
|
23d151c919 | ||
|
|
6d1c4ff81a | ||
|
|
7a6e6e8333 | ||
|
|
0eba6bdcf4 | ||
|
|
d7ecaf5932 | ||
|
|
1fc6db114d | ||
|
|
8d203b8df4 | ||
|
|
39924ff078 | ||
|
|
de25f106ec | ||
|
|
e0d85f458b | ||
|
|
3a9cd46e6e | ||
|
|
301edab8ad | ||
|
|
aed39c2ec4 | ||
|
|
cc594eca52 | ||
|
|
3fbca2835e | ||
|
|
61ed5c4fa7 | ||
|
|
e31774ae9d | ||
|
|
00f09179a8 | ||
|
|
4196901e01 | ||
|
|
bf35dd072a | ||
|
|
75e3fd018d | ||
|
|
965dfff7c6 | ||
|
|
ff64dd22ad | ||
|
|
e7cceab065 | ||
|
|
f789b8f7a2 | ||
|
|
c0b82c5125 | ||
|
|
1c1b5baefe | ||
|
|
8b17aeaea2 | ||
|
|
cb0d03c965 | ||
|
|
9e77f3d64e | ||
|
|
25f61815dc | ||
|
|
3a2252e69c | ||
|
|
8a08980e6a | ||
|
|
d0f31771ad | ||
|
|
e9e2c9372d | ||
|
|
70c4a095a0 | ||
|
|
82e2fc4445 | ||
|
|
6661d81fdf | ||
|
|
e76abaafa8 | ||
|
|
fbfc1c05d6 | ||
|
|
839c211c64 | ||
|
|
f3ff82143a | ||
|
|
0dd0abae87 | ||
|
|
13781c993e | ||
|
|
7616cacbec | ||
|
|
cab7d3c012 | ||
|
|
dfce676fdf | ||
|
|
1890cab623 | ||
|
|
4307d8fe43 | ||
|
|
8df6fdbee6 | ||
|
|
ac5c2a3088 | ||
|
|
0212b677ae | ||
|
|
1a0d62d8f3 | ||
|
|
8f4d1c7aea | ||
|
|
2a4735c6f2 | ||
|
|
9b6ba3548f | ||
|
|
5a2ee88cbc | ||
|
|
efa4c71b8a | ||
|
|
ecfc1a4bf8 | ||
|
|
246b855c76 | ||
|
|
b23f199334 | ||
|
|
ed5dabee9f | ||
|
|
0c88c5a522 | ||
|
|
640e7015c8 | ||
|
|
80aa848617 | ||
|
|
acf045965e | ||
|
|
5a7f64dc35 | ||
|
|
de447b0727 | ||
|
|
a2906abf71 | ||
|
|
2fd306f435 | ||
|
|
e3f779dbd1 | ||
|
|
1064f11663 | ||
|
|
46abc54cb0 | ||
|
|
2c808a937a | ||
|
|
52cf9a420f | ||
|
|
2d29be45f5 | ||
|
|
a44222c3ba | ||
|
|
efc82101c1 | ||
|
|
09ce2fb6be | ||
|
|
a383388e54 | ||
|
|
362078b12c | ||
|
|
9d0c3410a5 | ||
|
|
dfb9c28f3a | ||
|
|
8d155283dd | ||
|
|
ace16efd93 | ||
|
|
13ae5ae756 | ||
|
|
d991ead86a | ||
|
|
33a202fa1c | ||
|
|
ff5463a84c | ||
|
|
aa7c3ae4a9 | ||
|
|
2e66bfde4b | ||
|
|
f1b035032f | ||
|
|
16e21adb20 | ||
|
|
01c5682c3d | ||
|
|
cfc8ff2837 | ||
|
|
edf25b471a | ||
|
|
bb894e6107 | ||
|
|
6b3a0cdb13 | ||
|
|
8c98a26160 | ||
|
|
bcd9310f26 | ||
|
|
57e9b67207 | ||
|
|
7bf53858f0 | ||
|
|
02780d0bcd | ||
|
|
6356aed06b | ||
|
|
69fd7b2cb5 | ||
|
|
5d1eed1494 | ||
|
|
e2e2824f37 | ||
|
|
d461135980 | ||
|
|
65fbae7610 | ||
|
|
761428502a | ||
|
|
6eb2ceaf53 | ||
|
|
66813b9824 | ||
|
|
140d8e4eac | ||
|
|
0d730e0576 | ||
|
|
ca18fcecf0 | ||
|
|
009069169f | ||
|
|
6d166efd11 | ||
|
|
1b117a67aa | ||
|
|
9037160362 | ||
|
|
505d85c256 | ||
|
|
71a3fe04a0 | ||
|
|
1555afe771 | ||
|
|
79bb27524c | ||
|
|
9651b7adb4 | ||
|
|
59e5bec731 | ||
|
|
182e66216f | ||
|
|
a2832f1e1a | ||
|
|
56f34508fa | ||
|
|
2408fd091e | ||
|
|
0e38a24664 | ||
|
|
aa643d1782 | ||
|
|
cdc91e9cda | ||
|
|
3f0590e0fe | ||
|
|
5efb294ff2 | ||
|
|
011e2d83dc | ||
|
|
e36b511530 | ||
|
|
b6a2a85d1d | ||
|
|
f1cceeb5e6 | ||
|
|
6fee2548fd | ||
|
|
91005066cf | ||
|
|
e1f17ef47d | ||
|
|
8a40071c35 | ||
|
|
430957eb3d | ||
|
|
25874be679 | ||
|
|
ed8440434f | ||
|
|
0f9884c1b6 | ||
|
|
d126a05347 | ||
|
|
3e26bbbbba | ||
|
|
982cc8f7a0 | ||
|
|
d1770698e0 | ||
|
|
b78e8617c4 | ||
|
|
3ce9ca0986 | ||
|
|
04d17c3680 | ||
|
|
d26e15f91c | ||
|
|
83c46091b3 | ||
|
|
d640d7c040 | ||
|
|
c934eb2b08 | ||
|
|
24dd5e32ad | ||
|
|
a23103b2c3 | ||
|
|
35aa4235e4 | ||
|
|
e9be1cf99a | ||
|
|
c7b0821081 | ||
|
|
4a4618dd32 | ||
|
|
d223e83c60 | ||
|
|
d1f1a20126 | ||
|
|
4b89e3762f | ||
|
|
bc110288de | ||
|
|
e94e16b52c | ||
|
|
76aacee016 | ||
|
|
1a3bd5d1f5 | ||
|
|
3f6e89d716 | ||
|
|
361ec84638 | ||
|
|
35cc1e4f62 | ||
|
|
64a976e888 | ||
|
|
7e2cb8491f | ||
|
|
2cdab7522d | ||
|
|
a07eabc408 | ||
|
|
7279c23fe2 | ||
|
|
80707d77d9 | ||
|
|
d5920c5885 | ||
|
|
6a3a6b4485 | ||
|
|
2b1df0c61a | ||
|
|
216725f306 | ||
|
|
3183f89a62 | ||
|
|
f5cc16c3bd | ||
|
|
e78dc76e56 | ||
|
|
bfdb5ef0a6 | ||
|
|
35353d3fce | ||
|
|
7a02c8ad34 | ||
|
|
331d9b0010 | ||
|
|
ef5686cce7 | ||
|
|
fa87111f4a | ||
|
|
94967b0f6c | ||
|
|
3f796b300d | ||
|
|
a07c16fb52 | ||
|
|
fd6905ea9f | ||
|
|
08f58b5f4f | ||
|
|
bd8012fe3e | ||
|
|
fa531b34fd | ||
|
|
354443c98b | ||
|
|
ee30ab195f | ||
|
|
02c1712d22 | ||
|
|
37def7a956 | ||
|
|
653e3d05e0 | ||
|
|
d8467c24b1 | ||
|
|
fc9056c1ac | ||
|
|
9b1d295b82 | ||
|
|
e2ea3f4ddc | ||
|
|
99d0b01a24 | ||
|
|
131c672020 | ||
|
|
338f2fb2dd | ||
|
|
4552a9a856 | ||
|
|
daeb753f9e | ||
|
|
f50a99d0b8 | ||
|
|
e6d0d3efd5 | ||
|
|
057460467c | ||
|
|
5aee9d955e | ||
|
|
7fb1da31e4 | ||
|
|
1aa92c0a69 | ||
|
|
4c097a69fd | ||
|
|
2634933e84 | ||
|
|
d60c0850c2 | ||
|
|
491bc3371c | ||
|
|
c1bab07b08 | ||
|
|
95f3ebd51d | ||
|
|
0e5b72b54f | ||
|
|
33ce592379 | ||
|
|
9f6591d7e3 | ||
|
|
5343ca00d8 | ||
|
|
f764f1f22f | ||
|
|
978af5037e | ||
|
|
39e88052c7 | ||
|
|
889351caf1 | ||
|
|
e6476dc230 | ||
|
|
e7de45c94f | ||
|
|
2f822aba71 | ||
|
|
ba687bf8e2 | ||
|
|
618cbe5a21 | ||
|
|
d723150b6d | ||
|
|
b1ad983ca5 | ||
|
|
4711d0510e | ||
|
|
93854c287f | ||
|
|
687a3047fd | ||
|
|
c067c6e74d | ||
|
|
04b44f40ba | ||
|
|
ce2126bd34 | ||
|
|
638efe8899 | ||
|
|
23353355e4 | ||
|
|
1be64798c5 | ||
|
|
a92a458cf1 | ||
|
|
703fdf9c02 | ||
|
|
bc239cf5d6 | ||
|
|
86780db478 | ||
|
|
5d5afa20c7 | ||
|
|
d662c1a981 | ||
|
|
927bb8c78c | ||
|
|
509171c484 | ||
|
|
ae98b50a21 | ||
|
|
f3abafd5ed | ||
|
|
939e6803e1 | ||
|
|
6a055a4fc6 | ||
|
|
5076ea696c | ||
|
|
3a60e6422c | ||
|
|
b90b5d5c03 | ||
|
|
970e14b2e1 | ||
|
|
d5a8140d4f | ||
|
|
5f5d84da87 | ||
|
|
4c9e48550e | ||
|
|
8504ec6c4d | ||
|
|
a0ad8b3f34 | ||
|
|
53990a9ba3 |
113
.all-contributorsrc
Normal file
113
.all-contributorsrc
Normal file
@@ -0,0 +1,113 @@
|
||||
{
|
||||
"projectName": "adapter",
|
||||
"projectOwner": "sasjs",
|
||||
"repoType": "github",
|
||||
"repoHost": "https://github.com",
|
||||
"files": [
|
||||
"README.md"
|
||||
],
|
||||
"imageSize": 100,
|
||||
"commit": false,
|
||||
"commitConvention": "angular",
|
||||
"contributors": [
|
||||
{
|
||||
"login": "krishna-acondy",
|
||||
"name": "Krishna Acondy",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/2980428?v=4",
|
||||
"profile": "https://krishna-acondy.io/",
|
||||
"contributions": [
|
||||
"code",
|
||||
"infra",
|
||||
"blog",
|
||||
"content",
|
||||
"ideas",
|
||||
"video"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "YuryShkoda",
|
||||
"name": "Yury Shkoda",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/25773492?v=4",
|
||||
"profile": "https://www.erudicat.com/",
|
||||
"contributions": [
|
||||
"code",
|
||||
"infra",
|
||||
"ideas",
|
||||
"test",
|
||||
"video"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "medjedovicm",
|
||||
"name": "Mihajlo Medjedovic",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/18329105?v=4",
|
||||
"profile": "https://github.com/medjedovicm",
|
||||
"contributions": [
|
||||
"code",
|
||||
"infra",
|
||||
"test",
|
||||
"review"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "allanbowe",
|
||||
"name": "Allan Bowe",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4420615?v=4",
|
||||
"profile": "https://github.com/allanbowe",
|
||||
"contributions": [
|
||||
"code",
|
||||
"review",
|
||||
"test",
|
||||
"mentoring",
|
||||
"maintenance"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "saadjutt01",
|
||||
"name": "Muhammad Saad ",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/8914650?v=4",
|
||||
"profile": "https://github.com/saadjutt01",
|
||||
"contributions": [
|
||||
"code",
|
||||
"review",
|
||||
"test",
|
||||
"mentoring",
|
||||
"infra"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "sabhas",
|
||||
"name": "Sabir Hassan",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/82647447?v=4",
|
||||
"profile": "https://github.com/sabhas",
|
||||
"contributions": [
|
||||
"code",
|
||||
"review",
|
||||
"test",
|
||||
"ideas"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "VladislavParhomchik",
|
||||
"name": "VladislavParhomchik",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/83717836?v=4",
|
||||
"profile": "https://github.com/VladislavParhomchik",
|
||||
"contributions": [
|
||||
"test",
|
||||
"review"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "rudvfaden",
|
||||
"name": "Rud Faden",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/2445577?v=4",
|
||||
"profile": "http://rudvfaden.github.io/",
|
||||
"contributions": [
|
||||
"userTesting",
|
||||
"doc"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
"skipCi": true
|
||||
}
|
||||
2
.env.example
Normal file
2
.env.example
Normal file
@@ -0,0 +1,2 @@
|
||||
SERVER_URL=https://server.com
|
||||
DEFAULT_COMPUTE_CONTEXT=SAS Job Execution compute context
|
||||
18
.git-hooks/commit-msg
Executable file
18
.git-hooks/commit-msg
Executable file
@@ -0,0 +1,18 @@
|
||||
#!/bin/sh
|
||||
RED="\033[1;31m"
|
||||
GREEN="\033[1;32m"
|
||||
|
||||
# Get the commit message (the parameter we're given is just the path to the
|
||||
# temporary file which holds the message).
|
||||
commit_message=$(cat "$1")
|
||||
|
||||
if (echo "$commit_message" | grep -Eq "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z0-9 -\*]+\))?!?: .+$") then
|
||||
echo "${GREEN} ✔ Commit message meets Conventional Commit standards"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "${RED}❌ Commit message does not meet the Conventional Commit standard!"
|
||||
echo "An example of a valid message is:"
|
||||
echo " feat(login): add the 'remember me' button"
|
||||
echo "ℹ More details at: https://www.conventionalcommits.org/en/v1.0.0/#summary"
|
||||
exit 1
|
||||
12
.git-hooks/pre-commit
Executable file
12
.git-hooks/pre-commit
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Using `--silent` helps for showing any errs in the first line of the response
|
||||
# The first line is picked up by the VS Code GIT UI popup when rc is not 0
|
||||
|
||||
if npm run --silent lint:silent ; then
|
||||
exit 0
|
||||
else
|
||||
npm run --silent lint:fix
|
||||
echo "❌ Prettier check failed! We ran lint:fix for you. Please add & commit again."
|
||||
exit 1
|
||||
fi
|
||||
128
.github/CODE_OF_CONDUCT.md
vendored
Normal file
128
.github/CODE_OF_CONDUCT.md
vendored
Normal file
@@ -0,0 +1,128 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
https://sasapps.io/contact-us.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
20
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
20
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
## Issue
|
||||
|
||||
Link any related issue(s) in this section.
|
||||
|
||||
## Intent
|
||||
|
||||
What this PR intends to achieve.
|
||||
|
||||
## Implementation
|
||||
|
||||
What code changes have been made to achieve the intent.
|
||||
|
||||
## Checks
|
||||
|
||||
No PR (that involves a non-trivial code change) should be merged, unless all items below are confirmed! If an urgent fix is needed - use a tar file.
|
||||
|
||||
|
||||
- [ ] All `sasjs-cli` unit tests are passing (`npm test`).
|
||||
- (CI Runs this) All `sasjs-tests` are passing. If you want to run it manually (instructions available [here](https://github.com/sasjs/adapter/blob/master/sasjs-tests/README.md)).
|
||||
- [ ] [Data Controller](https://datacontroller.io) builds and is functional on both SAS 9 and Viya
|
||||
7
.github/dependabot.yml
vendored
Normal file
7
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
open-pull-requests-limit: 1
|
||||
11
.github/reviewer-lottery.yml
vendored
Normal file
11
.github/reviewer-lottery.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
groups:
|
||||
- name: SASjs Devs # name of the group
|
||||
reviewers: 1 # how many reviewers do you want to assign?
|
||||
usernames: # github usernames of the reviewers
|
||||
- YuryShkoda
|
||||
- medjedovicm
|
||||
- sabhas
|
||||
- name: SASjs QA
|
||||
reviewers: 1
|
||||
usernames:
|
||||
- VladislavParhomchik
|
||||
13
.github/workflows/assign-reviewer.yml
vendored
Normal file
13
.github/workflows/assign-reviewer.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
name: 'Assign Reviewer'
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: uesteibar/reviewer-lottery@v1
|
||||
with:
|
||||
repo-token: ${{ secrets.GH_TOKEN }}
|
||||
45
.github/workflows/build.yml
vendored
45
.github/workflows/build.yml
vendored
@@ -4,7 +4,6 @@
|
||||
name: SASjs Build
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
@@ -13,21 +12,61 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [12.x]
|
||||
node-version: [lts/fermium]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: npm
|
||||
|
||||
- name: Check npm audit
|
||||
run: npm audit --production --audit-level=low
|
||||
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Check code style
|
||||
run: npm run lint
|
||||
|
||||
- name: Run unit tests
|
||||
run: npm test
|
||||
|
||||
- name: Build Package
|
||||
run: npm run package:lib
|
||||
env:
|
||||
CI: true
|
||||
|
||||
- name: Install SSH Key
|
||||
uses: shimataro/ssh-key-action@v2
|
||||
with:
|
||||
key: ${{ secrets.DCGITLAB_KEY }}
|
||||
known_hosts: 'placeholder'
|
||||
|
||||
- name: Deploy sasjs-tests
|
||||
run: |
|
||||
npm install -g replace-in-files-cli
|
||||
cd sasjs-tests
|
||||
replace-in-files --regex='"@sasjs/adapter".*' --replacement='"@sasjs/adapter":"latest",' ./package.json
|
||||
npm i
|
||||
replace-in-files --regex='"serverUrl".*' --replacement='"serverUrl":"${{ secrets.SASJS_SERVER_URL }}",' ./public/config.json
|
||||
replace-in-files --regex='"userName".*' --replacement='"userName":"${{ secrets.SASJS_USERNAME }}",' ./public/config.json
|
||||
replace-in-files --regex='"password".*' --replacement='"password":"${{ secrets.SASJS_PASSWORD }}",' ./public/config.json
|
||||
replace-in-files --regex='"serverType".*' --replacement='"serverType":"SASJS",' ./public/config.json
|
||||
npm run update:adapter && npm run build
|
||||
scp -o stricthostkeychecking=no -r ./build/* ${{ secrets.DCGITLAB_DEPLOY_PATH_VIYA }}
|
||||
|
||||
- name: Run cypress on sasjs
|
||||
run: |
|
||||
replace-in-files --regex='"sasjsTestsUrl".*' --replacement='"sasjsTestsUrl":"${{ secrets.SASJS_TEST_URL_VIYA }}",' ./cypress.json
|
||||
replace-in-files --regex='"username".*' --replacement='"username":"${{ secrets.SASJS_USERNAME }}",' ./cypress.json
|
||||
replace-in-files --regex='"password".*' --replacement='"password":"${{ secrets.SASJS_PASSWORD }}",' ./cypress.json
|
||||
sh ./sasjs-tests/sasjs-cypress-run.sh ${{ secrets.MATRIX_TOKEN }} https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}
|
||||
|
||||
# For some reason if coverage report action is run before other commands, those commands can't access the directories and files on which they depend on
|
||||
- name: Generate coverage report
|
||||
uses: artiomtr/jest-coverage-report-action@v2.0-rc.2
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
44
.github/workflows/generateDocs.yml
vendored
Normal file
44
.github/workflows/generateDocs.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
name: Generate docs and Push to docs Branch
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
generate_and_push_docs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [lts/fermium]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: npm
|
||||
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Ensure docs folder exists
|
||||
run: |
|
||||
rm -rf docs || true # avoid error if docs folder does not exist
|
||||
mkdir docs
|
||||
|
||||
- name: Generate Docs
|
||||
run: npm run typedoc
|
||||
|
||||
- name: Push generated docs
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GH_TOKEN }}
|
||||
publish_branch: gh-pages
|
||||
publish_dir: ./docs
|
||||
cname: adapter.sasjs.io
|
||||
|
||||
20
.github/workflows/npmpublish.yml
vendored
20
.github/workflows/npmpublish.yml
vendored
@@ -11,17 +11,33 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [lts/fermium]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: npm
|
||||
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Check code style
|
||||
run: npm run lint
|
||||
|
||||
- name: Build Project
|
||||
run: npm run build
|
||||
|
||||
- name: Semantic Release
|
||||
uses: cycjimmy/semantic-release-action@v2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Send Matrix message
|
||||
run: curl -XPOST -d "{\"msgtype\":\"m.text\", \"body\":\"New version of @sasjs/adapter has been released! \n Please deploy and run 'dctests' with new adapter to make sure everything is still in place.\"}" https://matrix.4gl.io/_matrix/client/r0/rooms/!jRebyiGmHZlpfDwYXN:4gl.io/send/m.room.message?access_token=${{ secrets.MATRIX_TOKEN }}
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -1,2 +1,10 @@
|
||||
node_modules
|
||||
build
|
||||
|
||||
docs
|
||||
|
||||
.env
|
||||
|
||||
/coverage
|
||||
|
||||
.DS_Store
|
||||
2
.gitpod.yml
Normal file
2
.gitpod.yml
Normal file
@@ -0,0 +1,2 @@
|
||||
tasks:
|
||||
- init: npm install && npm run build
|
||||
16
.npmignore
Normal file
16
.npmignore
Normal file
@@ -0,0 +1,16 @@
|
||||
sasjs-tests/
|
||||
docs/
|
||||
.github/
|
||||
*.md
|
||||
*.spec.ts
|
||||
.all-contributorsrc
|
||||
cypress/
|
||||
.gitpod.yml
|
||||
.prettierrc
|
||||
cypress.json
|
||||
jest.config.js
|
||||
sasjs-cypress-run.sh
|
||||
tsconfig.json
|
||||
tslint.json
|
||||
typedoc.json
|
||||
webpack.config.js
|
||||
@@ -1,5 +1,9 @@
|
||||
# Change Log
|
||||
|
||||
Since March 2020 the changelog is managed by github releases - see [https://github.com/sasjs/adapter/releases](https://github.com/sasjs/adapter/releases).
|
||||
|
||||
## Changes up to 5th March 2020
|
||||
|
||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||
|
||||
<a name="1.9.0"></a>
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at support@macropeople.com. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Macro People
|
||||
Copyright (c) 2021 Macro People
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
329
README.md
329
README.md
@@ -1,12 +1,26 @@
|
||||
[](https://www.jsdelivr.com/package/npm/@sasjs/adapter)
|
||||
|
||||
# @sasjs/adapter
|
||||
|
||||
[![npm package][npm-image]][npm-url]
|
||||
[![Github Workflow][githubworkflow-image]][githubworkflow-url]
|
||||
[]()
|
||||

|
||||
[](/LICENSE)
|
||||

|
||||

|
||||
[](https://gitpod.io/#https://github.com/sasjs/adapter)
|
||||
|
||||
|
||||
[npm-image]:https://img.shields.io/npm/v/@sasjs/adapter.svg
|
||||
[npm-url]:http://npmjs.org/package/@sasjs/adapter
|
||||
[githubworkflow-image]:https://github.com/sasjs/adapter/actions/workflows/build.yml/badge.svg
|
||||
[githubworkflow-url]:https://github.com/sasjs/adapter/blob/main/.github/workflows/build.yml
|
||||
[dependency-image]:https://david-dm.org/sasjs/adapter.svg
|
||||
|
||||
SASjs is a open-source framework for building Web Apps on SAS® platforms. You can use as much or as little of it as you like. This repository contains the JS adapter, the part that handles the to/from SAS communication on the client side. There are 3 ways to install it:
|
||||
|
||||
1 - `npm install @sasjs/adapter` - for use in a node project
|
||||
1 - `npm install @sasjs/adapter` - for use in a nodeJS project (recommended)
|
||||
|
||||
2 - [Download](https://cdn.jsdelivr.net/npm/@sasjs/adapter@1/index.js) and use a copy of the latest JS file
|
||||
2 - [Download](https://cdn.jsdelivr.net/npm/@sasjs/adapter@3/index.min.js) and use a copy of the latest JS file
|
||||
|
||||
3 - Reference directly from the CDN - in which case click [here](https://www.jsdelivr.com/package/npm/@sasjs/adapter?tab=collection) and select "SRI" to get the script tag with the integrity hash.
|
||||
|
||||
@@ -16,11 +30,11 @@ For more information on building web apps with SAS, check out [sasjs.io](https:/
|
||||
|
||||
## None of this makes sense. How do I build an app with it?
|
||||
|
||||
Ok ok. Deploy this [example.html](https://raw.githubusercontent.com/sasjs/adapter/master/example.html) file to your web server, and update `servertype` to `SAS9` or `SASVIYA` depending on your backend.
|
||||
Ok ok. Deploy this [example.html](https://raw.githubusercontent.com/sasjs/adapter/master/example.html) file to your web server, and update `servertype` to `SAS9`, `SASVIYA`, or `SASJS` depending on your backend.
|
||||
|
||||
The backend part can be deployed as follows:
|
||||
|
||||
```
|
||||
```sas
|
||||
%let appLoc=/Public/app/readme; /* Metadata or Viya Folder per SASjs config */
|
||||
filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas";
|
||||
%inc mc; /* compile macros (can also be downloaded & compiled seperately) */
|
||||
@@ -36,13 +50,314 @@ parmcards4;
|
||||
%webout(OBJ,areas)
|
||||
%webout(CLOSE)
|
||||
;;;;
|
||||
%mp_createwebservice(path=&appLoc/common,name=getdata)
|
||||
%mx_createwebservice(path=&appLoc/common,name=getdata)
|
||||
```
|
||||
|
||||
You now have a simple web app with a backend service!
|
||||
|
||||
## Detailed Overview
|
||||
|
||||
The SASjs adapter is a JS library and a set of SAS Macros that handle the communication between the frontend app and backend SAS services.
|
||||
|
||||
There are three parts to consider:
|
||||
|
||||
1. JS request / response
|
||||
2. SAS inputs / outputs
|
||||
3. Configuration
|
||||
|
||||
### JS Request / Response
|
||||
|
||||
To install the library you can simply run `npm i @sasjs/adapter` or include a `<script>` tag with a reference to our [CDN](https://www.jsdelivr.com/package/npm/@sasjs/adapter).
|
||||
|
||||
Full technical documentation is available [here](https://adapter.sasjs.io). The main parts are:
|
||||
|
||||
### Instantiation
|
||||
The following code will instantiate an instance of the adapter:
|
||||
|
||||
```javascript
|
||||
let sasJs = new SASjs.default(
|
||||
{
|
||||
appLoc: "/Your/SAS/Folder",
|
||||
serverType:"SAS9"
|
||||
}
|
||||
);
|
||||
```
|
||||
If you've installed it via NPM, you can import it as a default import like so:
|
||||
```js
|
||||
import SASjs from '@sasjs/adapter';
|
||||
```
|
||||
You can then instantiate it with:
|
||||
```js
|
||||
const sasJs = new SASjs({your config})
|
||||
```
|
||||
|
||||
More on the config later.
|
||||
|
||||
### SAS Logon
|
||||
All authentication from the adapter is done against SASLogon. There are two approaches that can be taken, which are configured using the `loginMechanism` attribute of the sasJs config object (above):
|
||||
|
||||
* `loginMechanism:'Redirected'` - this approach enables authentication through a SASLogon window, supporting complex authentication flows (such as 2FA) and avoids the need to handle passwords in the application itself. The styling of the window can be modified using CSS.
|
||||
* `loginMechanism:'Default'` - this approach requires that the username and password are captured, and used within the `.login()` method. This can be helpful for development, or automated testing.
|
||||
|
||||
Sample code for logging in with the `Default` approach:
|
||||
|
||||
```javascript
|
||||
sasJs.logIn('USERNAME','PASSWORD'
|
||||
).then((response) => {
|
||||
if (response.isLoggedIn === true) {
|
||||
console.log('do stuff')
|
||||
} else {
|
||||
console.log('do other stuff')
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
More examples of using authentication, and more, can be found in the [SASjs Seed Apps](https://github.com/search?q=topic%3Asasjs-app+org%3Asasjs+fork%3Atrue) on github.
|
||||
|
||||
### Request / Response
|
||||
A simple request can be sent to SAS in the following fashion:
|
||||
|
||||
```javascript
|
||||
sasJs.request("/path/to/my/service", dataObject)
|
||||
.then((response) => {
|
||||
// all tables are in the response object, eg:
|
||||
console.log(response.tablewith2cols1row[0].COL1.value)
|
||||
})
|
||||
```
|
||||
|
||||
We supply the path to the SAS service, and a data object.
|
||||
|
||||
If the path starts with a `/` then it should be a full path to the service. If there is no leading `/` then it is relative to the `appLoc`.
|
||||
|
||||
The data object can be null (for services with no input), or can contain one or more "tables" in the following format:
|
||||
|
||||
```javascript
|
||||
let dataObject={
|
||||
"tablewith2cols1row": [{
|
||||
"col1": "val1",
|
||||
"col2": 42
|
||||
}],
|
||||
"tablewith1col2rows": [{
|
||||
"col": "row1"
|
||||
}, {
|
||||
"col": "row2"
|
||||
}]
|
||||
};
|
||||
```
|
||||
|
||||
These tables (`tablewith2cols1row` and `tablewith1col2rows`) will be created in SAS WORK after running `%webout(FETCH)` in your SAS service.
|
||||
|
||||
The `request()` method also has optional parameters such as a config object and a callback login function.
|
||||
|
||||
The response object will contain returned tables and columns. Table names are always lowercase, and column names uppercase.
|
||||
|
||||
The adapter will also cache the logs (if debug enabled) and even the work tables. For performance, it is best to keep debug mode off.
|
||||
|
||||
### Variable Types
|
||||
|
||||
The SAS type (char/numeric) of the values is determined according to a set of rules:
|
||||
|
||||
* If the values are numeric, the SAS type is numeric
|
||||
* If the values are all string, the SAS type is character
|
||||
* If the values contain a single character (a-Z + underscore + .) AND a numeric, then the SAS type is numeric (with special missing values).
|
||||
* `null` is set to either '.' or '' depending on the assigned or derived type per the above rules. If entire column is `null` then the type will be numeric.
|
||||
|
||||
The following table illustrates the formats applied to columns under various scenarios:
|
||||
|
||||
|JS Values |SAS Format|
|
||||
|---|---|
|
||||
|'a', 'a' |$char1.|
|
||||
|0, '_' |best.|
|
||||
|'Z', 0 |best.|
|
||||
|'a', 'aaa' |$char3.|
|
||||
|null, 'a', 'aaa' | $char3.|
|
||||
|null, 'a', 0 | best.|
|
||||
|null, null | best.|
|
||||
|null, '' | $char1.|
|
||||
|null, 'a' | $char1.|
|
||||
|'a' | $char1.|
|
||||
|'a', null | $char1.|
|
||||
|'a', null, 0 | best.|
|
||||
|
||||
Validation is also performed on the values. The following combinations will throw errors:
|
||||
|
||||
|JS Values |SAS Format|
|
||||
|---|---|
|
||||
|null, 'aaaa', 0 | Error: mixed types. 'aaaa' is not a special missing value.|
|
||||
|0, 'a', '!' | Error: mixed types. '!' is not a special missing value|
|
||||
|1.1, '.', 0| Error: mixed types. For regular nulls, use `null`|
|
||||
|
||||
### Variable Format Override
|
||||
The auto-detect functionality above is thwarted in the following scenarios:
|
||||
|
||||
* A character column containing only `null` values (is considered numeric)
|
||||
* A numeric column containing only special missing values (is considered character)
|
||||
|
||||
To cater for these scenarios, an optional array of formats can be passed along with the data to ensure that SAS will read them in correctly.
|
||||
|
||||
To understand these formats, it should be noted that the JSON data is NOT passed directly (as JSON) to SAS. It is first converted into CSV, and the header row is actually an `infile` statement in disguise. It looks a bit like this:
|
||||
|
||||
```csv
|
||||
CHARVAR1:$char4. CHARVAR2:$char1. NUMVAR:best.
|
||||
LOAD,,0
|
||||
ABCD,X,.
|
||||
```
|
||||
|
||||
To provide overrides to this header row, the tables object can be constructed as follows (with a leading '$' in the table name):
|
||||
|
||||
```javascript
|
||||
let specialData={
|
||||
"tablewith2cols2rows": [
|
||||
{"col1": "val1","specialMissingsCol": "A"},
|
||||
{"col1": "val2","specialMissingsCol": "_"}
|
||||
],
|
||||
"$tablewith2cols2rows":{"formats":{"specialMissingsCol":"best."}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
It is not necessary to provide formats for ALL the columns, only the ones that need to be overridden.
|
||||
|
||||
## SAS Inputs / Outputs
|
||||
|
||||
The SAS side is handled by a number of macros in the [macro core](https://github.com/sasjs/core) library.
|
||||
|
||||
The following snippet shows the process of SAS tables arriving / leaving:
|
||||
|
||||
```sas
|
||||
/* convert frontend input tables from into SASWORK datasets */
|
||||
%webout(FETCH)
|
||||
|
||||
/* some sas code */
|
||||
data a b c;
|
||||
set from js;
|
||||
run;
|
||||
|
||||
%webout(OPEN) /* Open the JSON to be returned */
|
||||
%webout(OBJ,a) /* Rows in table `a` are objects (easy to use) */
|
||||
%webout(ARR,b) /* Rows in table `b` are arrays (compact) */
|
||||
%webout(OBJ,c,fmt=N) /* Table `c` is sent unformatted (raw) */
|
||||
%webout(OBJ,c,dslabel=d) /* Rename table as `d` in output JSON */
|
||||
%webout(OBJ,c,dslabel=e, maxobs=10) /* send only 10 rows back */
|
||||
%webout(CLOSE) /* Close the JSON and add default variables */
|
||||
```
|
||||
|
||||
By default, special SAS numeric missings (_a-Z) are converted to `null` in the JSON. If you'd like to preserve these, use the `missing=STRING` option as follows:
|
||||
|
||||
```sas
|
||||
%webout(OBJ,a,missing=STRING)
|
||||
```
|
||||
In this case, special missings (such as `.a`, `.b`) are converted to javascript string values (`'A', 'B'`).
|
||||
|
||||
Where an entire column is made up of special missing numerics, there would be no way to distinguish it from a single-character column by looking at the values. To cater for this scenario, it is possible to export the variable types (and other attributes such as label and format) by adding a `showmeta` param to the `webout()` macro as follows:
|
||||
|
||||
```sas
|
||||
%webout(OBJ,a,missing=STRING,showmeta=YES)
|
||||
```
|
||||
|
||||
The `%webout()` macro itself is just a wrapper for the [mp_jsonout](https://core.sasjs.io/mp__jsonout_8sas.html) macro.
|
||||
|
||||
## Configuration
|
||||
|
||||
Configuration on the client side involves passing an object on startup, which can also be passed with each request. Technical documentation on the SASjsConfig class is available [here](https://adapter.sasjs.io/classes/types.sasjsconfig.html). The main config items are:
|
||||
|
||||
* `appLoc` - this is the folder (eg in metadata or SAS Drive) under which the SAS services are created.
|
||||
* `serverType` - either `SAS9`, `SASVIYA` or `SASJS`. The `SASJS` server type is for use with [sasjs/server](https://github.com/sasjs/server).
|
||||
* `serverUrl` - the location (including http protocol and port) of the SAS Server. Can be omitted, eg if serving directly from the SAS Web Server, or in streaming mode.
|
||||
* `debug` - if `true` then SAS Logs and extra debug information is returned.
|
||||
* `loginMechanism` - either `Default` or `Redirected`. See [SAS Logon](#sas-logon) section.
|
||||
* `useComputeApi` - Only relevant when the serverType is `SASVIYA`. If `true` the [Compute API](#using-the-compute-api) is used. If `false` the [JES API](#using-the-jes-api) is used. If `null` or `undefined` the [Web](#using-jes-web-app) approach is used.
|
||||
* `contextName` - Compute context on which the requests will be called. If missing or not provided, defaults to `Job Execution Compute context`.
|
||||
* `requestHistoryLimit` - Request history limit. Increasing this limit may affect browser performance, especially with debug (logs) enabled. Default is 10.
|
||||
|
||||
The adapter supports a number of approaches for interfacing with Viya (`serverType` is `SASVIYA`). For maximum performance, be sure to [configure your compute context](https://sasjs.io/guide-viya/#shared-account-and-server-re-use) with `reuseServerProcesses` as `true` and a system account in `runServerAs`. This functionality is available since Viya 3.5. This configuration is supported when [creating contexts using the CLI](https://sasjs.io/sasjs-cli-context/#sasjs-context-create).
|
||||
|
||||
### Using JES Web App
|
||||
|
||||
In this setup, all requests are routed through the JES web app, at `YOURSERVER/SASJobExecution?_program=/your/program`. This is the most reliable method, and also the slowest. One request is made to the JES app, and remaining requests (getting job uri, session spawning, passing parameters, running the program, fetching the log) are handled by the SAS server inside the JES app.
|
||||
|
||||
```
|
||||
{
|
||||
appLoc:"/Your/Path",
|
||||
serverType:"SASVIYA",
|
||||
contextName: 'yourComputeContext'
|
||||
}
|
||||
```
|
||||
|
||||
Note - to use the web approach, the `useComputeApi` property must be `undefined` or `null`.
|
||||
|
||||
### Using the JES API
|
||||
Here we are running Jobs using the Job Execution Service except this time we are making the requests directly using the REST API instead of through the JES Web App. This is helpful when we need to call web services outside of a browser (eg with the SASjs CLI or other commandline tools). To save one network request, the adapter prefetches the JOB URIs and passes them in the `__job` parameter. Depending on your network bandwidth, it may or may not be faster than the JES Web approach.
|
||||
|
||||
This approach (`useComputeApi: false`) also ensures that jobs are displayed in Environment Manager.
|
||||
|
||||
```json
|
||||
{
|
||||
appLoc:"/Your/Path",
|
||||
serverType:"SASVIYA",
|
||||
useComputeApi: false,
|
||||
contextName: 'yourComputeContext'
|
||||
}
|
||||
```
|
||||
|
||||
### Using the Compute API
|
||||
This approach is by far the fastest, as a result of the optimisations we have built into the adapter. With this configuration, in the first sasjs request, we take a URI map of the services in the target folder, and create a session manager. This manager will spawn a additional session every time a request is made. Subsequent requests will use the existing 'hot' session, if it exists. Sessions are always deleted after every use, which actually makes this _less_ resource intensive than a typical JES web app, in which all sessions are kept alive by default for 15 minutes.
|
||||
|
||||
With this approach (`useComputeApi: true`), the requests/logs will _not_ appear in the list in Environment manager.
|
||||
|
||||
```json
|
||||
{
|
||||
appLoc:"/Your/Path",
|
||||
serverType:"SASVIYA",
|
||||
useComputeApi: true,
|
||||
contextName: "yourComputeContext"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
# More resources
|
||||
|
||||
For more information and examples specific to this adapter you can check out the [user guide](https://sasjs.io/sasjs-adapter/) or the [technical](http://adapter.sasjs.io/) documentation.
|
||||
|
||||
For more information on building web apps in general, check out these [resources](https://sasjs.io/training/resources/) or contact the [author](https://www.linkedin.com/in/allanbowe/) directly.
|
||||
|
||||
As a SAS customer you can also request a copy of [Data Controller](https://datacontroller.io) - free for up to 5 users, this tool makes use of all parts of the SASjs framework.
|
||||
|
||||
|
||||
## Star Gazing
|
||||
|
||||
If you find this library useful, help us grow our star graph!
|
||||
|
||||

|
||||
|
||||
## Contributors ✨
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||
[](#contributors-)
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||
|
||||
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||
<!-- prettier-ignore-start -->
|
||||
<!-- markdownlint-disable -->
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center"><a href="https://krishna-acondy.io/"><img src="https://avatars.githubusercontent.com/u/2980428?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Krishna Acondy</b></sub></a><br /><a href="https://github.com/sasjs/adapter/commits?author=krishna-acondy" title="Code">💻</a> <a href="#infra-krishna-acondy" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="#blog-krishna-acondy" title="Blogposts">📝</a> <a href="#content-krishna-acondy" title="Content">🖋</a> <a href="#ideas-krishna-acondy" title="Ideas, Planning, & Feedback">🤔</a> <a href="#video-krishna-acondy" title="Videos">📹</a></td>
|
||||
<td align="center"><a href="https://www.erudicat.com/"><img src="https://avatars.githubusercontent.com/u/25773492?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Yury Shkoda</b></sub></a><br /><a href="https://github.com/sasjs/adapter/commits?author=YuryShkoda" title="Code">💻</a> <a href="#infra-YuryShkoda" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="#ideas-YuryShkoda" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/sasjs/adapter/commits?author=YuryShkoda" title="Tests">⚠️</a> <a href="#video-YuryShkoda" title="Videos">📹</a></td>
|
||||
<td align="center"><a href="https://github.com/medjedovicm"><img src="https://avatars.githubusercontent.com/u/18329105?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Mihajlo Medjedovic</b></sub></a><br /><a href="https://github.com/sasjs/adapter/commits?author=medjedovicm" title="Code">💻</a> <a href="#infra-medjedovicm" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/sasjs/adapter/commits?author=medjedovicm" title="Tests">⚠️</a> <a href="https://github.com/sasjs/adapter/pulls?q=is%3Apr+reviewed-by%3Amedjedovicm" title="Reviewed Pull Requests">👀</a></td>
|
||||
<td align="center"><a href="https://github.com/allanbowe"><img src="https://avatars.githubusercontent.com/u/4420615?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Allan Bowe</b></sub></a><br /><a href="https://github.com/sasjs/adapter/commits?author=allanbowe" title="Code">💻</a> <a href="https://github.com/sasjs/adapter/pulls?q=is%3Apr+reviewed-by%3Aallanbowe" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/sasjs/adapter/commits?author=allanbowe" title="Tests">⚠️</a> <a href="#mentoring-allanbowe" title="Mentoring">🧑🏫</a> <a href="#maintenance-allanbowe" title="Maintenance">🚧</a></td>
|
||||
<td align="center"><a href="https://github.com/saadjutt01"><img src="https://avatars.githubusercontent.com/u/8914650?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Muhammad Saad </b></sub></a><br /><a href="https://github.com/sasjs/adapter/commits?author=saadjutt01" title="Code">💻</a> <a href="https://github.com/sasjs/adapter/pulls?q=is%3Apr+reviewed-by%3Asaadjutt01" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/sasjs/adapter/commits?author=saadjutt01" title="Tests">⚠️</a> <a href="#mentoring-saadjutt01" title="Mentoring">🧑🏫</a> <a href="#infra-saadjutt01" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
||||
<td align="center"><a href="https://github.com/sabhas"><img src="https://avatars.githubusercontent.com/u/82647447?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Sabir Hassan</b></sub></a><br /><a href="https://github.com/sasjs/adapter/commits?author=sabhas" title="Code">💻</a> <a href="https://github.com/sasjs/adapter/pulls?q=is%3Apr+reviewed-by%3Asabhas" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/sasjs/adapter/commits?author=sabhas" title="Tests">⚠️</a> <a href="#ideas-sabhas" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center"><a href="https://github.com/VladislavParhomchik"><img src="https://avatars.githubusercontent.com/u/83717836?v=4?s=100" width="100px;" alt=""/><br /><sub><b>VladislavParhomchik</b></sub></a><br /><a href="https://github.com/sasjs/adapter/commits?author=VladislavParhomchik" title="Tests">⚠️</a> <a href="https://github.com/sasjs/adapter/pulls?q=is%3Apr+reviewed-by%3AVladislavParhomchik" title="Reviewed Pull Requests">👀</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="http://rudvfaden.github.io/"><img src="https://avatars.githubusercontent.com/u/2445577?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Rud Faden</b></sub></a><br /><a href="#userTesting-rudvfaden" title="User Testing">📓</a> <a href="https://github.com/sasjs/adapter/commits?author=rudvfaden" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- markdownlint-restore -->
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
|
||||
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
|
||||
|
||||
16
checkNodeVersion.js
Normal file
16
checkNodeVersion.js
Normal file
@@ -0,0 +1,16 @@
|
||||
const result = process.versions
|
||||
if (result && result.node) {
|
||||
if (parseInt(result.node) < 14) {
|
||||
console.log(
|
||||
'\x1b[31m%s\x1b[0m',
|
||||
`❌ Process failed due to Node Version,\nPlease install and use Node Version >= 14\nYour current Node Version is: ${result.node}`
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
'\x1b[31m%s\x1b[0m',
|
||||
'Something went wrong while checking Node version'
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
35
createTSDocs.js
Normal file
35
createTSDocs.js
Normal file
@@ -0,0 +1,35 @@
|
||||
const td = require('typedoc')
|
||||
const ts = require('typescript')
|
||||
|
||||
const typedocJson = require('./typedoc.json')
|
||||
|
||||
async function createTSDocs() {
|
||||
if (!typedocJson.entryPoints?.length) {
|
||||
throw new Error(
|
||||
'Typedoc error: entryPoints option is missing in typedoc configuration.'
|
||||
)
|
||||
}
|
||||
|
||||
if (!typedocJson.out) {
|
||||
throw new Error(
|
||||
'Typedoc error: out option is missing in typedoc configuration.'
|
||||
)
|
||||
}
|
||||
const app = new td.Application()
|
||||
app.options.addReader(new td.TSConfigReader())
|
||||
|
||||
app.bootstrap({
|
||||
...typedocJson,
|
||||
tsconfig: 'tsconfig.json'
|
||||
})
|
||||
|
||||
const project = app.converter.convert(app.getEntryPoints() ?? [])
|
||||
|
||||
if (project) {
|
||||
await app.generateDocs(project, typedocJson.out)
|
||||
} else {
|
||||
throw new Error('Typedoc error: error creating the TS docs.')
|
||||
}
|
||||
}
|
||||
|
||||
createTSDocs()
|
||||
12
cypress.json
12
cypress.json
@@ -1,13 +1,11 @@
|
||||
{
|
||||
"defaultCommandTimeout": 10000,
|
||||
"chromeWebSecurity": false,
|
||||
"screenshotOnRunFailure": false,
|
||||
"defaultCommandTimeout": 20000,
|
||||
"env": {
|
||||
"serverUrl": "",
|
||||
"appLoc": "/Public/app",
|
||||
"serverType": "SAS9",
|
||||
"debug": false,
|
||||
"sasjsTestsUrl": "",
|
||||
"username": "",
|
||||
"password": ""
|
||||
"password": "",
|
||||
"screenshotOnRunFailure": false,
|
||||
"testingFinishTimeout": 600000
|
||||
}
|
||||
}
|
||||
|
||||
78
cypress/integration/sasjs.tests.ts
Normal file
78
cypress/integration/sasjs.tests.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
const sasjsTestsUrl = Cypress.env('sasjsTestsUrl')
|
||||
const username = Cypress.env('username')
|
||||
const password = Cypress.env('password')
|
||||
const testingFinishTimeout = Cypress.env('testingFinishTimeout')
|
||||
|
||||
context('sasjs-tests', function () {
|
||||
this.beforeAll(() => {
|
||||
cy.visit(sasjsTestsUrl)
|
||||
})
|
||||
|
||||
this.beforeEach(() => {
|
||||
cy.reload()
|
||||
})
|
||||
|
||||
it('Should have all tests successfull', (done) => {
|
||||
cy.get('body').then(($body) => {
|
||||
if ($body.find('input[placeholder="User Name"]').length > 0) {
|
||||
cy.get('input[placeholder="User Name"]').type(username)
|
||||
cy.get('input[placeholder="Password"]').type(password)
|
||||
cy.get('.submit-button').click()
|
||||
}
|
||||
|
||||
cy.get('input[placeholder="User Name"]', { timeout: 40000 })
|
||||
.should('not.exist')
|
||||
.then(() => {
|
||||
cy.get('.ui.massive.icon.primary.left.labeled.button')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.get('.ui.massive.loading.primary.button', {
|
||||
timeout: testingFinishTimeout
|
||||
})
|
||||
.should('not.exist')
|
||||
.then(() => {
|
||||
cy.get('span.icon.failed')
|
||||
.should('not.exist')
|
||||
.then(() => {
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('Should have all tests successfull with debug on', (done) => {
|
||||
cy.get('body').then(($body) => {
|
||||
if ($body.find('input[placeholder="User Name"]').length > 0) {
|
||||
cy.get('input[placeholder="User Name"]').type(username)
|
||||
cy.get('input[placeholder="Password"]').type(password)
|
||||
cy.get('.submit-button').click()
|
||||
}
|
||||
|
||||
cy.get('.ui.fitted.toggle.checkbox label')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.get('input[placeholder="User Name"]', { timeout: 40000 })
|
||||
.should('not.exist')
|
||||
.then(() => {
|
||||
cy.get('.ui.massive.icon.primary.left.labeled.button')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.get('.ui.massive.loading.primary.button', {
|
||||
timeout: testingFinishTimeout
|
||||
})
|
||||
.should('not.exist')
|
||||
.then(() => {
|
||||
cy.get('span.icon.failed')
|
||||
.should('not.exist')
|
||||
.then(() => {
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
31
cypress/plugins/cy-ts-preprocessor.js
Normal file
31
cypress/plugins/cy-ts-preprocessor.js
Normal file
@@ -0,0 +1,31 @@
|
||||
const wp = require('@cypress/webpack-preprocessor')
|
||||
|
||||
const webpackOptions = {
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js']
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.ts$/,
|
||||
loaders: ['ts-loader'],
|
||||
exclude: [/node_modules/]
|
||||
},
|
||||
{
|
||||
test: /\.(html|css)$/,
|
||||
loader: 'raw-loader',
|
||||
exclude: /\.async\.(html|css)$/
|
||||
},
|
||||
{
|
||||
test: /\.async\.(html|css)$/,
|
||||
loaders: ['file?name=[name].[hash].[ext]', 'extract']
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const options = {
|
||||
webpackOptions
|
||||
}
|
||||
|
||||
module.exports = wp(options)
|
||||
42
cypress/plugins/index.js
Normal file
42
cypress/plugins/index.js
Normal file
@@ -0,0 +1,42 @@
|
||||
/// <reference types="cypress" />
|
||||
// ***********************************************************
|
||||
// This example plugins/index.js can be used to load plugins
|
||||
//
|
||||
// You can change the location of this file or turn off loading
|
||||
// the plugins file with the 'pluginsFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/plugins-guide
|
||||
// ***********************************************************
|
||||
|
||||
// This function is called when a project is opened or re-opened (e.g. due to
|
||||
// the project's config changing)
|
||||
|
||||
const wp = require('@cypress/webpack-preprocessor')
|
||||
|
||||
/**
|
||||
* @type {Cypress.PluginConfig}
|
||||
*/
|
||||
module.exports = (on, config) => {
|
||||
// `on` is used to hook into various events Cypress emits
|
||||
// `config` is the resolved Cypress config
|
||||
|
||||
const options = {
|
||||
webpackOptions: require('../webpack.config.js')
|
||||
}
|
||||
on('file:preprocessor', wp(options))
|
||||
|
||||
on('before:browser:launch', (browser = {}, launchOptions) => {
|
||||
if (browser.name === 'chrome') {
|
||||
launchOptions.args.push('--disable-site-isolation-trials')
|
||||
launchOptions.args.push('--auto-open-devtools-for-tabs')
|
||||
launchOptions.args.push('--aggressive-cache-discard')
|
||||
launchOptions.args.push('--disable-cache')
|
||||
launchOptions.args.push('--disable-application-cache')
|
||||
launchOptions.args.push('--disable-offline-load-stale-cache')
|
||||
launchOptions.args.push('--disk-cache-size=0')
|
||||
|
||||
return launchOptions
|
||||
}
|
||||
})
|
||||
}
|
||||
10
cypress/tsconfig.json
Normal file
10
cypress/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"baseUrl": "../node_modules",
|
||||
"target": "es6",
|
||||
"lib": ["es2019", "dom"],
|
||||
"types": ["cypress"]
|
||||
},
|
||||
"include": ["**/*.ts"]
|
||||
}
|
||||
23
cypress/webpack.config.js
Normal file
23
cypress/webpack.config.js
Normal file
@@ -0,0 +1,23 @@
|
||||
module.exports = {
|
||||
mode: 'development',
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js']
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.ts$/,
|
||||
exclude: [/node_modules/],
|
||||
use: [
|
||||
{
|
||||
loader: 'ts-loader',
|
||||
options: {
|
||||
// skip typechecking for speed
|
||||
transpileOnly: true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
adapter.sasjs.io
|
||||
@@ -1 +0,0 @@
|
||||
theme: jekyll-theme-minimal
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 9.4 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 28 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 480 B |
Binary file not shown.
|
Before Width: | Height: | Size: 855 B |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
129
docs/index.html
129
docs/index.html
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -30,8 +30,8 @@
|
||||
$('#chart-container').append('<canvas id="myChart" style="display: none;"></canvas>')
|
||||
// make a request to a SAS service
|
||||
var type = $("#cars")[0].options[$("#cars")[0].selectedIndex].value;
|
||||
// request data from an endpoint under your appLoc
|
||||
sasJs.request("/common/getdata", {
|
||||
// request data from an endpoint under your appLoc (missing opening slash implies relative path)
|
||||
sasJs.request("common/getdata", {
|
||||
// send data as an array of objects - each object is one row
|
||||
fromjs: [{ type: type }]
|
||||
}).then((response) => {
|
||||
|
||||
188
jest.config.js
188
jest.config.js
@@ -1,10 +1,184 @@
|
||||
// For a detailed explanation regarding each configuration property, visit:
|
||||
// https://jestjs.io/docs/en/configuration.html
|
||||
|
||||
module.exports = {
|
||||
roots: ["<rootDir>/src"],
|
||||
testMatch: [
|
||||
"**/__tests__/**/*.+(ts|tsx|js)",
|
||||
"**/?(*.)+(spec|test).+(ts|tsx|js)"
|
||||
],
|
||||
testTimeout: 90000,
|
||||
// All imported modules in your tests should be mocked automatically
|
||||
// automock: false,
|
||||
|
||||
// Stop running tests after `n` failures
|
||||
// bail: 1,
|
||||
|
||||
// Respect "browser" field in package.json when resolving modules
|
||||
// browser: false,
|
||||
|
||||
// The directory where Jest should store its cached dependency information
|
||||
// cacheDirectory: "/private/var/folders/7y/nmqg1srj29q6210rs9dfsdzc0000gn/T/jest_dx",
|
||||
|
||||
// Automatically clear mock calls and instances between every test
|
||||
clearMocks: true,
|
||||
|
||||
// Indicates whether the coverage information should be collected while executing the test
|
||||
// collectCoverage: false,
|
||||
|
||||
// An array of glob patterns indicating a set of files for which coverage information should be collected
|
||||
// collectCoverageFrom: undefined,
|
||||
|
||||
// The directory where Jest should output its coverage files
|
||||
coverageDirectory: 'coverage',
|
||||
|
||||
// An array of regexp pattern strings used to skip coverage collection
|
||||
// coveragePathIgnorePatterns: [
|
||||
// "/node_modules/"
|
||||
// ],
|
||||
|
||||
// A list of reporter names that Jest uses when writing coverage reports
|
||||
// coverageReporters: [
|
||||
// "json",
|
||||
// "text",
|
||||
// "lcov",
|
||||
// "clover"
|
||||
// ],
|
||||
|
||||
// An object that configures minimum threshold enforcement for coverage results
|
||||
// coverageThreshold: undefined,
|
||||
|
||||
// A path to a custom dependency extractor
|
||||
// dependencyExtractor: undefined,
|
||||
|
||||
// Make calling deprecated APIs throw helpful error messages
|
||||
// errorOnDeprecated: false,
|
||||
|
||||
// Force coverage collection from ignored files using an array of glob patterns
|
||||
// forceCoverageMatch: [],
|
||||
|
||||
// A path to a module which exports an async function that is triggered once before all test suites
|
||||
// globalSetup: undefined,
|
||||
|
||||
// A path to a module which exports an async function that is triggered once after all test suites
|
||||
// globalTeardown: undefined,
|
||||
|
||||
// A set of global variables that need to be available in all test environments
|
||||
// globals: {},
|
||||
|
||||
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
|
||||
// maxWorkers: "50%",
|
||||
|
||||
// An array of directory names to be searched recursively up from the requiring module's location
|
||||
// moduleDirectories: [
|
||||
// "node_modules"
|
||||
// ],
|
||||
|
||||
// An array of file extensions your modules use
|
||||
// moduleFileExtensions: [
|
||||
// "js",
|
||||
// "json",
|
||||
// "jsx",
|
||||
// "ts",
|
||||
// "tsx",
|
||||
// "node"
|
||||
// ],
|
||||
|
||||
// A map from regular expressions to module names that allow to stub out resources with a single module
|
||||
moduleNameMapper: {},
|
||||
|
||||
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
|
||||
// modulePathIgnorePatterns: [],
|
||||
|
||||
// Activates notifications for test results
|
||||
// notify: false,
|
||||
|
||||
// An enum that specifies notification mode. Requires { notify: true }
|
||||
// notifyMode: "failure-change",
|
||||
|
||||
// A preset that is used as a base for Jest's configuration
|
||||
preset: 'ts-jest/presets/js-with-ts',
|
||||
|
||||
// Run tests from one or more projects
|
||||
// projects: undefined,
|
||||
|
||||
// Use this configuration option to add custom reporters to Jest
|
||||
// reporters: undefined,
|
||||
|
||||
// Automatically reset mock state between every test
|
||||
// resetMocks: false,
|
||||
|
||||
// Reset the module registry before running each individual test
|
||||
// resetModules: false,
|
||||
|
||||
// A path to a custom resolver
|
||||
// resolver: undefined,
|
||||
|
||||
// Automatically restore mock state between every test
|
||||
// restoreMocks: false,
|
||||
|
||||
// The root directory that Jest should scan for tests and modules within
|
||||
// rootDir: undefined,
|
||||
|
||||
// A list of paths to directories that Jest should use to search for files in
|
||||
// roots: [
|
||||
// "<rootDir>"
|
||||
// ],
|
||||
|
||||
// Allows you to use a custom runner instead of Jest's default test runner
|
||||
// runner: "jest-runner",
|
||||
|
||||
// The paths to modules that run some code to configure or set up the testing environment before each test
|
||||
setupFiles: [],
|
||||
|
||||
// A list of paths to modules that run some code to configure or set up the testing framework before each test
|
||||
setupFilesAfterEnv: ['jest-extended/all'],
|
||||
|
||||
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
|
||||
// snapshotSerializers: [],
|
||||
|
||||
// Options that will be passed to the testEnvironment
|
||||
// testEnvironmentOptions: {},
|
||||
|
||||
// Adds a location field to test results
|
||||
// testLocationInResults: false,
|
||||
|
||||
// The glob patterns Jest uses to detect test files
|
||||
testMatch: ['**/*spec.[j|t]s?(x)'],
|
||||
|
||||
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
|
||||
testPathIgnorePatterns: ['/node_modules/', '/build'],
|
||||
|
||||
// The regexp pattern or array of patterns that Jest uses to detect test files
|
||||
// testRegex: [],
|
||||
|
||||
// This option allows the use of a custom results processor
|
||||
// testResultsProcessor: undefined,
|
||||
|
||||
// This option allows use of a custom test runner
|
||||
// testRunner: "jasmine2",
|
||||
|
||||
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
|
||||
// testURL: "http://localhost",
|
||||
|
||||
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
|
||||
// timers: "real",
|
||||
|
||||
// A map from regular expressions to paths to transformers
|
||||
transform: {
|
||||
"^.+\\.(ts|tsx)$": "ts-jest"
|
||||
'^.+\\.ts?$': 'ts-jest'
|
||||
}
|
||||
};
|
||||
|
||||
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
|
||||
// transformIgnorePatterns: [
|
||||
// '**/test/**/*.ts?(x)',
|
||||
// '**/?(*.)+(spec|test).ts?(x)'
|
||||
// ]
|
||||
|
||||
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
|
||||
// unmockedModulePathPatterns: undefined,
|
||||
|
||||
// Indicates whether each individual test should be reported during the run
|
||||
// verbose: undefined,
|
||||
|
||||
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
|
||||
// watchPathIgnorePatterns: [],
|
||||
|
||||
// Whether to use watchman for file crawling
|
||||
// watchman: true,
|
||||
}
|
||||
|
||||
33061
package-lock.json
generated
33061
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
79
package.json
79
package.json
@@ -1,17 +1,25 @@
|
||||
{
|
||||
"name": "@sasjs/adapter",
|
||||
"description": "JavaScript adapter for SAS",
|
||||
"homepage": "https://adapter.sasjs.io",
|
||||
"scripts": {
|
||||
"build": "rimraf build && webpack",
|
||||
"package:lib": "npm run build && cp ./package.json build && cd build && npm version \"5.0.0\" && npm pack",
|
||||
"nodeVersionMessage": "echo \u001b[33m make sure you are running node lts version \u001b[0m",
|
||||
"preinstall": "npm run nodeVersionMessage",
|
||||
"prebuild": "npm run nodeVersionMessage",
|
||||
"build": "rimraf build && rimraf node && mkdir node && copyfiles -u 1 \"./src/**/*\" ./node && webpack && rimraf build/src && rimraf node",
|
||||
"package:lib": "npm run build && copyfiles ./package.json build && cd build && npm version \"5.0.0\" && npm pack",
|
||||
"publish:lib": "npm run build && cd build && npm publish",
|
||||
"lint:fix": "npx prettier --write 'src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}'",
|
||||
"lint": "npx prettier --check 'src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}'",
|
||||
"test": "jest",
|
||||
"lint:fix": "npx prettier --loglevel silent --write \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --loglevel silent --write \"sasjs-tests/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --loglevel silent --write \"cypress/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\"",
|
||||
"lint": "npx prettier --check \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --check \"sasjs-tests/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --check \"cypress/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\"",
|
||||
"lint:silent": "npx prettier --loglevel silent --check \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --loglevel silent --check \"sasjs-tests/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --loglevel silent --check \"cypress/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\"",
|
||||
"test": "jest --silent --coverage",
|
||||
"prepublishOnly": "cp -r ./build/* . && rm -rf ./build",
|
||||
"postpublish": "git clean -fd",
|
||||
"semantic-release": "semantic-release",
|
||||
"typedoc": "typedoc"
|
||||
"typedoc": "node createTSDocs",
|
||||
"prepare": "git rev-parse --git-dir && git config core.hooksPath ./.git-hooks && git config core.autocrlf false || true",
|
||||
"cypress": "cypress open",
|
||||
"cy:run": "cypress run"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
@@ -36,29 +44,46 @@
|
||||
},
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/isomorphic-fetch": "0.0.35",
|
||||
"@types/jest": "^26.0.13",
|
||||
"cp": "^0.2.0",
|
||||
"jest": "^25.5.4",
|
||||
"path": "^0.12.7",
|
||||
"rimraf": "^3.0.2",
|
||||
"semantic-release": "^17.1.1",
|
||||
"ts-jest": "^25.5.1",
|
||||
"ts-loader": "^8.0.3",
|
||||
"tslint": "^6.1.3",
|
||||
"tslint-config-prettier": "^1.18.0",
|
||||
"typedoc": "^0.17.8",
|
||||
"typedoc-neo-theme": "^1.0.10",
|
||||
"typedoc-plugin-external-module-name": "^4.0.3",
|
||||
"typescript": "^3.9.7",
|
||||
"uglifyjs-webpack-plugin": "^2.2.0",
|
||||
"webpack": "^4.44.1",
|
||||
"webpack-cli": "^3.3.12"
|
||||
"@cypress/webpack-preprocessor": "5.9.1",
|
||||
"@types/axios": "0.14.0",
|
||||
"@types/express": "4.17.13",
|
||||
"@types/form-data": "2.5.0",
|
||||
"@types/jest": "27.4.0",
|
||||
"@types/mime": "2.0.3",
|
||||
"@types/pem": "1.9.6",
|
||||
"@types/tough-cookie": "4.0.1",
|
||||
"copyfiles": "2.4.1",
|
||||
"cp": "0.2.0",
|
||||
"cypress": "7.7.0",
|
||||
"dotenv": "16.0.0",
|
||||
"express": "4.17.3",
|
||||
"jest": "27.4.7",
|
||||
"jest-extended": "2.0.0",
|
||||
"node-polyfill-webpack-plugin": "1.1.4",
|
||||
"path": "0.12.7",
|
||||
"pem": "1.14.6",
|
||||
"prettier": "2.7.1",
|
||||
"process": "0.11.10",
|
||||
"rimraf": "3.0.2",
|
||||
"semantic-release": "19.0.3",
|
||||
"terser-webpack-plugin": "5.3.1",
|
||||
"ts-jest": "27.1.3",
|
||||
"ts-loader": "9.4.0",
|
||||
"tslint": "6.1.3",
|
||||
"tslint-config-prettier": "1.18.0",
|
||||
"typedoc": "0.23.15",
|
||||
"typedoc-plugin-rename-defaults": "0.4.0",
|
||||
"typescript": "4.8.3",
|
||||
"webpack": "5.69.0",
|
||||
"webpack-cli": "4.9.2"
|
||||
},
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"es6-promise": "^4.2.8",
|
||||
"form-data": "^3.0.0",
|
||||
"isomorphic-fetch": "^2.2.1"
|
||||
"@sasjs/utils": "2.48.2",
|
||||
"axios": "0.26.0",
|
||||
"axios-cookiejar-support": "1.0.1",
|
||||
"form-data": "4.0.0",
|
||||
"https": "1.0.0",
|
||||
"tough-cookie": "4.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"trailingComma": "none",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": false
|
||||
"semi": false,
|
||||
"singleQuote": true
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ When developing on `@sasjs/adapter`, it's good practice to run the test suite ag
|
||||
|
||||
You can use the provided `update:adapter` NPM script for this.
|
||||
|
||||
```
|
||||
```bash
|
||||
npm run update:adapter
|
||||
```
|
||||
|
||||
@@ -37,82 +37,90 @@ To be able to run the `deploy` script, two environment variables need to be set:
|
||||
|
||||
So you can run the script like so:
|
||||
|
||||
```
|
||||
```bash
|
||||
SSH_ACCOUNT=me@my-sas-server.com DEPLOY_PATH=/var/www/html/my-folder/sasjs-tests npm run deploy
|
||||
```
|
||||
|
||||
If you are on `WINDOWS`, you will first need to install one dependency:
|
||||
```bash
|
||||
npm i -g copyfiles
|
||||
```
|
||||
and then run to build:
|
||||
```bash
|
||||
npm run update:adapter && npm run build
|
||||
```
|
||||
when it finishes run to deploy:
|
||||
```bash
|
||||
scp -rp ./build/* me@my-sas-server.com:/var/www/html/my-folder/sasjs-tests
|
||||
```
|
||||
|
||||
If you'd like to deploy just `sasjs-tests` without changing the adapter version, you can use the `deploy:tests` script, while also setting the same environment variables as above.
|
||||
|
||||
## 3. Creating the required SAS services
|
||||
|
||||
The below services need to be created on your SAS server, at the location specified as the `appLoc` in the SASjs configuration.
|
||||
|
||||
### SAS 9
|
||||
|
||||
```
|
||||
The code below will work on ALL SAS platforms (Viya, SAS 9 EBI, SASjs Server).
|
||||
|
||||
```sas
|
||||
filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas";
|
||||
%inc mc;
|
||||
filename ft15f001 temp;
|
||||
parmcards4;
|
||||
%webout(OPEN)
|
||||
%macro x();
|
||||
%do i=1 %to &_webin_file_count; %webout(OBJ,&&_webin_name&i) %end;
|
||||
%mend; %x()
|
||||
%webout(CLOSE)
|
||||
;;;;
|
||||
%mm_createwebservice(path=/Public/app/common,name=sendObj)
|
||||
parmcards4;
|
||||
%webout(OPEN)
|
||||
%macro x();
|
||||
%do i=1 %to &_webin_file_count; %webout(ARR,&&_webin_name&i) %end;
|
||||
%mend; %x()
|
||||
%webout(CLOSE)
|
||||
;;;;
|
||||
%mm_createwebservice(path=/Public/app/common,name=sendArr)
|
||||
```
|
||||
|
||||
### SAS Viya
|
||||
|
||||
```
|
||||
filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas";
|
||||
%inc mc;
|
||||
filename ft15f001 temp;
|
||||
filename ft15f001 temp lrecl=1000;
|
||||
parmcards4;
|
||||
%webout(FETCH)
|
||||
%webout(OPEN)
|
||||
%macro x();
|
||||
%do i=1 %to %sysfunc(countw(&sasjs_tables));
|
||||
%if %symexist(sasjs_tables) %then %do i=1 %to %sysfunc(countw(&sasjs_tables));
|
||||
%let table=%scan(&sasjs_tables,&i);
|
||||
%webout(OBJ,&table)
|
||||
%webout(OBJ,&table,missing=STRING,showmeta=YES)
|
||||
%end;
|
||||
%mend;
|
||||
%x()
|
||||
%else %do i=1 %to &_webin_file_count;
|
||||
%webout(OBJ,&&_webin_name&i,missing=STRING,showmeta=YES)
|
||||
%end;
|
||||
%mend; %x()
|
||||
%webout(CLOSE)
|
||||
;;;;
|
||||
%mp_createwebservice(path=/Public/app/common,name=sendObj)
|
||||
filename ft15f001 temp;
|
||||
%mx_createwebservice(path=/Public/app/common,name=sendObj)
|
||||
parmcards4;
|
||||
%webout(FETCH)
|
||||
%webout(OPEN)
|
||||
%macro x();
|
||||
%do i=1 %to %sysfunc(countw(&sasjs_tables));
|
||||
%if %symexist(sasjs_tables) %then %do i=1 %to %sysfunc(countw(&sasjs_tables));
|
||||
%let table=%scan(&sasjs_tables,&i);
|
||||
%webout(ARR,&table)
|
||||
%webout(ARR,&table,missing=STRING,showmeta=YES)
|
||||
%end;
|
||||
%mend;
|
||||
%x()
|
||||
%else %do i=1 %to &_webin_file_count;
|
||||
%webout(ARR,&&_webin_name&i,missing=STRING,showmeta=YES)
|
||||
%end;
|
||||
%mend; %x()
|
||||
%webout(CLOSE)
|
||||
;;;;
|
||||
%mp_createwebservice(path=/Public/app/common,name=sendArr)
|
||||
filename ft15f001 temp;
|
||||
%mx_createwebservice(path=/Public/app/common,name=sendArr)
|
||||
parmcards4;
|
||||
data work.macvars;
|
||||
set sashelp.vmacro;
|
||||
run;
|
||||
%webout(OPEN)
|
||||
%webout(OBJ,macvars)
|
||||
%webout(CLOSE)
|
||||
;;;;
|
||||
%mx_createwebservice(path=/Public/app/common,name=sendMacVars)
|
||||
parmcards4;
|
||||
If you can keep your head when all about you
|
||||
Are losing theirs and blaming it on you,
|
||||
If you can trust yourself when all men doubt you,
|
||||
But make allowance for their doubting too;
|
||||
;;;;
|
||||
%mp_createwebservice(path=/Public/app/common,name=makeErr)
|
||||
%mx_createwebservice(path=/Public/app/common,name=makeErr)
|
||||
parmcards4;
|
||||
%webout(OPEN)
|
||||
data _null_;
|
||||
file _webout;
|
||||
put ' the discovery channel ';
|
||||
run;
|
||||
%webout(CLOSE)
|
||||
;;;;
|
||||
%mx_createwebservice(path=/Public/app/common,name=invalidJSON)
|
||||
```
|
||||
|
||||
You should now be able to access the tests in your browser at the deployed path on your server.
|
||||
|
||||
36889
sasjs-tests/package-lock.json
generated
36889
sasjs-tests/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,30 +4,28 @@
|
||||
"homepage": ".",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@sasjs/adapter": "^1.3.13",
|
||||
"@sasjs/test-framework": "^1.4.0",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.5.0",
|
||||
"@testing-library/user-event": "^7.2.1",
|
||||
"@types/jest": "^26.0.3",
|
||||
"@types/node": "^14.0.14",
|
||||
"@types/react": "^16.9.41",
|
||||
"@types/react-dom": "^16.9.8",
|
||||
"@types/react-router-dom": "^5.1.5",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"@sasjs/adapter": "file:../build/sasjs-adapter-5.0.0.tgz",
|
||||
"@sasjs/test-framework": "1.5.7",
|
||||
"@types/jest": "^26.0.20",
|
||||
"@types/node": "^14.14.41",
|
||||
"@types/react": "^17.0.1",
|
||||
"@types/react-dom": "^17.0.0",
|
||||
"@types/react-router-dom": "^5.1.7",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "3.4.1",
|
||||
"typescript": "^3.9.6"
|
||||
"react-scripts": "^5.0.1",
|
||||
"typescript": "^4.1.3"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"update:adapter": "cd .. && npm run package:lib && cd sasjs-tests && npm i ../build/sasjs-adapter-5.0.0.tgz",
|
||||
"deploy:tests": "npm run build && rsync -avhe ssh ./build/* --delete $SSH_ACCOUNT:$DEPLOY_PATH",
|
||||
"deploy": "npm run update:adapter && npm run deploy:tests"
|
||||
"update:adapter": "cd .. && npm run package:lib && cd sasjs-tests && npm i ../build/sasjs-adapter-5.0.0.tgz --legacy-peer-deps",
|
||||
"deploy:tests": "rsync -avhe ssh ./build/* --delete $SSH_ACCOUNT:$DEPLOY_PATH || npm run deploy:tests-win",
|
||||
"deploy:tests-win": "scp %DEPLOY_PATH% ./build/*",
|
||||
"deploy": "npm run update:adapter && npm run build && npm run deploy:tests"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
@@ -45,6 +43,6 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"node-sass": "^4.14.1"
|
||||
"node-sass": "7.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"appLoc": "/Public/app",
|
||||
"serverType": "SASVIYA",
|
||||
"debug": false,
|
||||
"contextName": "SharedCompute",
|
||||
"contextName": "sasjs adapter compute context",
|
||||
"useComputeApi": true
|
||||
}
|
||||
}
|
||||
|
||||
10
sasjs-tests/sasjs-cypress-run.sh
Executable file
10
sasjs-tests/sasjs-cypress-run.sh
Executable file
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
if npm run cy:run -- --spec "cypress/integration/sasjs.tests.ts" ; then
|
||||
echo "Cypress sasjs testing passed!"
|
||||
else
|
||||
echo '{"msgtype":"m.text", "body":"Automated sasjs-tests failed on the @sasjs/adapter PR: '$2'"}'
|
||||
curl -XPOST -d '{"msgtype":"m.text", "body":"Automated sasjs-tests failed on the @sasjs/adapter PR: '$2'"}' https://matrix.4gl.io/_matrix/client/r0/rooms/%21jRebyiGmHZlpfDwYXN:4gl.io:4gl.io/send/m.room.message?access_token=$1
|
||||
echo "Cypress sasjs testing failed!"
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,9 +0,0 @@
|
||||
import React from "react";
|
||||
import { render } from "@testing-library/react";
|
||||
import App from "./App";
|
||||
|
||||
test("renders learn react link", () => {
|
||||
const { getByText } = render(<App />);
|
||||
const linkElement = getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
||||
@@ -1,32 +1,42 @@
|
||||
import React, { ReactElement, useState, useContext, useEffect } from "react";
|
||||
import { TestSuiteRunner, TestSuite, AppContext } from "@sasjs/test-framework";
|
||||
import { basicTests } from "./testSuites/Basic";
|
||||
import { sendArrTests, sendObjTests } from "./testSuites/RequestData";
|
||||
import { specialCaseTests } from "./testSuites/SpecialCases";
|
||||
import { sasjsRequestTests } from "./testSuites/SasjsRequests";
|
||||
import "@sasjs/test-framework/dist/index.css";
|
||||
import React, { ReactElement, useState, useContext, useEffect } from 'react'
|
||||
import { TestSuiteRunner, TestSuite, AppContext } from '@sasjs/test-framework'
|
||||
import { basicTests } from './testSuites/Basic'
|
||||
import { sendArrTests, sendObjTests } from './testSuites/RequestData'
|
||||
import { specialCaseTests } from './testSuites/SpecialCases'
|
||||
import { sasjsRequestTests } from './testSuites/SasjsRequests'
|
||||
import '@sasjs/test-framework/dist/index.css'
|
||||
import { computeTests } from './testSuites/Compute'
|
||||
import { fileUploadTests } from './testSuites/FileUpload'
|
||||
|
||||
const App = (): ReactElement<{}> => {
|
||||
const { adapter, config } = useContext(AppContext);
|
||||
const [testSuites, setTestSuites] = useState<TestSuite[]>([]);
|
||||
const { adapter, config } = useContext(AppContext)
|
||||
const [testSuites, setTestSuites] = useState<TestSuite[]>([])
|
||||
const appLoc = config.sasJsConfig.appLoc
|
||||
|
||||
useEffect(() => {
|
||||
if (adapter) {
|
||||
setTestSuites([
|
||||
const testSuites = [
|
||||
basicTests(adapter, config.userName, config.password),
|
||||
sendArrTests(adapter),
|
||||
sendArrTests(adapter, appLoc),
|
||||
sendObjTests(adapter),
|
||||
specialCaseTests(adapter),
|
||||
sasjsRequestTests(adapter)
|
||||
]);
|
||||
sasjsRequestTests(adapter),
|
||||
fileUploadTests(adapter)
|
||||
]
|
||||
|
||||
if (adapter.getSasjsConfig().serverType === 'SASVIYA') {
|
||||
testSuites.push(computeTests(adapter, appLoc))
|
||||
}
|
||||
|
||||
setTestSuites(testSuites)
|
||||
}
|
||||
}, [adapter, config]);
|
||||
}, [adapter, config, appLoc])
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
{adapter && testSuites && <TestSuiteRunner testSuites={testSuites} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default App;
|
||||
export default App
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import React, { ReactElement, useState, useCallback, useContext } from "react";
|
||||
import "./Login.scss";
|
||||
import { AppContext } from "@sasjs/test-framework";
|
||||
import { Redirect } from "react-router-dom";
|
||||
import React, { ReactElement, useState, useCallback, useContext } from 'react'
|
||||
import './Login.scss'
|
||||
import { AppContext } from '@sasjs/test-framework'
|
||||
import { Redirect } from 'react-router-dom'
|
||||
|
||||
const Login = (): ReactElement<{}> => {
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const appContext = useContext(AppContext);
|
||||
const [username, setUsername] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const appContext = useContext(AppContext)
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
(e: any) => {
|
||||
e.preventDefault()
|
||||
appContext.adapter.logIn(username, password).then((res) => {
|
||||
appContext.setIsLoggedIn(res.isLoggedIn);
|
||||
});
|
||||
appContext.setIsLoggedIn(res.isLoggedIn)
|
||||
})
|
||||
},
|
||||
[username, password, appContext]
|
||||
);
|
||||
)
|
||||
|
||||
return !appContext.isLoggedIn ? (
|
||||
<div className="login-container">
|
||||
@@ -28,7 +28,7 @@ const Login = (): ReactElement<{}> => {
|
||||
placeholder="User Name"
|
||||
value={username}
|
||||
required
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
onChange={(e: any) => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="row">
|
||||
@@ -38,7 +38,7 @@ const Login = (): ReactElement<{}> => {
|
||||
type="password"
|
||||
value={password}
|
||||
required
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
onChange={(e: any) => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" className="submit-button">
|
||||
@@ -48,7 +48,7 @@ const Login = (): ReactElement<{}> => {
|
||||
</div>
|
||||
) : (
|
||||
<Redirect to="/" />
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default Login;
|
||||
export default Login
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import React, { ReactElement, useContext, FunctionComponent } from "react";
|
||||
import { Redirect, Route } from "react-router-dom";
|
||||
import { AppContext } from "@sasjs/test-framework";
|
||||
import React, { ReactElement, useContext, FunctionComponent } from 'react'
|
||||
import { Redirect, Route } from 'react-router-dom'
|
||||
import { AppContext } from '@sasjs/test-framework'
|
||||
|
||||
interface PrivateRouteProps {
|
||||
component: FunctionComponent;
|
||||
exact?: boolean;
|
||||
path: string;
|
||||
component: FunctionComponent
|
||||
exact?: boolean
|
||||
path: string
|
||||
}
|
||||
|
||||
const PrivateRoute = (
|
||||
props: PrivateRouteProps
|
||||
): ReactElement<PrivateRouteProps> => {
|
||||
const { component, path, exact } = props;
|
||||
const appContext = useContext(AppContext);
|
||||
const { component, path, exact } = props
|
||||
const appContext = useContext(AppContext)
|
||||
return appContext.isLoggedIn ? (
|
||||
<Route component={component} path={path} exact={exact} />
|
||||
) : (
|
||||
<Redirect to="/login" />
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default PrivateRoute;
|
||||
export default PrivateRoute
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { Route, HashRouter, Switch } from "react-router-dom";
|
||||
import "./index.scss";
|
||||
import * as serviceWorker from "./serviceWorker";
|
||||
import { AppProvider } from "@sasjs/test-framework";
|
||||
import PrivateRoute from "./PrivateRoute";
|
||||
import Login from "./Login";
|
||||
import App from "./App";
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { Route, HashRouter, Switch } from 'react-router-dom'
|
||||
import './index.scss'
|
||||
import * as serviceWorker from './serviceWorker'
|
||||
import { AppProvider } from '@sasjs/test-framework'
|
||||
import PrivateRoute from './PrivateRoute'
|
||||
import Login from './Login'
|
||||
import App from './App'
|
||||
|
||||
ReactDOM.render(
|
||||
<AppProvider>
|
||||
@@ -17,10 +17,10 @@ ReactDOM.render(
|
||||
</Switch>
|
||||
</HashRouter>
|
||||
</AppProvider>,
|
||||
document.getElementById("root")
|
||||
);
|
||||
document.getElementById('root')
|
||||
)
|
||||
|
||||
// If you want your app to work offline and load faster, you can change
|
||||
// unregister() to register() below. Note this comes with some pitfalls.
|
||||
// Learn more about service workers: https://bit.ly/CRA-PWA
|
||||
serviceWorker.unregister();
|
||||
serviceWorker.unregister()
|
||||
|
||||
@@ -11,46 +11,46 @@
|
||||
// opt-in, read https://bit.ly/CRA-PWA
|
||||
|
||||
const isLocalhost = Boolean(
|
||||
window.location.hostname === "localhost" ||
|
||||
window.location.hostname === 'localhost' ||
|
||||
// [::1] is the IPv6 localhost address.
|
||||
window.location.hostname === "[::1]" ||
|
||||
window.location.hostname === '[::1]' ||
|
||||
// 127.0.0.0/8 are considered localhost for IPv4.
|
||||
window.location.hostname.match(
|
||||
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
||||
)
|
||||
);
|
||||
)
|
||||
|
||||
export function register(config) {
|
||||
if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
|
||||
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||
// The URL constructor is available in all browsers that support SW.
|
||||
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
|
||||
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href)
|
||||
if (publicUrl.origin !== window.location.origin) {
|
||||
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||
// from what our page is served on. This might happen if a CDN is used to
|
||||
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
window.addEventListener("load", () => {
|
||||
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
||||
window.addEventListener('load', () => {
|
||||
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`
|
||||
|
||||
if (isLocalhost) {
|
||||
// This is running on localhost. Let's check if a service worker still exists or not.
|
||||
checkValidServiceWorker(swUrl, config);
|
||||
checkValidServiceWorker(swUrl, config)
|
||||
|
||||
// Add some additional logging to localhost, pointing developers to the
|
||||
// service worker/PWA documentation.
|
||||
navigator.serviceWorker.ready.then(() => {
|
||||
console.log(
|
||||
"This web app is being served cache-first by a service " +
|
||||
"worker. To learn more, visit https://bit.ly/CRA-PWA"
|
||||
);
|
||||
});
|
||||
'This web app is being served cache-first by a service ' +
|
||||
'worker. To learn more, visit https://bit.ly/CRA-PWA'
|
||||
)
|
||||
})
|
||||
} else {
|
||||
// Is not localhost. Just register service worker
|
||||
registerValidSW(swUrl, config);
|
||||
registerValidSW(swUrl, config)
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,83 +59,83 @@ function registerValidSW(swUrl, config) {
|
||||
.register(swUrl)
|
||||
.then((registration) => {
|
||||
registration.onupdatefound = () => {
|
||||
const installingWorker = registration.installing;
|
||||
const installingWorker = registration.installing
|
||||
if (installingWorker == null) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
installingWorker.onstatechange = () => {
|
||||
if (installingWorker.state === "installed") {
|
||||
if (installingWorker.state === 'installed') {
|
||||
if (navigator.serviceWorker.controller) {
|
||||
// At this point, the updated precached content has been fetched,
|
||||
// but the previous service worker will still serve the older
|
||||
// content until all client tabs are closed.
|
||||
console.log(
|
||||
"New content is available and will be used when all " +
|
||||
"tabs for this page are closed. See https://bit.ly/CRA-PWA."
|
||||
);
|
||||
'New content is available and will be used when all ' +
|
||||
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
|
||||
)
|
||||
|
||||
// Execute callback
|
||||
if (config && config.onUpdate) {
|
||||
config.onUpdate(registration);
|
||||
config.onUpdate(registration)
|
||||
}
|
||||
} else {
|
||||
// At this point, everything has been precached.
|
||||
// It's the perfect time to display a
|
||||
// "Content is cached for offline use." message.
|
||||
console.log("Content is cached for offline use.");
|
||||
console.log('Content is cached for offline use.')
|
||||
|
||||
// Execute callback
|
||||
if (config && config.onSuccess) {
|
||||
config.onSuccess(registration);
|
||||
config.onSuccess(registration)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error during service worker registration:", error);
|
||||
});
|
||||
console.error('Error during service worker registration:', error)
|
||||
})
|
||||
}
|
||||
|
||||
function checkValidServiceWorker(swUrl, config) {
|
||||
// Check if the service worker can be found. If it can't reload the page.
|
||||
fetch(swUrl, {
|
||||
headers: { "Service-Worker": "script" }
|
||||
headers: { 'Service-Worker': 'script' }
|
||||
})
|
||||
.then((response) => {
|
||||
// Ensure service worker exists, and that we really are getting a JS file.
|
||||
const contentType = response.headers.get("content-type");
|
||||
const contentType = response.headers.get('content-type')
|
||||
if (
|
||||
response.status === 404 ||
|
||||
(contentType != null && contentType.indexOf("javascript") === -1)
|
||||
(contentType != null && contentType.indexOf('javascript') === -1)
|
||||
) {
|
||||
// No service worker found. Probably a different app. Reload the page.
|
||||
navigator.serviceWorker.ready.then((registration) => {
|
||||
registration.unregister().then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
window.location.reload()
|
||||
})
|
||||
})
|
||||
} else {
|
||||
// Service worker found. Proceed as normal.
|
||||
registerValidSW(swUrl, config);
|
||||
registerValidSW(swUrl, config)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
console.log(
|
||||
"No internet connection found. App is running in offline mode."
|
||||
);
|
||||
});
|
||||
'No internet connection found. App is running in offline mode.'
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export function unregister() {
|
||||
if ("serviceWorker" in navigator) {
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.ready
|
||||
.then((registration) => {
|
||||
registration.unregister();
|
||||
registration.unregister()
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error.message);
|
||||
});
|
||||
console.error(error.message)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import "@testing-library/jest-dom/extend-expect";
|
||||
import '@testing-library/jest-dom/extend-expect'
|
||||
|
||||
@@ -1,51 +1,117 @@
|
||||
import SASjs, { ServerType, SASjsConfig } from "@sasjs/adapter";
|
||||
import { TestSuite } from "@sasjs/test-framework";
|
||||
import SASjs, { LoginMechanism, SASjsConfig } from '@sasjs/adapter'
|
||||
import { TestSuite } from '@sasjs/test-framework'
|
||||
import { ServerType } from '@sasjs/utils/types'
|
||||
|
||||
const stringData: any = { table1: [{ col1: 'first col value' }] }
|
||||
|
||||
const defaultConfig: SASjsConfig = {
|
||||
serverUrl: window.location.origin,
|
||||
pathSAS9: "/SASStoredProcess/do",
|
||||
pathSASViya: "/SASJobExecution",
|
||||
appLoc: "/Public/seedapp",
|
||||
serverType: ServerType.SASViya,
|
||||
debug: true,
|
||||
contextName: "SAS Job Execution compute context",
|
||||
useComputeApi: false
|
||||
};
|
||||
pathSASJS: '/SASjsApi/stp/execute',
|
||||
pathSAS9: '/SASStoredProcess/do',
|
||||
pathSASViya: '/SASJobExecution',
|
||||
appLoc: '/Public/seedapp',
|
||||
serverType: ServerType.SasViya,
|
||||
debug: false,
|
||||
contextName: 'SAS Job Execution compute context',
|
||||
useComputeApi: false,
|
||||
loginMechanism: LoginMechanism.Default
|
||||
}
|
||||
|
||||
const customConfig = {
|
||||
serverUrl: "url",
|
||||
pathSAS9: "sas9",
|
||||
pathSASViya: "viya",
|
||||
appLoc: "/Public/seedapp",
|
||||
serverType: ServerType.SAS9,
|
||||
serverUrl: 'http://url.com',
|
||||
pathSAS9: 'sas9',
|
||||
pathSASViya: 'viya',
|
||||
appLoc: '/Public/seedapp',
|
||||
serverType: ServerType.Sas9,
|
||||
debug: false
|
||||
};
|
||||
}
|
||||
|
||||
export const basicTests = (
|
||||
adapter: SASjs,
|
||||
userName: string,
|
||||
password: string
|
||||
): TestSuite => ({
|
||||
name: "Basic Tests",
|
||||
name: 'Basic Tests',
|
||||
tests: [
|
||||
{
|
||||
title: "Log in",
|
||||
description: "Should log the user in",
|
||||
title: 'Log in',
|
||||
description: 'Should log the user in',
|
||||
test: async () => {
|
||||
return adapter.logIn(userName, password);
|
||||
return adapter.logIn(userName, password)
|
||||
},
|
||||
assertion: (response: any) =>
|
||||
response && response.isLoggedIn && response.userName === userName
|
||||
},
|
||||
{
|
||||
title: "Default config",
|
||||
description:
|
||||
"Should instantiate with default config when none is provided",
|
||||
title: 'Fetch username for already logged in user',
|
||||
description: 'Should log the user in',
|
||||
test: async () => {
|
||||
return Promise.resolve(new SASjs());
|
||||
await adapter.logIn(userName, password)
|
||||
|
||||
const newAdapterIns = new SASjs(adapter.getSasjsConfig())
|
||||
|
||||
return await newAdapterIns.checkSession()
|
||||
},
|
||||
assertion: (response: any) =>
|
||||
response?.isLoggedIn && response?.userName === userName
|
||||
},
|
||||
{
|
||||
title: 'Multiple Log in attempts',
|
||||
description:
|
||||
'Should fail on first attempt and should log the user in on second attempt',
|
||||
test: async () => {
|
||||
await adapter.logOut()
|
||||
await adapter.logIn('invalid', 'invalid').catch((err: any) => {})
|
||||
return await adapter.logIn(userName, password)
|
||||
},
|
||||
assertion: (response: any) =>
|
||||
response && response.isLoggedIn && response.userName === userName
|
||||
},
|
||||
{
|
||||
title: 'Trigger login callback',
|
||||
description:
|
||||
'Should trigger required login callback and after successful login, it should finish the request',
|
||||
test: async () => {
|
||||
await adapter.logOut()
|
||||
|
||||
return await adapter.request(
|
||||
'common/sendArr',
|
||||
stringData,
|
||||
undefined,
|
||||
async () => {
|
||||
await adapter.logIn(userName, password)
|
||||
}
|
||||
)
|
||||
},
|
||||
assertion: (response: any) => {
|
||||
return response.table1[0][0] === stringData.table1[0].col1
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Request with debug on',
|
||||
description:
|
||||
'Should complete successful request with debugging switched on',
|
||||
test: async () => {
|
||||
const config = {
|
||||
debug: true
|
||||
}
|
||||
|
||||
return await adapter.request('common/sendArr', stringData, config)
|
||||
},
|
||||
assertion: (response: any) => {
|
||||
return response.table1[0][0] === stringData.table1[0].col1
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Default config',
|
||||
description:
|
||||
'Should instantiate with default config when none is provided',
|
||||
test: async () => {
|
||||
return Promise.resolve(new SASjs())
|
||||
},
|
||||
assertion: (sasjsInstance: SASjs) => {
|
||||
const sasjsConfig = sasjsInstance.getSasjsConfig();
|
||||
const sasjsConfig = sasjsInstance.getSasjsConfig()
|
||||
|
||||
return (
|
||||
sasjsConfig.serverUrl === defaultConfig.serverUrl &&
|
||||
sasjsConfig.pathSAS9 === defaultConfig.pathSAS9 &&
|
||||
@@ -53,17 +119,17 @@ export const basicTests = (
|
||||
sasjsConfig.appLoc === defaultConfig.appLoc &&
|
||||
sasjsConfig.serverType === defaultConfig.serverType &&
|
||||
sasjsConfig.debug === defaultConfig.debug
|
||||
);
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Custom config",
|
||||
description: "Should use fully custom config whenever supplied",
|
||||
title: 'Custom config',
|
||||
description: 'Should use fully custom config whenever supplied',
|
||||
test: async () => {
|
||||
return Promise.resolve(new SASjs(customConfig));
|
||||
return Promise.resolve(new SASjs(customConfig))
|
||||
},
|
||||
assertion: (sasjsInstance: SASjs) => {
|
||||
const sasjsConfig = sasjsInstance.getSasjsConfig();
|
||||
const sasjsConfig = sasjsInstance.getSasjsConfig()
|
||||
return (
|
||||
sasjsConfig.serverUrl === customConfig.serverUrl &&
|
||||
sasjsConfig.pathSAS9 === customConfig.pathSAS9 &&
|
||||
@@ -71,28 +137,42 @@ export const basicTests = (
|
||||
sasjsConfig.appLoc === customConfig.appLoc &&
|
||||
sasjsConfig.serverType === customConfig.serverType &&
|
||||
sasjsConfig.debug === customConfig.debug
|
||||
);
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Config overrides",
|
||||
description: "Should override default config with supplied properties",
|
||||
title: 'Config overrides',
|
||||
description: 'Should override default config with supplied properties',
|
||||
test: async () => {
|
||||
return Promise.resolve(
|
||||
new SASjs({ serverUrl: "http://test.com", debug: false })
|
||||
);
|
||||
new SASjs({ serverUrl: 'http://test.com', debug: false })
|
||||
)
|
||||
},
|
||||
assertion: (sasjsInstance: SASjs) => {
|
||||
const sasjsConfig = sasjsInstance.getSasjsConfig();
|
||||
const sasjsConfig = sasjsInstance.getSasjsConfig()
|
||||
return (
|
||||
sasjsConfig.serverUrl === "http://test.com" &&
|
||||
sasjsConfig.serverUrl === 'http://test.com' &&
|
||||
sasjsConfig.pathSAS9 === defaultConfig.pathSAS9 &&
|
||||
sasjsConfig.pathSASViya === defaultConfig.pathSASViya &&
|
||||
sasjsConfig.appLoc === defaultConfig.appLoc &&
|
||||
sasjsConfig.serverType === defaultConfig.serverType &&
|
||||
sasjsConfig.debug === false
|
||||
);
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Web request',
|
||||
description: 'Should run the request with old web approach',
|
||||
test: async () => {
|
||||
const config: Partial<SASjsConfig> = {
|
||||
useComputeApi: false
|
||||
}
|
||||
|
||||
return await adapter.request('common/sendArr', stringData, config)
|
||||
},
|
||||
assertion: (response: any) => {
|
||||
return response.table1[0][0] === stringData.table1[0].col1
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
})
|
||||
|
||||
124
sasjs-tests/src/testSuites/Compute.ts
Normal file
124
sasjs-tests/src/testSuites/Compute.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import SASjs from '@sasjs/adapter'
|
||||
import { TestSuite } from '@sasjs/test-framework'
|
||||
|
||||
const stringData: any = { table1: [{ col1: 'first col value' }] }
|
||||
|
||||
export const computeTests = (adapter: SASjs, appLoc: string): TestSuite => ({
|
||||
name: 'Compute',
|
||||
tests: [
|
||||
{
|
||||
title: 'Compute API request',
|
||||
description: 'Should run the request with compute API approach',
|
||||
test: async () => {
|
||||
return await adapter.request('common/sendArr', stringData)
|
||||
},
|
||||
assertion: (response: any) => {
|
||||
return response.table1[0][0] === stringData.table1[0].col1
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'JES API request',
|
||||
description: 'Should run the request with JES API approach',
|
||||
test: async () => {
|
||||
const config = {
|
||||
useComputeApi: false
|
||||
}
|
||||
|
||||
return await adapter.request('common/sendArr', stringData, config)
|
||||
},
|
||||
assertion: (response: any) => {
|
||||
return response.table1[0][0] === stringData.table1[0].col1
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Start Compute Job - not waiting for result',
|
||||
description: 'Should start a compute job and return the session',
|
||||
test: () => {
|
||||
const data: any = { table1: [{ col1: 'first col value' }] }
|
||||
return adapter.startComputeJob(`${appLoc}/common/sendArr`, data)
|
||||
},
|
||||
assertion: (res: any) => {
|
||||
const expectedProperties = ['id', 'applicationName', 'attributes']
|
||||
return validate(expectedProperties, res)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Start Compute Job - waiting for result',
|
||||
description: 'Should start a compute job and return the job',
|
||||
test: () => {
|
||||
const data: any = { table1: [{ col1: 'first col value' }] }
|
||||
return adapter.startComputeJob(
|
||||
'/Public/app/common/sendArr',
|
||||
data,
|
||||
{},
|
||||
undefined,
|
||||
true
|
||||
)
|
||||
},
|
||||
assertion: (res: any) => {
|
||||
const expectedProperties = [
|
||||
'id',
|
||||
'state',
|
||||
'creationTimeStamp',
|
||||
'jobConditionCode'
|
||||
]
|
||||
return validate(expectedProperties, res.job)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Execute Script Viya - complete job',
|
||||
description: 'Should execute sas file and return log',
|
||||
test: () => {
|
||||
const fileLines = [`data;`, `do x=1 to 100;`, `output;`, `end;`, `run;`]
|
||||
|
||||
return adapter.executeScript({
|
||||
fileName: 'sasCode.sas',
|
||||
linesOfCode: fileLines,
|
||||
contextName: 'SAS Studio compute context',
|
||||
debug: true
|
||||
})
|
||||
},
|
||||
assertion: (res: any) => {
|
||||
const expectedLogContent = `1 data;\\n2 do x=1 to 100;\\n3 output;\\n4 end;\\n5 run;\\n\\n`
|
||||
|
||||
return validateLog(expectedLogContent, res.log)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Execute Script Viya - failed job',
|
||||
description: 'Should execute sas file and return log',
|
||||
test: () => {
|
||||
const fileLines = [`%abort;`]
|
||||
|
||||
return adapter
|
||||
.executeScript({
|
||||
fileName: 'sasCode.sas',
|
||||
linesOfCode: fileLines,
|
||||
contextName: 'SAS Studio compute context',
|
||||
debug: true
|
||||
})
|
||||
.catch((err: any) => err)
|
||||
},
|
||||
assertion: (res: any) => {
|
||||
const expectedLogContent = `1 %abort;\\nERROR: The %ABORT statement is not valid in open code.\\n`
|
||||
|
||||
return validateLog(expectedLogContent, res.log)
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const validateLog = (text: string, log: string): boolean => {
|
||||
const isValid = JSON.stringify(log).includes(text)
|
||||
|
||||
return isValid
|
||||
}
|
||||
|
||||
const validate = (expectedProperties: string[], data: any): boolean => {
|
||||
const actualProperties = Object.keys(data)
|
||||
|
||||
const isValid = expectedProperties.every((property) =>
|
||||
actualProperties.includes(property)
|
||||
)
|
||||
return isValid
|
||||
}
|
||||
35
sasjs-tests/src/testSuites/FileUpload.ts
Normal file
35
sasjs-tests/src/testSuites/FileUpload.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import SASjs from '@sasjs/adapter'
|
||||
import { TestSuite } from '@sasjs/test-framework'
|
||||
|
||||
export const fileUploadTests = (adapter: SASjs): TestSuite => ({
|
||||
name: 'File Upload Tests',
|
||||
tests: [
|
||||
{
|
||||
title: 'Upload File',
|
||||
description: 'Should upload the file to VIYA',
|
||||
test: async () => {
|
||||
let blob: any = new Blob(['test'], { type: 'text/html' })
|
||||
blob['lastModifiedDate'] = ''
|
||||
blob['name'] = 'macvars_testfile'
|
||||
let file = blob
|
||||
|
||||
const filesToUpload = [
|
||||
{
|
||||
file: file,
|
||||
fileName: file.name
|
||||
}
|
||||
]
|
||||
|
||||
return adapter.uploadFile('common/sendMacVars', filesToUpload, null)
|
||||
},
|
||||
assertion: (response: any) =>
|
||||
(response.macvars as any[]).findIndex(
|
||||
(el: any) => el.NAME === '_WEBIN_FILE_COUNT' && el.VALUE === '1'
|
||||
) > -1 &&
|
||||
(response.macvars as any[]).findIndex(
|
||||
(el: any) =>
|
||||
el.NAME === '_WEBIN_FILENAME' && el.VALUE === 'macvars_testfile'
|
||||
) > -1
|
||||
}
|
||||
]
|
||||
})
|
||||
@@ -1,101 +1,112 @@
|
||||
import SASjs from "@sasjs/adapter";
|
||||
import { TestSuite } from "@sasjs/test-framework";
|
||||
import SASjs from '@sasjs/adapter'
|
||||
import { TestSuite } from '@sasjs/test-framework'
|
||||
|
||||
const stringData: any = { table1: [{ col1: "first col value" }] };
|
||||
const numericData: any = { table1: [{ col1: 3.14159265 }] };
|
||||
const stringData: any = { table1: [{ col1: 'first col value' }] }
|
||||
const numericData: any = { table1: [{ col1: 3.14159265 }] }
|
||||
const multiColumnData: any = {
|
||||
table1: [{ col1: 42, col2: 1.618, col3: "x", col4: "x" }]
|
||||
};
|
||||
table1: [{ col1: 42, col2: 1.618, col3: 'x', col4: 'x' }]
|
||||
}
|
||||
const multipleRowsWithNulls: any = {
|
||||
table1: [
|
||||
{ col1: 42, col2: null, col3: "x", col4: "" },
|
||||
{ col1: 42, col2: null, col3: "x", col4: "" },
|
||||
{ col1: 42, col2: null, col3: "x", col4: "" },
|
||||
{ col1: 42, col2: 1.62, col3: "x", col4: "x" },
|
||||
{ col1: 42, col2: 1.62, col3: "x", col4: "x" }
|
||||
{ col1: 42, col2: null, col3: 'x', col4: '' },
|
||||
{ col1: 42, col2: null, col3: 'x', col4: '' },
|
||||
{ col1: 42, col2: null, col3: 'x', col4: '' },
|
||||
{ col1: 42, col2: 1.62, col3: 'x', col4: 'x' },
|
||||
{ col1: 42, col2: 1.62, col3: 'x', col4: 'x' }
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
const multipleColumnsWithNulls: any = {
|
||||
table1: [
|
||||
{ col1: 42, col2: null, col3: "x", col4: null },
|
||||
{ col1: 42, col2: null, col3: "x", col4: null },
|
||||
{ col1: 42, col2: null, col3: "x", col4: null },
|
||||
{ col1: 42, col2: null, col3: "x", col4: "" },
|
||||
{ col1: 42, col2: null, col3: "x", col4: "" }
|
||||
{ col1: 42, col2: null, col3: 'x', col4: null },
|
||||
{ col1: 42, col2: null, col3: 'x', col4: null },
|
||||
{ col1: 42, col2: null, col3: 'x', col4: null },
|
||||
{ col1: 42, col2: null, col3: 'x', col4: '' },
|
||||
{ col1: 42, col2: null, col3: 'x', col4: '' }
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
const getLongStringData = (length = 32764) => {
|
||||
let x = "X";
|
||||
let x = 'X'
|
||||
for (let i = 1; i <= length; i++) {
|
||||
x = x + "X";
|
||||
x = x + 'X'
|
||||
}
|
||||
const data: any = { table1: [{ col1: x }] };
|
||||
return data;
|
||||
};
|
||||
const data: any = { table1: [{ col1: x }] }
|
||||
return data
|
||||
}
|
||||
|
||||
const getLargeObjectData = () => {
|
||||
const data = { table1: [{ big: "data" }] };
|
||||
const data = { table1: [{ big: 'data' }] }
|
||||
|
||||
for (let i = 1; i < 10000; i++) {
|
||||
data.table1.push(data.table1[0]);
|
||||
data.table1.push(data.table1[0])
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
return data
|
||||
}
|
||||
|
||||
export const sendArrTests = (adapter: SASjs): TestSuite => ({
|
||||
name: "sendArr",
|
||||
export const sendArrTests = (adapter: SASjs, appLoc: string): TestSuite => ({
|
||||
name: 'sendArr',
|
||||
tests: [
|
||||
{
|
||||
title: "Single string value",
|
||||
description: "Should send an array with a single string value",
|
||||
title: 'Absolute paths',
|
||||
description: 'Should work with absolute paths to SAS jobs',
|
||||
test: () => {
|
||||
return adapter.request("common/sendArr", stringData);
|
||||
return adapter.request(`${appLoc}/common/sendArr`, stringData)
|
||||
},
|
||||
assertion: (res: any) => {
|
||||
return res.table1[0][0] === stringData.table1[0].col1;
|
||||
return res.table1[0][0] === stringData.table1[0].col1
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Long string value",
|
||||
description:
|
||||
"Should send an array with a long string value under 32765 characters",
|
||||
title: 'Single string value',
|
||||
description: 'Should send an array with a single string value',
|
||||
test: () => {
|
||||
return adapter.request("common/sendArr", getLongStringData());
|
||||
return adapter.request('common/sendArr', stringData)
|
||||
},
|
||||
assertion: (res: any) => {
|
||||
const longStringData = getLongStringData();
|
||||
return res.table1[0][0] === longStringData.table1[0].col1;
|
||||
return res.table1[0][0] === stringData.table1[0].col1
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Overly long string value",
|
||||
title: 'Long string value',
|
||||
description:
|
||||
"Should error out with long string values over 32765 characters",
|
||||
'Should send an array with a long string value under 32765 characters',
|
||||
test: () => {
|
||||
const data = getLongStringData(32767);
|
||||
return adapter.request("common/sendArr", data).catch((e) => e);
|
||||
return adapter.request('common/sendArr', getLongStringData())
|
||||
},
|
||||
assertion: (res: any) => {
|
||||
const longStringData = getLongStringData()
|
||||
return res.table1[0][0] === longStringData.table1[0].col1
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Overly long string value',
|
||||
description:
|
||||
'Should error out with long string values over 32765 characters',
|
||||
test: () => {
|
||||
const data = getLongStringData(32767)
|
||||
return adapter.request('common/sendArr', data).catch((e: any) => e)
|
||||
},
|
||||
assertion: (error: any) => {
|
||||
return !!error && !!error.MESSAGE;
|
||||
return !!error && !!error.error && !!error.error.message
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Single numeric value",
|
||||
description: "Should send an array with a single numeric value",
|
||||
title: 'Single numeric value',
|
||||
description: 'Should send an array with a single numeric value',
|
||||
test: () => {
|
||||
return adapter.request("common/sendArr", numericData);
|
||||
return adapter.request('common/sendArr', numericData)
|
||||
},
|
||||
assertion: (res: any) => {
|
||||
return res.table1[0][0] === numericData.table1[0].col1;
|
||||
return res.table1[0][0] === numericData.table1[0].col1
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Multiple columns",
|
||||
description: "Should handle data with multiple columns",
|
||||
title: 'Multiple columns',
|
||||
description: 'Should handle data with multiple columns',
|
||||
test: () => {
|
||||
return adapter.request("common/sendArr", multiColumnData);
|
||||
return adapter.request('common/sendArr', multiColumnData)
|
||||
},
|
||||
assertion: (res: any) => {
|
||||
return (
|
||||
@@ -103,142 +114,200 @@ export const sendArrTests = (adapter: SASjs): TestSuite => ({
|
||||
res.table1[0][1] === multiColumnData.table1[0].col2 &&
|
||||
res.table1[0][2] === multiColumnData.table1[0].col3 &&
|
||||
res.table1[0][3] === multiColumnData.table1[0].col4
|
||||
);
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Multiple rows with nulls",
|
||||
description: "Should handle data with multiple rows with null values",
|
||||
title: 'Multiple rows with nulls',
|
||||
description: 'Should handle data with multiple rows with null values',
|
||||
test: () => {
|
||||
return adapter.request("common/sendArr", multipleRowsWithNulls);
|
||||
return adapter.request('common/sendArr', multipleRowsWithNulls)
|
||||
},
|
||||
assertion: (res: any) => {
|
||||
let result = true;
|
||||
let result = true
|
||||
multipleRowsWithNulls.table1.forEach((_: any, index: number) => {
|
||||
result =
|
||||
result &&
|
||||
res.table1[index][0] === multipleRowsWithNulls.table1[index].col1;
|
||||
res.table1[index][0] === multipleRowsWithNulls.table1[index].col1
|
||||
result =
|
||||
result &&
|
||||
res.table1[index][1] === multipleRowsWithNulls.table1[index].col2;
|
||||
res.table1[index][1] === multipleRowsWithNulls.table1[index].col2
|
||||
result =
|
||||
result &&
|
||||
res.table1[index][2] === multipleRowsWithNulls.table1[index].col3;
|
||||
result =
|
||||
result &&
|
||||
res.table1[index][3] === multipleRowsWithNulls.table1[index].col4;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Multiple columns with nulls",
|
||||
description: "Should handle data with multiple columns with null values",
|
||||
test: () => {
|
||||
return adapter.request("common/sendArr", multipleColumnsWithNulls);
|
||||
},
|
||||
assertion: (res: any) => {
|
||||
let result = true;
|
||||
multipleColumnsWithNulls.table1.forEach((_: any, index: number) => {
|
||||
result =
|
||||
result &&
|
||||
res.table1[index][0] ===
|
||||
multipleColumnsWithNulls.table1[index].col1;
|
||||
result =
|
||||
result &&
|
||||
res.table1[index][1] ===
|
||||
multipleColumnsWithNulls.table1[index].col2;
|
||||
result =
|
||||
result &&
|
||||
res.table1[index][2] ===
|
||||
multipleColumnsWithNulls.table1[index].col3;
|
||||
res.table1[index][2] === multipleRowsWithNulls.table1[index].col3
|
||||
result =
|
||||
result &&
|
||||
res.table1[index][3] ===
|
||||
(multipleColumnsWithNulls.table1[index].col4 || "");
|
||||
});
|
||||
return result;
|
||||
(multipleRowsWithNulls.table1[index].col4 || '')
|
||||
})
|
||||
return result
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Multiple columns with nulls',
|
||||
description: 'Should handle data with multiple columns with null values',
|
||||
test: () => {
|
||||
return adapter.request('common/sendArr', multipleColumnsWithNulls)
|
||||
},
|
||||
assertion: (res: any) => {
|
||||
let result = true
|
||||
multipleColumnsWithNulls.table1.forEach((_: any, index: number) => {
|
||||
result =
|
||||
result &&
|
||||
res.table1[index][0] === multipleColumnsWithNulls.table1[index].col1
|
||||
result =
|
||||
result &&
|
||||
res.table1[index][1] === multipleColumnsWithNulls.table1[index].col2
|
||||
result =
|
||||
result &&
|
||||
res.table1[index][2] === multipleColumnsWithNulls.table1[index].col3
|
||||
result =
|
||||
result &&
|
||||
res.table1[index][3] ===
|
||||
(multipleColumnsWithNulls.table1[index].col4 || '')
|
||||
})
|
||||
return result
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
})
|
||||
|
||||
export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
||||
name: "sendObj",
|
||||
name: 'sendObj',
|
||||
tests: [
|
||||
{
|
||||
title: "Invalid column name",
|
||||
description: "Should throw an error",
|
||||
title: 'Table name starts with numeric',
|
||||
description: 'Should throw an error',
|
||||
test: async () => {
|
||||
const invalidData: any = {
|
||||
"1 invalid table": [{ col1: 42 }]
|
||||
};
|
||||
return adapter.request("common/sendObj", invalidData).catch((e) => e);
|
||||
'1InvalidTable': [{ col1: 42 }]
|
||||
}
|
||||
return adapter
|
||||
.request('common/sendObj', invalidData)
|
||||
.catch((e: any) => e)
|
||||
},
|
||||
assertion: (error: any) => !!error && !!error.MESSAGE
|
||||
assertion: (error: any) =>
|
||||
!!error && !!error.error && !!error.error.message
|
||||
},
|
||||
{
|
||||
title: "Single string value",
|
||||
description: "Should send an object with a single string value",
|
||||
title: 'Table name contains a space',
|
||||
description: 'Should throw an error',
|
||||
test: async () => {
|
||||
const invalidData: any = {
|
||||
'an invalidTable': [{ col1: 42 }]
|
||||
}
|
||||
return adapter
|
||||
.request('common/sendObj', invalidData)
|
||||
.catch((e: any) => e)
|
||||
},
|
||||
assertion: (error: any) =>
|
||||
!!error && !!error.error && !!error.error.message
|
||||
},
|
||||
{
|
||||
title: 'Table name contains a special character',
|
||||
description: 'Should throw an error',
|
||||
test: async () => {
|
||||
const invalidData: any = {
|
||||
'anInvalidTable#': [{ col1: 42 }]
|
||||
}
|
||||
return adapter
|
||||
.request('common/sendObj', invalidData)
|
||||
.catch((e: any) => e)
|
||||
},
|
||||
assertion: (error: any) =>
|
||||
!!error && !!error.error && !!error.error.message
|
||||
},
|
||||
{
|
||||
title: 'Table name exceeds max length of 32 characters',
|
||||
description: 'Should throw an error',
|
||||
test: async () => {
|
||||
const invalidData: any = {
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx: [{ col1: 42 }]
|
||||
}
|
||||
|
||||
return adapter
|
||||
.request('common/sendObj', invalidData)
|
||||
.catch((e: any) => e)
|
||||
},
|
||||
assertion: (error: any) =>
|
||||
!!error && !!error.error && !!error.error.message
|
||||
},
|
||||
{
|
||||
title: "Invalid data object's structure",
|
||||
description: 'Should throw an error',
|
||||
test: async () => {
|
||||
const invalidData: any = {
|
||||
inData: [[{ data: 'value' }]]
|
||||
}
|
||||
return adapter
|
||||
.request('common/sendObj', invalidData)
|
||||
.catch((e: any) => e)
|
||||
},
|
||||
assertion: (error: any) =>
|
||||
!!error && !!error.error && !!error.error.message
|
||||
},
|
||||
{
|
||||
title: 'Single string value',
|
||||
description: 'Should send an object with a single string value',
|
||||
test: () => {
|
||||
return adapter.request("common/sendObj", stringData);
|
||||
return adapter.request('common/sendObj', stringData)
|
||||
},
|
||||
assertion: (res: any) => {
|
||||
return res.table1[0].COL1 === stringData.table1[0].col1;
|
||||
return res.table1[0].COL1 === stringData.table1[0].col1
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Long string value",
|
||||
title: 'Long string value',
|
||||
description:
|
||||
"Should send an object with a long string value under 32765 characters",
|
||||
'Should send an object with a long string value under 32765 characters',
|
||||
test: () => {
|
||||
return adapter.request("common/sendObj", getLongStringData());
|
||||
return adapter.request('common/sendObj', getLongStringData())
|
||||
},
|
||||
assertion: (res: any) => {
|
||||
const longStringData = getLongStringData();
|
||||
return res.table1[0].COL1 === longStringData.table1[0].col1;
|
||||
const longStringData = getLongStringData()
|
||||
return res.table1[0].COL1 === longStringData.table1[0].col1
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Overly long string value",
|
||||
title: 'Overly long string value',
|
||||
description:
|
||||
"Should error out with long string values over 32765 characters",
|
||||
'Should error out with long string values over 32765 characters',
|
||||
test: () => {
|
||||
return adapter
|
||||
.request("common/sendObj", getLongStringData(32767))
|
||||
.catch((e) => e);
|
||||
.request('common/sendObj', getLongStringData(32767))
|
||||
.catch((e: any) => e)
|
||||
},
|
||||
assertion: (error: any) => {
|
||||
return !!error && !!error.MESSAGE;
|
||||
return !!error && !!error.error && !!error.error.message
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Single numeric value",
|
||||
description: "Should send an object with a single numeric value",
|
||||
title: 'Single numeric value',
|
||||
description: 'Should send an object with a single numeric value',
|
||||
test: () => {
|
||||
return adapter.request("common/sendObj", numericData);
|
||||
return adapter.request('common/sendObj', numericData)
|
||||
},
|
||||
assertion: (res: any) => {
|
||||
return res.table1[0].COL1 === numericData.table1[0].col1;
|
||||
return res.table1[0].COL1 === numericData.table1[0].col1
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
title: "Large data volume",
|
||||
description: "Should send an object with a large amount of data",
|
||||
title: 'Large data volume',
|
||||
description: 'Should send an object with a large amount of data',
|
||||
test: () => {
|
||||
return adapter.request("common/sendObj", getLargeObjectData());
|
||||
return adapter.request('common/sendObj', getLargeObjectData())
|
||||
},
|
||||
assertion: (res: any) => {
|
||||
const data = getLargeObjectData();
|
||||
return res.table1[9000].BIG === data.table1[9000].big;
|
||||
const data = getLargeObjectData()
|
||||
return res.table1[9000].BIG === data.table1[9000].big
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Multiple columns",
|
||||
description: "Should handle data with multiple columns",
|
||||
title: 'Multiple columns',
|
||||
description: 'Should handle data with multiple columns',
|
||||
test: () => {
|
||||
return adapter.request("common/sendObj", multiColumnData);
|
||||
return adapter.request('common/sendObj', multiColumnData)
|
||||
},
|
||||
assertion: (res: any) => {
|
||||
return (
|
||||
@@ -246,62 +315,63 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
||||
res.table1[0].COL2 === multiColumnData.table1[0].col2 &&
|
||||
res.table1[0].COL3 === multiColumnData.table1[0].col3 &&
|
||||
res.table1[0].COL4 === multiColumnData.table1[0].col4
|
||||
);
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Multiple rows with nulls",
|
||||
description: "Should handle data with multiple rows with null values",
|
||||
title: 'Multiple rows with nulls',
|
||||
description: 'Should handle data with multiple rows with null values',
|
||||
test: () => {
|
||||
return adapter.request("common/sendObj", multipleRowsWithNulls);
|
||||
return adapter.request('common/sendObj', multipleRowsWithNulls)
|
||||
},
|
||||
assertion: (res: any) => {
|
||||
let result = true;
|
||||
let result = true
|
||||
multipleRowsWithNulls.table1.forEach((_: any, index: number) => {
|
||||
result =
|
||||
result &&
|
||||
res.table1[index].COL1 === multipleRowsWithNulls.table1[index].col1;
|
||||
res.table1[index].COL1 === multipleRowsWithNulls.table1[index].col1
|
||||
result =
|
||||
result &&
|
||||
res.table1[index].COL2 === multipleRowsWithNulls.table1[index].col2;
|
||||
res.table1[index].COL2 === multipleRowsWithNulls.table1[index].col2
|
||||
result =
|
||||
result &&
|
||||
res.table1[index].COL3 === multipleRowsWithNulls.table1[index].col3;
|
||||
res.table1[index].COL3 === multipleRowsWithNulls.table1[index].col3
|
||||
result =
|
||||
result &&
|
||||
res.table1[index].COL4 === multipleRowsWithNulls.table1[index].col4;
|
||||
});
|
||||
return result;
|
||||
res.table1[index].COL4 ===
|
||||
(multipleRowsWithNulls.table1[index].col4 || '')
|
||||
})
|
||||
return result
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Multiple columns with nulls",
|
||||
description: "Should handle data with multiple columns with null values",
|
||||
title: 'Multiple columns with nulls',
|
||||
description: 'Should handle data with multiple columns with null values',
|
||||
test: () => {
|
||||
return adapter.request("common/sendObj", multipleColumnsWithNulls);
|
||||
return adapter.request('common/sendObj', multipleColumnsWithNulls)
|
||||
},
|
||||
assertion: (res: any) => {
|
||||
let result = true;
|
||||
let result = true
|
||||
multipleColumnsWithNulls.table1.forEach((_: any, index: number) => {
|
||||
result =
|
||||
result &&
|
||||
res.table1[index].COL1 ===
|
||||
multipleColumnsWithNulls.table1[index].col1;
|
||||
multipleColumnsWithNulls.table1[index].col1
|
||||
result =
|
||||
result &&
|
||||
res.table1[index].COL2 ===
|
||||
multipleColumnsWithNulls.table1[index].col2;
|
||||
multipleColumnsWithNulls.table1[index].col2
|
||||
result =
|
||||
result &&
|
||||
res.table1[index].COL3 ===
|
||||
multipleColumnsWithNulls.table1[index].col3;
|
||||
multipleColumnsWithNulls.table1[index].col3
|
||||
result =
|
||||
result &&
|
||||
res.table1[index].COL4 ===
|
||||
(multipleColumnsWithNulls.table1[index].col4 || "");
|
||||
});
|
||||
return result;
|
||||
(multipleColumnsWithNulls.table1[index].col4 || '')
|
||||
})
|
||||
return result
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
})
|
||||
|
||||
@@ -1,50 +1,49 @@
|
||||
import SASjs from "@sasjs/adapter";
|
||||
import { TestSuite } from "@sasjs/test-framework";
|
||||
import SASjs from '@sasjs/adapter'
|
||||
import { TestSuite } from '@sasjs/test-framework'
|
||||
|
||||
const data: any = { table1: [{ col1: "first col value" }] };
|
||||
const data: any = { table1: [{ col1: 'first col value' }] }
|
||||
|
||||
export const sasjsRequestTests = (adapter: SASjs): TestSuite => ({
|
||||
name: "SASjs Requests",
|
||||
name: 'SASjs Requests',
|
||||
tests: [
|
||||
{
|
||||
title: "WORK tables",
|
||||
description: "Should get WORK tables after request",
|
||||
title: 'WORK tables',
|
||||
description: 'Should get WORK tables after request',
|
||||
test: async () => {
|
||||
return adapter.request("common/sendArr", data);
|
||||
return adapter.request('common/sendArr', data)
|
||||
},
|
||||
assertion: () => {
|
||||
const requests = adapter.getSasRequests();
|
||||
const requests = adapter.getSasRequests()
|
||||
if (adapter.getSasjsConfig().debug) {
|
||||
return requests[0].SASWORK !== null;
|
||||
return requests[0].SASWORK !== null
|
||||
} else {
|
||||
return requests[0].SASWORK === null;
|
||||
return requests[0].SASWORK === null
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Make error and capture log",
|
||||
description: "Should make an error and capture log",
|
||||
title: 'Make error and capture log',
|
||||
description:
|
||||
'Should make an error and capture log, in the same time it is testing if debug override is working',
|
||||
test: async () => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
adapter
|
||||
.request("common/makeErr", data)
|
||||
.then((res) => {
|
||||
//no action here, this request must throw error
|
||||
})
|
||||
.catch((err) => {
|
||||
let sasRequests = adapter.getSasRequests();
|
||||
let makeErrRequest =
|
||||
sasRequests.find((req) =>
|
||||
req.serviceLink.includes("makeErr")
|
||||
) || null;
|
||||
return adapter
|
||||
.request('common/makeErr', data, { debug: true })
|
||||
.catch(() => {
|
||||
const sasRequests = adapter.getSasRequests()
|
||||
const makeErrRequest: any =
|
||||
sasRequests.find((req) => req.serviceLink.includes('makeErr')) ||
|
||||
null
|
||||
|
||||
resolve(!!makeErrRequest);
|
||||
});
|
||||
});
|
||||
if (!makeErrRequest) return false
|
||||
|
||||
return !!(
|
||||
makeErrRequest.logFile && makeErrRequest.logFile.length > 0
|
||||
)
|
||||
})
|
||||
},
|
||||
assertion: (response) => {
|
||||
return response;
|
||||
return response
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
})
|
||||
|
||||
@@ -1,91 +1,115 @@
|
||||
import SASjs from "@sasjs/adapter";
|
||||
import { TestSuite } from "@sasjs/test-framework";
|
||||
import SASjs from '@sasjs/adapter'
|
||||
import { TestSuite } from '@sasjs/test-framework'
|
||||
|
||||
const specialCharData: any = {
|
||||
table1: [
|
||||
{
|
||||
tab: "\t",
|
||||
lf: "\n",
|
||||
cr: "\r",
|
||||
semicolon: ";semi",
|
||||
percent: "%",
|
||||
tab: '\t',
|
||||
lf: '\n',
|
||||
cr: '\r',
|
||||
semicolon: ';semi',
|
||||
percent: '%',
|
||||
singleQuote: "'",
|
||||
doubleQuote: '"',
|
||||
crlf: "\r\n",
|
||||
euro: "€euro",
|
||||
banghash: "!#banghash"
|
||||
crlf: '\r\n',
|
||||
euro: '€euro',
|
||||
banghash: '!#banghash',
|
||||
dot: '.'
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
const moreSpecialCharData: any = {
|
||||
table1: [
|
||||
{
|
||||
speech0: '"speech',
|
||||
pct: "%percent",
|
||||
pct: '%percent',
|
||||
speech: '"speech',
|
||||
slash: "\\slash",
|
||||
slashWithSpecial: "\\\tslash",
|
||||
macvar: "&sysuserid",
|
||||
chinese: "传/傳chinese",
|
||||
sigma: "Σsigma",
|
||||
at: "@at",
|
||||
serbian: "Српски",
|
||||
dollar: "$"
|
||||
slash: '\\slash',
|
||||
slashWithSpecial: '\\\tslash',
|
||||
macvar: '&sysuserid',
|
||||
chinese: '传/傳chinese',
|
||||
sigma: 'Σsigma',
|
||||
at: '@at',
|
||||
serbian: 'Српски',
|
||||
dollar: '$'
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
const stringData: any = { table1: [{ col1: 'first col value' }] }
|
||||
|
||||
const getWideData = () => {
|
||||
const cols: any = {};
|
||||
const cols: any = {}
|
||||
for (let i = 1; i <= 10000; i++) {
|
||||
cols["col" + i] = "test" + i;
|
||||
cols['col' + i] = 'test' + i
|
||||
}
|
||||
|
||||
const data: any = {
|
||||
table1: [cols]
|
||||
};
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
return data
|
||||
}
|
||||
|
||||
const getTables = () => {
|
||||
const tables: any = {};
|
||||
const tables: any = {}
|
||||
|
||||
for (let i = 1; i <= 100; i++) {
|
||||
tables["table" + i] = [{ col1: "x", col2: "x", col3: "x", col4: "x" }];
|
||||
tables['table' + i] = [{ col1: 'x', col2: 'x', col3: 'x', col4: 'x' }]
|
||||
}
|
||||
return tables;
|
||||
};
|
||||
return tables
|
||||
}
|
||||
|
||||
const getLargeDataset = () => {
|
||||
const rows: any = [];
|
||||
const rows: any = []
|
||||
const colData: string =
|
||||
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
|
||||
'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
|
||||
for (let i = 1; i <= 10000; i++) {
|
||||
rows.push({ col1: colData, col2: colData, col3: colData, col4: colData });
|
||||
rows.push({ col1: colData, col2: colData, col3: colData, col4: colData })
|
||||
}
|
||||
|
||||
const data: any = {
|
||||
table1: rows
|
||||
};
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
return data
|
||||
}
|
||||
|
||||
const errorAndCsrfData: any = {
|
||||
error: [{ col1: "q", col2: "w", col3: "e", col4: "r" }],
|
||||
_csrf: [{ col1: "q", col2: "w", col3: "e", col4: "r" }]
|
||||
};
|
||||
error: [{ col1: 'q', col2: 'w', col3: 'e', col4: 'r' }],
|
||||
_csrf: [{ col1: 'q', col2: 'w', col3: 'e', col4: 'r' }]
|
||||
}
|
||||
|
||||
const testTable = 'sometable'
|
||||
export const testTableWithSpecialNumeric: { [key: string]: any } = {
|
||||
[testTable]: [
|
||||
{ var1: 'string', var2: 232, specialnumeric: 'A' },
|
||||
{ var1: 'string', var2: 232, specialnumeric: 'B' },
|
||||
{ var1: 'string', var2: 232, specialnumeric: '_' },
|
||||
{ var1: 'string', var2: 232, specialnumeric: 0 },
|
||||
{ var1: 'string', var2: 232, specialnumeric: 'Z' },
|
||||
{ var1: 'string', var2: 232, specialnumeric: null }
|
||||
],
|
||||
[`$${testTable}`]: { formats: { var1: '$char12.', specialnumeric: 'best.' } }
|
||||
}
|
||||
export const testTableWithSpecialNumericOneRow: { [key: string]: any } = {
|
||||
[testTable]: [{ var1: 'string', var2: 232, specialnumeric: 'S' }],
|
||||
[`$${testTable}`]: { formats: { var1: '$char12.', specialnumeric: 'best.' } }
|
||||
}
|
||||
export const testTableWithSpecialNumericLowercase: { [key: string]: any } = {
|
||||
[testTable]: [{ var1: 'string', var2: 232, specialnumeric: 's' }],
|
||||
[`$${testTable}`]: { formats: { var1: '$char12.', specialnumeric: 'best.' } }
|
||||
}
|
||||
|
||||
export const specialCaseTests = (adapter: SASjs): TestSuite => ({
|
||||
name: "Special Cases",
|
||||
name: 'Special Cases',
|
||||
tests: [
|
||||
{
|
||||
title: "Common special characters",
|
||||
description: "Should handle common special characters",
|
||||
title: 'Common special characters',
|
||||
description: 'Should handle common special characters',
|
||||
test: () => {
|
||||
return adapter.request("common/sendArr", specialCharData);
|
||||
return adapter.request('common/sendArr', specialCharData)
|
||||
},
|
||||
assertion: (res: any) => {
|
||||
return (
|
||||
@@ -96,17 +120,18 @@ export const specialCaseTests = (adapter: SASjs): TestSuite => ({
|
||||
res.table1[0][4] === specialCharData.table1[0].percent &&
|
||||
res.table1[0][5] === specialCharData.table1[0].singleQuote &&
|
||||
res.table1[0][6] === specialCharData.table1[0].doubleQuote &&
|
||||
res.table1[0][7] === "\n" &&
|
||||
res.table1[0][7] === '\n' &&
|
||||
res.table1[0][8] === specialCharData.table1[0].euro &&
|
||||
res.table1[0][9] === specialCharData.table1[0].banghash
|
||||
);
|
||||
res.table1[0][9] === specialCharData.table1[0].banghash &&
|
||||
res.table1[0][10] === specialCharData.table1[0].dot
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Other special characters",
|
||||
description: "Should handle other special characters",
|
||||
title: 'Other special characters',
|
||||
description: 'Should handle other special characters',
|
||||
test: () => {
|
||||
return adapter.request("common/sendArr", moreSpecialCharData);
|
||||
return adapter.request('common/sendArr', moreSpecialCharData)
|
||||
},
|
||||
assertion: (res: any) => {
|
||||
return (
|
||||
@@ -121,50 +146,50 @@ export const specialCaseTests = (adapter: SASjs): TestSuite => ({
|
||||
res.table1[0][8] === moreSpecialCharData.table1[0].at &&
|
||||
res.table1[0][9] === moreSpecialCharData.table1[0].serbian &&
|
||||
res.table1[0][10] === moreSpecialCharData.table1[0].dollar
|
||||
);
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Wide table with sendArr",
|
||||
description: "Should handle data with 10000 columns",
|
||||
title: 'Wide table with sendArr',
|
||||
description: 'Should handle data with 10000 columns',
|
||||
test: () => {
|
||||
return adapter.request("common/sendArr", getWideData());
|
||||
return adapter.request('common/sendArr', getWideData())
|
||||
},
|
||||
assertion: (res: any) => {
|
||||
const data = getWideData();
|
||||
let result = true;
|
||||
const data = getWideData()
|
||||
let result = true
|
||||
for (let i = 0; i <= 10; i++) {
|
||||
result =
|
||||
result && res.table1[0][i] === data.table1[0]["col" + (i + 1)];
|
||||
result && res.table1[0][i] === data.table1[0]['col' + (i + 1)]
|
||||
}
|
||||
return result;
|
||||
return result
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Wide table with sendObj",
|
||||
description: "Should handle data with 10000 columns",
|
||||
title: 'Wide table with sendObj',
|
||||
description: 'Should handle data with 10000 columns',
|
||||
test: () => {
|
||||
return adapter.request("common/sendObj", getWideData());
|
||||
return adapter.request('common/sendObj', getWideData())
|
||||
},
|
||||
assertion: (res: any) => {
|
||||
const data = getWideData();
|
||||
let result = true;
|
||||
const data = getWideData()
|
||||
let result = true
|
||||
for (let i = 0; i <= 10; i++) {
|
||||
result =
|
||||
result &&
|
||||
res.table1[0]["COL" + (i + 1)] === data.table1[0]["col" + (i + 1)];
|
||||
res.table1[0]['COL' + (i + 1)] === data.table1[0]['col' + (i + 1)]
|
||||
}
|
||||
return result;
|
||||
return result
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Multiple tables",
|
||||
description: "Should handle data with 100 tables",
|
||||
title: 'Multiple tables',
|
||||
description: 'Should handle data with 100 tables',
|
||||
test: () => {
|
||||
return adapter.request("common/sendArr", getTables());
|
||||
return adapter.request('common/sendArr', getTables())
|
||||
},
|
||||
assertion: (res: any) => {
|
||||
const data = getTables();
|
||||
const data = getTables()
|
||||
return (
|
||||
res.table1[0][0] === data.table1[0].col1 &&
|
||||
res.table1[0][1] === data.table1[0].col2 &&
|
||||
@@ -174,45 +199,45 @@ export const specialCaseTests = (adapter: SASjs): TestSuite => ({
|
||||
res.table50[0][1] === data.table50[0].col2 &&
|
||||
res.table50[0][2] === data.table50[0].col3 &&
|
||||
res.table50[0][3] === data.table50[0].col4
|
||||
);
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Large dataset with sendObj",
|
||||
description: "Should handle 5mb of data",
|
||||
title: 'Large dataset with sendObj',
|
||||
description: 'Should handle 5mb of data',
|
||||
test: () => {
|
||||
return adapter.request("common/sendObj", getLargeDataset());
|
||||
return adapter.request('common/sendObj', getLargeDataset())
|
||||
},
|
||||
assertion: (res: any) => {
|
||||
const data = getLargeDataset();
|
||||
let result = true;
|
||||
const data = getLargeDataset()
|
||||
let result = true
|
||||
for (let i = 0; i <= 10; i++) {
|
||||
result = result && res.table1[i][0] === data.table1[i][0];
|
||||
result = result && res.table1[i][0] === data.table1[i][0]
|
||||
}
|
||||
return result;
|
||||
return result
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Large dataset with sendArr",
|
||||
description: "Should handle 5mb of data",
|
||||
title: 'Large dataset with sendArr',
|
||||
description: 'Should handle 5mb of data',
|
||||
test: () => {
|
||||
return adapter.request("common/sendArr", getLargeDataset());
|
||||
return adapter.request('common/sendArr', getLargeDataset())
|
||||
},
|
||||
assertion: (res: any) => {
|
||||
const data = getLargeDataset();
|
||||
let result = true;
|
||||
const data = getLargeDataset()
|
||||
let result = true
|
||||
for (let i = 0; i <= 10; i++) {
|
||||
result =
|
||||
result && res.table1[i][0] === Object.values(data.table1[i])[0];
|
||||
result && res.table1[i][0] === Object.values(data.table1[i])[0]
|
||||
}
|
||||
return result;
|
||||
return result
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Error and _csrf tables with sendArr",
|
||||
description: "Should handle error and _csrf tables",
|
||||
title: 'Error and _csrf tables with sendArr',
|
||||
description: 'Should handle error and _csrf tables',
|
||||
test: () => {
|
||||
return adapter.request("common/sendArr", errorAndCsrfData);
|
||||
return adapter.request('common/sendArr', errorAndCsrfData)
|
||||
},
|
||||
assertion: (res: any) => {
|
||||
return (
|
||||
@@ -224,14 +249,14 @@ export const specialCaseTests = (adapter: SASjs): TestSuite => ({
|
||||
res._csrf[0][1] === errorAndCsrfData._csrf[0].col2 &&
|
||||
res._csrf[0][2] === errorAndCsrfData._csrf[0].col3 &&
|
||||
res._csrf[0][3] === errorAndCsrfData._csrf[0].col4
|
||||
);
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Error and _csrf tables with sendObj",
|
||||
description: "Should handle error and _csrf tables",
|
||||
title: 'Error and _csrf tables with sendObj',
|
||||
description: 'Should handle error and _csrf tables',
|
||||
test: () => {
|
||||
return adapter.request("common/sendObj", errorAndCsrfData);
|
||||
return adapter.request('common/sendObj', errorAndCsrfData)
|
||||
},
|
||||
assertion: (res: any) => {
|
||||
return (
|
||||
@@ -243,8 +268,233 @@ export const specialCaseTests = (adapter: SASjs): TestSuite => ({
|
||||
res._csrf[0].COL2 === errorAndCsrfData._csrf[0].col2 &&
|
||||
res._csrf[0].COL3 === errorAndCsrfData._csrf[0].col3 &&
|
||||
res._csrf[0].COL4 === errorAndCsrfData._csrf[0].col4
|
||||
);
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Request with extra attributes on JES approach',
|
||||
description:
|
||||
'Should complete successful request with extra attributes present in response',
|
||||
test: async () => {
|
||||
if (adapter.getSasjsConfig().serverType !== 'SASVIYA')
|
||||
return Promise.resolve('skip')
|
||||
|
||||
const config = {
|
||||
useComputeApi: false
|
||||
}
|
||||
|
||||
return await adapter.request(
|
||||
'common/sendArr',
|
||||
stringData,
|
||||
config,
|
||||
undefined,
|
||||
undefined,
|
||||
['file', 'data']
|
||||
)
|
||||
},
|
||||
assertion: (response: any) => {
|
||||
if (response === 'skip') return true
|
||||
|
||||
const responseKeys: any = Object.keys(response)
|
||||
return responseKeys.includes('file') && responseKeys.includes('data')
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Special missing values',
|
||||
description: 'Should support special missing values',
|
||||
test: () => {
|
||||
return adapter.request('common/sendObj', testTableWithSpecialNumeric)
|
||||
},
|
||||
assertion: (res: any) => {
|
||||
let assertionRes = true
|
||||
|
||||
// We receive formats in response. We compare it with formats that we included in request to make sure they are equal
|
||||
const resVars = res[`$${testTable}`].vars
|
||||
|
||||
Object.keys(resVars).forEach((key: any, i: number) => {
|
||||
let formatValue =
|
||||
testTableWithSpecialNumeric[`$${testTable}`].formats[
|
||||
key.toLowerCase()
|
||||
]
|
||||
// If it is char, we change it to $ to be compatible for comparsion
|
||||
// If it is number, it will already be compatible to comapre (best.)
|
||||
formatValue = formatValue?.includes('$') ? '$' : formatValue
|
||||
|
||||
if (
|
||||
formatValue !== undefined &&
|
||||
!resVars[key].format.includes(formatValue)
|
||||
) {
|
||||
assertionRes = false
|
||||
}
|
||||
})
|
||||
|
||||
// Here we will compare the response values with values we send
|
||||
const resValues = res[testTable]
|
||||
|
||||
testTableWithSpecialNumeric[testTable].forEach(
|
||||
(row: { [key: string]: any }, i: number) =>
|
||||
Object.keys(row).forEach((col: string) => {
|
||||
if (resValues[i][col.toUpperCase()] !== row[col]) {
|
||||
assertionRes = false
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
return assertionRes
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Special missing values (ONE ROW)',
|
||||
description:
|
||||
'Should support special missing values, when one row is send',
|
||||
test: () => {
|
||||
return adapter.request(
|
||||
'common/sendObj',
|
||||
testTableWithSpecialNumericOneRow
|
||||
)
|
||||
},
|
||||
assertion: (res: any) => {
|
||||
let assertionRes = true
|
||||
|
||||
// We receive formats in response. We compare it with formats that we included in request to make sure they are equal
|
||||
const resVars = res[`$${testTable}`].vars
|
||||
|
||||
Object.keys(resVars).forEach((key: any, i: number) => {
|
||||
let formatValue =
|
||||
testTableWithSpecialNumeric[`$${testTable}`].formats[
|
||||
key.toLowerCase()
|
||||
]
|
||||
// If it is char, we change it to $ to be compatible for comparsion
|
||||
// If it is number, it will already be compatible to comapre (best.)
|
||||
formatValue = formatValue?.includes('$') ? '$' : formatValue
|
||||
|
||||
if (
|
||||
formatValue !== undefined &&
|
||||
!resVars[key].format.includes(formatValue)
|
||||
) {
|
||||
assertionRes = false
|
||||
}
|
||||
})
|
||||
|
||||
// Here we will compare the response values with values we send
|
||||
const resValues = res[testTable]
|
||||
|
||||
testTableWithSpecialNumericOneRow[testTable].forEach(
|
||||
(row: { [key: string]: any }, i: number) =>
|
||||
Object.keys(row).forEach((col: string) => {
|
||||
if (resValues[i][col.toUpperCase()] !== row[col]) {
|
||||
assertionRes = false
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
return assertionRes
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Special missing values (LOWERCASE)',
|
||||
description:
|
||||
'Should support special missing values, when LOWERCASE value is sent',
|
||||
test: () => {
|
||||
return adapter.request(
|
||||
'common/sendObj',
|
||||
testTableWithSpecialNumericLowercase
|
||||
)
|
||||
},
|
||||
assertion: (res: any) => {
|
||||
let assertionRes = true
|
||||
|
||||
// We receive formats in response. We compare it with formats that we included in request to make sure they are equal
|
||||
const resVars = res[`$${testTable}`].vars
|
||||
|
||||
Object.keys(resVars).forEach((key: any, i: number) => {
|
||||
let formatValue =
|
||||
testTableWithSpecialNumericLowercase[`$${testTable}`].formats[
|
||||
key.toLowerCase()
|
||||
]
|
||||
// If it is a char, we change it to $ to be compatible for comparison
|
||||
// If it is a number, it will already be compatible to compare (best.)
|
||||
formatValue = formatValue?.includes('$') ? '$' : formatValue
|
||||
|
||||
if (
|
||||
formatValue !== undefined &&
|
||||
!resVars[key].format.includes(formatValue)
|
||||
) {
|
||||
assertionRes = false
|
||||
}
|
||||
})
|
||||
|
||||
// Here we will compare the response values with values we send
|
||||
const resValues = res[testTable]
|
||||
|
||||
testTableWithSpecialNumericLowercase[testTable].forEach(
|
||||
(row: { [key: string]: any }, i: number) =>
|
||||
Object.keys(row).forEach((col: string) => {
|
||||
if (col === 'specialnumeric') {
|
||||
if (
|
||||
resValues[i][col.toUpperCase()] !== row[col].toUpperCase()
|
||||
) {
|
||||
assertionRes = false
|
||||
}
|
||||
} else {
|
||||
if (resValues[i][col.toUpperCase()] !== row[col]) {
|
||||
assertionRes = false
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
return assertionRes
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Special missing values (ONE ROW) useComputeApi undefined',
|
||||
description:
|
||||
'Should support special missing values, when one row is send (On VIYA Web Approach)',
|
||||
test: () => {
|
||||
return adapter.request(
|
||||
'common/sendObj',
|
||||
testTableWithSpecialNumericOneRow,
|
||||
{ useComputeApi: undefined }
|
||||
)
|
||||
},
|
||||
assertion: (res: any) => {
|
||||
let assertionRes = true
|
||||
|
||||
// We receive formats in response. We compare it with formats that we included in request to make sure they are equal
|
||||
const resVars = res[`$${testTable}`].vars
|
||||
|
||||
Object.keys(resVars).forEach((key: any, i: number) => {
|
||||
let formatValue =
|
||||
testTableWithSpecialNumeric[`$${testTable}`].formats[
|
||||
key.toLowerCase()
|
||||
]
|
||||
// If it is char, we change it to $ to be compatible for comparsion
|
||||
// If it is number, it will already be compatible to comapre (best.)
|
||||
formatValue = formatValue?.includes('$') ? '$' : formatValue
|
||||
|
||||
if (
|
||||
formatValue !== undefined &&
|
||||
!resVars[key].format.includes(formatValue)
|
||||
) {
|
||||
assertionRes = false
|
||||
}
|
||||
})
|
||||
|
||||
// Here we will compare the response values with values we send
|
||||
const resValues = res[testTable]
|
||||
|
||||
testTableWithSpecialNumericOneRow[testTable].forEach(
|
||||
(row: { [key: string]: any }, i: number) =>
|
||||
Object.keys(row).forEach((col: string) => {
|
||||
if (resValues[i][col.toUpperCase()] !== row[col]) {
|
||||
assertionRes = false
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
return assertionRes
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
})
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
export const assert = (
|
||||
expression: boolean | (() => boolean),
|
||||
message = "Assertion failed"
|
||||
message = 'Assertion failed'
|
||||
) => {
|
||||
let result;
|
||||
let result
|
||||
try {
|
||||
if (typeof expression === "boolean") {
|
||||
result = expression;
|
||||
if (typeof expression === 'boolean') {
|
||||
result = expression
|
||||
} else {
|
||||
result = expression();
|
||||
result = expression()
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(message);
|
||||
throw new Error(message);
|
||||
} catch (e: any) {
|
||||
console.error(message)
|
||||
throw new Error(message)
|
||||
}
|
||||
if (!!result) {
|
||||
return;
|
||||
return
|
||||
} else {
|
||||
console.error(message);
|
||||
throw new Error(message);
|
||||
console.error(message)
|
||||
throw new Error(message)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react"
|
||||
"jsx": "react-jsx",
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
|
||||
449
src/ContextManager.ts
Normal file
449
src/ContextManager.ts
Normal file
@@ -0,0 +1,449 @@
|
||||
import { Context, EditContextInput, ContextAllAttributes } from './types'
|
||||
import { isUrl } from './utils'
|
||||
import { prefixMessage } from '@sasjs/utils/error'
|
||||
import { RequestClient } from './request/RequestClient'
|
||||
import { AuthConfig } from '@sasjs/utils/types'
|
||||
|
||||
export class ContextManager {
|
||||
private defaultComputeContexts = [
|
||||
'CAS Formats service compute context',
|
||||
'Data Mining compute context',
|
||||
'Import 9 service compute context',
|
||||
'SAS Job Execution compute context',
|
||||
'SAS Model Manager compute context',
|
||||
'SAS Studio compute context',
|
||||
'SAS Visual Forecasting compute context'
|
||||
]
|
||||
private defaultLauncherContexts = [
|
||||
'CAS Formats service launcher context',
|
||||
'Data Mining launcher context',
|
||||
'Import 9 service launcher context',
|
||||
'Job Flow Execution launcher context',
|
||||
'SAS Job Execution launcher context',
|
||||
'SAS Model Manager launcher context',
|
||||
'SAS Studio launcher context',
|
||||
'SAS Visual Forecasting launcher context'
|
||||
]
|
||||
|
||||
get getDefaultComputeContexts() {
|
||||
return this.defaultComputeContexts
|
||||
}
|
||||
get getDefaultLauncherContexts() {
|
||||
return this.defaultLauncherContexts
|
||||
}
|
||||
|
||||
constructor(private serverUrl: string, private requestClient: RequestClient) {
|
||||
if (serverUrl) isUrl(serverUrl)
|
||||
}
|
||||
|
||||
public async getComputeContexts(accessToken?: string) {
|
||||
const { result: contexts } = await this.requestClient
|
||||
.get<{ items: Context[] }>(
|
||||
`${this.serverUrl}/compute/contexts?limit=10000`,
|
||||
accessToken
|
||||
)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while getting compute contexts. ')
|
||||
})
|
||||
|
||||
const contextsList = contexts && contexts.items ? contexts.items : []
|
||||
|
||||
return contextsList.map((context: any) => ({
|
||||
createdBy: context.createdBy,
|
||||
id: context.id,
|
||||
name: context.name,
|
||||
version: context.version,
|
||||
attributes: {}
|
||||
}))
|
||||
}
|
||||
|
||||
public async getLauncherContexts(accessToken?: string) {
|
||||
const { result: contexts } = await this.requestClient
|
||||
.get<{ items: Context[] }>(
|
||||
`${this.serverUrl}/launcher/contexts?limit=10000`,
|
||||
accessToken
|
||||
)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while getting launcher contexts. ')
|
||||
})
|
||||
|
||||
const contextsList = contexts && contexts.items ? contexts.items : []
|
||||
|
||||
return contextsList.map((context: any) => ({
|
||||
createdBy: context.createdBy,
|
||||
id: context.id,
|
||||
name: context.name,
|
||||
version: context.version,
|
||||
attributes: {}
|
||||
}))
|
||||
}
|
||||
|
||||
public async createComputeContext(
|
||||
contextName: string,
|
||||
launchContextName: string,
|
||||
sharedAccountId: string,
|
||||
autoExecLines: string[],
|
||||
accessToken?: string,
|
||||
authorizedUsers?: string[]
|
||||
) {
|
||||
this.validateContextName(contextName)
|
||||
|
||||
this.isDefaultContext(
|
||||
contextName,
|
||||
this.defaultComputeContexts,
|
||||
`Compute context '${contextName}' already exists.`
|
||||
)
|
||||
|
||||
const existingComputeContexts = await this.getComputeContexts(accessToken)
|
||||
|
||||
if (
|
||||
existingComputeContexts.find((context) => context.name === contextName)
|
||||
) {
|
||||
throw new Error(`Compute context '${contextName}' already exists.`)
|
||||
}
|
||||
|
||||
if (launchContextName) {
|
||||
if (!this.defaultLauncherContexts.includes(launchContextName)) {
|
||||
const launcherContexts = await this.getLauncherContexts(accessToken)
|
||||
|
||||
if (
|
||||
!launcherContexts.find(
|
||||
(context) => context.name === launchContextName
|
||||
)
|
||||
) {
|
||||
const description = `The launcher context for ${launchContextName}`
|
||||
const launchType = 'direct'
|
||||
|
||||
const newLauncherContext = await this.createLauncherContext(
|
||||
launchContextName,
|
||||
description,
|
||||
launchType,
|
||||
accessToken
|
||||
).catch((err) => {
|
||||
throw new Error(`Error while creating launcher context. ${err}`)
|
||||
})
|
||||
|
||||
if (newLauncherContext && newLauncherContext.name) {
|
||||
launchContextName = newLauncherContext.name
|
||||
} else {
|
||||
throw new Error('Error while creating launcher context.')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const headers: any = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
if (accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`
|
||||
}
|
||||
|
||||
let attributes = { reuseServerProcesses: true } as object
|
||||
|
||||
if (sharedAccountId)
|
||||
attributes = { ...attributes, runServerAs: sharedAccountId }
|
||||
|
||||
const requestBody: any = {
|
||||
name: contextName,
|
||||
launchContext: {
|
||||
contextName: launchContextName || ''
|
||||
},
|
||||
attributes
|
||||
}
|
||||
|
||||
if (authorizedUsers && authorizedUsers.length) {
|
||||
requestBody['authorizedUsers'] = authorizedUsers
|
||||
} else {
|
||||
requestBody['authorizeAllAuthenticatedUsers'] = true
|
||||
}
|
||||
|
||||
if (autoExecLines) {
|
||||
requestBody.environment = { autoExecLines }
|
||||
}
|
||||
|
||||
const { result: context } = await this.requestClient
|
||||
.post<Context>(
|
||||
`${this.serverUrl}/compute/contexts`,
|
||||
requestBody,
|
||||
accessToken
|
||||
)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while creating compute context. ')
|
||||
})
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
public async createLauncherContext(
|
||||
contextName: string,
|
||||
description: string,
|
||||
launchType = 'direct',
|
||||
accessToken?: string
|
||||
) {
|
||||
if (!contextName) {
|
||||
throw new Error('Context name is required.')
|
||||
}
|
||||
|
||||
this.isDefaultContext(
|
||||
contextName,
|
||||
this.defaultLauncherContexts,
|
||||
`Launcher context '${contextName}' already exists.`
|
||||
)
|
||||
|
||||
const existingLauncherContexts = await this.getLauncherContexts(accessToken)
|
||||
|
||||
if (
|
||||
existingLauncherContexts.find((context) => context.name === contextName)
|
||||
) {
|
||||
throw new Error(`Launcher context '${contextName}' already exists.`)
|
||||
}
|
||||
|
||||
const headers: any = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
if (accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`
|
||||
}
|
||||
|
||||
const requestBody: any = {
|
||||
name: contextName,
|
||||
description: description,
|
||||
launchType
|
||||
}
|
||||
|
||||
const { result: context } = await this.requestClient
|
||||
.post<Context>(
|
||||
`${this.serverUrl}/launcher/contexts`,
|
||||
requestBody,
|
||||
accessToken
|
||||
)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while creating launcher context. ')
|
||||
})
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
public async editComputeContext(
|
||||
contextName: string,
|
||||
editedContext: EditContextInput,
|
||||
accessToken?: string
|
||||
) {
|
||||
this.validateContextName(contextName)
|
||||
|
||||
this.isDefaultContext(
|
||||
contextName,
|
||||
this.defaultComputeContexts,
|
||||
'Editing default SAS compute contexts is not allowed.',
|
||||
true
|
||||
)
|
||||
|
||||
let originalContext
|
||||
|
||||
originalContext = await this.getComputeContextByName(
|
||||
contextName,
|
||||
accessToken
|
||||
)
|
||||
|
||||
// Try to find context by id, when context name has been changed.
|
||||
if (!originalContext) {
|
||||
originalContext = await this.getComputeContextById(
|
||||
editedContext.id!,
|
||||
accessToken
|
||||
)
|
||||
}
|
||||
|
||||
const { result: context, etag } = await this.requestClient
|
||||
.get<Context>(
|
||||
`${this.serverUrl}/compute/contexts/${originalContext.id}`,
|
||||
accessToken
|
||||
)
|
||||
.catch((err) => {
|
||||
if (err && err.status === 404) {
|
||||
throw new Error(
|
||||
`The context '${contextName}' was not found on this server.`
|
||||
)
|
||||
}
|
||||
|
||||
throw err
|
||||
})
|
||||
|
||||
// An If-Match header with the value of the last ETag for the context
|
||||
// is required to be able to update it
|
||||
// https://developer.sas.com/apis/rest/Compute/#update-a-context-definition
|
||||
return await this.requestClient.put<Context>(
|
||||
`/compute/contexts/${context.id}`,
|
||||
{
|
||||
...context,
|
||||
...editedContext,
|
||||
attributes: { ...context.attributes, ...editedContext.attributes }
|
||||
},
|
||||
accessToken,
|
||||
{ 'If-Match': etag }
|
||||
)
|
||||
}
|
||||
|
||||
public async getComputeContextByName(
|
||||
contextName: string,
|
||||
accessToken?: string
|
||||
): Promise<Context> {
|
||||
const { result: contexts } = await this.requestClient
|
||||
.get<{ items: Context[] }>(
|
||||
`${this.serverUrl}/compute/contexts?filter=eq(name, "${contextName}")`,
|
||||
accessToken
|
||||
)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(
|
||||
err,
|
||||
'Error while getting compute context by name. '
|
||||
)
|
||||
})
|
||||
|
||||
if (!contexts || !(contexts.items && contexts.items.length)) {
|
||||
throw new Error(
|
||||
`The context '${contextName}' was not found at '${this.serverUrl}'.`
|
||||
)
|
||||
}
|
||||
|
||||
return contexts.items[0]
|
||||
}
|
||||
|
||||
public async getComputeContextById(
|
||||
contextId: string,
|
||||
accessToken?: string
|
||||
): Promise<ContextAllAttributes> {
|
||||
const { result: context } = await this.requestClient
|
||||
.get<ContextAllAttributes>(
|
||||
`${this.serverUrl}/compute/contexts/${contextId}`,
|
||||
accessToken
|
||||
)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while getting compute context by id. ')
|
||||
})
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
public async getExecutableContexts(
|
||||
executeScript: Function,
|
||||
authConfig?: AuthConfig
|
||||
) {
|
||||
const { result: contexts } = await this.requestClient
|
||||
.get<{ items: Context[] }>(
|
||||
`${this.serverUrl}/compute/contexts?limit=10000`,
|
||||
authConfig?.access_token
|
||||
)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while fetching compute contexts.')
|
||||
})
|
||||
|
||||
const contextsList = contexts.items || []
|
||||
const executableContexts: any[] = []
|
||||
|
||||
const promises = contextsList.map((context: any) => {
|
||||
const linesOfCode = ['%put &=sysuserid;']
|
||||
|
||||
return () =>
|
||||
executeScript(
|
||||
`test-${context.name}`,
|
||||
linesOfCode,
|
||||
context.name,
|
||||
authConfig,
|
||||
null,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
).catch((err: any) => err)
|
||||
})
|
||||
|
||||
let results: any[] = []
|
||||
|
||||
for (const promise of promises) results.push(await promise())
|
||||
|
||||
results.forEach((result: any, index: number) => {
|
||||
if (result && result.log) {
|
||||
try {
|
||||
const resultParsed = result.log
|
||||
let sysUserId = ''
|
||||
|
||||
const sysUserIdLog = resultParsed
|
||||
.split('\n')
|
||||
.find((line: string) => line.startsWith('SYSUSERID='))
|
||||
|
||||
if (sysUserIdLog) {
|
||||
sysUserId = sysUserIdLog.replace('SYSUSERID=', '')
|
||||
|
||||
executableContexts.push({
|
||||
createdBy: contextsList[index].createdBy,
|
||||
id: contextsList[index].id,
|
||||
name: contextsList[index].name,
|
||||
version: contextsList[index].version,
|
||||
attributes: {
|
||||
sysUserId
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return executableContexts
|
||||
}
|
||||
|
||||
public async deleteComputeContext(contextName: string, accessToken?: string) {
|
||||
this.validateContextName(contextName)
|
||||
|
||||
this.isDefaultContext(
|
||||
contextName,
|
||||
this.defaultComputeContexts,
|
||||
'Deleting default SAS compute contexts is not allowed.',
|
||||
true
|
||||
)
|
||||
|
||||
const headers: any = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
if (accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`
|
||||
}
|
||||
|
||||
const context = await this.getComputeContextByName(contextName, accessToken)
|
||||
|
||||
return await this.requestClient.delete<Context>(
|
||||
`${this.serverUrl}/compute/contexts/${context.id}`,
|
||||
accessToken
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: implement editLauncherContext method
|
||||
|
||||
// TODO: implement deleteLauncherContext method
|
||||
|
||||
private validateContextName(name: string) {
|
||||
if (!name) throw new Error('Context name is required.')
|
||||
}
|
||||
|
||||
public isDefaultContext(
|
||||
context: string,
|
||||
defaultContexts: string[] = this.defaultComputeContexts,
|
||||
errorMessage = '',
|
||||
listDefaults = false
|
||||
) {
|
||||
if (defaultContexts.includes(context)) {
|
||||
throw new Error(
|
||||
`${errorMessage}${
|
||||
listDefaults
|
||||
? '\nDefault contexts:' +
|
||||
defaultContexts.map((context, i) => `\n${i + 1}. ${context}`)
|
||||
: ''
|
||||
}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
import { isLogInRequired, needsRetry, isUrl } from './utils'
|
||||
import { CsrfToken } from './types/CsrfToken'
|
||||
import { UploadFile } from './types/UploadFile'
|
||||
|
||||
const requestRetryLimit = 5
|
||||
|
||||
export class FileUploader {
|
||||
constructor(
|
||||
private appLoc: string,
|
||||
private serverUrl: string,
|
||||
private jobsPath: string,
|
||||
private setCsrfTokenWeb: any,
|
||||
private csrfToken: CsrfToken | null = null
|
||||
) {
|
||||
if (serverUrl) isUrl(serverUrl)
|
||||
}
|
||||
|
||||
private retryCount = 0
|
||||
|
||||
public uploadFile(sasJob: string, files: UploadFile[], params: any) {
|
||||
if (files?.length < 1)
|
||||
throw new Error('At least one file must be provided.')
|
||||
|
||||
let paramsString = ''
|
||||
|
||||
for (let param in params) {
|
||||
if (params.hasOwnProperty(param)) {
|
||||
paramsString += `&${param}=${params[param]}`
|
||||
}
|
||||
}
|
||||
|
||||
const program = this.appLoc
|
||||
? this.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '')
|
||||
: sasJob
|
||||
const uploadUrl = `${this.serverUrl}${this.jobsPath}/?${
|
||||
'_program=' + program
|
||||
}${paramsString}`
|
||||
|
||||
const headers = {
|
||||
'cache-control': 'no-cache'
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const formData = new FormData()
|
||||
|
||||
for (let file of files) {
|
||||
formData.append('file', file.file, file.fileName)
|
||||
}
|
||||
|
||||
if (this.csrfToken) formData.append('_csrf', this.csrfToken.value)
|
||||
|
||||
fetch(uploadUrl, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
referrerPolicy: 'same-origin',
|
||||
headers
|
||||
})
|
||||
.then(async (response) => {
|
||||
if (!response.ok) {
|
||||
if (response.status === 403) {
|
||||
const tokenHeader = response.headers.get('X-CSRF-HEADER')
|
||||
|
||||
if (tokenHeader) {
|
||||
const token = response.headers.get(tokenHeader)
|
||||
this.csrfToken = {
|
||||
headerName: tokenHeader,
|
||||
value: token || ''
|
||||
}
|
||||
|
||||
this.setCsrfTokenWeb(this.csrfToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return response.text()
|
||||
})
|
||||
.then((responseText) => {
|
||||
if (isLogInRequired(responseText))
|
||||
reject('You must be logged in to upload a file')
|
||||
|
||||
if (needsRetry(responseText)) {
|
||||
if (this.retryCount < requestRetryLimit) {
|
||||
this.retryCount++
|
||||
this.uploadFile(sasJob, files, params).then(
|
||||
(res: any) => resolve(res),
|
||||
(err: any) => reject(err)
|
||||
)
|
||||
} else {
|
||||
this.retryCount = 0
|
||||
reject(responseText)
|
||||
}
|
||||
} else {
|
||||
this.retryCount = 0
|
||||
|
||||
try {
|
||||
resolve(JSON.parse(responseText))
|
||||
} catch (e) {
|
||||
reject(e)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
import * as https from 'https'
|
||||
import { generateTimestamp } from '@sasjs/utils/time'
|
||||
import * as NodeFormData from 'form-data'
|
||||
import { Sas9RequestClient } from './request/Sas9RequestClient'
|
||||
import { isUrl } from './utils'
|
||||
|
||||
/**
|
||||
@@ -5,8 +9,15 @@ import { isUrl } from './utils'
|
||||
*
|
||||
*/
|
||||
export class SAS9ApiClient {
|
||||
constructor(private serverUrl: string) {
|
||||
private requestClient: Sas9RequestClient
|
||||
|
||||
constructor(
|
||||
private serverUrl: string,
|
||||
private jobsPath: string,
|
||||
httpsAgentOptions?: https.AgentOptions
|
||||
) {
|
||||
if (serverUrl) isUrl(serverUrl)
|
||||
this.requestClient = new Sas9RequestClient(serverUrl, httpsAgentOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -29,27 +40,61 @@ export class SAS9ApiClient {
|
||||
/**
|
||||
* Executes code on a SAS9 server.
|
||||
* @param linesOfCode - an array of code lines to execute.
|
||||
* @param serverName - the server to execute the code on.
|
||||
* @param repositoryName - the repository to execute the code in.
|
||||
* @param userName - the user name to log into the current SAS server.
|
||||
* @param password - the password to log into the current SAS server.
|
||||
*/
|
||||
public async executeScript(
|
||||
linesOfCode: string[],
|
||||
serverName: string,
|
||||
repositoryName: string
|
||||
userName: string,
|
||||
password: string
|
||||
) {
|
||||
const requestPayload = linesOfCode.join('\n')
|
||||
const executeScriptRequest = {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
},
|
||||
body: `command=${requestPayload}`
|
||||
}
|
||||
const executeScriptResponse = await fetch(
|
||||
`${this.serverUrl}/sas/servers/${serverName}/cmd?repositoryName=${repositoryName}`,
|
||||
executeScriptRequest
|
||||
).then((res) => res.text())
|
||||
await this.requestClient.login(userName, password, this.jobsPath)
|
||||
|
||||
return executeScriptResponse
|
||||
// This piece of code forces a webout to prevent Stored Process Errors.
|
||||
const forceOutputCode = [
|
||||
'data _null_;',
|
||||
'file _webout;',
|
||||
`put 'Executed sasjs run';`,
|
||||
'run;'
|
||||
]
|
||||
const formData = generateFileUploadForm(
|
||||
[...linesOfCode, ...forceOutputCode].join('\n')
|
||||
)
|
||||
|
||||
const codeInjectorPath = `/User Folders/${userName}/My Folder/sasjs/runner`
|
||||
const contentType =
|
||||
'multipart/form-data; boundary=' + formData.getBoundary()
|
||||
const contentLength = formData.getLengthSync()
|
||||
|
||||
const headers = {
|
||||
'cache-control': 'no-cache',
|
||||
Accept: '*/*',
|
||||
'Content-Type': contentType,
|
||||
'Content-Length': contentLength,
|
||||
Connection: 'keep-alive'
|
||||
}
|
||||
const storedProcessUrl = `${this.jobsPath}/?${
|
||||
'_program=' + codeInjectorPath + '&_debug=log'
|
||||
}`
|
||||
const response = await this.requestClient.post(
|
||||
storedProcessUrl,
|
||||
formData,
|
||||
undefined,
|
||||
contentType,
|
||||
headers
|
||||
)
|
||||
|
||||
return response.result as string
|
||||
}
|
||||
}
|
||||
|
||||
const generateFileUploadForm = (data: any): NodeFormData => {
|
||||
const formData = new NodeFormData()
|
||||
const filename = `sasjs-execute-sas9-${generateTimestamp('')}.sas`
|
||||
formData.append(filename, data, {
|
||||
filename,
|
||||
contentType: 'text/plain'
|
||||
})
|
||||
|
||||
return formData
|
||||
}
|
||||
|
||||
51
src/SASViyaApiClient.spec.ts
Normal file
51
src/SASViyaApiClient.spec.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Logger, LogLevel } from '@sasjs/utils/logger'
|
||||
import { RequestClient } from './request/RequestClient'
|
||||
import { SASViyaApiClient } from './SASViyaApiClient'
|
||||
import { Folder } from './types'
|
||||
import { RootFolderNotFoundError } from './types/errors'
|
||||
|
||||
const mockFolder: Folder = {
|
||||
id: '1',
|
||||
uri: '/folder',
|
||||
links: [],
|
||||
memberCount: 1
|
||||
}
|
||||
|
||||
const requestClient = new (<jest.Mock<RequestClient>>RequestClient)()
|
||||
const sasViyaApiClient = new SASViyaApiClient(
|
||||
'https://test.com',
|
||||
'/test',
|
||||
'test context',
|
||||
requestClient
|
||||
)
|
||||
|
||||
describe('SASViyaApiClient', () => {
|
||||
beforeEach(() => {
|
||||
;(process as any).logger = new Logger(LogLevel.Off)
|
||||
setupMocks()
|
||||
})
|
||||
|
||||
it('should throw an error when the root folder is not found on the server', async () => {
|
||||
jest
|
||||
.spyOn(requestClient, 'get')
|
||||
.mockImplementation(() => Promise.reject('Not Found'))
|
||||
const error = await sasViyaApiClient
|
||||
.createFolder('test', '/foo')
|
||||
.catch((e: any) => e)
|
||||
expect(error).toBeInstanceOf(RootFolderNotFoundError)
|
||||
})
|
||||
})
|
||||
|
||||
const setupMocks = () => {
|
||||
jest
|
||||
.spyOn(requestClient, 'get')
|
||||
.mockImplementation(() =>
|
||||
Promise.resolve({ result: mockFolder, etag: '', status: 200 })
|
||||
)
|
||||
|
||||
jest
|
||||
.spyOn(requestClient, 'post')
|
||||
.mockImplementation(() =>
|
||||
Promise.resolve({ result: mockFolder, etag: '', status: 200 })
|
||||
)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
1583
src/SASjs.ts
1583
src/SASjs.ts
File diff suppressed because it is too large
Load Diff
160
src/SASjsApiClient.ts
Normal file
160
src/SASjsApiClient.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import * as NodeFormData from 'form-data'
|
||||
import { createReadStream } from '@sasjs/utils/file'
|
||||
import { AuthConfig, ServerType, ServicePackSASjs } from '@sasjs/utils/types'
|
||||
import { ExecutionQuery } from './types'
|
||||
import { RequestClient } from './request/RequestClient'
|
||||
import { getAccessTokenForSasjs } from './auth/getAccessTokenForSasjs'
|
||||
import { refreshTokensForSasjs } from './auth/refreshTokensForSasjs'
|
||||
import { getTokens } from './auth/getTokens'
|
||||
|
||||
export class SASjsApiClient {
|
||||
constructor(private requestClient: RequestClient) {}
|
||||
|
||||
private async getAccessTokenForRequest(authConfig?: AuthConfig) {
|
||||
if (authConfig) {
|
||||
const { access_token } = await getTokens(
|
||||
this.requestClient,
|
||||
authConfig,
|
||||
ServerType.Sasjs
|
||||
)
|
||||
|
||||
return access_token
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the folders and services at the given location `appLoc` on the given server `serverUrl`.
|
||||
* @param dataJson - the JSON specifying the folders and files to be created, can also includes
|
||||
* appLoc, streamServiceName, streamWebFolder, streamLogo
|
||||
* @param appLoc - the base folder in which to create the new folders and services.
|
||||
* @param authConfig - (optional) a valid client, secret, refresh and access tokens that are authorised to execute compute jobs.
|
||||
*/
|
||||
public async deploy(
|
||||
dataJson: ServicePackSASjs,
|
||||
appLoc: string,
|
||||
authConfig?: AuthConfig
|
||||
) {
|
||||
const access_token = await this.getAccessTokenForRequest(authConfig)
|
||||
dataJson.appLoc = dataJson.appLoc || appLoc
|
||||
|
||||
const { result } = await this.requestClient.post<{
|
||||
status: string
|
||||
message: string
|
||||
streamServiceName?: string
|
||||
example?: {}
|
||||
}>(
|
||||
'SASjsApi/drive/deploy',
|
||||
dataJson,
|
||||
access_token,
|
||||
undefined,
|
||||
{},
|
||||
{ maxContentLength: Infinity, maxBodyLength: Infinity }
|
||||
)
|
||||
|
||||
return Promise.resolve(result)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates/updates files within SASjs drive using uploaded json compressed file.
|
||||
* @param zipFilePath - Compressed file path; 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!
|
||||
* @param authConfig - (optional) a valid client, secret, refresh and access tokens that are authorised to execute compute jobs.
|
||||
*/
|
||||
public async deployZipFile(zipFilePath: string, authConfig?: AuthConfig) {
|
||||
const access_token = await this.getAccessTokenForRequest(authConfig)
|
||||
|
||||
const file = await createReadStream(zipFilePath)
|
||||
const formData = new NodeFormData()
|
||||
formData.append('file', file)
|
||||
|
||||
const contentType = `multipart/form-data; boundary=${formData.getBoundary()}`
|
||||
|
||||
const { result } = await this.requestClient.post<{
|
||||
status: string
|
||||
message: string
|
||||
streamServiceName?: string
|
||||
example?: {}
|
||||
}>(
|
||||
'SASjsApi/drive/deploy/upload',
|
||||
formData,
|
||||
access_token,
|
||||
contentType,
|
||||
{},
|
||||
{ maxContentLength: Infinity, maxBodyLength: Infinity }
|
||||
)
|
||||
|
||||
return Promise.resolve(result)
|
||||
}
|
||||
|
||||
public async executeJob(
|
||||
query: ExecutionQuery,
|
||||
appLoc: string,
|
||||
authConfig?: AuthConfig
|
||||
) {
|
||||
const access_token = authConfig ? authConfig.access_token : undefined
|
||||
|
||||
let _program
|
||||
if (query._program.startsWith('/')) {
|
||||
_program = query._program
|
||||
} else _program = `${appLoc}/${query._program}`
|
||||
|
||||
const response: any = await this.requestClient.post(
|
||||
'SASjsApi/stp/execute',
|
||||
{ _debug: 131, ...query, _program },
|
||||
access_token
|
||||
)
|
||||
|
||||
return { result: response.result, log: response.log }
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes code on a SASJS server.
|
||||
* @param code - a string of code to execute.
|
||||
* @param runTime - a string to representing runTime for code execution
|
||||
* @param authConfig - an object for authentication.
|
||||
*/
|
||||
public async executeScript(
|
||||
code: string,
|
||||
runTime: string = 'sas',
|
||||
authConfig?: AuthConfig
|
||||
) {
|
||||
const access_token = await this.getAccessTokenForRequest(authConfig)
|
||||
|
||||
let parsedSasjsServerLog = ''
|
||||
|
||||
await this.requestClient
|
||||
.post('SASjsApi/code/execute', { code, runTime }, access_token)
|
||||
.then((res: any) => {
|
||||
if (res.log) parsedSasjsServerLog = res.log
|
||||
})
|
||||
|
||||
return parsedSasjsServerLog
|
||||
}
|
||||
|
||||
/**
|
||||
* Exchanges the auth code for an access token for the given client.
|
||||
* @param clientId - the client ID to authenticate with.
|
||||
* @param authCode - the auth code received from the server.
|
||||
*/
|
||||
public async getAccessToken(
|
||||
clientId: string,
|
||||
authCode: string
|
||||
): Promise<SASjsAuthResponse> {
|
||||
return getAccessTokenForSasjs(this.requestClient, clientId, authCode)
|
||||
}
|
||||
|
||||
/**
|
||||
* Exchanges the refresh token for an access token.
|
||||
* @param refreshToken - the refresh token received from the server.
|
||||
*/
|
||||
public async refreshTokens(refreshToken: string): Promise<SASjsAuthResponse> {
|
||||
return refreshTokensForSasjs(this.requestClient, refreshToken)
|
||||
}
|
||||
}
|
||||
|
||||
// todo move to sasjs/utils
|
||||
export interface SASjsAuthResponse {
|
||||
access_token: string
|
||||
refresh_token: string
|
||||
}
|
||||
@@ -1,86 +1,121 @@
|
||||
import { Session, Context, CsrfToken } from './types'
|
||||
import { asyncForEach, makeRequest, isUrl } from './utils'
|
||||
import { Session, Context, SessionVariable } from './types'
|
||||
import { NoSessionStateError } from './types/errors'
|
||||
import { asyncForEach, isUrl } from './utils'
|
||||
import { prefixMessage } from '@sasjs/utils/error'
|
||||
import { RequestClient } from './request/RequestClient'
|
||||
|
||||
const MAX_SESSION_COUNT = 1
|
||||
|
||||
export class SessionManager {
|
||||
private loggedErrors: NoSessionStateError[] = []
|
||||
|
||||
constructor(
|
||||
private serverUrl: string,
|
||||
private contextName: string,
|
||||
private setCsrfToken: (csrfToken: CsrfToken) => void
|
||||
private requestClient: RequestClient
|
||||
) {
|
||||
if (serverUrl) isUrl(serverUrl)
|
||||
}
|
||||
|
||||
private sessions: Session[] = []
|
||||
private currentContext: Context | null = null
|
||||
private csrfToken: CsrfToken | null = null
|
||||
private _debug: boolean = false
|
||||
private printedSessionState = {
|
||||
printed: false,
|
||||
state: ''
|
||||
}
|
||||
|
||||
public get debug() {
|
||||
return this._debug
|
||||
}
|
||||
|
||||
public set debug(value: boolean) {
|
||||
this._debug = value
|
||||
}
|
||||
|
||||
async getSession(accessToken?: string) {
|
||||
await this.createSessions(accessToken)
|
||||
this.createAndWaitForSession(accessToken)
|
||||
await this.createAndWaitForSession(accessToken)
|
||||
const session = this.sessions.pop()
|
||||
const secondsSinceSessionCreation =
|
||||
(new Date().getTime() - new Date(session!.creationTimeStamp).getTime()) /
|
||||
1000
|
||||
|
||||
if (
|
||||
!session!.attributes ||
|
||||
secondsSinceSessionCreation >= session!.attributes.sessionInactiveTimeout
|
||||
) {
|
||||
await this.createSessions(accessToken)
|
||||
const freshSession = this.sessions.pop()
|
||||
|
||||
return freshSession
|
||||
}
|
||||
|
||||
return session
|
||||
}
|
||||
|
||||
async clearSession(id: string, accessToken?: string) {
|
||||
const deleteSessionRequest = {
|
||||
method: 'DELETE',
|
||||
headers: this.getHeaders(accessToken)
|
||||
}
|
||||
return await this.request<Session>(
|
||||
`${this.serverUrl}/compute/sessions/${id}`,
|
||||
deleteSessionRequest
|
||||
).then(() => {
|
||||
this.sessions = this.sessions.filter((s) => s.id !== id)
|
||||
})
|
||||
return await this.requestClient
|
||||
.delete<Session>(`/compute/sessions/${id}`, accessToken)
|
||||
.then(() => {
|
||||
this.sessions = this.sessions.filter((s) => s.id !== id)
|
||||
})
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while deleting session. ')
|
||||
})
|
||||
}
|
||||
|
||||
private async createSessions(accessToken?: string) {
|
||||
if (!this.sessions.length) {
|
||||
if (!this.currentContext) {
|
||||
await this.setCurrentContext(accessToken)
|
||||
await this.setCurrentContext(accessToken).catch((err) => {
|
||||
throw err
|
||||
})
|
||||
}
|
||||
|
||||
await asyncForEach(new Array(MAX_SESSION_COUNT), async () => {
|
||||
const createdSession = await this.createAndWaitForSession(accessToken)
|
||||
const createdSession = await this.createAndWaitForSession(
|
||||
accessToken
|
||||
).catch((err) => {
|
||||
throw err
|
||||
})
|
||||
|
||||
this.sessions.push(createdSession)
|
||||
}).catch((err) => {
|
||||
throw err
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private async createAndWaitForSession(accessToken?: string) {
|
||||
const createSessionRequest = {
|
||||
method: 'POST',
|
||||
headers: this.getHeaders(accessToken)
|
||||
}
|
||||
const { result: createdSession, etag } = await this.request<Session>(
|
||||
`${this.serverUrl}/compute/contexts/${this.currentContext!.id}/sessions`,
|
||||
createSessionRequest
|
||||
)
|
||||
const { result: createdSession, etag } = await this.requestClient
|
||||
.post<Session>(
|
||||
`${this.serverUrl}/compute/contexts/${
|
||||
this.currentContext!.id
|
||||
}/sessions`,
|
||||
{},
|
||||
accessToken
|
||||
)
|
||||
.catch((err) => {
|
||||
throw err
|
||||
})
|
||||
|
||||
await this.waitForSession(createdSession, etag, accessToken)
|
||||
|
||||
this.sessions.push(createdSession)
|
||||
|
||||
return createdSession
|
||||
}
|
||||
|
||||
private async setCurrentContext(accessToken?: string) {
|
||||
if (!this.currentContext) {
|
||||
const { result: contexts } = await this.request<{
|
||||
items: Context[]
|
||||
}>(`${this.serverUrl}/compute/contexts`, {
|
||||
headers: this.getHeaders(accessToken)
|
||||
})
|
||||
const { result: contexts } = await this.requestClient
|
||||
.get<{
|
||||
items: Context[]
|
||||
}>(`${this.serverUrl}/compute/contexts?limit=10000`, accessToken)
|
||||
.catch((err) => {
|
||||
throw err
|
||||
})
|
||||
|
||||
const contextsList =
|
||||
contexts && contexts.items && contexts.items.length
|
||||
@@ -98,6 +133,8 @@ export class SessionManager {
|
||||
}
|
||||
|
||||
this.currentContext = currentContext
|
||||
|
||||
Promise.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +142,7 @@ export class SessionManager {
|
||||
const headers: any = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
if (accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`
|
||||
}
|
||||
@@ -115,60 +153,105 @@ export class SessionManager {
|
||||
private async waitForSession(
|
||||
session: Session,
|
||||
etag: string | null,
|
||||
accessToken?: string,
|
||||
silent = false
|
||||
) {
|
||||
accessToken?: string
|
||||
): Promise<string> {
|
||||
const logger = process.logger || console
|
||||
|
||||
let sessionState = session.state
|
||||
const headers: any = {
|
||||
...this.getHeaders(accessToken),
|
||||
'If-None-Match': etag
|
||||
}
|
||||
|
||||
const stateLink = session.links.find((l: any) => l.rel === 'state')
|
||||
return new Promise(async (resolve, _) => {
|
||||
if (sessionState === 'pending') {
|
||||
if (stateLink) {
|
||||
if (!silent) {
|
||||
console.log('Polling session status... \n') // ?
|
||||
}
|
||||
const { result: state } = await this.request<string>(
|
||||
|
||||
if (
|
||||
sessionState === 'pending' ||
|
||||
sessionState === 'running' ||
|
||||
sessionState === ''
|
||||
) {
|
||||
if (stateLink) {
|
||||
if (this.debug && !this.printedSessionState.printed) {
|
||||
logger.info(`Polling: ${this.serverUrl + stateLink.href}`)
|
||||
|
||||
this.printedSessionState.printed = true
|
||||
}
|
||||
|
||||
const { result: state, responseStatus: responseStatus } =
|
||||
await this.getSessionState(
|
||||
`${this.serverUrl}${stateLink.href}?wait=30`,
|
||||
{
|
||||
headers
|
||||
},
|
||||
'text'
|
||||
etag!,
|
||||
accessToken
|
||||
).catch((err) => {
|
||||
throw prefixMessage(err, 'Error while getting session state.')
|
||||
})
|
||||
|
||||
sessionState = state.trim()
|
||||
|
||||
if (this.debug && this.printedSessionState.state !== sessionState) {
|
||||
logger.info(`Current session state is '${sessionState}'`)
|
||||
|
||||
this.printedSessionState.state = sessionState
|
||||
this.printedSessionState.printed = false
|
||||
}
|
||||
|
||||
if (!sessionState) {
|
||||
const stateError = new NoSessionStateError(
|
||||
responseStatus,
|
||||
this.serverUrl + stateLink.href,
|
||||
session.links.find((l: any) => l.rel === 'log')?.href as string
|
||||
)
|
||||
|
||||
sessionState = state.trim()
|
||||
if (!silent) {
|
||||
console.log(`Current state is '${sessionState}'\n`)
|
||||
if (
|
||||
!this.loggedErrors.find(
|
||||
(err: NoSessionStateError) =>
|
||||
err.serverResponseStatus === stateError.serverResponseStatus
|
||||
)
|
||||
) {
|
||||
this.loggedErrors.push(stateError)
|
||||
|
||||
logger.info(stateError.message)
|
||||
}
|
||||
resolve(sessionState)
|
||||
|
||||
return await this.waitForSession(session, etag, accessToken)
|
||||
}
|
||||
|
||||
this.loggedErrors = []
|
||||
|
||||
return sessionState
|
||||
} else {
|
||||
resolve(sessionState)
|
||||
throw 'Error while getting session state link.'
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.loggedErrors = []
|
||||
|
||||
return sessionState
|
||||
}
|
||||
}
|
||||
|
||||
private async request<T>(
|
||||
private async getSessionState(
|
||||
url: string,
|
||||
options: RequestInit,
|
||||
contentType: 'text' | 'json' = 'json'
|
||||
etag: string,
|
||||
accessToken?: string
|
||||
) {
|
||||
if (this.csrfToken) {
|
||||
options.headers = {
|
||||
...options.headers,
|
||||
[this.csrfToken.headerName]: this.csrfToken.value
|
||||
}
|
||||
}
|
||||
return await makeRequest<T>(
|
||||
url,
|
||||
options,
|
||||
(token) => {
|
||||
this.csrfToken = token
|
||||
this.setCsrfToken(token)
|
||||
},
|
||||
contentType
|
||||
)
|
||||
return await this.requestClient
|
||||
.get(url, accessToken, 'text/plain', { 'If-None-Match': etag })
|
||||
.then((res) => ({
|
||||
result: res.result as string,
|
||||
responseStatus: res.status
|
||||
}))
|
||||
.catch((err) => {
|
||||
throw err
|
||||
})
|
||||
}
|
||||
|
||||
async getVariable(sessionId: string, variable: string, accessToken?: string) {
|
||||
return await this.requestClient
|
||||
.get<SessionVariable>(
|
||||
`${this.serverUrl}/compute/sessions/${sessionId}/variables/${variable}`,
|
||||
accessToken
|
||||
)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(
|
||||
err,
|
||||
`Error while fetching session variable '${variable}'.`
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
7
src/__mocks__/axios.ts
Normal file
7
src/__mocks__/axios.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { AxiosStatic } from 'axios'
|
||||
|
||||
const mockAxios = jest.genMockFromModule('axios') as AxiosStatic
|
||||
|
||||
mockAxios.create = jest.fn(() => mockAxios)
|
||||
|
||||
export default mockAxios
|
||||
299
src/api/viya/executeScript.ts
Normal file
299
src/api/viya/executeScript.ts
Normal file
@@ -0,0 +1,299 @@
|
||||
import { timestampToYYYYMMDDHHMMSS } from '@sasjs/utils/time'
|
||||
import { AuthConfig, MacroVar } from '@sasjs/utils/types'
|
||||
import { prefixMessage } from '@sasjs/utils/error'
|
||||
import {
|
||||
PollOptions,
|
||||
Job,
|
||||
ComputeJobExecutionError,
|
||||
NotFoundError
|
||||
} from '../..'
|
||||
import { getTokens } from '../../auth/getTokens'
|
||||
import { RequestClient } from '../../request/RequestClient'
|
||||
import { SessionManager } from '../../SessionManager'
|
||||
import { isRelativePath, fetchLogByChunks } from '../../utils'
|
||||
import { formatDataForRequest } from '../../utils/formatDataForRequest'
|
||||
import { pollJobState } from './pollJobState'
|
||||
import { uploadTables } from './uploadTables'
|
||||
|
||||
/**
|
||||
* Executes code on the current SAS Viya server.
|
||||
* @param jobPath - the path to the file being submitted for execution.
|
||||
* @param linesOfCode - an array of code lines to execute.
|
||||
* @param contextName - the context to execute the code in.
|
||||
* @param authConfig - an object containing an access token, refresh token, client ID and secret.
|
||||
* @param data - execution data.
|
||||
* @param debug - when set to true, the log will be returned.
|
||||
* @param expectWebout - when set to true, the automatic _webout fileref will be checked for content, and that content returned. This fileref is used when the Job contains a SASjs web request (as opposed to executing arbitrary SAS code).
|
||||
* @param waitForResult - when set to true, function will return the session
|
||||
* @param pollOptions - an object that represents poll interval(milliseconds) and maximum amount of attempts. Object example: { MAX_POLL_COUNT: 24 * 60 * 60, POLL_INTERVAL: 1000 }.
|
||||
* @param printPid - a boolean that indicates whether the function should print (PID) of the started job.
|
||||
* @param variables - an object that represents macro variables.
|
||||
*/
|
||||
export async function executeScript(
|
||||
requestClient: RequestClient,
|
||||
sessionManager: SessionManager,
|
||||
rootFolderName: string,
|
||||
jobPath: string,
|
||||
linesOfCode: string[],
|
||||
contextName: string,
|
||||
authConfig?: AuthConfig,
|
||||
data: any = null,
|
||||
debug: boolean = false,
|
||||
expectWebout = false,
|
||||
waitForResult = true,
|
||||
pollOptions?: PollOptions,
|
||||
printPid = false,
|
||||
variables?: MacroVar
|
||||
): Promise<any> {
|
||||
let access_token = (authConfig || {}).access_token
|
||||
if (authConfig) {
|
||||
;({ access_token } = await getTokens(requestClient, authConfig))
|
||||
}
|
||||
|
||||
const logger = process.logger || console
|
||||
|
||||
try {
|
||||
let executionSessionId: string
|
||||
|
||||
const session = await sessionManager
|
||||
.getSession(access_token)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while getting session. ')
|
||||
})
|
||||
|
||||
executionSessionId = session!.id
|
||||
|
||||
if (printPid) {
|
||||
const { result: jobIdVariable } = await sessionManager
|
||||
.getVariable(executionSessionId, 'SYSJOBID', access_token)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while getting session variable. ')
|
||||
})
|
||||
|
||||
if (jobIdVariable && jobIdVariable.value) {
|
||||
const relativeJobPath = rootFolderName
|
||||
? jobPath.split(rootFolderName).join('').replace(/^\//, '')
|
||||
: jobPath
|
||||
|
||||
const logger = process.logger || console
|
||||
|
||||
logger.info(
|
||||
`Triggered '${relativeJobPath}' with PID ${
|
||||
jobIdVariable.value
|
||||
} at ${timestampToYYYYMMDDHHMMSS()}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const jobArguments: { [key: string]: any } = {
|
||||
_contextName: contextName,
|
||||
_OMITJSONLISTING: true,
|
||||
_OMITJSONLOG: true,
|
||||
_OMITSESSIONRESULTS: true,
|
||||
_OMITTEXTLISTING: true,
|
||||
_OMITTEXTLOG: true
|
||||
}
|
||||
|
||||
if (debug) {
|
||||
jobArguments['_OMITTEXTLOG'] = false
|
||||
jobArguments['_OMITSESSIONRESULTS'] = false
|
||||
}
|
||||
|
||||
let fileName
|
||||
|
||||
if (isRelativePath(jobPath)) {
|
||||
fileName = `exec-${
|
||||
jobPath.includes('/') ? jobPath.split('/')[1] : jobPath
|
||||
}`
|
||||
} else {
|
||||
const jobPathParts = jobPath.split('/')
|
||||
fileName = jobPathParts.pop()
|
||||
}
|
||||
|
||||
let jobVariables: any = {
|
||||
SYS_JES_JOB_URI: '',
|
||||
_program: isRelativePath(jobPath)
|
||||
? rootFolderName + '/' + jobPath
|
||||
: jobPath
|
||||
}
|
||||
|
||||
if (variables) jobVariables = { ...jobVariables, ...variables }
|
||||
|
||||
if (debug) jobVariables = { ...jobVariables, _DEBUG: 131 }
|
||||
|
||||
let files: any[] = []
|
||||
|
||||
if (data) {
|
||||
if (JSON.stringify(data).includes(';')) {
|
||||
files = await uploadTables(requestClient, data, access_token).catch(
|
||||
(err) => {
|
||||
throw prefixMessage(err, 'Error while uploading tables. ')
|
||||
}
|
||||
)
|
||||
|
||||
jobVariables['_webin_file_count'] = files.length
|
||||
|
||||
files.forEach((fileInfo, index) => {
|
||||
jobVariables[
|
||||
`_webin_fileuri${index + 1}`
|
||||
] = `/files/files/${fileInfo.file.id}`
|
||||
jobVariables[`_webin_name${index + 1}`] = fileInfo.tableName
|
||||
})
|
||||
} else {
|
||||
jobVariables = { ...jobVariables, ...formatDataForRequest(data) }
|
||||
}
|
||||
}
|
||||
|
||||
// Execute job in session
|
||||
const jobRequestBody = {
|
||||
name: fileName,
|
||||
description: 'Powered by SASjs',
|
||||
code: linesOfCode,
|
||||
variables: jobVariables,
|
||||
arguments: jobArguments
|
||||
}
|
||||
|
||||
const { result: postedJob, etag } = await requestClient
|
||||
.post<Job>(
|
||||
`/compute/sessions/${executionSessionId}/jobs`,
|
||||
jobRequestBody,
|
||||
access_token
|
||||
)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while posting job. ')
|
||||
})
|
||||
|
||||
if (!waitForResult) return session
|
||||
|
||||
if (debug) {
|
||||
logger.info(`Job has been submitted for '${fileName}'.`)
|
||||
logger.info(
|
||||
`You can monitor the job progress at '${requestClient.getBaseUrl()}${
|
||||
postedJob.links.find((l: any) => l.rel === 'state')!.href
|
||||
}'.`
|
||||
)
|
||||
}
|
||||
|
||||
const jobStatus = await pollJobState(
|
||||
requestClient,
|
||||
postedJob,
|
||||
debug,
|
||||
authConfig,
|
||||
pollOptions
|
||||
).catch(async (err) => {
|
||||
const error = err?.response?.data
|
||||
const result = /err=[0-9]*,/.exec(error)
|
||||
|
||||
const errorCode = '5113'
|
||||
if (result?.[0]?.slice(4, -1) === errorCode) {
|
||||
const sessionLogUrl =
|
||||
postedJob.links.find((l: any) => l.rel === 'up')!.href + '/log'
|
||||
const logCount = 1000000
|
||||
err.log = await fetchLogByChunks(
|
||||
requestClient,
|
||||
access_token!,
|
||||
sessionLogUrl,
|
||||
logCount
|
||||
)
|
||||
}
|
||||
throw prefixMessage(err, 'Error while polling job status. ')
|
||||
})
|
||||
|
||||
if (authConfig) {
|
||||
;({ access_token } = await getTokens(requestClient, authConfig))
|
||||
}
|
||||
|
||||
const { result: currentJob } = await requestClient
|
||||
.get<Job>(
|
||||
`/compute/sessions/${executionSessionId}/jobs/${postedJob.id}`,
|
||||
access_token
|
||||
)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while getting job. ')
|
||||
})
|
||||
|
||||
let jobResult
|
||||
let log = ''
|
||||
|
||||
const logLink = currentJob.links.find((l) => l.rel === 'log')
|
||||
|
||||
if (debug && logLink) {
|
||||
const logUrl = `${logLink.href}/content`
|
||||
const logCount = currentJob.logStatistics?.lineCount ?? 1000000
|
||||
log = await fetchLogByChunks(
|
||||
requestClient,
|
||||
access_token!,
|
||||
logUrl,
|
||||
logCount
|
||||
)
|
||||
}
|
||||
|
||||
if (jobStatus === 'failed' || jobStatus === 'error') {
|
||||
throw new ComputeJobExecutionError(currentJob, log)
|
||||
}
|
||||
|
||||
if (!expectWebout) {
|
||||
return { job: currentJob, log }
|
||||
}
|
||||
|
||||
const resultLink = `/compute/sessions/${executionSessionId}/filerefs/_webout/content`
|
||||
|
||||
jobResult = await requestClient
|
||||
.get<any>(resultLink, access_token, 'text/plain')
|
||||
.catch(async (e: any) => {
|
||||
if (e instanceof NotFoundError) {
|
||||
if (logLink) {
|
||||
const logUrl = `${logLink.href}/content`
|
||||
const logCount = currentJob.logStatistics?.lineCount ?? 1000000
|
||||
log = await fetchLogByChunks(
|
||||
requestClient,
|
||||
access_token!,
|
||||
logUrl,
|
||||
logCount
|
||||
)
|
||||
|
||||
return Promise.reject({
|
||||
status: 500,
|
||||
log
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
result: JSON.stringify(e)
|
||||
}
|
||||
})
|
||||
|
||||
await sessionManager
|
||||
.clearSession(executionSessionId, access_token)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while clearing session. ')
|
||||
})
|
||||
|
||||
return { result: jobResult?.result, log }
|
||||
} catch (e: any) {
|
||||
interface HttpError {
|
||||
status: number
|
||||
}
|
||||
|
||||
const error = e as HttpError
|
||||
|
||||
if (error.status === 404) {
|
||||
return executeScript(
|
||||
requestClient,
|
||||
sessionManager,
|
||||
rootFolderName,
|
||||
jobPath,
|
||||
linesOfCode,
|
||||
contextName,
|
||||
authConfig,
|
||||
data,
|
||||
debug,
|
||||
false,
|
||||
true
|
||||
)
|
||||
} else {
|
||||
throw prefixMessage(e as Error, 'Error while executing script. ')
|
||||
}
|
||||
}
|
||||
}
|
||||
17
src/api/viya/getFileStream.ts
Normal file
17
src/api/viya/getFileStream.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { isFolder } from '@sasjs/utils/file'
|
||||
import { generateTimestamp } from '@sasjs/utils/time'
|
||||
import { Job } from '../../types'
|
||||
|
||||
export const getFileStream = async (job: Job, filePath?: string) => {
|
||||
const { createWriteStream } = require('@sasjs/utils/file')
|
||||
const logPath = filePath || process.cwd()
|
||||
const isFolderPath = await isFolder(logPath)
|
||||
if (isFolderPath) {
|
||||
const logFileName = `${job.name || 'job'}-${generateTimestamp()}.log`
|
||||
const path = require('path')
|
||||
const logFilePath = path.join(filePath || process.cwd(), logFileName)
|
||||
return await createWriteStream(logFilePath)
|
||||
} else {
|
||||
return await createWriteStream(logPath)
|
||||
}
|
||||
}
|
||||
249
src/api/viya/pollJobState.ts
Normal file
249
src/api/viya/pollJobState.ts
Normal file
@@ -0,0 +1,249 @@
|
||||
import { AuthConfig } from '@sasjs/utils/types'
|
||||
import { Job, PollOptions } from '../..'
|
||||
import { getTokens } from '../../auth/getTokens'
|
||||
import { RequestClient } from '../../request/RequestClient'
|
||||
import { JobStatePollError } from '../../types/errors'
|
||||
import { Link, WriteStream } from '../../types'
|
||||
import { delay, isNode } from '../../utils'
|
||||
|
||||
export async function pollJobState(
|
||||
requestClient: RequestClient,
|
||||
postedJob: Job,
|
||||
debug: boolean,
|
||||
authConfig?: AuthConfig,
|
||||
pollOptions?: PollOptions
|
||||
) {
|
||||
const logger = process.logger || console
|
||||
|
||||
let pollInterval = 300
|
||||
let maxPollCount = 1000
|
||||
|
||||
const defaultPollOptions: PollOptions = {
|
||||
maxPollCount,
|
||||
pollInterval,
|
||||
streamLog: false
|
||||
}
|
||||
|
||||
pollOptions = { ...defaultPollOptions, ...(pollOptions || {}) }
|
||||
|
||||
const stateLink = postedJob.links.find((l: any) => l.rel === 'state')
|
||||
if (!stateLink) {
|
||||
throw new Error(`Job state link was not found.`)
|
||||
}
|
||||
|
||||
let currentState = await getJobState(
|
||||
requestClient,
|
||||
postedJob,
|
||||
'',
|
||||
debug,
|
||||
authConfig
|
||||
).catch((err) => {
|
||||
logger.error(
|
||||
`Error fetching job state from ${stateLink.href}. Starting poll, assuming job to be running.`,
|
||||
err
|
||||
)
|
||||
return 'unavailable'
|
||||
})
|
||||
|
||||
let pollCount = 0
|
||||
|
||||
if (currentState === 'completed') {
|
||||
return Promise.resolve(currentState)
|
||||
}
|
||||
|
||||
let logFileStream
|
||||
if (pollOptions.streamLog && isNode()) {
|
||||
const { getFileStream } = require('./getFileStream')
|
||||
logFileStream = await getFileStream(postedJob, pollOptions.logFolderPath)
|
||||
}
|
||||
|
||||
// Poll up to the first 100 times with the specified poll interval
|
||||
let result = await doPoll(
|
||||
requestClient,
|
||||
postedJob,
|
||||
currentState,
|
||||
debug,
|
||||
pollCount,
|
||||
authConfig,
|
||||
{
|
||||
...pollOptions,
|
||||
maxPollCount:
|
||||
pollOptions.maxPollCount <= 100 ? pollOptions.maxPollCount : 100
|
||||
},
|
||||
logFileStream
|
||||
)
|
||||
|
||||
currentState = result.state
|
||||
pollCount = result.pollCount
|
||||
|
||||
if (!needsRetry(currentState) || pollCount >= pollOptions.maxPollCount) {
|
||||
return currentState
|
||||
}
|
||||
|
||||
// If we get to this point, this is a long-running job that needs longer polling.
|
||||
// We will resume polling with a bigger interval of 1 minute
|
||||
let longJobPollOptions: PollOptions = {
|
||||
maxPollCount: 24 * 60,
|
||||
pollInterval: 60000,
|
||||
streamLog: false
|
||||
}
|
||||
if (pollOptions) {
|
||||
longJobPollOptions.streamLog = pollOptions.streamLog
|
||||
longJobPollOptions.logFolderPath = pollOptions.logFolderPath
|
||||
}
|
||||
|
||||
result = await doPoll(
|
||||
requestClient,
|
||||
postedJob,
|
||||
currentState,
|
||||
debug,
|
||||
pollCount,
|
||||
authConfig,
|
||||
longJobPollOptions,
|
||||
logFileStream
|
||||
)
|
||||
|
||||
currentState = result.state
|
||||
pollCount = result.pollCount
|
||||
|
||||
if (logFileStream) {
|
||||
logFileStream.end()
|
||||
}
|
||||
|
||||
return currentState
|
||||
}
|
||||
|
||||
const getJobState = async (
|
||||
requestClient: RequestClient,
|
||||
job: Job,
|
||||
currentState: string,
|
||||
debug: boolean,
|
||||
authConfig?: AuthConfig
|
||||
) => {
|
||||
const stateLink = job.links.find((l: any) => l.rel === 'state')
|
||||
if (!stateLink) {
|
||||
throw new Error(`Job state link was not found.`)
|
||||
}
|
||||
|
||||
if (needsRetry(currentState)) {
|
||||
let tokens
|
||||
if (authConfig) {
|
||||
tokens = await getTokens(requestClient, authConfig)
|
||||
}
|
||||
|
||||
const { result: jobState } = await requestClient
|
||||
.get<string>(
|
||||
`${stateLink.href}?_action=wait&wait=300`,
|
||||
tokens?.access_token,
|
||||
'text/plain',
|
||||
{},
|
||||
debug
|
||||
)
|
||||
.catch((err) => {
|
||||
throw new JobStatePollError(job.id, err)
|
||||
})
|
||||
|
||||
return jobState.trim()
|
||||
} else {
|
||||
return currentState
|
||||
}
|
||||
}
|
||||
|
||||
const needsRetry = (state: string) =>
|
||||
state === 'running' ||
|
||||
state === '' ||
|
||||
state === 'pending' ||
|
||||
state === 'unavailable'
|
||||
|
||||
const doPoll = async (
|
||||
requestClient: RequestClient,
|
||||
postedJob: Job,
|
||||
currentState: string,
|
||||
debug: boolean,
|
||||
pollCount: number,
|
||||
authConfig?: AuthConfig,
|
||||
pollOptions?: PollOptions,
|
||||
logStream?: WriteStream
|
||||
): Promise<{ state: string; pollCount: number }> => {
|
||||
let pollInterval = 300
|
||||
let maxPollCount = 1000
|
||||
let maxErrorCount = 5
|
||||
let errorCount = 0
|
||||
let state = currentState
|
||||
let printedState = ''
|
||||
let startLogLine = 0
|
||||
|
||||
const logger = process.logger || console
|
||||
|
||||
if (pollOptions) {
|
||||
pollInterval = pollOptions.pollInterval || pollInterval
|
||||
maxPollCount = pollOptions.maxPollCount || maxPollCount
|
||||
}
|
||||
|
||||
const stateLink = postedJob.links.find((l: Link) => l.rel === 'state')
|
||||
if (!stateLink) {
|
||||
throw new Error(`Job state link was not found.`)
|
||||
}
|
||||
|
||||
while (needsRetry(state) && pollCount <= maxPollCount) {
|
||||
state = await getJobState(
|
||||
requestClient,
|
||||
postedJob,
|
||||
state,
|
||||
debug,
|
||||
authConfig
|
||||
).catch((err) => {
|
||||
errorCount++
|
||||
if (pollCount >= maxPollCount || errorCount >= maxErrorCount) {
|
||||
throw err
|
||||
}
|
||||
logger.error(
|
||||
`Error fetching job state from ${stateLink.href}. Resuming poll, assuming job to be running.`,
|
||||
err
|
||||
)
|
||||
return 'unavailable'
|
||||
})
|
||||
|
||||
pollCount++
|
||||
|
||||
const jobHref = postedJob.links.find((l: Link) => l.rel === 'self')!.href
|
||||
|
||||
if (pollOptions?.streamLog) {
|
||||
const { result: job } = await requestClient.get<Job>(
|
||||
jobHref,
|
||||
authConfig?.access_token
|
||||
)
|
||||
|
||||
const endLogLine = job.logStatistics?.lineCount ?? 1000000
|
||||
|
||||
const { saveLog } = isNode() ? require('./saveLog') : { saveLog: null }
|
||||
if (saveLog) {
|
||||
await saveLog(
|
||||
postedJob,
|
||||
requestClient,
|
||||
startLogLine,
|
||||
endLogLine,
|
||||
logStream,
|
||||
authConfig?.access_token
|
||||
)
|
||||
}
|
||||
|
||||
startLogLine += endLogLine
|
||||
}
|
||||
|
||||
if (debug && printedState !== state) {
|
||||
logger.info(`Polling: ${requestClient.getBaseUrl() + jobHref}/state`)
|
||||
logger.info(`Current job state: ${state}`)
|
||||
|
||||
printedState = state
|
||||
}
|
||||
|
||||
if (state != 'unavailable' && errorCount > 0) {
|
||||
errorCount = 0
|
||||
}
|
||||
|
||||
await delay(pollInterval)
|
||||
}
|
||||
|
||||
return { state, pollCount }
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user