Compare commits

..

307 Commits

Author SHA1 Message Date
Daniel
67b845c32e simplified setup 2021-01-04 17:03:58 +01:00
Dmitriynj
fe52eec28e add right datetime format 2021-01-04 14:37:01 +01:00
Dmitriynj
4816028fc1 changing package.json scripts. adjusting mock datetime format 2021-01-04 14:37:01 +01:00
Dmitriynj
1c9a24444a change jest timeout. add @capire/chinook as dependency 2021-01-04 14:37:01 +01:00
Dmitriynj
193e762554 returning package.json dependencies 2021-01-04 14:37:01 +01:00
Dmitriynj
9abeb67d82 returning subfolders names 2021-01-04 14:37:01 +01:00
Dmitriynj
9d28ca9844 changing chinook sql kind property to sql 2021-01-04 14:37:01 +01:00
Dmitriynj
a45d79e8e8 change tests 2021-01-04 14:37:01 +01:00
Dmitriynj
4fd0b74b8c change folders structure 2021-01-04 14:37:01 +01:00
Daniel
69e510a407 Moved to chinook + added .env 2021-01-04 14:37:01 +01:00
Daniel
5cec82fa00 re-including app folder 2021-01-04 14:37:01 +01:00
Daniel
ef0f5bea65 Cleanup folder layout 2021-01-04 14:37:01 +01:00
Dmitriynj
317d45074a change tests 2021-01-04 14:37:01 +01:00
Dmitriynj
cb71e2ed9b update tests 2021-01-04 14:37:01 +01:00
Dmitriynj
145becb1c4 change retry count 2021-01-04 14:37:01 +01:00
Dmitriynj
aeafb1d010 change invoice action impl. changing .http sample file 2021-01-04 14:37:01 +01:00
Dmitriynj
72616ae4ce changing retry number. add readme.md point 2021-01-04 14:37:01 +01:00
Dmitriynj
fc41981eb9 resolve quotation mark error when searching 2021-01-04 14:37:01 +01:00
Dmitriynj
b3ea0cc4f1 add retry interceptor impl 2021-01-04 14:37:01 +01:00
Dmitriynj
09dd526f22 remove unnecessary assosiation. add /index.html route to main page 2021-01-04 14:37:01 +01:00
Dmitriynj
1dd1863266 changing webpack-dev-server-config 2021-01-04 14:37:01 +01:00
Dmitriynj
4ebc20f8ce changing readme.md style 2021-01-04 14:37:01 +01:00
Dmitriynj
bebc18a3e6 changing endings 2021-01-04 14:37:01 +01:00
Dmitriynj
fcd1bf9c20 change words to lowwer case in readme.md 2021-01-04 14:37:01 +01:00
Dmitriynj
d3d4b32c79 change readme.md. add folders description 2021-01-04 14:37:01 +01:00
Dmitriynj
de04a896d1 changing cds services 2021-01-04 14:37:01 +01:00
Dmitriynj
723bd93ef3 change readme.md 2021-01-04 14:37:01 +01:00
Dzmitry_Tamashevich@epam.com
64cc4ec26a move xs-securiry.json 2021-01-04 14:37:01 +01:00
Dzmitry_Tamashevich@epam.com
ee63541845 change gitignore. change server.js 2021-01-04 14:37:01 +01:00
Dzmitry_Tamashevich@epam.com
00474edffe add app to gitignore 2021-01-04 14:37:01 +01:00
Dzmitry_Tamashevich@epam.com
dbe4b8a7bd moving front app to subfolder. add webpack config with watch and dev-server mode. moving deploy things in subfolder 2021-01-04 14:37:01 +01:00
Tamashevich, Dzmitry
0e86e1e1fd change token live time 2021-01-04 14:37:01 +01:00
Dzmitry_Tamashevich@epam.com
90fc300ada add .mta files to gitignore 2021-01-04 14:37:01 +01:00
Dzmitry_Tamashevich@epam.com
a04cc0c25f changing tests. removing importData func 2021-01-04 14:37:01 +01:00
Tamashevich, Dzmitry
029ba61098 changing invoice request 2021-01-04 14:37:01 +01:00
Dzmitry_Tamashevich@epam.com
d9b607919a changing logger 2021-01-04 14:37:01 +01:00
Dzmitry_Tamashevich@epam.com
f439119e73 change readme.md. clean up console.logs. add check for exsisting invoices 2021-01-04 14:37:01 +01:00
Dzmitry_Tamashevich@epam.com
fe0562f38b edit person page. remove invoicedItems on logout 2021-01-04 14:37:01 +01:00
Dzmitry_Tamashevich@epam.com
58af1879f7 clean up things 2021-01-04 14:37:01 +01:00
Dzmitry_Tamashevich@epam.com
6454019713 now create tracks method works properly 2021-01-04 14:37:01 +01:00
Dzmitry_Tamashevich@epam.com
3d176237c1 refactoring error page 2021-01-04 14:37:01 +01:00
Dzmitry_Tamashevich@epam.com
938abb6387 add response interceptors for refreshTokens method 2021-01-04 14:37:01 +01:00
Dzmitry_Tamashevich@epam.com
76cbf7f9ca add flow when invalid credentials 2021-01-04 14:37:01 +01:00
Dzmitry_Tamashevich@epam.com
4b4fe2dc3f refactoring user settings in the frontend 2021-01-04 14:37:01 +01:00
Dzmitry_Tamashevich@epam.com
d78e759bf7 add hana kind 2021-01-04 14:37:01 +01:00
Dzmitry_Tamashevich@epam.com
185e6b939f change invoiceDate impl 2021-01-04 14:37:01 +01:00
Dzmitry_Tamashevich@epam.com
7045914e57 add env vars. change db kind 2021-01-04 14:37:01 +01:00
Tamashevich, Dzmitry
05550a14b1 add frontend code. add deploy config 2021-01-04 14:37:01 +01:00
Tamashevich, Dzmitry
25bdc0a6b2 refactor csv data 2021-01-04 14:37:01 +01:00
Tamashevich, Dzmitry
29fb47f2e9 add dev only things 2021-01-04 14:37:01 +01:00
Dzmitry_Tamashevich@epam.com
fdd2a7a2c5 providing initial data. replacing bycript with bycriptjs 2021-01-04 14:37:01 +01:00
Tamashevich, Dzmitry
3d1502ddfe return bool flag 2021-01-04 14:37:01 +01:00
Dzmitry_Tamashevich@epam.com
c932b486e1 add compositions for cascade delete 2021-01-04 14:37:01 +01:00
Dzmitry_Tamashevich@epam.com
e08b1c6246 refactoring code 2021-01-04 14:37:01 +01:00
Dzmitry_Tamashevich@epam.com
ecdc32bad1 replacing restriction conditions 2021-01-04 14:37:01 +01:00
Dzmitry_Tamashevich@epam.com
34acef85b6 refactoring requests 2021-01-04 14:37:01 +01:00
Dzmitry_Tamashevich@epam.com
70b0c85346 add custom authentication checks 2021-01-04 14:37:01 +01:00
Dzmitry_Tamashevich@epam.com
3cf02cb567 refactor import usage. refactor invoices implementation 2021-01-04 14:37:01 +01:00
Dzmitry_Tamashevich@epam.com
bcfce87276 add customer restriction when browsing invoices 2021-01-04 14:37:01 +01:00
Dzmitry_Tamashevich@epam.com
a319199e10 add cancel invoice action 2021-01-04 14:37:01 +01:00
Dzmitry_Tamashevich@epam.com
52f00c62b7 add status to invoices 2021-01-04 14:37:01 +01:00
Dzmitry_Tamashevich@epam.com
9a63f406ec rename person type 2021-01-04 14:37:01 +01:00
Dzmitry_Tamashevich@epam.com
49b8f4ef95 reaname services 2021-01-04 14:37:01 +01:00
Dzmitry_Tamashevich@epam.com
30d5c789bc add getUser method 2021-01-04 14:37:01 +01:00
Dzmitry_Tamashevich@epam.com
0690762207 add invoice action 2021-01-04 14:37:01 +01:00
Dzmitry_Tamashevich@epam.com
49f6b8c060 add mocked auth 2021-01-04 14:37:01 +01:00
Dzmitry_Tamashevich@epam.com
937d9caf2b add app bundles to medi-store 2021-01-04 14:37:01 +01:00
Dzmitry_Tamashevich@epam.com
ae05e8a609 remove compiled bundles from app 2021-01-04 14:37:01 +01:00
Dzmitry_Tamashevich@epam.com
a917dac1b2 add getMyTracks option 2021-01-04 14:37:01 +01:00
Tamashevich, Dzmitry
99d4da34d7 adjust README.md with frontend repo link provided 2021-01-04 14:37:01 +01:00
Dzmitry_Tamashevich@epam.com
bdbd9d425b add tracks main page. add get my tracks action 2021-01-04 14:37:01 +01:00
Dzmitry_Tamashevich@epam.com
0f6444589f return timeout for mocha runner 2021-01-04 14:37:01 +01:00
Dzmitry_Tamashevich@epam.com
d796bf1ec9 remove redundant args 2021-01-04 14:37:01 +01:00
Dzmitry_Tamashevich@epam.com
41da47aa4f adjast regex for omiting id from column name 2021-01-04 14:37:01 +01:00
Dzmitry_Tamashevich@epam.com
4783a5729d add tests. add to many associations 2021-01-04 14:37:01 +01:00
Dzmitry_Tamashevich@epam.com
e58ad84a2f add reorder for column names 2021-01-04 14:37:01 +01:00
Dzmitry_Tamashevich@epam.com
fc07f7ebba remove redundantt folder 2021-01-04 14:37:01 +01:00
Dzmitry_Tamashevich@epam.com
7a0e8fdba6 remove redundantt folder 2021-01-04 14:37:01 +01:00
Dzmitry_Tamashevich@epam.com
e7be207911 add cli tool 2021-01-04 14:37:01 +01:00
Dzmitry_Tamashevich@epam.com
e33a455154 creating cds model of chinook db 2021-01-04 14:37:01 +01:00
Christian Georgi
dc72442764 Better brand name 2020-12-18 15:09:38 +01:00
Manuel Blechschmidt
7e04f50852 Added installation for cds (#177)
When running these samples for the first time,
it is necessary to install the cds binaries globally.

Co-authored-by: Christian Georgi <chgeo@users.noreply.github.com>
Co-authored-by: Daniel Hutzel <daniel.hutzel@sap.com>
2020-12-18 09:25:11 +01:00
Daniel
ea6e274810 Fixed: missing await srv.emit 2020-12-17 16:39:06 +01:00
Christian Georgi
86e5c429bd Example for draft choreography 2020-12-16 17:58:03 +01:00
Daniel
f32398ba8d Using cds.User.default in 4.4.4 2020-12-07 12:41:22 +01:00
Daniel
684c2d53f1 Preparing test for upcomming release 4.4.4 2020-12-03 14:02:15 +01:00
Daniel
b4594e23c5 remove .db file 2020-11-23 14:23:36 +01:00
Daniel
b6028721af make tests more robust 2020-11-22 10:57:13 +01:00
Daniel
e15a6192b6 Aligned orders w/ managed compositions 2020-11-21 01:32:39 +01:00
Daniel
d1eb14f638 Minor Cleanup 2020-11-20 17:22:20 +01:00
Daniel
394a8b5d12 Revised layout of .html pages 2020-11-20 16:11:33 +01:00
Daniel
dae8e96fe1 Adding Vue.js apps for reviews service 2020-11-20 16:11:33 +01:00
Daniel
8f01bf911e Using fake Products entity in @capire/orders 2020-11-20 12:53:25 +01:00
Daniel Hutzel
932f56812c Update server.js 2020-11-17 20:32:47 +01:00
Daniel
d80084bfb7 Added @capire:registry 2020-11-17 17:51:18 +01:00
Daniel
f2c37ec162 composites 2020-11-17 07:35:43 +01:00
Daniel Hutzel
d0d08b1ee1 Enable authorizations w/ dummy-auth (#158)
* Enable authorizations w/ dummy-auth

* fixed: some tests run in privileged mode

* Fixed tests to skip auth

* npm test --silent

* Added dependency to passport
2020-11-12 23:56:04 +01:00
Daniel
ffaec7aa07 Using sqlite 5 2020-11-12 17:06:50 +01:00
Christian Georgi
1587521bd3 Use newer syntax 2020-11-05 09:40:18 +01:00
Andre Meyering
c04c423c0d cat-services.cds: Use "type of" to use "Books:id"
The syntax `Books.id` (dot) is deprecated and is warned about in the latest
CDS releases.  It should be replaced by either `Books:id` (colon, supported
since cds-compiler v1.41.0) or `type of Books:id` (tested with cds-compiler
v1.20.0 from November 2019).
2020-11-05 09:40:18 +01:00
Daniel
7b5c60c3c7 cleanup mocha settings 2020-10-29 17:35:57 +01:00
Christian Georgi
8317503b20 Update issue template 2020-10-29 15:39:38 +01:00
Christian Georgi
f6c838e6ea Update question--feedback-or-bug-.md 2020-10-29 15:35:06 +01:00
Christian Georgi
5d64ca3555 Use test API from cds lib instead of custom one 2020-10-29 11:12:18 +01:00
Daniel
392106d44a Quick fix for affected rows 2020-10-28 16:55:05 +01:00
Daniel
df55110b9b Updated to use srv-to-srv + declared events 2020-10-28 15:17:28 +01:00
Daniel
d9a06d16f1 clean up legacy stuff 2020-10-26 16:21:30 +01:00
Daniel
c424517770 fixing reviews sample 2020-10-22 18:11:46 +02:00
Christian Georgi
363e9aa9a8 Update requests.http 2020-10-12 15:17:03 +02:00
Christian Georgi
7131c13500 Add bookshop image streaming impl 2020-10-12 15:17:03 +02:00
Daniel
934a00eff1 updated gaps 2020-10-07 23:12:48 +02:00
Christian Georgi
a238390b73 Add Node.js 14 to CI 2020-09-30 11:00:21 +02:00
Christian Georgi
54e170ef88 Fix contact name 2020-09-30 10:56:50 +02:00
Christian Georgi
df3cfbb956 Add license badge 2020-09-30 09:40:49 +02:00
Christian Georgi
14756e47f7 Add license files compliant to reuse tool
See https://github.com/fsfe/reuse-tool
2020-09-30 09:24:44 +02:00
Gregor Wolf
90edc4289f Add REST Client test for media 2020-09-30 08:55:29 +02:00
Gregor Wolf
7ae618d803 Add media to scripts and dependencies 2020-09-30 08:55:29 +02:00
Iwona Hahn
e19447b700 update cat-service.js
based on feedback in the community, see also [PR](https://github.wdf.sap.corp/cap/cap.github.wdf.sap.corp/pull/1864)
2020-09-29 18:55:45 +02:00
Dr. David Kunz
ab18c12a69 Merge pull request #135 from SAP-samples/messaging-cds-4
new messaging
2020-09-15 19:03:06 +02:00
D065023
a456eae8ba Merge branch 'messaging-cds-4' of https://github.com/SAP-samples/cloud-cap-samples into messaging-cds-4 2020-09-15 16:36:23 +02:00
D065023
d8308fe7a3 review/reviewed -> reviewed 2020-09-15 16:36:04 +02:00
Dr. David Kunz
a75f8bdc45 Update reviews/srv/reviews-service.js
Co-authored-by: Daniel Hutzel <daniel.hutzel@sap.com>
2020-09-15 16:35:24 +02:00
D065023
a71aaf75a1 new messaging 2020-09-15 15:30:14 +02:00
Daniel Hutzel
6fdd91b8c8 Merge pull request #130 from SAP-samples/test-for-4.2
Updated tests for 4.2
2020-08-27 13:29:24 +02:00
Daniel
62c3969185 Updated tests for 4.2 2020-08-27 13:26:46 +02:00
Christian Georgi
2d608cd882 Add script IDs for UI5 and Fiori bootstrap
Fixes the CORS issues with the hana.ondemand servers
2020-08-25 11:02:02 +02:00
Daniel Hutzel
ae8fe083ed Merge pull request #127 from SAP-samples/cds.test
Prepare moving test kit over to @sap/cds
2020-08-19 12:38:38 +02:00
Daniel
6b3d9db4e9 Remove obsolete dep to @capire/tests 2020-08-19 12:28:53 +02:00
Daniel
332fae3761 Also monkey-patching cds.load 2020-08-19 10:06:36 +02:00
Daniel
039f62209c Prepare moving test kit over to @sap/cds 2020-08-19 09:38:07 +02:00
Daniel Hutzel
a43ade103c Merge pull request #126 from SAP-samples/fix-call-next
Fixing tests to prefer proper calls to next()
2020-08-12 14:00:20 +02:00
Daniel
a6f1d48670 Fixing tests to prefer proper calls to next() 2020-08-12 13:56:49 +02:00
Daniel Hutzel
bd65af43eb Merge pull request #125 from SAP-samples/simplified-reviewed
Simplified reviewed/server.js
2020-08-10 23:24:51 +02:00
Daniel
6f9133cd4f Simplified reviewed/server.js 2020-08-06 19:32:07 +02:00
Daniel Hutzel
441c82b4c9 Merge pull request #123 from SAP-samples/Pinning-Fiori-to-last-known-good
Pinning Fiori to last-known-good
2020-08-01 14:47:30 +02:00
Daniel
fa7cff4123 Pinning Fiori to last-known-good 2020-08-01 14:44:13 +02:00
Christian Georgi
1b69064752 Make cuid.ID Core.Computed for now
+ TODO to check w/ Fiori
2020-07-31 17:18:14 +02:00
Christian Georgi
ada05cf279 Cosmetics 2020-07-31 16:28:08 +02:00
Christian Georgi
4b78a8b637 Do no longer use run blocks - removed in cds 4 2020-07-31 16:28:08 +02:00
Daniel Hutzel
5c5afd2790 Merge pull request #118 from SAP-samples/fixed-tests
Fxed cwd in tests
2020-07-23 02:02:01 +02:00
Daniel
74ee6f34e4 Fixed jest mocha 2020-07-23 02:00:34 +02:00
Daniel
9a9b7aeb86 Fxed cwd in tests 2020-07-23 01:56:56 +02:00
Daniel Hutzel
cfc01bbc4f Merge pull request #117 from SAP-samples/fixed-tests
Secured cwd in test
2020-07-23 01:26:27 +02:00
Daniel
aaac6cc678 Secured cwd in test 2020-07-23 01:24:39 +02:00
Daniel Hutzel
99fdf0c038 Merge pull request #116 from SAP-samples/using-dot-env
Doing bindings in .env
2020-07-17 16:05:34 +02:00
Daniel
6ccecfecae Doing bindings in .env 2020-07-17 16:04:03 +02:00
Daniel Hutzel
e5f0a7ef73 Merge pull request #115 from SAP-samples/prepare-4
Prepare tests for upcomming rel 4
2020-07-17 01:52:02 +02:00
Daniel
de54f70570 Prepare tests for upcomming rel 4 2020-07-17 01:45:43 +02:00
Daniel Hutzel
e1c6118cb4 Merge pull request #114 from SAP-samples/fixed-messaging-test
Fixed messaging test
2020-07-17 01:05:14 +02:00
Daniel
a3e4865d97 Fixed messaging test 2020-07-17 01:03:30 +02:00
Daniel Hutzel
641df50422 Merge pull request #111 from SAP-samples/Recommending-VSCode-extension-for-CDS
Recommending vscode extension for CDS
2020-07-09 10:40:30 +02:00
Daniel
0f026ed56c It's finally out... 2020-07-09 10:37:35 +02:00
Daniel Hutzel
57a3c5f178 Merge pull request #110 from SAP-samples/simplified-debugging
Simplified debugging
2020-07-07 17:31:34 +02:00
Daniel
522ec8e071 Simplified debugging 2020-07-07 17:29:30 +02:00
Christian Georgi
7b1c3d8b3a More UI annotations
Make sure all entities can be properly displayed in Fiori preview.
Especially UI.LineItems seems to be mandatory in newer Fiori versions.
2020-07-02 19:35:04 +02:00
Christian Georgi
5ba69b5021 Update issue templates 2020-06-30 09:05:27 +02:00
Christian Georgi
fd796b54ef Redirect to community 2020-06-25 09:25:11 +02:00
ianquigley-sap
660344b623 Delete .npmrc
The @sap scoped registry should now point to registry.npmjs.org rather than npm.sap.com.  Please see https://jam4.sapjam.com/feed/item/eOtNGT1RSxVzknBtpSy2My for full details on the migration.  npm.sap.com will not be updated going forward for the CDS library
2020-06-24 12:07:40 +02:00
Daniel Hutzel
d20c29a758 Merge pull request #101 from SAP-samples/mocha-parallel
Running all mocha tests in parallel
2020-06-19 11:40:20 +02:00
Daniel
604cc0514c Running all mocha tests in parallel 2020-06-19 09:51:37 +02:00
I051442
ff351455dd Update for sap.fe usage starting with UI5 1.78 2020-06-18 17:19:02 +02:00
Daniel Hutzel
8f74bd32a9 Merge pull request #91 from SAP-samples/fix-for-mocha
Stick to standard .skip
2020-06-02 11:02:01 +02:00
Daniel
c181afe8c6 Stick to standard .skip 2020-06-02 10:57:57 +02:00
Daniel Hutzel
9ade3e6b6a Merge pull request #90 from SAP-samples/reviewed
Separated reviewed and reviews
2020-05-29 16:58:37 +02:00
Daniel
6f9737ae38 Separated reviewed and reviews 2020-05-29 16:56:03 +02:00
Christian Georgi
0a552b4346 Try with other node 12 version 2020-05-14 21:50:30 +02:00
Christian Georgi
6367081e9d CI badge 2020-05-14 21:19:18 +02:00
Christian Georgi
3e73683d99 Rename job 2020-05-14 21:05:26 +02:00
Christian Georgi
2b345ca447 Remove call to build script 2020-05-14 21:05:26 +02:00
Christian Georgi
20593f2fa2 Workflow file for node.js 2020-05-14 20:46:17 +02:00
Vitaly Kozyura
ca45aa1cf7 Merge pull request #74 from SAP-samples/temporary-skip-test
Temporary disable test
2020-05-07 09:59:39 +02:00
Christian Georgi
e408836c2a Fix typo in bootstrap event 2020-05-06 18:33:16 +02:00
D051920
3000a9e2df Temporary disable test 2020-05-06 18:13:40 +02:00
Heiko Witteborg
b83236de2a Merge pull request #73 from SAP-samples/add-test-for-subselect
add test for subselect in query api as documented
2020-05-06 13:03:50 +02:00
d045778
46b3b8aaec add test for subselect in query api as documented 2020-05-06 11:52:30 +02:00
Iwona Hahn
59f5c82684 Cosmetics 2020-05-05 15:01:53 +02:00
Vitaly Kozyura
bf20760a4f Merge pull request #61 from SAP-samples/temporary-disable-test
Temporary disable test
2020-04-30 10:05:51 +02:00
D051920
113852a518 Temporary disable test 2020-04-29 15:55:56 +02:00
Daniel
e3670b5337 Prepared for @sap/cds@4 2020-04-28 22:24:40 +02:00
Christian Georgi
d0894e50d7 Cosmetics 2020-04-27 16:54:02 +02:00
René Jeglinsky
8a3681c391 removed broken links (#56) 2020-04-27 16:48:16 +02:00
Christian Georgi
278b9ebf7f Fiori: Show and edit author name instead of its ID
Same for genre
2020-04-27 16:43:54 +02:00
Daniel
0e27923739 Added process.emit('shutdown') to @capire/tests 2020-04-27 13:42:47 +02:00
Daniel
0959a43fb9 Always await cds.connect.to 2020-04-27 00:16:07 +02:00
Daniel
3831951b65 prepared for cdr 2020-04-21 21:21:45 +02:00
Daniel
50428b4d26 beautified bookshop/requests.http 2020-04-18 13:25:08 +02:00
Christian Georgi
0ed239b28b Fix usage of deprecated UI component 2020-04-16 15:22:11 +02:00
Daniel
63a617be65 Secure cwd 2020-04-16 09:43:05 +02:00
Daniel
e93c7648d1 Adopting cds.compile.cdl 2020-04-16 08:36:40 +02:00
Daniel
2cefb0e829 Update server.js 2020-04-15 22:58:00 +02:00
D065023
3d150e8308 nicer 2020-04-15 08:10:19 +02:00
D065023
570f7a82de nicer 2020-04-15 08:09:29 +02:00
D065023
2bae61e99b make it nicer 2020-04-15 08:08:09 +02:00
Daniel
31357cde95 Improved and documented server.js 2020-04-12 06:15:23 +02:00
Daniel
93f4cd5c6e Improved and documented server.js 2020-04-12 06:12:26 +02:00
Daniel
cf3c45466a Added missing Books to OrdersService 2020-04-10 08:05:10 +02:00
Daniel
b86d36373f Added launch configurations for VSCode 2020-04-07 18:13:18 +02:00
Daniel Hutzel
4cc10826b7 Merge pull request #52 from SAP-samples/cds.localized-false
Added tests for programmatical service consumption
2020-04-07 16:39:34 +02:00
Daniel
0dcb669548 Added tests for programmatical service consumption 2020-04-07 16:38:39 +02:00
Dr. David Kunz
0fcd6db32d Some computers are just very slow 2020-04-07 16:28:30 +02:00
Daniel Hutzel
d0685e6d83 Merge pull request #50 from SAP-samples/cds.localized-false
Added test for cds.localized:false
2020-04-07 01:47:02 +02:00
Daniel
1b338e450c Added test for cds.localized:false 2020-04-07 01:46:20 +02:00
Daniel
083266b11d copy error 2020-04-06 13:34:03 +02:00
Daniel
6277f39aec Re-enabled secured hierarchical-data test 2020-04-06 13:33:40 +02:00
Daniel
834bcb79c0 Revert "Re-enable hierachical data test"
This reverts commit 97b946a3b8.
2020-04-06 13:19:05 +02:00
Daniel
97b946a3b8 Re-enable hierachical data test 2020-04-06 13:08:13 +02:00
Daniel
212e43683e Temporarily switch of hierarchical test 2020-04-06 12:43:20 +02:00
Daniel
d2b267e683 Reset process.cwd after cds run 2020-04-06 12:12:48 +02:00
Daniel
4225b809c5 Copy error 2020-04-06 10:14:48 +02:00
Daniel
73760f6499 Prepared test kit to be merged into @sap/cds 2020-04-06 10:08:11 +02:00
Daniel
ef520571d5 Cleaned up tests 2020-04-06 09:12:59 +02:00
Daniel
caca6995a1 Fine tuning error handling 2020-04-05 20:06:35 +02:00
Daniel
188d8430a2 . 2020-04-05 19:54:44 +02:00
Daniel
f15a4f807e Added more tests 2020-04-05 18:55:30 +02:00
Daniel
428f1ce29d Fixed hanging test servers 2020-04-05 14:41:14 +02:00
Daniel
b362d955c4 Added npm test script 2020-04-05 03:54:12 +02:00
Daniel
faa910161a Adding tests for mocha and jest 2020-04-04 18:03:04 +02:00
Daniel
db0658c785 Using streamlined compiler 2020-04-04 18:02:33 +02:00
Daniel
b339e40ac1 Using relative npm dependencies 2020-04-04 18:01:58 +02:00
Iwona Hahn
fb5ca615e7 Merge pull request #46 from SAP-samples/d053053-fix
removed link target ...
2020-04-02 11:53:27 +02:00
Iwona Hahn
63be66e602 removed link target ... 2020-04-02 11:36:05 +02:00
Iwona Hahn
62efa30dc0 Merge pull request #45 from SAP-samples/d053053-UA-review
minor edits & UA review
2020-04-02 11:31:33 +02:00
Iwona Hahn
4f3d54fb87 minor edits & UA review 2020-04-02 08:07:00 +02:00
Daniel
2394a725e4 Merge branch 'master' of https://github.com/SAP-samples/cloud-cap-samples 2020-03-29 17:22:07 +02:00
Daniel
a8de029389 Aligned content with latest capire docs 2020-03-29 17:22:04 +02:00
Daniel
251d07f38e . 2020-03-28 02:23:33 +01:00
Brian Bernard
9ec7e67b17 Update LICENSE
Changed to Apache 2.0
2020-03-27 10:39:10 -07:00
Brian Bernard
6cf9a95d78 Update README.md
Changed license text
2020-03-27 10:38:30 -07:00
Daniel Hutzel
387ada93ca Update README.md 2020-03-26 19:20:51 +01:00
Daniel Hutzel
c63ac4783f Update README.md 2020-03-26 19:18:27 +01:00
Christian Georgi
2f837593b7 Fix csv file header. Leads to HANA deployment error. 2020-03-26 11:43:46 +01:00
Daniel
bdacbb6a35 Fine-tuning the vue.js client 2020-03-23 12:14:18 +01:00
Daniel
1679764a7f Workaround for mediocre cds-tests 2020-03-23 12:02:42 +01:00
Daniel
c6de0be951 Correct fix 2020-03-23 11:34:47 +01:00
Dr. David Kunz
59aefda4b1 Merge pull request #40 from SAP-samples/fix-broken-dependency
rm broken dependency
2020-03-23 11:22:06 +01:00
D065023
20eb7ab29c rm broken dependency 2020-03-23 11:16:47 +01:00
Daniel
2c1d1646e1 Moved reusing extensions to separate file 2020-03-23 08:47:22 +01:00
Daniel Hutzel
94b1c7256b Update app.js 2020-03-22 22:50:21 +01:00
Daniel
3d26f288f5 Using snapi for ./fiori 2020-03-22 22:28:42 +01:00
Daniel
73d3352f90 Moved vue.js app to sub folder 2020-03-22 22:19:11 +01:00
Daniel
84dbb94b5d Simplified instructions to run 2020-03-22 22:14:09 +01:00
Daniel
d5db52264a cosmetics 2020-03-22 22:11:37 +01:00
Daniel
9998997a73 Using port 5005 by default 2020-03-22 22:11:26 +01:00
Daniel
7c953050f2 Using snapi by default 2020-03-22 22:10:53 +01:00
Daniel
99cda67246 Adds a simple Vue.js UI 2020-03-22 14:41:14 +01:00
Daniel
073b082935 Activating pre-built extensions in cat-service 2020-03-22 14:40:53 +01:00
Daniel
3872ac21a3 Improved reviews-service 2020-03-20 15:29:10 +01:00
Daniel Hutzel
76829742d8 Merge pull request #38 from SAP-samples/booksgenres
Book data with genres
2020-03-19 14:14:08 +01:00
Christian Georgi
4f28aa930c Book data with genres 2020-03-19 14:05:55 +01:00
René Jeglinsky
7ce20182c8 Update schema.cds
typo
2020-03-17 09:20:10 +01:00
Daniel
b32568047d Fixed: sqlite3 dependency added twice 2020-03-16 15:54:02 +01:00
Daniel Hutzel
3524d056f1 Merge pull request #34 from SAP-samples/nototal
Remove order.total
2020-03-16 15:00:35 +01:00
Daniel Hutzel
992018186f Update package.json 2020-03-16 14:44:26 +01:00
Daniel Hutzel
c0de1b2805 Added sqlite3 as dev dependency 2020-03-16 14:44:13 +01:00
Daniel Hutzel
c3745777fb Added version 2020-03-16 14:40:37 +01:00
Christian Georgi
b922f4d1c6 Add sqlite3 as devDependency
Otherwise tests won't start or deploy
2020-03-16 10:42:50 +01:00
Christian Georgi
92bf470989 Remove order.total
Up to now, we don't have a best-practise way to calculate this on the
fly.
2020-03-16 10:21:38 +01:00
Daniel Hutzel
c0486a1b7b Update README.md 2020-03-15 12:21:57 +01:00
Daniel Hutzel
fe1eb32926 cap/samples reloaded
- cleaned up set of samples
- added new showcases, e.g. draft of localized data
- removed lerna
2020-03-15 12:21:19 +01:00
Daniel
4ae16e8fd2 Cleaned up delegation 2020-03-15 12:12:18 +01:00
Daniel
e4983b8bde Moved managed data annotations 2020-03-15 12:11:07 +01:00
Daniel
eefdf6c976 less prominent workaround 2020-03-09 16:34:53 +01:00
Daniel
8613475988 beautified impl 2020-03-08 16:16:27 +01:00
Daniel
8c50d05776 Fixed launch.json 2020-03-08 15:22:07 +01:00
Daniel
fb3cf9c315 Removed left-over of ./reviewed 2020-03-08 15:02:55 +01:00
Daniel
f50e5312c3 Added ref to cap/issues#4112 2020-03-08 14:39:15 +01:00
Daniel
79d624e798 Added workaround for bug in compiler 2020-03-08 14:02:33 +01:00
Daniel
d8d3b57929 Adjusted launch cinfigs for vscode 2020-03-08 14:01:47 +01:00
Daniel
48fa640f5b cleanup 2020-03-08 12:42:33 +01:00
Daniel
84a815e7e6 Added reviews/test/bookshop 2020-03-08 12:42:26 +01:00
Daniel
737027ded4 automonous package.jsons 2020-03-08 12:41:28 +01:00
Daniel
8c9e8a08dd Added recommended extensions for vscode 2020-03-04 07:52:46 +01:00
Daniel
ea01007716 Fixed tests and prepared for mocha 2020-03-03 09:08:11 +01:00
Daniel
531b6cbf69 Fixed package dependencies 2020-03-02 18:49:39 +01:00
Daniel
ef43d31dd3 Merge branch 'cap-samples-reloaded' of https://github.com/SAP-samples/cloud-cap-samples into cap-samples-reloaded 2020-03-02 18:33:35 +01:00
Daniel
93579f83f2 fixed git clone link 2020-03-02 18:33:30 +01:00
Dr. David Kunz
2a78b8fb64 Update README.md 2020-03-02 14:54:56 +01:00
Daniel
2744fe1d9c fixed copy error 2020-03-02 10:38:31 +01:00
Daniel
8429bdc9a3 Added samples overview 2020-03-02 10:24:33 +01:00
Daniel
946331168a readme 2020-03-02 08:48:36 +01:00
Daniel
ddb25d5ff5 cleaned up package.jsons 2020-03-02 08:45:39 +01:00
Daniel
125cb6e5c2 Added hello world 2020-03-02 08:35:31 +01:00
Daniel
c6eb21ec51 cleaned up 2020-03-02 08:29:49 +01:00
Daniel
cb066233c9 reviews + events 2020-03-02 02:43:15 +01:00
Daniel
9921b2f3de . 2020-03-02 01:22:59 +01:00
Daniel
26d7fc767c ... 2020-03-02 00:08:49 +01:00
Daniel
d9df2930cb Added language texts 2020-03-01 18:28:25 +01:00
Daniel
658a961459 Added languages data 2020-03-01 18:27:12 +01:00
Daniel
3731a7ea23 reloaded 2020-03-01 18:25:53 +01:00
Daniel
06755978b2 Removed work-around for auto-exposed entities 2020-02-15 14:31:07 +01:00
Daniel
02469acebb Moved @odata.draft.enabled to app model 2020-02-15 14:25:15 +01:00
Dr. David Kunz
e2b47228db Update services.js 2020-02-12 17:13:35 +01:00
Christian Georgi
13480ad99e Added issue URL again 2020-02-04 09:11:28 +01:00
Daniel
8071faa62d Adding requires.db: {kind:'sql'} 2020-02-03 07:55:56 +01:00
johannes-vogel
9ea294586a remove link 2020-01-31 08:49:54 +01:00
Lakshmi C Rajeev
a56a11ff3e Create index.cds 2020-01-28 10:58:24 +05:30
Matthias Bühl
b4084b45cb rollback change in master 2020-01-27 15:33:14 +01:00
Matthias Bühl
26e3c0d753 check autthorization in cat service 2020-01-27 15:29:43 +01:00
Lakshmi C Rajeev
6d0194acc0 Merge pull request #12 from LakshmiCR/master
Media-server implementation
2020-01-17 15:02:15 +05:30
Lakshmi C Rajeev
db75a99808 Merge branch 'master' into master 2020-01-17 15:01:59 +05:30
Volker Buzek
a04755efed feat(npm): add .npmrc for @sap-scope
- `npm set @sap...` is unnecessary
- update README.md accordingly
2020-01-14 14:55:36 +01:00
Lakshmi C Rajeev
ba72d7f478 Update package.json 2020-01-03 14:29:53 +05:30
Lakshmi C Rajeev
cd808c76dd Update media-service.js 2020-01-03 14:07:17 +05:30
Lakshmi C Rajeev
630bb2b19c Update package.json 2019-12-19 10:59:57 +05:30
Lakshmi C Rajeev
f9a7aa59de Update media-service.js 2019-12-19 10:43:22 +05:30
Lakshmi C Rajeev
9205e0893a Update media-service.js 2019-12-02 15:27:38 +05:30
Lakshmi C Rajeev
7137bf227e Media-server 2019-11-29 12:30:07 +05:30
276 changed files with 23224 additions and 8199 deletions

View File

@@ -1,9 +1,11 @@
{ {
"extends": "eslint:recommended", "extends": "eslint:recommended",
"env": { "env": {
"browser": true,
"node": true, "node": true,
"es6": true, "es6": true,
"jest": true "jest": true,
"mocha": true
}, },
"parserOptions": { "parserOptions": {
"ecmaVersion": 2018 "ecmaVersion": 2018
@@ -19,6 +21,7 @@
}, },
"rules": { "rules": {
"no-console": "off", "no-console": "off",
"require-atomic-updates": "off" "require-atomic-updates": "off",
"require-await":"warn"
} }
} }

View File

@@ -0,0 +1,10 @@
---
name: This channel is CLOSED.
about: Use our community at https://answers.sap.com/tags/9f13aee1-834c-4105-8e43-ee442775e5ce
title: ''
labels: ''
assignees: ''
---
Please use our community on https://answers.sap.com/tags/9f13aee1-834c-4105-8e43-ee442775e5ce

28
.github/workflows/node.js.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: CI
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [10.x, 12.x, 14.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm install
- run: npm test

2
.gitignore vendored
View File

@@ -13,3 +13,5 @@ target/
connection.properties connection.properties
default-env.json default-env.json
packages/messageBox packages/messageBox
reviews/msg-box
reviews/db/test.db

71
.registry/server.js Normal file
View File

@@ -0,0 +1,71 @@
const { exec } = require ('child_process')
const express = require ('express')
const fs = require ('fs')
const app = express()
const { PORT=4444 } = process.env
const [,,port=PORT] = process.argv
app.use('/-/:tarball', (req,res,next) => {
const url = decodeURIComponent(req.url)
console.debug ('GET', req.params)
try {
const { tarball } = req.params
const [, pkg ] = /^capire-(\w+)/.exec(tarball)
fs.lstat(tarball,(err => {
if (err) exec(`npm pack ../${pkg}`,next)
else next()
}))
} catch (e) {
console.error(e)
res.sendStatus(500)
}
})
app.use('/-', express.static(__dirname))
app.get('/*', (req,res)=>{
const url = decodeURIComponent(req.url)
console.debug ('GET',url)
try {
const [, capire, pkg ] = /^\/(@capire)\/(\w+)/.exec(url)
const package = require (`${capire}/${pkg}/package.json`)
const tarball = `capire-${pkg}-${package.version}.tgz`
// https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md
res.json({
"name": package.name,
"dist-tags": {
"latest": package.version
},
"versions": {
[package.version]: {
"name": package.name,
"version": package.version,
"dist": {
"tarball": `http://localhost:${port}/-/${tarball}`
},
}
},
})
} catch (e) {
console.error(e)
res.sendStatus(404)
}
})
app.listen(port, ()=>{
console.log (`npm set @capire:registry=http://localhost:${port}`)
console.log (`@capire registry listening on http://localhost:${port}`)
exec(`npm set @capire:registry=http://localhost:${port}`)
})
const _exit = ()=>{
console.log ('\nnpm conf rm @capire:registry')
exec('npm conf rm @capire:registry')
exec('rm *.tgz')
process.exit()
}
process.on ('SIGTERM',_exit)
process.on ('SIGHUP',_exit)
process.on ('SIGINT',_exit)
process.on ('SIGUSR2',_exit)

29
.reuse/dep5 Normal file
View File

@@ -0,0 +1,29 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: cloud-cap-samples
Upstream-Contact: <Christian Georgi (christian.georgi@sap.com)>
Source: https://github.com/SAP-samples/cloud-cap-samples
Disclaimer: The code in this project may include calls to APIs (“API Calls”) of
SAP or third-party products or services developed outside of this project
(“External Products”).
“APIs” means application programming interfaces, as well as their respective
specifications and implementing code that allows software to communicate with
other software.
API Calls to External Products are not licensed under the open source license
that governs this project. The use of such API Calls and related External
Products are subject to applicable additional agreements with the relevant
provider of the External Products. In no event shall the open source license
that governs this project grant any rights in or to any External Products,or
alter, expand or supersede any terms of the applicable additional agreements.
If you have a valid license agreement with SAP for the use of a particular SAP
External Product, then you may make use of any API Calls included in this
projects code for that SAP External Product, subject to the terms of such
license agreement. If you do not have a valid license agreement for the use of
a particular SAP External Product, then you may only make use of any API Calls
in this project for that SAP External Product for your internal, non-productive
and non-commercial test and evaluation of such API Calls. Nothing herein grants
you any rights to use or access any SAP External Product, or provide any third
parties the right to use of access any SAP External Product, through API Calls.
Files: *
Copyright: 2019-2020 SAP SE or an SAP affiliate company and cap-cloud-samples
License: Apache-2.0

20
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,20 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
// List of extensions which should be recommended for users of this workspace.
"recommendations": [
"SAPSE.vscode-cds",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"mechatroner.rainbow-csv",
"humao.rest-client",
"alexcvzz.vscode-sqlite",
"hbenl.vscode-mocha-test-adapter",
"sdras.night-owl"
],
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
"unwantedRecommendations": [
]
}

37
.vscode/launch.json vendored
View File

@@ -4,33 +4,36 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{
"name": "Attach by Process ID",
"processId": "${command:PickProcess}",
"request": "attach",
"skipFiles": [
"<node_internals>/**"
],
"type": "pwa-node"
},
{ {
"name": "bookshop", "request": "launch", "type": "node", "runtimeExecutable": "npx", "runtimeArgs": [ "-n" ], "name": "bookshop",
"args": [ "--", "cds", "run", "--in-memory" ], "command": "cds watch bookshop",
"cwd": "${workspaceFolder}/packages/bookshop", "request": "launch",
"console": "integratedTerminal", "type": "node-terminal",
"skipFiles": ["<node_internals>/**"] "skipFiles": ["<node_internals>/**"]
}, },
{ {
"name": "cds run ...", "request": "launch", "type": "node", "runtimeExecutable": "npx", "runtimeArgs": [ "-n" ], "name": "Fiori app",
"args": [ "--", "cds", "run", "--with-mocks", "--in-memory?" ], "command": "cds watch fiori",
"cwd": "${workspaceFolder}/packages/${input:service}", "request": "launch",
"console": "integratedTerminal", "type": "node-terminal",
"skipFiles": ["<node_internals>/**"] "skipFiles": ["<node_internals>/**"]
} }
], ],
"inputs": [ "inputs": [
{ {
"type": "pickString", "type": "pickString",
"id": "service", "id": "sample",
"description": "Which service do you want to start?", "description": "Which sample do you want to start?",
"options": [ "options": ["bookshop", "fiori", "reviews", "reviews/test/bookshop"],
"bookshop",
"bookstore",
"media-server",
"office-supplies",
"reviews-service"
],
"default": "bookshop" "default": "bookshop"
} }
] ]

28
.vscode/tasks.json vendored
View File

@@ -1,17 +1,15 @@
{ {
// See https://go.microsoft.com/fwlink/?LinkId=733558 // See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format // for the documentation about the tasks.json format
"version": "2.0.0", "version": "2.0.0",
"tasks": [ "tasks": [
{ {
"type": "npm", "script": "watch", "path": "packages/bookshop/", "type": "npm",
"options": { "env": { "PORT": "4004" }}, "script": "jest",
"presentation": { "group": "A" } "group": {
}, "kind": "test",
{ "isDefault": true
"type": "npm", "script": "watch", "path": "packages/reviews-service/", }
"options": { "env": { "PORT": "5005" }}, }
"presentation": { "group": "A" } ]
}
]
} }

398
LICENSE
View File

@@ -1,190 +1,208 @@
SAP SAMPLE CODE LICENSE AGREEMENT Apache License
Please scroll down and read the following SAP Sample Code License Agreement Version 2.0, January 2004
carefully ("Agreement"). By downloading, installing, or otherwise using the
SAP sample code or any materials that accompany the sample code documentation http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION,
(collectively, the "Sample Code"), You agree that this Agreement forms a legally AND DISTRIBUTION
binding agreement between You ("You" or "Your") and SAP SE, for and on behalf
of itself and its subsidiaries and affiliates (as defined in Section 15 of the 1. Definitions.
German Stock Corporation Act), and You agree to be bound by all of the terms
and conditions stated in this Agreement. If You are trying to access or download
the Sample Code on behalf of Your employer or as a consultant or agent of a
third party (either "Your Company"), You represent and warrant that You have "License" shall mean the terms and conditions for use, reproduction, and distribution
the authority to act on behalf of and bind Your Company to the terms of this as defined by Sections 1 through 9 of this document.
Agreement and everywhere in this Agreement that refers to 'You' or 'Your' shall
also include Your Company. If You do not agree to these terms, do not attempt
to access or use the Sample Code.
"Licensor" shall mean the copyright owner or entity authorized by the copyright
1. LICENSE: Subject to the terms of this Agreement, SAP grants You a nonexclusive, owner that is granting the License.
non-transferable, non-sublicensable, revocable, royalty-free,
limited license to use, copy, and modify the Sample Code solely for Your internal
business purposes.
"Legal Entity" shall mean the union of the acting entity and all other entities
2. RESTRICTIONS: You must not use the Sample Code to: (a) impair, degrade or that control, are controlled by, or are under common control with that entity.
reduce the performance or security of any SAP products, services or related For the purposes of this definition, "control" means (i) the power, direct
technology (collectively, "SAP Products"); (b) enable the bypassing or or indirect, to cause the direction or management of such entity, whether
circumventing of SAP's license restrictions and/or provide users with access to by contract or otherwise, or (ii) ownership of fifty percent (50%) or more
the SAP Products to which such users are not licensed; or (c) permit mass data of the outstanding shares, or (iii) beneficial ownership of such entity.
extraction from an SAP Product to a non-SAP Product, including use,
modification, saving or other processing of such data in the non-SAP Product.
Further, You must not: (i) provide or make the Sample Code available to any
third party other than your authorized employees, contractors and agents "You" (or "Your") shall mean an individual or Legal Entity exercising permissions
(collectively, “Representatives”) and solely to be used by Your Representatives granted by this License.
for Your own internal business purposes; ii) remove or modify any marks or
proprietary notices from the Sample Code; iii) assign this Agreement, or any
interest therein, to any third party; (iv) use any SAP name, trademark or logo
without the prior written authorization of SAP; or (v) use the Sample Code to "Source" form shall mean the preferred form for making modifications, including
modify an SAP Product or decompile, disassemble or reverse engineer an SAP but not limited to software source code, documentation source, and configuration
Product (except to the extent permitted by applicable law). You are responsible files.
for any breach of the terms of this Agreement by You or Your Representatives.
3. INTELLECTUAL PROPERTY: SAP or its licensors retain all ownership and
intellectual property rights in and to the Sample Code and SAP Products. In "Object" form shall mean any form resulting from mechanical transformation
exchange for the right to use, copy and modify the Sample Code provided under or translation of a Source form, including but not limited to compiled object
this Agreement, You covenant not to assert any intellectual property rights in code, generated documentation, and conversions to other media types.
or to any of Your products, services, or related technology that are based on
or incorporate the Sample Code against any individual or entity in respect of
any current or future SAP Products.
"Work" shall mean the work of authorship, whether in Source or Object form,
4. SAP AND THIRD PARTY APIS: The Sample Code may include API (application made available under the License, as indicated by a copyright notice that
programming interface) calls to SAP and third-party products or services. The is included in or attached to the work (an example is provided in the Appendix
access or use of the third-party products and services to which the API calls below).
are directed may be subject to additional terms and conditions between you and
SAP or such third parties. You (and not SAP) are solely responsible for
understanding and complying with any additional terms and conditions that apply
to the access or use of those APIs and/or third-party products and services. "Derivative Works" shall mean any work, whether in Source or Object form,
SAP does not grant You any rights in or to these APIs, products or services that is based on (or derived from) the Work and for which the editorial revisions,
under this Agreement. annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative
5. FREE AND OPEN SOURCE COMPONENTS: The Sample Code may include third party Works shall not include works that remain separable from, or merely link (or
free or open source components ("FOSS Components"). You may have additional bind by name) to the interfaces of, the Work and Derivative Works thereof.
rights in such FOSS Components that are provided by the third party licensors
of those components.
6. THIRD PARTY DEPENDENCIES: The Sample Code may require third party software
dependencies ("Dependencies") for the use or operation of the Sample Code. These "Contribution" shall mean any work of authorship, including the original version
Dependencies may be identified by SAP in Maven POM files, documentation or by of the Work and any modifications or additions to that Work or Derivative
other means. SAP does not grant You any rights in or to such Dependencies under Works thereof, that is intentionally submitted to Licensor for inclusion in
this Agreement. You are solely responsible for the acquisition, installation the Work by the copyright owner or by an individual or Legal Entity authorized
and use of such Dependencies. to submit on behalf of the copyright owner. For the purposes of this definition,
7. WARRANTY: "submitted" means any form of electronic, verbal, or written communication
a) If You are located outside the US or Canada: AS THE SAMPLE CODE IS PROVIDED sent to the Licensor or its representatives, including but not limited to
TO YOU FREE OF CHARGE, SAP DOES NOT GUARANTEE OR WARRANT ANY FEATURES OR communication on electronic mailing lists, source code control systems, and
QUALITIES OF THE SAMPLE CODE OR GIVE ANY UNDERTAKING WITH REGARD TO ANY OTHER issue tracking systems that are managed by, or on behalf of, the Licensor
QUALITY. NO SUCH WARRANTY OR UNDERTAKING SHALL BE IMPLIED BY YOU FROM ANY for the purpose of discussing and improving the Work, but excluding communication
DESCRIPTION IN THE SAMPLE CODE OR ANY OTHER MATERIALS, COMMUNICATION OR that is conspicuously marked or otherwise designated in writing by the copyright
ADVERTISEMENT. IN PARTICULAR, SAP DOES NOT WARRANT THAT THE SAMPLE CODE WILL BE owner as "Not a Contribution."
AVAILABLE UNINTERRUPTED, ERROR FREE, OR PERMANENTLY AVAILABLE. ALL WARRANTY
CLAIMS RESPECTING THE SAMPLE CODE ARE SUBJECT TO THE LIMITATION OF LIABILITY
STIPULATED IN SECTION 8 BELOW.
b) If You are located in the US or Canada: THE SAMPLE CODE IS LICENSED TO YOU "Contributor" shall mean Licensor and any individual or Legal Entity on behalf
"AS IS", WITHOUT ANY WARRANTY, ESCROW, TRAINING, MAINTENANCE, OR SERVICE of whom a Contribution has been received by Licensor and subsequently incorporated
OBLIGATIONS WHATSOEVER ON THE PART OF SAP. SAP MAKES NO EXPRESS OR IMPLIED within the Work.
WARRANTIES OR CONDITIONS OF SALE OF ANY TYPE WHATSOEVER, INCLUDING BUT NOT
LIMITED TO IMPLIED WARRANTIES OF MERCHANTABILITY AND OF FITNESS FOR A PARTICULAR 2. Grant of Copyright License. Subject to the terms and conditions of this
PURPOSE. IN PARTICULAR, SAP DOES NOT WARRANT THAT THE SAMPLE CODE WILL BE License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive,
AVAILABLE UNINTERRUPTED, ERROR FREE, OR PERMANENTLY AVAILABLE. YOU ASSUME ALL no-charge, royalty-free, irrevocable copyright license to reproduce, prepare
RISKS ASSOCIATED WITH THE USE OF THE SAMPLE CODE, INCLUDING WITHOUT LIMITATION Derivative Works of, publicly display, publicly perform, sublicense, and distribute
RISKS RELATING TO QUALITY, AVAILABILITY, PERFORMANCE, DATA LOSS, AND UTILITY IN the Work and such Derivative Works in Source or Object form.
A PRODUCTION ENVIRONMENT.
c) For all locations: SAP DOES NOT MAKE ANY REPRESENTATIONS OR WARRANTIES IN 3. Grant of Patent License. Subject to the terms and conditions of this License,
RESPECT OF THIRD PARTY DEPENDENCIES, APIS, PRODUCTS AND SERVICES, INCLUDING BUT each Contributor hereby grants to You a perpetual, worldwide, non-exclusive,
NOT LIMITED TO IMPLIED WARRANTIES OF MERCHANTABILITY AND OF FITNESS FOR A no-charge, royalty-free, irrevocable (except as stated in this section) patent
PARTICULAR PURPOSE. IN PARTICULAR, SAP DOES NOT WARRANT THAT THIRDPARTY license to make, have made, use, offer to sell, sell, import, and otherwise
DEPENDENCIES, APIS, PRODUCTS AND SERVICES WILL BE AVAILABLE, ERROR FREE, transfer the Work, where such license applies only to those patent claims
INTEROPERABLE WITH THE SAMPLE CODE, SUITABLE FOR ANY PARTICULAR PURPOSE OR NONINFRINGING. licensable by such Contributor that are necessarily infringed by their Contribution(s)
YOU ASSUME ALL RISKS ASSOCIATED WITH THE USE OF THIRD alone or by combination of their Contribution(s) with the Work to which such
PARTY DEPENDENCIES, APIS, PRODUCTS AND SERVICES, INCLUDING WITHOUT LIMITATION Contribution(s) was submitted. If You institute patent litigation against
RISKS RELATING TO QUALITY, AVAILABILITY, PERFORMANCE, DATA LOSS, UTILITY IN A any entity (including a cross-claim or counterclaim in a lawsuit) alleging
PRODUCTION ENVIRONMENT, AND NON-INFRINGEMENT. IN NO EVENT WILL SAP BE LIABLE that the Work or a Contribution incorporated within the Work constitutes direct
DIRECTLY OR INDIRECTLY IN RESPECT OF ANY USE OF THIRD PARTY DEPENDENCIES, APIS, or contributory patent infringement, then any patent licenses granted to You
PRODUCTS AND SERVICES BY YOU. under this License for that Work shall terminate as of the date such litigation
is filed.
8. LIMITATION OF LIABILITY:
a) If You are located outside the US or Canada: IRRESPECTIVE OF THE LEGAL 4. Redistribution. You may reproduce and distribute copies of the Work or
REASONS, SAP SHALL ONLY BE LIABLE FOR DAMAGES UNDER THIS AGREEMENT IF SUCH Derivative Works thereof in any medium, with or without modifications, and
DAMAGE (I) CAN BE CLAIMED UNDER THE GERMAN PRODUCT LIABILITY ACT OR (II) IS in Source or Object form, provided that You meet the following conditions:
CAUSED BY INTENTIONAL MISCONDUCT OF SAP OR (III) CONSISTS OF PERSONAL INJURY.
IN ALL OTHER CASES, NEITHER SAP NOR ITS EMPLOYEES, AGENTS AND SUBCONTRACTORS (a) You must give any other recipients of the Work or Derivative Works a copy
SHALL BE LIABLE FOR ANY KIND OF DAMAGE OR CLAIMS HEREUNDER. of this License; and
b) If You are located in the US or Canada: IN NO EVENT SHALL SAP BE LIABLE TO
YOU, YOUR COMPANY OR TO ANY THIRD PARTY FOR ANY DAMAGES IN AN AMOUNT IN EXCESS (b) You must cause any modified files to carry prominent notices stating that
OF $100 ARISING IN CONNECTION WITH YOUR USE OF OR INABILITY TO USE THE SAMPLE You changed the files; and
CODE OR IN CONNECTION WITH SAP'S PROVISION OF OR FAILURE TO PROVIDE SERVICES
PERTAINING TO THE SAMPLE CODE, OR AS A RESULT OF ANY DEFECT IN THE SAMPLE COED. (c) You must retain, in the Source form of any Derivative Works that You distribute,
THIS DISCLAIMER OF LIABILITY SHALL APPLY REGARDLESS OF THE FORM OF ACTION THAT all copyright, patent, trademark, and attribution notices from the Source
MAY BE BROUGHT AGAINST SAP, WHETHER IN CONTRACT OR TORT, INCLUDING WITHOUT form of the Work, excluding those notices that do not pertain to any part
LIMITATION ANY ACTION FOR NEGLIGENCE. YOUR SOLE REMEDY IN THE EVENT OF BREACH of the Derivative Works; and
OF THIS AGREEMENT BY SAP OR FOR ANY OTHER CLAIM RELATED TO THE SAMPLE CODE SHALL
BE TERMINATION OF THIS AGREEMENT. NOTWITHSTANDING ANYTHING TO THE CONTRARY (d) If the Work includes a "NOTICE" text file as part of its distribution,
HEREIN, UNDER NO CIRCUMSTANCES SHALL SAP OR ITS LICENSORS BE LIABLE TO YOU OR then any Derivative Works that You distribute must include a readable copy
ANY OTHER PERSON OR ENTITY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, OR of the attribution notices contained within such NOTICE file, excluding those
INDIRECT DAMAGES, LOSS OF GOOD WILL OR BUSINESS PROFITS, WORK STOPPAGE, DATA notices that do not pertain to any part of the Derivative Works, in at least
LOSS, COMPUTER FAILURE OR MALFUNCTION, ANY AND ALL OTHER COMMERCIAL DAMAGES OR one of the following places: within a NOTICE text file distributed as part
LOSS, OR EXEMPLARY OR PUNITIVE DAMAGES. of the Derivative Works; within the Source form or documentation, if provided
along with the Derivative Works; or, within a display generated by the Derivative
9. INDEMNITY: You will fully indemnify, hold harmless and defend SAP against Works, if and wherever such third-party notices normally appear. The contents
law suits based on any claim: (a) that any of Your products, services or related of the NOTICE file are for informational purposes only and do not modify the
technology that are based on or incorporate the Sample Code infringes or License. You may add Your own attribution notices within Derivative Works
misappropriates any patent, copyright, trademark, trade secrets, or other that You distribute, alongside or as an addendum to the NOTICE text from the
proprietary rights of a third party, or (b) related to Your alleged violation Work, provided that such additional attribution notices cannot be construed
of the terms of this Agreement. as modifying the License.
10. EXPORT: The Sample Code is subject to German, EU and US export control You may add Your own copyright statement to Your modifications and may provide
regulations. You confirm that: a) You will not use the Sample Code for, and additional or different license terms and conditions for use, reproduction,
will not allow the Sample Code to be used for, any purposes prohibited by or distribution of Your modifications, or for any such Derivative Works as
German, EU and US law, including, without limitation, for the development, a whole, provided Your use, reproduction, and distribution of the Work otherwise
design, manufacture or production of nuclear, chemical or biological weapons of complies with the conditions stated in this License.
mass destruction; b) You are not located in Cuba, Iran, Sudan, Iraq, North
Korea, Syria, nor any other country to which the United States has prohibited 5. Submission of Contributions. Unless You explicitly state otherwise, any
export or that has been designated by the U.S. Government as a "terrorist Contribution intentionally submitted for inclusion in the Work by You to the
supporting" country (any, an "US Embargoed Country"); c) You are not a citizen, Licensor shall be under the terms and conditions of this License, without
national or resident of, and are not under the control of, a US Embargoed any additional terms or conditions. Notwithstanding the above, nothing herein
Country; d) You will not download or otherwise export or re-export the Sample shall supersede or modify the terms of any separate license agreement you
Code, directly or indirectly, to a US Embargoed Country nor to citizens, may have executed with Licensor regarding such Contributions.
nationals or residents of a US Embargoed Country; e) You are not listed on the
United States Department of Treasury lists of Specially Designated Nationals, 6. Trademarks. This License does not grant permission to use the trade names,
Specially Designated Terrorists, and Specially Designated Narcotic Traffickers, trademarks, service marks, or product names of the Licensor, except as required
nor listed on the United States Department of Commerce Table of Denial Orders for reasonable and customary use in describing the origin of the Work and
or any other U.S. government list of prohibited or restricted parties and f) reproducing the content of the NOTICE file.
You will not download or otherwise export or re-export the Sample Code, directly
or indirectly, to persons on the above-mentioned lists. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to
in writing, Licensor provides the Work (and each Contributor provides its
11. SUPPORT: SAP does not offer support for the Sample Code. Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied, including, without limitation, any warranties
12. TERM AND TERMINATION: You may terminate this Agreement by destroying all or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR
copies of the Sample Code in Your possession or control. SAP may terminate Your A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness
license to use the Sample Code immediately if You fail to comply with any of of using or redistributing the Work and assume any risks associated with Your
the terms of this Agreement, or, for SAP's convenience by providing you with exercise of permissions under this License.
ten (10) days written notice of termination. In case of termination or
expiration of this Agreement, You must immediately destroy all copies of the 8. Limitation of Liability. In no event and under no legal theory, whether
Sample Code in your possession or control. In the event Your Company is acquired in tort (including negligence), contract, or otherwise, unless required by
(by merger, purchase of stock, assets or intellectual property or exclusive applicable law (such as deliberate and grossly negligent acts) or agreed to
license), or You become employed, by a direct competitor of SAP, then this in writing, shall any Contributor be liable to You for damages, including
Agreement and all licenses granted to You in this Agreement shall immediately any direct, indirect, special, incidental, or consequential damages of any
terminate upon the date of such acquisition or change of employment. character arising as a result of this License or out of the use or inability
to use the Work (including but not limited to damages for loss of goodwill,
13. LAW/VENUE: work stoppage, computer failure or malfunction, or any and all other commercial
a) If You are located outside the US or Canada: This Agreement is governed by damages or losses), even if such Contributor has been advised of the possibility
and construed in accordance with the laws of Germany without reference to its of such damages.
conflicts of law principles. You and SAP agree to submit to the exclusive
jurisdiction of, and venue in, the courts located in Karlsruhe, Germany in any 9. Accepting Warranty or Additional Liability. While redistributing the Work
dispute arising out of or relating to this Agreement or the Sample Code. The or Derivative Works thereof, You may choose to offer, and charge a fee for,
United Nations Convention on Contracts for the International Sale of Goods shall acceptance of support, warranty, indemnity, or other liability obligations
not apply to this Agreement. and/or rights consistent with this License. However, in accepting such obligations,
b) If You are located in the US or Canada: This Agreement shall be governed by You may act only on Your own behalf and on Your sole responsibility, not on
and construed in accordance with the laws of the State of New York, USA without behalf of any other Contributor, and only if You agree to indemnify, defend,
reference to its conflicts of law principles. You and SAP agree to submit to and hold each Contributor harmless for any liability incurred by, or claims
the exclusive jurisdiction of, and venue in, the courts located in New York, asserted against, such Contributor by reason of your accepting any such warranty
New York, USA in any dispute arising out of or relating to this Agreement or or additional liability. END OF TERMS AND CONDITIONS
the Sample Code. The United Nations Convention on Contracts for the
International Sale of Goods shall not apply to this Agreement. APPENDIX: How to apply the Apache License to your work.
14. MISCELLANEOUS: This Agreement is the complete agreement between the parties To apply the Apache License to your work, attach the following boilerplate
respecting the Sample Code. This Agreement supersedes all prior or notice, with the fields enclosed by brackets "[]" replaced with your own identifying
contemporaneous agreements or representations with regards to the Sample Code. information. (Don't include the brackets!) The text should be enclosed in
If any term of this Agreement is found to be invalid or unenforceable, the the appropriate comment syntax for the file format. We also recommend that
surviving provisions shall remain effective. SAP's failure to enforce any right a file or class name and description of purpose be included on the same "printed
or provisions stipulated in this Agreement will not constitute a waiver of such page" as the copyright notice for easier identification within third-party
provision, or any other provision of this Agreement. archives.
v1.0-071618 Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

1
NOTICE
View File

@@ -1 +0,0 @@
Copyright (c) 2019 SAP SE or an SAP affiliate company. All rights reserved.

101
README.md
View File

@@ -1,73 +1,78 @@
# cloud-cap-samples # Welcome to cap/samples
This is a monorepository for sample projects on [SAP Cloud Application Programming Model](https://cap.cloud.sap). Find here a collection of samples for the [SAP Cloud Application Programming Model](https://cap.cloud.sap) organized in a simplistic [monorepo setup](samples.md#all-in-one-monorepo). &rarr; See [**Overview** of contained samples](samples.md)
## Description ![](https://github.com/SAP-samples/cloud-cap-samples/workflows/CI/badge.svg)
[![REUSE status](https://api.reuse.software/badge/github.com/SAP-samples/cloud-cap-samples)](https://api.reuse.software/info/github.com/SAP-samples/cloud-cap-samples)
This repository provides a list of samples and reusable packages created based on SAP Cloud Application Programming Model.
The SAP Cloud Application Programming Model enables you to quickly create business applications by allowing you to focus on your domain logic. It offers a consistent end-to-end programming model that includes languages, libraries and APIs tailored for full-stack development on SAP Cloud Platform.
The samples provided can be run in a local setup on SQLite Database.
## Requirements ### Preliminaries
* [Node.js](https://nodejs.org/en/) v8 or higher
* [Git](https://git-scm.com)
* [SQLite DB](https://www.sqlite.org/download.html) (Windows only; pre-installed on Mac/Linux)
#### Optional (if you want to import the code into an editor) 1. [Install @sap/cds-dk](https://cap.cloud.sap/docs/get-started/) globally as documented in [capire](https://cap.cloud.sap)
* [VS Code](https://code.visualstudio.com) ```sh
* [Add CDS extension to VS](https://cap.cloud.sap/docs/get-started/in-vscode#add-cds-editor) npm i -g @sap/cds-dk
```
2. _Optional:_ [Use Visual Studio Code](https://cap.cloud.sap/docs/get-started/in-vscode)
## Download and Installation ### Download
Clone this repo as shown below, if you have [git](https://git-scm.com/downloads) installed,
otherwise [download as zip file](archive/master.zip).
#### Install `cds` development kit
```sh ```sh
# sets the registry for `@sap` packages git clone https://github.com/sap-samples/cloud-cap-samples samples
npm set @sap:registry=https://npm.sap.com cd samples
npm install -g @sap/cds-dk
cds #> test-run it
``` ```
Got issues? Check out the [documentation](https://cap.cloud.sap/docs/get-started/).
#### Clone and build the application ### Setup
`git clone https://github.com/SAP-samples/cloud-cap-samples samples && cd samples && npm i`
#### Run the samples In the samples folder run:
With that you're ready to run the samples, e.g. start the [_bookshop_](./packages/bookshop) sample as follows: ```sh
npm install
```
`npm run bookshop` ### Run
## Test With that you're ready to run the samples, for example:
For example, try these links in your browser: ```sh
- <http://localhost:4004> to test with generic index page. cds watch bookshop
- <http://localhost:4004/fiori.html> to test with Fiori sandbox. ```
After that open this link in your browser: [http://localhost:4004](http://localhost:4004)
### Testing
Run the provided tests with [_jest_](http://jestjs.io) or [_mocha_](http://mochajs.org), for example:
```sh
npx jest
```
> While mocha is a bit smaller and faster, jest runs tests in parallel and isolation, which allows to run all tests.
## Debug ### Serve `npm`
For example, in [VS Code](https://code.visualstudio.com) switch to _Debug_ view and launch one of the prepared _cds run_ launch configurations. We've simple npm registry mock included which allows you to do an `npm install @capire/<package>` anywhere locally. Use it as follows:
1. Start the @capire registry:
```sh
npm run registry
```
> While running this will have `@capire:registry=http://localhost:4444` set with npmrc.
2. Install one of the @capire packages wherever you like, e.g.:
```sh
npm add @capire/common @capire/bookshop
```
## Limitations ## Get Support
None Check out the documentation at [https://cap.cloud.sap](https://cap.cloud.sap). <br>
In case you have a question, find a bug, or otherwise need support, please use our [community](https://answers.sap.com/tags/9f13aee1-834c-4105-8e43-ee442775e5ce).
## Known Issues
None
## How to obtain support
Check out the documentation on https://cap.cloud.sap. In case you find a bug, or you need additional support, please open an issue [here](https://github.com/SAP-samples/cloud-cap-samples/issues/new) in GitHub.
## To-Do (upcoming changes)
None
## License ## License
Copyright (c) 2019 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under SAP Sample Code License Agreement, except as noted otherwise in the [LICENSE](/LICENSE) file. Copyright (c) 2020 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, version 2.0 except as noted otherwise in the [LICENSE](LICENSES/Apache-2.0.txt) file.

25
bookshop/.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,25 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Attach to...",
"type": "node",
"request": "attach",
"processId": "${command:PickProcess}",
"skipFiles": ["<node_internals>/**"]
},
{
"name": "cds run",
"type": "node",
"request": "launch",
"runtimeExecutable": "npx",
"runtimeArgs": ["-n"],
"args": ["--", "cds", "run", "--with-mocks", "--in-memory?"],
"console": "integratedTerminal",
"skipFiles": ["<node_internals>/**"]
}
]
}

2
bookshop/app/index.cds Normal file
View File

@@ -0,0 +1,2 @@
// Incorporate pre-build extensions from...
using from '@capire/common';

48
bookshop/app/vue/app.js Normal file
View File

@@ -0,0 +1,48 @@
/* global Vue axios */ //> from vue.html
const $ = sel => document.querySelector(sel)
const GET = (url) => axios.get('/browse'+url)
const POST = (cmd,data) => axios.post('/browse'+cmd,data)
const books = new Vue ({
el:'#app',
data: {
list: [],
book: undefined,
order: { amount:1, succeeded:'', failed:'' }
},
methods: {
search: ({target:{value:v}}) => books.fetch(v && '&$search='+v),
async fetch (etc='') {
const {data} = await GET(`/ListOfBooks?$expand=genre,currency${etc}`)
books.list = data.value
},
async inspect (eve) {
const book = books.book = books.list [eve.currentTarget.rowIndex-1]
const res = await GET(`/Books/${book.ID}?$select=descr,stock,image`)
Object.assign (book, res.data)
books.order = { amount:1 }
setTimeout (()=> $('form > input').focus(), 111)
},
async submitOrder () {
const {book,order} = books, amount = parseInt (order.amount) || 1 // REVISIT: Okra should be less strict
try {
const res = await POST(`/submitOrder`, { amount, book: book.ID })
book.stock = res.data.stock
books.order = { amount, succeeded: `Successfully orderd ${amount} item(s).` }
} catch (e) {
books.order = { amount, failed: e.response.data.error.message }
}
}
}
})
// initially fill list of books
books.fetch()

View File

@@ -0,0 +1,65 @@
<!DOCTYPE html>
<html>
<head>
<title> Capire Books </title>
<link rel="stylesheet" href="https://unpkg.com/primitive-ui/dist/css/main.css">
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<style>
.hovering tr:hover td { color:cyan; background: #123; cursor: pointer; }
.rating-stars { color:teal }
.succeeded { color:teal }
.failed { color:red }
</style>
</head>
<body class="small-container", style="margin-top: 70px;">
<div id='app'>
<h1> {{ document.title }} </h1>
<input type="text" placeholder="Search..." @input="search">
<table id='books' class="hovering">
<thead>
<th> Book </th>
<th> Author </th>
<th> Genre </th>
<th> Rating </th>
<th> Price </th>
</thead>
<tr v-for="book in list" v-bind:id="book.ID" v-on:click="inspect">
<td>{{ book.title }}</td>
<td>{{ book.author }}</td>
<td>{{ book.genre.name }}</td>
<td class="rating-stars">
{{ ('★'.repeat(Math.round(book.rating))+'☆☆☆☆☆').slice(0,5) }}
</td>
<td>{{ book.currency.symbol }} {{ book.price }}</td>
</tr>
</table>
<div v-if="book">
<img v-bind:src="book.image" alt=""/>
<label style="text-align:right">
<span class="succeeded"> {{ order.succeeded }} </span>
<span class="failed"> {{ order.failed }} </span>
&nbsp;&nbsp; {{ book.stock }} in stock
</label>
<form @submit.prevent="submitOrder" style="float:right; display:flex; flex-direction:row-reverse">
<input type="number" v-model="order.amount" v-bind:class="{ failed: order.failed }" style="width:5em">
<input type="submit" value="Order:" class="muted-button">
</form>
<h4> {{ book.title }} </h4>
<p> {{ book.descr }} </p>
</div>
<div v-else>
( click on a row to see details... )
</div>
</div>
</body>
<script src="app.js"></script>
</html>

View File

@@ -1,6 +1,6 @@
ID;title;descr;author_ID;stock;price;currency_code ID;title;descr;author_ID;stock;price;currency_code;genre_ID
201;Wuthering Heights;"Wuthering Heights, Emily Brontë's only novel, was published in 1847 under the pseudonym ""Ellis Bell"". It was written between October 1845 and June 1846. Wuthering Heights and Anne Brontë's Agnes Grey were accepted by publisher Thomas Newby before the success of their sister Charlotte's novel Jane Eyre. After Emily's death, Charlotte edited the manuscript of Wuthering Heights and arranged for the edited version to be published as a posthumous second edition in 1850.";101;12;11.11;GBP 201;Wuthering Heights;"Wuthering Heights, Emily Brontë's only novel, was published in 1847 under the pseudonym ""Ellis Bell"". It was written between October 1845 and June 1846. Wuthering Heights and Anne Brontë's Agnes Grey were accepted by publisher Thomas Newby before the success of their sister Charlotte's novel Jane Eyre. After Emily's death, Charlotte edited the manuscript of Wuthering Heights and arranged for the edited version to be published as a posthumous second edition in 1850.";101;12;11.11;GBP;11
207;Jane Eyre;"Jane Eyre /ɛər/ (originally published as Jane Eyre: An Autobiography) is a novel by English writer Charlotte Brontë, published under the pen name ""Currer Bell"", on 16 October 1847, by Smith, Elder & Co. of London. The first American edition was published the following year by Harper & Brothers of New York. Primarily a bildungsroman, Jane Eyre follows the experiences of its eponymous heroine, including her growth to adulthood and her love for Mr. Rochester, the brooding master of Thornfield Hall. The novel revolutionised prose fiction in that the focus on Jane's moral and spiritual development is told through an intimate, first-person narrative, where actions and events are coloured by a psychological intensity. The book contains elements of social criticism, with a strong sense of Christian morality at its core and is considered by many to be ahead of its time because of Jane's individualistic character and how the novel approaches the topics of class, sexuality, religion and feminism.";107;11;12.34;GBP 207;Jane Eyre;"Jane Eyre /ɛər/ (originally published as Jane Eyre: An Autobiography) is a novel by English writer Charlotte Brontë, published under the pen name ""Currer Bell"", on 16 October 1847, by Smith, Elder & Co. of London. The first American edition was published the following year by Harper & Brothers of New York. Primarily a bildungsroman, Jane Eyre follows the experiences of its eponymous heroine, including her growth to adulthood and her love for Mr. Rochester, the brooding master of Thornfield Hall. The novel revolutionised prose fiction in that the focus on Jane's moral and spiritual development is told through an intimate, first-person narrative, where actions and events are coloured by a psychological intensity. The book contains elements of social criticism, with a strong sense of Christian morality at its core and is considered by many to be ahead of its time because of Jane's individualistic character and how the novel approaches the topics of class, sexuality, religion and feminism.";107;11;12.34;GBP;11
251;The Raven;"""The Raven"" is a narrative poem by American writer Edgar Allan Poe. First published in January 1845, the poem is often noted for its musicality, stylized language, and supernatural atmosphere. It tells of a talking raven's mysterious visit to a distraught lover, tracing the man's slow fall into madness. The lover, often identified as being a student, is lamenting the loss of his love, Lenore. Sitting on a bust of Pallas, the raven seems to further distress the protagonist with its constant repetition of the word ""Nevermore"". The poem makes use of folk, mythological, religious, and classical references.";150;333;13.13;USD 251;The Raven;"""The Raven"" is a narrative poem by American writer Edgar Allan Poe. First published in January 1845, the poem is often noted for its musicality, stylized language, and supernatural atmosphere. It tells of a talking raven's mysterious visit to a distraught lover, tracing the man's slow fall into madness. The lover, often identified as being a student, is lamenting the loss of his love, Lenore. Sitting on a bust of Pallas, the raven seems to further distress the protagonist with its constant repetition of the word ""Nevermore"". The poem makes use of folk, mythological, religious, and classical references.";150;333;13.13;USD;16
252;Eleonora;"""Eleonora"" is a short story by Edgar Allan Poe, first published in 1842 in Philadelphia in the literary annual The Gift. It is often regarded as somewhat autobiographical and has a relatively ""happy"" ending.";150;555;14;USD 252;Eleonora;"""Eleonora"" is a short story by Edgar Allan Poe, first published in 1842 in Philadelphia in the literary annual The Gift. It is often regarded as somewhat autobiographical and has a relatively ""happy"" ending.";150;555;14;USD;16
271;Catweazle;Catweazle is a British fantasy television series, starring Geoffrey Bayldon in the title role, and created by Richard Carpenter for London Weekend Television. The first series, produced and directed by Quentin Lawrence, was screened in the UK on ITV in 1970. The second series, directed by David Reid and David Lane, was shown in 1971. Each series had thirteen episodes, most but not all written by Carpenter, who also published two books based on the scripts.;170;22;15;EUR 271;Catweazle;Catweazle is a British fantasy television series, starring Geoffrey Bayldon in the title role, and created by Richard Carpenter for London Weekend Television. The first series, produced and directed by Quentin Lawrence, was screened in the UK on ITV in 1970. The second series, directed by David Reid and David Lane, was shown in 1971. Each series had thirteen episodes, most but not all written by Carpenter, who also published two books based on the scripts.;170;22;15;EUR;13
1 ID title descr author_ID stock price currency_code genre_ID
2 201 Wuthering Heights Wuthering Heights, Emily Brontë's only novel, was published in 1847 under the pseudonym "Ellis Bell". It was written between October 1845 and June 1846. Wuthering Heights and Anne Brontë's Agnes Grey were accepted by publisher Thomas Newby before the success of their sister Charlotte's novel Jane Eyre. After Emily's death, Charlotte edited the manuscript of Wuthering Heights and arranged for the edited version to be published as a posthumous second edition in 1850. 101 12 11.11 GBP 11
3 207 Jane Eyre Jane Eyre /ɛər/ (originally published as Jane Eyre: An Autobiography) is a novel by English writer Charlotte Brontë, published under the pen name "Currer Bell", on 16 October 1847, by Smith, Elder & Co. of London. The first American edition was published the following year by Harper & Brothers of New York. Primarily a bildungsroman, Jane Eyre follows the experiences of its eponymous heroine, including her growth to adulthood and her love for Mr. Rochester, the brooding master of Thornfield Hall. The novel revolutionised prose fiction in that the focus on Jane's moral and spiritual development is told through an intimate, first-person narrative, where actions and events are coloured by a psychological intensity. The book contains elements of social criticism, with a strong sense of Christian morality at its core and is considered by many to be ahead of its time because of Jane's individualistic character and how the novel approaches the topics of class, sexuality, religion and feminism. 107 11 12.34 GBP 11
4 251 The Raven "The Raven" is a narrative poem by American writer Edgar Allan Poe. First published in January 1845, the poem is often noted for its musicality, stylized language, and supernatural atmosphere. It tells of a talking raven's mysterious visit to a distraught lover, tracing the man's slow fall into madness. The lover, often identified as being a student, is lamenting the loss of his love, Lenore. Sitting on a bust of Pallas, the raven seems to further distress the protagonist with its constant repetition of the word "Nevermore". The poem makes use of folk, mythological, religious, and classical references. 150 333 13.13 USD 16
5 252 Eleonora "Eleonora" is a short story by Edgar Allan Poe, first published in 1842 in Philadelphia in the literary annual The Gift. It is often regarded as somewhat autobiographical and has a relatively "happy" ending. 150 555 14 USD 16
6 271 Catweazle Catweazle is a British fantasy television series, starring Geoffrey Bayldon in the title role, and created by Richard Carpenter for London Weekend Television. The first series, produced and directed by Quentin Lawrence, was screened in the UK on ITV in 1970. The second series, directed by David Reid and David Lane, was shown in 1971. Each series had thirteen episodes, most but not all written by Carpenter, who also published two books based on the scripts. 170 22 15 EUR 13

View File

@@ -1,4 +1,5 @@
ID;locale;title;descr ID;locale;title;descr
201;de;Sturmhöhe;Sturmhöhe (Originaltitel: Wuthering Heights) ist der einzige Roman der englischen Schriftstellerin Emily Brontë (18181848). Der 1847 unter dem Pseudonym Ellis Bell veröffentlichte Roman wurde vom viktorianischen Publikum weitgehend abgelehnt, heute gilt er als ein Klassiker der britischen Romanliteratur des 19. Jahrhunderts. 201;de;Sturmhöhe;Sturmhöhe (Originaltitel: Wuthering Heights) ist der einzige Roman der englischen Schriftstellerin Emily Brontë (18181848). Der 1847 unter dem Pseudonym Ellis Bell veröffentlichte Roman wurde vom viktorianischen Publikum weitgehend abgelehnt, heute gilt er als ein Klassiker der britischen Romanliteratur des 19. Jahrhunderts.
201;fr;Les Hauts de Hurlevent;Les Hauts de Hurlevent (titre original : Wuthering Heights), parfois orthographié Les Hauts de Hurle-Vent, est l'unique roman d'Emily Brontë, publié pour la première fois en 1847 sous le pseudonyme dEllis Bell. Loin d'être un récit moralisateur, Emily Brontë achève néanmoins le roman dans une atmosphère sereine, suggérant le triomphe de la paix et du Bien sur la vengeance et le Mal.
207;de;Jane Eyre;Jane Eyre. Eine Autobiographie (Originaltitel: Jane Eyre. An Autobiography), erstmals erschienen im Jahr 1847 unter dem Pseudonym Currer Bell, ist der erste veröffentlichte Roman der britischen Autorin Charlotte Brontë und ein Klassiker der viktorianischen Romanliteratur des 19. Jahrhunderts. Der Roman erzählt in Form einer Ich-Erzählung die Lebensgeschichte von Jane Eyre (ausgesprochen /ˌdʒeɪn ˈɛə/), die nach einer schweren Kindheit eine Stelle als Gouvernante annimmt und sich in ihren Arbeitgeber verliebt, jedoch immer wieder um ihre Freiheit und Selbstbestimmung kämpfen muss. Als klein, dünn, blass, stets schlicht dunkel gekleidet und mit strengem Mittelscheitel beschrieben, gilt die Heldin des Romans Jane Eyre nicht zuletzt aufgrund der Kino- und Fernsehversionen der melodramatischen Romanvorlage als die bekannteste englische Gouvernante der Literaturgeschichte 207;de;Jane Eyre;Jane Eyre. Eine Autobiographie (Originaltitel: Jane Eyre. An Autobiography), erstmals erschienen im Jahr 1847 unter dem Pseudonym Currer Bell, ist der erste veröffentlichte Roman der britischen Autorin Charlotte Brontë und ein Klassiker der viktorianischen Romanliteratur des 19. Jahrhunderts. Der Roman erzählt in Form einer Ich-Erzählung die Lebensgeschichte von Jane Eyre (ausgesprochen /ˌdʒeɪn ˈɛə/), die nach einer schweren Kindheit eine Stelle als Gouvernante annimmt und sich in ihren Arbeitgeber verliebt, jedoch immer wieder um ihre Freiheit und Selbstbestimmung kämpfen muss. Als klein, dünn, blass, stets schlicht dunkel gekleidet und mit strengem Mittelscheitel beschrieben, gilt die Heldin des Romans Jane Eyre nicht zuletzt aufgrund der Kino- und Fernsehversionen der melodramatischen Romanvorlage als die bekannteste englische Gouvernante der Literaturgeschichte
252;de;Eleonora;“Eleonora” ist eine Erzählung von Edgar Allan Poe. Sie wurde 1841 erstveröffentlicht. In ihr geht es um das Paradox der Treue in der Treulosigkeit. 252;de;Eleonora;“Eleonora” ist eine Erzählung von Edgar Allan Poe. Sie wurde 1841 erstveröffentlicht. In ihr geht es um das Paradox der Treue in der Treulosigkeit.
1 ID locale title descr
2 201 de Sturmhöhe Sturmhöhe (Originaltitel: Wuthering Heights) ist der einzige Roman der englischen Schriftstellerin Emily Brontë (1818–1848). Der 1847 unter dem Pseudonym Ellis Bell veröffentlichte Roman wurde vom viktorianischen Publikum weitgehend abgelehnt, heute gilt er als ein Klassiker der britischen Romanliteratur des 19. Jahrhunderts.
3 201 fr Les Hauts de Hurlevent Les Hauts de Hurlevent (titre original : Wuthering Heights), parfois orthographié Les Hauts de Hurle-Vent, est l'unique roman d'Emily Brontë, publié pour la première fois en 1847 sous le pseudonyme d’Ellis Bell. Loin d'être un récit moralisateur, Emily Brontë achève néanmoins le roman dans une atmosphère sereine, suggérant le triomphe de la paix et du Bien sur la vengeance et le Mal.
4 207 de Jane Eyre Jane Eyre. Eine Autobiographie (Originaltitel: Jane Eyre. An Autobiography), erstmals erschienen im Jahr 1847 unter dem Pseudonym Currer Bell, ist der erste veröffentlichte Roman der britischen Autorin Charlotte Brontë und ein Klassiker der viktorianischen Romanliteratur des 19. Jahrhunderts. Der Roman erzählt in Form einer Ich-Erzählung die Lebensgeschichte von Jane Eyre (ausgesprochen /ˌdʒeɪn ˈɛə/), die nach einer schweren Kindheit eine Stelle als Gouvernante annimmt und sich in ihren Arbeitgeber verliebt, jedoch immer wieder um ihre Freiheit und Selbstbestimmung kämpfen muss. Als klein, dünn, blass, stets schlicht dunkel gekleidet und mit strengem Mittelscheitel beschrieben, gilt die Heldin des Romans Jane Eyre nicht zuletzt aufgrund der Kino- und Fernsehversionen der melodramatischen Romanvorlage als die bekannteste englische Gouvernante der Literaturgeschichte
5 252 de Eleonora “Eleonora” ist eine Erzählung von Edgar Allan Poe. Sie wurde 1841 erstveröffentlicht. In ihr geht es um das Paradox der Treue in der Treulosigkeit.

View File

@@ -0,0 +1,16 @@
ID;parent_ID;name
10;;Fiction
11;10;Drama
12;10;Poetry
13;10;Fantasy
14;10;Science Fiction
15;10;Romance
16;10;Mystery
17;10;Thriller
18;10;Dystopia
19;10;Fairy Tale
20;;Non-Fiction
21;20;Biography
22;21;Autobiography
23;20;Essay
24;20;Speech
1 ID parent_ID name
2 10 Fiction
3 11 10 Drama
4 12 10 Poetry
5 13 10 Fantasy
6 14 10 Science Fiction
7 15 10 Romance
8 16 10 Mystery
9 17 10 Thriller
10 18 10 Dystopia
11 19 10 Fairy Tale
12 20 Non-Fiction
13 21 20 Biography
14 22 21 Autobiography
15 23 20 Essay
16 24 20 Speech

View File

@@ -1,14 +1,16 @@
using { Currency, managed, sap } from '@sap/cds/common';
namespace sap.capire.bookshop; namespace sap.capire.bookshop;
using { Currency, managed, cuid } from '@sap/cds/common';
entity Books : managed { entity Books : managed {
key ID : Integer; key ID : Integer;
title : localized String(111); title : localized String(111);
descr : localized String(1111); descr : localized String(1111);
author : Association to Authors; author : Association to Authors;
genre : Association to Genres;
stock : Integer; stock : Integer;
price : Decimal(9,2); price : Decimal;
currency : Currency; currency : Currency;
image : LargeBinary @Core.MediaType : 'image/png';
} }
entity Authors : managed { entity Authors : managed {
@@ -21,15 +23,9 @@ entity Authors : managed {
books : Association to many Books on books.author = $self; books : Association to many Books on books.author = $self;
} }
entity Orders : cuid, managed { /** Hierarchically organized Code List for Genres */
OrderNo : String @title:'Order Number'; //> readable key entity Genres : sap.common.CodeList {
Items : Composition of many OrderItems on Items.parent = $self; key ID : Integer;
total : Decimal(9,2) @readonly; parent : Association to Genres;
currency : Currency; children : Composition of many Genres on children.parent = $self;
}
entity OrderItems : cuid {
parent : Association to Orders;
book : Association to Books;
amount : Integer;
netAmount : Decimal(9,2);
} }

4
bookshop/index.cds Normal file
View File

@@ -0,0 +1,4 @@
namespace sap.capire.bookshop; //> important for reflection
using from './db/schema';
using from './srv/cat-service';
using from './srv/admin-service';

1
bookshop/index.js Normal file
View File

@@ -0,0 +1 @@
exports.CatalogService = require('./srv/cat-service')

23
bookshop/package.json Normal file
View File

@@ -0,0 +1,23 @@
{
"name": "@capire/bookshop",
"version": "1.0.0",
"description": "A simple self-contained bookshop service.",
"dependencies": {
"@capire/common": "*",
"@sap/cds": "^4",
"express": "^4.17.1",
"passport": "0.4.1"
},
"scripts": {
"genres": "cds serve test/genres.cds",
"start": "cds run",
"watch": "cds watch"
},
"cds": {
"requires": {
"db": {
"kind": "sql"
}
}
}
}

31
bookshop/readme.md Normal file
View File

@@ -0,0 +1,31 @@
# Bookshop Getting Started Sample
This stand-alone sample introduces the essential tasks in the development of CAP-based services as also covered in the [Getting Started guide in capire](https://cap.cloud.sap/docs/get-started/in-a-nutshell).
## Hypothetical Use Cases
1. Build a service that allows to browse _Books_ and _Authors_.
2. Books have assigned _Genres_ which are organized hierarchically.
3. All users may browse books without login.
4. All entries are maintained by Administrators.
5. End users may order books (the actual order mgmt being out of scope)
## Running the Sample
```sh
npm run watch
```
## Content & Best Practices
| Links to capire | Sample files / folders |
| --------------------------------------------------------------------------------------------------------- | ------------------------------------ |
| [Project Setup and Layouts](https://cap.cloud.sap/docs/get-started/projects#sharing-and-reusing-content) | [`./`](./) |
| [Defining Domain Models](https://cap.cloud.sap/docs/guides/domain-models) | [`./db/schema.cds`](./db/schema.cds) |
| [Defining Services](https://cap.cloud.sap/docs/guides/providing-services) | [`./srv/*.cds`](./srv) |
| [Single-purposed Services](https://cap.cloud.sap/docs/guides/providing-services#single-purposed-services) | [`./srv/*.cds`](./srv) |
| [Generic Providers](https://cap.cloud.sap/docs/guides/providing-services) | http://localhost:4004 |
| Using Databases | [`./db/data/*.csv`](./db/data) |
| [Adding Custom Logic](https://cap.cloud.sap/docs/guides/service-impl) | [`./srv/*.js`](./srv) |
| Adding Tests | [`./test`](./test) |
| [Sharing for Reuse](https://cap.cloud.sap/docs/get-started/projects#sharing-and-reusing-content) | [`./index.cds`](./index.cds) |

View File

@@ -0,0 +1,5 @@
using { sap.capire.bookshop as my } from '../db/schema';
service AdminService @(requires:'admin') {
entity Books as projection on my.Books;
entity Authors as projection on my.Authors;
}

View File

@@ -0,0 +1,12 @@
const cds = require('@sap/cds')
module.exports = cds.service.impl (function(){
this.before ('NEW','Authors', genid)
this.before ('NEW','Books', genid)
})
/** Generate primary keys for target entity in request */
async function genid (req) {
const {ID} = await cds.tx(req).run (SELECT.one.from(req.target).columns('max(ID) as ID'))
req.data.ID = ID - ID % 100 + 100 + 1
}

View File

@@ -0,0 +1,14 @@
using { sap.capire.bookshop as my } from '../db/schema';
service CatalogService @(path:'/browse') {
@readonly entity Books as SELECT from my.Books { *,
author.name as author
} excluding { createdBy, modifiedBy };
@readonly entity ListOfBooks as SELECT from Books
excluding { descr };
@requires: 'authenticated-user'
action submitOrder ( book: Books:ID, amount: Integer ) returns { stock: Integer };
event OrderedBook : { book: Books:ID; amount: Integer; buyer: String };
}

View File

@@ -0,0 +1,28 @@
const cds = require('@sap/cds')
const { Books } = cds.entities ('sap.capire.bookshop')
class CatalogService extends cds.ApplicationService { init(){
// Reduce stock of ordered books if available stock suffices
this.on ('submitOrder', async req => {
const {book,amount} = req.data, tx = cds.tx(req)
let {stock} = await tx.read('stock').from(Books,book)
if (stock >= amount) {
await tx.update (Books,book).with ({ stock: stock -= amount })
await this.emit ('OrderedBook', { book, amount, buyer:req.user.id })
return { stock }
}
else return req.error (409,`${amount} exceeds stock for book #${book}`)
})
// Add some discount for overstocked books
this.after ('READ','Books', each => {
if (each.stock > 111) {
each.title += ` -- 11% discount!`
}
})
return super.init()
}}
module.exports = { CatalogService }

4
bookshop/test/genres.cds Normal file
View File

@@ -0,0 +1,4 @@
using { sap.capire.bookshop as my } from '../db/schema';
service TestService {
entity Genres as projection on my.Genres;
}

View File

@@ -3,10 +3,15 @@
# Genres # Genres
# #
GET http://localhost:4004/admin/Genres? GET http://localhost:4004/test/Genres?
### ###
POST http://localhost:4004/admin/Genres? GET http://localhost:4004/test/Genres?
&$filter=parent_ID eq null&$select=name
&$expand=children($select=name)
###
POST http://localhost:4004/test/Genres?
Content-Type: application/json Content-Type: application/json
{ "ID":100, "name":"Some Sample Genres...", "children":[ { "ID":100, "name":"Some Sample Genres...", "children":[
@@ -21,13 +26,13 @@ Content-Type: application/json
]} ]}
### ###
GET http://localhost:4004/admin/Genres(100)? GET http://localhost:4004/test/Genres(100)?
# &$expand=children # &$expand=children
# &$expand=children($expand=children($expand=children($expand=children))) # &$expand=children($expand=children($expand=children($expand=children)))
### ###
DELETE http://localhost:4004/admin/Genres(103) DELETE http://localhost:4004/test/Genres(103)
### ###
DELETE http://localhost:4004/admin/Genres(100) DELETE http://localhost:4004/test/Genres(100)
### ###

View File

@@ -0,0 +1,82 @@
@server = http://localhost:4004
@me = Authorization: Basic {{$processEnv USER}}:
### ------------------------------------------------------------------------
# Get service info
GET {{server}}/browse
{{me}}
### ------------------------------------------------------------------------
# Get $metadata document
GET {{server}}/browse/$metadata
{{me}}
### ------------------------------------------------------------------------
# Browse Books as any user
GET {{server}}/browse/Books?
# &$select=title,stock
# &$expand=currency
# &sap-language=de
{{me}}
### ------------------------------------------------------------------------
# Fetch Authors as admin
GET {{server}}/admin/Authors?
# &$select=name,dateOfBirth,placeOfBirth
# &$expand=books($select=title;$expand=currency)
# &$filter=ID eq 101
# &sap-language=de
Authorization: Basic alice:
### ------------------------------------------------------------------------
# Create book
POST {{server}}/admin/Books
Content-Type: application/json;IEEE754Compatible=true
Authorization: Basic alice:
{
"ID": 2,
"title": "Poems : Pocket Poets",
"descr": "The Everyman's Library Pocket Poets hardcover series is popular for its compact size and reasonable price which does not compromise content. Poems: Bronte contains poems that demonstrate a sensibility elemental in its force with an imaginative discipline and flexibility of the highest order. Also included are an Editor's Note and an index of first lines.",
"author": { "ID": 101 },
"genre": { "ID": 12 },
"stock": 5,
"price": "12.05",
"currency": { "code": "USD" }
}
### ------------------------------------------------------------------------
# Put image to books
PUT {{server}}/admin/Books(2)/image
Content-Type: image/png
Authorization: Basic alice:
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAANwAAADcCAYAAAAbWs+BAAAGwElEQVR4Ae3cwZFbNxBFUY5rkrDTmKAUk5QT03Aa44U22KC7NHptw+DRikVAXf8fzC3u8Hj4R4AAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgZzAW26USQT+e4HPx+Mz+RRvj0e0kT+SD2cWAQK1gOBqH6sEogKCi3IaRqAWEFztY5VAVEBwUU7DCNQCgqt9rBKICgguymkYgVpAcLWPVQJRAcFFOQ0jUAsIrvaxSiAqILgop2EEagHB1T5WCUQFBBflNIxALSC42scqgaiA4KKchhGoBQRX+1glEBUQXJTTMAK1gOBqH6sEogKCi3IaRqAWeK+Xb1z9iN558fHxcSPS9p2ezx/ROz4e4TtIHt+3j/61hW9f+2+7/+UXbifjewIDAoIbQDWSwE5AcDsZ3xMYEBDcAKqRBHYCgtvJ+J7AgIDgBlCNJLATENxOxvcEBgQEN4BqJIGdgOB2Mr4nMCAguAFUIwnsBAS3k/E9gQEBwQ2gGklgJyC4nYzvCQwICG4A1UgCOwHB7WR8T2BAQHADqEYS2AkIbifjewIDAoIbQDWSwE5AcDsZ3xMYEEjfTzHwiK91B8npd6Q8n8/oGQ/ckRJ9vvQwv3BpUfMIFAKCK3AsEUgLCC4tah6BQkBwBY4lAmkBwaVFzSNQCAiuwLFEIC0guLSoeQQKAcEVOJYIpAUElxY1j0AhILgCxxKBtIDg0qLmESgEBFfgWCKQFhBcWtQ8AoWA4AocSwTSAoJLi5pHoBAQXIFjiUBaQHBpUfMIFAKCK3AsEUgLCC4tah6BQmDgTpPsHSTFs39p6fQ7Q770UsV/Ov19X+2OFL9wxR+rJQJpAcGlRc0jUAgIrsCxRCAtILi0qHkECgHBFTiWCKQFBJcWNY9AISC4AscSgbSA4NKi5hEoBARX4FgikBYQXFrUPAKFgOAKHEsE0gKCS4uaR6AQEFyBY4lAWkBwaVHzCBQCgitwLBFICwguLWoegUJAcAWOJQJpAcGlRc0jUAgIrsCxRCAt8J4eePq89B0ar3ZnyOnve/rfn1+400/I810lILirjtPLnC4guNNPyPNdJSC4q47Ty5wuILjTT8jzXSUguKuO08ucLiC400/I810lILirjtPLnC4guNNPyPNdJSC4q47Ty5wuILjTT8jzXSUguKuO08ucLiC400/I810lILirjtPLnC4guNNPyPNdJSC4q47Ty5wuILjTT8jzXSUguKuO08ucLiC400/I810l8JZ/m78+szP/zI47fJo7Q37vgJ7PHwN/07/3TOv/9gu3avhMYFhAcMPAxhNYBQS3avhMYFhAcMPAxhNYBQS3avhMYFhAcMPAxhNYBQS3avhMYFhAcMPAxhNYBQS3avhMYFhAcMPAxhNYBQS3avhMYFhAcMPAxhNYBQS3avhMYFhAcMPAxhNYBQS3avhMYFhAcMPAxhNYBQS3avhMYFhAcMPAxhNYBQS3avhMYFhg4P6H9J0maYHXuiMlrXf+vOfA33Turf3C5SxNItAKCK4lsoFATkBwOUuTCLQCgmuJbCCQExBcztIkAq2A4FoiGwjkBASXszSJQCsguJbIBgI5AcHlLE0i0AoIriWygUBOQHA5S5MItAKCa4lsIJATEFzO0iQCrYDgWiIbCOQEBJezNIlAKyC4lsgGAjkBweUsTSLQCgiuJbKBQE5AcDlLkwi0Akff//Dz6U+/I6U1/sUNr3bnytl3kPzi4bXb/cK1RDYQyAkILmdpEoFWQHAtkQ0EcgKCy1maRKAVEFxLZAOBnIDgcpYmEWgFBNcS2UAgJyC4nKVJBFoBwbVENhDICQguZ2kSgVZAcC2RDQRyAoLLWZpEoBUQXEtkA4GcgOByliYRaAUE1xLZQCAnILicpUkEWgHBtUQ2EMgJCC5naRKBVkBwLZENBHIC/4M7TXIv+3PS22d24qvdQfL3C/7N5P5i/MLlLE0i0AoIriWygUBOQHA5S5MItAKCa4lsIJATEFzO0iQCrYDgWiIbCOQEBJezNIlAKyC4lsgGAjkBweUsTSLQCgiuJbKBQE5AcDlLkwi0AoJriWwgkBMQXM7SJAKtgOBaIhsI5AQEl7M0iUArILiWyAYCOQHB5SxNItAKCK4lsoFATkBwOUuTCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIDAvyrwDySEJ2VQgUSoAAAAAElFTkSuQmCC
### ------------------------------------------------------------------------
# Reading image from from the server directly
GET {{server}}/browse/Books(2)/image
### ------------------------------------------------------------------------
# Submit Order as authenticated user
# (send that three times to get out-of-stock message)
POST {{server}}/browse/submitOrder
Content-Type: application/json
{{me}}
{ "book":201, "amount":5 }
### ------------------------------------------------------------------------
# Browse Genres
GET {{server}}/browse/Genres?
# &$filter=parent_ID eq null&$select=name
# &$expand=children($select=name)
{{me}}

3
chinook/.env Normal file
View File

@@ -0,0 +1,3 @@
# REVISIT: This is not a good practice -> don't do it that way, we just did it to save some time :)
ACCESS_TOKEN_SECRET=secret
REFRESH_TOKEN_SECRET=refresh-secret

35
chinook/.gitignore vendored Normal file
View File

@@ -0,0 +1,35 @@
# CAP media-store
_out
*.db
connection.properties
default-*.json
gen/
node_modules/
target/
package-lock.json
app/build
# html5Deployer
app/deployers/html5Deployer/resources/
# Web IDE, App Studio
.che/
.gen/
# MTA
*_mta_build_tmp
*.mtar
*.mta
mta_archives/
# Other
.DS_Store
*.orig
*.log
*.iml
*.flattened-pom.xml
# IDEs
# .vscode
# .idea

20
chinook/.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,20 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
// List of extensions which should be recommended for users of this workspace.
"recommendations": [
// >>>>>>>> Add CDS Editor here as soon it is available of vscode marketplace!,
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"mechatroner.rainbow-csv",
"humao.rest-client",
"alexcvzz.vscode-sqlite",
"hbenl.vscode-mocha-test-adapter",
"sdras.night-owl"
],
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
"unwantedRecommendations": [
]
}

17
chinook/.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,17 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"command": "cds run --with-mocks --in-memory?",
"name": "cds run",
"request": "launch",
"type": "node-terminal",
"skipFiles": ["<node_internals>/**"]
}
]
}

8
chinook/.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,8 @@
{
"files.exclude": {
"**/.gitignore": true,
"**/.git": true,
"**/.vscode": true
},
"files.watcherExclude": {}
}

25
chinook/.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,25 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"type": "shell",
"label": "cds watch",
"command": "cds",
"args": ["watch"],
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": []
},
{
"type": "shell",
"label": "cds run",
"command": "cds",
"args": ["run", "--with-mocks", "--in-memory?"],
"problemMatcher": []
}
]
}

96
chinook/README.md Normal file
View File

@@ -0,0 +1,96 @@
# Getting Started
Welcome to your new project.
It contains these folders and files, following our recommended project layout:
| File or Folder | Purpose |
| ---------------- | ------------------------------------ |
| `app/` | will contain compiled front bundles |
| `app/front/` | contains frontend app on react |
| `app/deployers/` | contains deployment stuff |
| `db/` | your domain models and data go here |
| `srv/` | your service models and code go here |
| `test/` | your services tests |
| `package.json` | project metadata and configuration |
| `mta.yaml` | deployment config |
| `readme.md` | this getting started guide |
| `server.js` | initial server set up |
## Development
- Start cds service on 4004 port in watch mode:
```json
cds watch
```
- Open `app/front` folder and run next commands. This will install dependencies and run frontend src files watcher. When you will change src files your bundles in app directory will re-compiled. Now you can enjoy development:
```json
npm install
npm run watch
```
> For better frontend development experience use below command instead of watcher. This will start frontend dev server on 3000 port. Now your bundles will be hot reloaded, this means you do not need reload the page to see changes:
>
> ```json
> npm run start
> ```
## Test
- Change package.json db section
```json
"db": {
"kind": "sql"
}
```
- Run tests
```json
npm run test
```
## Deployment
- Make sure you already have hana trial instance in your cockpit dashboard (SAP Cloud Platform).
Or if you are using hana instance - change it in mta.yaml config file from hanatrial to hana
- Change package.json db section
```json
"db": {
"kind": "hana"
}
```
- Authenticate to the Cloud Foundry:
```json
cf login
```
- Open `app/front` folder and run the following commands. This will create frontend production bundles in app subfolder:
```json
npm install
npm run build:prod
```
- Clean up app/deployers/html5Deployer/resources folder from the previous frontend build
- From root directory run:
```json
mbt build -t ./
cf deploy media-store_1.0.0.mtar
```
- Now your services should be deployed with hanatrial instance and filled with initial data
## Learn More
- [Learn more about CAP](https://cap.cloud.sap/docs/get-started/)
- [Deploying to Cloud Foundry](https://cap.cloud.sap/docs/advanced/deploy-to-cloud)

View File

@@ -0,0 +1,11 @@
{
"name": "media-store-approuter",
"description": "Approuter",
"version": "1.0.0",
"dependencies": {
"@sap/approuter": "^6.8.2"
},
"scripts": {
"start": "node node_modules/@sap/approuter/approuter.js"
}
}

View File

@@ -0,0 +1,17 @@
{
"welcomeFile": "/index.html",
"authenticationMethod": "none",
"routes": [
{
"source": "/api/(.*)",
"target": "$1",
"destination": "srv-binding",
"authenticationType": "none"
},
{
"source": "^(.*)",
"target": "mediastore/$1",
"service": "html5-apps-repo-rt"
}
]
}

View File

@@ -0,0 +1,12 @@
{
"name": "media-store-html5deployer",
"engines": {
"node": ">=6.0.0"
},
"dependencies": {
"@sap/html5-app-deployer": "^2.0.0"
},
"scripts": {
"start": "node node_modules/@sap/html5-app-deployer/index.js"
}
}

View File

@@ -0,0 +1,7 @@
{
"xsappname": "media-store-xsuaa",
"tenant-mode": "dedicated",
"scopes": [],
"attributes": [],
"role-templates": []
}

View File

@@ -0,0 +1,5 @@
{
"presets": ["@babel/preset-react", "@babel/preset-env"],
"plugins": ["@babel/plugin-transform-runtime", "babel-plugin-syntax-dynamic-import"]
}

View File

@@ -0,0 +1,43 @@
{
"env": {
"browser": true,
"es2020": true
},
"extends": ["plugin:react/recommended", "airbnb", "prettier"],
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 11,
"sourceType": "module"
},
"plugins": ["react", "prettier"],
"rules": {
"prettier/prettier": ["error", { "parser": "flow", "endOfLine": "auto" }],
"linebreak-style": [0, "error", "windows"],
"import/prefer-default-export": "off",
"no-shadow": "off",
"react/forbid-prop-types": "off",
"no-alert": "off",
"jsx-a11y/label-has-associated-control": [
"error",
{
"required": {
"some": ["nesting", "id"]
}
}
],
"jsx-a11y/label-has-for": [
"error",
{
"required": {
"some": ["nesting", "id"]
}
}
],
"react/jsx-props-no-spreading": "off", // props spreading,
"no-console": "off",
"consistent-return": "off",
"prefer-destructuring": "off"
}
}

23
chinook/app/front/.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View File

@@ -0,0 +1,4 @@
{
"printWidth": 100,
"singleQuote": true
}

13
chinook/app/front/.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,13 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Chrome",
"type": "chrome",
"request": "launch",
"url": "http://localhost:3000",
"webRoot": "${workspaceRoot}/src"
}
]
}

View File

@@ -0,0 +1 @@
"# Media store UI"

View File

@@ -0,0 +1,67 @@
{
"name": "mediastore",
"version": "0.1.0",
"private": false,
"scripts": {
"start": "./node_modules/.bin/webpack-dev-server --config ./webpack/webpack-dev-server.js",
"watch": "./node_modules/.bin/webpack -w --config ./webpack/webpack.dev.js",
"build:dev": "./node_modules/.bin/webpack --config ./webpack/webpack.dev.js",
"build:prod": "./node_modules/.bin/webpack --config ./webpack/webpack.prod.js",
"lint": "./node_modules/.bin/eslint"
},
"dependencies": {
"@ant-design/icons": "4.3.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.4.3",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"@umijs/hooks": "^1.9.3",
"antd": "^4.8.2",
"axios": "^0.20.0",
"clean-webpack-plugin": "^3.0.0",
"copy-webpack-plugin": "^6.3.2",
"css-minimizer-webpack-plugin": "^1.1.5",
"events": "^3.2.0",
"html-webpack-plugin": "^4.5.0",
"lodash": "^4.17.20",
"mini-css-extract-plugin": "^1.3.1",
"moment": "^2.29.1",
"prop-types": "^15.7.2",
"react": "^16.14.0",
"react-dev-utils": "^11.0.1",
"react-dom": "^16.14.0",
"react-refresh": "^0.9.0",
"react-router-dom": "^5.2.0",
"terser-webpack-plugin": "^5.0.3",
"webpack": "5.8.0",
"webpack-dev-server": "^3.11.0",
"webpack-merge": "^5.4.0"
},
"devDependencies": {
"@babel/core": "^7.12.9",
"@babel/plugin-transform-runtime": "^7.12.1",
"@babel/polyfill": "^7.12.1",
"@babel/preset-env": "^7.12.7",
"@babel/preset-react": "^7.12.7",
"@babel/runtime": "^7.12.5",
"babel-loader": "^8.2.2",
"babel-plugin-syntax-dynamic-import": "^6.18.0",
"cowsay": "^1.4.0",
"css-loader": "^5.0.1",
"eslint": "^7.14.0",
"eslint-config-airbnb": "^18.2.1",
"eslint-config-prettier": "^6.15.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-prettier": "^3.1.4",
"eslint-plugin-react": "^7.21.5",
"eslint-plugin-react-hooks": "^4.2.0",
"prettier": "^2.2.1",
"style-loader": "^2.0.0",
"url-loader": "^4.1.1",
"webpack-cli": "^3.3.12"
},
"eslintConfig": {
"extends": "react-app"
}
}

View File

@@ -0,0 +1,44 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -0,0 +1,31 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff",
"sap.app": {
"id": "mediastore",
"applicationVersion": {
"version": "1.0.0"
}
}
}

View File

@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@@ -0,0 +1,10 @@
{
"welcomeFile": "/index.html",
"routes": [
{
"source": "^(.*)",
"target": "$1",
"service": "html5-apps-repo-rt"
}
]
}

View File

@@ -0,0 +1,57 @@
@import "~antd/dist/antd.css";
html {
overflow: hidden;
}
#root {
height: 100%;
}
section.ant-layout {
height: 100vh;
overflow: auto;
}
/* Layout
*/
.site-layout .site-layout-background {
background: #fff;
}
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@@ -0,0 +1,18 @@
import React from 'react';
import 'antd/dist/antd.css';
import './App.css';
import { Layout } from 'antd';
import { MyRouter } from './components/Router';
import { AppStateContextProvider } from './contexts/AppStateContext';
const App = () => {
return (
<Layout style={{ height: '100%' }}>
<AppStateContextProvider>
<MyRouter />
</AppStateContextProvider>
</Layout>
);
};
export default App;

View File

@@ -0,0 +1,168 @@
import axios from 'axios';
import { getUserFromLS, getLocaleFromLS } from '../util/localStorageService';
import { emitter } from '../util/EventEmitter';
const TIMEOUT = 2000;
const RETRY_COUNT = 3;
/**
* This is axios instance
*/
const axiosInstance = axios.create({
baseURL: process.env.SERVICE_URL,
timeout: TIMEOUT,
retryDelay: TIMEOUT,
retry: RETRY_COUNT,
});
/**
* Changing user axios default params,
* which are used in api call functions (calls.js)
* @param {*} currentUser current user from react state and local storage
*/
function changeUserDefaults(currentUser) {
if (currentUser) {
axiosInstance.defaults.headers.common.Authorization = `Basic ${currentUser.accessToken}`;
axiosInstance.defaults.userID = currentUser.ID;
if (currentUser.roles.includes('customer')) {
axiosInstance.defaults.userEntity = `Customers/${currentUser.ID}`;
axiosInstance.defaults.tracksEntity = 'MarkedTracks';
} else {
axiosInstance.defaults.userEntity = `Employees/${currentUser.ID}`;
axiosInstance.defaults.tracksEntity = 'Tracks';
}
} else {
axiosInstance.defaults.tracksEntity = 'Tracks';
}
}
/**
* This func changing axios instance default params
* @param {*} locale current locale from react state and local storage
*/
function changeLocaleDefaults(locale) {
if (locale) {
axiosInstance.defaults.headers.common['Accept-language'] = locale;
}
}
/**
* Init axios defaults
*/
const user = getUserFromLS();
const locale = getLocaleFromLS();
changeUserDefaults(user);
changeLocaleDefaults(locale);
/**
* Retry request if response time is too long
* See link below
* {@link https://github.com/axios/axios/issues/164#issuecomment-327837467 GitHub}
* @param {*} err response error object
*/
function axiosRetryInterceptor(err) {
const config = err.config;
// If config does not exist or the retry option is not set, reject
if (config && config.retry) {
// Set the variable for keeping track of the retry count
config.retryCount = config.retryCount || 0;
// Check if we've maxed out the total number of retries
if (config.retryCount >= config.retry) {
// Reject with the error
return Promise.reject(err);
}
// Increase the retry count
config.retryCount += 1;
// Create new promise to handle exponential backoff
const backoff = new Promise((resolve) => {
setTimeout(() => {
resolve();
}, config.retryDelay || 1);
});
// Return the promise in which recalls axios to retry the request
return backoff.then(() => {
return axios(config);
});
}
}
/**
* Things below needed for refresh tokens mechanism implementation
*/
let isRefreshing = false;
let subscribers = [];
const refreshTokens = (refreshToken) => {
return axiosInstance.post(
'users/refreshTokens',
{ refreshToken },
{
headers: { 'content-type': 'application/json' },
}
);
};
/**
* Refresh tokens interceptor
* See link below
* {@link https://gist.github.com/mkjiau/650013a99c341c9f23ca00ccb213db1c#gistcomment-3536511 GitHub}
* @param {*} error error response object
*/
function axiosRefreshTokensInterceptor(error) {
const originalRequest = error.config;
const user = getUserFromLS();
if (error.response && error.response.status === 401 && !!user) {
if (originalRequest.url === 'users/login') {
return Promise.reject(error);
}
// if users/refreshTokens request failed
if (isRefreshing && originalRequest.url === 'users/refreshTokens') {
subscribers.forEach((request) => request.reject(error));
subscribers = [];
isRefreshing = false;
return Promise.reject(error);
}
// if got a 401 error we sending users/refreshTokens request
if (!isRefreshing) {
isRefreshing = true;
refreshTokens(user.refreshToken)
.then((response) => {
emitter.emit('UPDATE_USER', response.data);
subscribers.forEach((request) => request.resolve(response.data.accessToken));
subscribers = [];
isRefreshing = false;
})
.catch(() => {
emitter.emit('UPDATE_USER', undefined);
});
}
// holding requests which should be sended after users/refreshTokens complete
// otherwise if users/refreshTokens failed an error will be thrown
return new Promise((resolve, reject) => {
subscribers.push({
resolve: (newAccessToken) => {
originalRequest.headers.Authorization = `Basic ${newAccessToken}`;
resolve(axiosInstance(originalRequest));
},
reject: (err) => {
reject(err);
},
});
});
}
}
axiosInstance.interceptors.response.use(null, (error) => {
return (
axiosRefreshTokensInterceptor(error) || axiosRetryInterceptor(error) || Promise.reject(error)
);
});
export { axiosInstance, changeLocaleDefaults, changeUserDefaults };

View File

@@ -0,0 +1,164 @@
import { isEmpty } from 'lodash';
import { axiosInstance } from './axiosInstance';
const BROWSE_TRACKS_SERVICE = 'browse-tracks';
const INVOICES_SERVICE = 'browse-invoices';
const USER_SERVICE = 'users';
const MANAGE_STORE = 'manage-store';
const constructGenresQuery = (genreIds) => {
return !isEmpty(genreIds)
? ` and ${genreIds.map((value) => `genre_ID eq ${value}`).join(' or ')}`
: '';
};
const fetchTacks = ({ $top = 20, $skip = 0, genreIds = [], substr = '' } = {}) => {
const serializeTracksUrl = () => {
return `$expand=genre,album($expand=artist)&$top=${$top}&$skip=${$skip}&$filter=${`contains(name,'${substr}')${constructGenresQuery(
genreIds
)}`}`;
};
return axiosInstance.get(`${BROWSE_TRACKS_SERVICE}/${axiosInstance.defaults.tracksEntity}`, {
params: {},
paramsSerializer: () => serializeTracksUrl(),
});
};
const countTracks = ({ genreIds = [], substr = '' } = {}) => {
const { tracksEntity } = axiosInstance.defaults;
return axiosInstance.get(
`${BROWSE_TRACKS_SERVICE}/${tracksEntity}/$count?$filter=${`contains(name,'${substr}')${constructGenresQuery(
genreIds
)}`}`
);
};
const fetchGenres = () => {
return axiosInstance.get(`${BROWSE_TRACKS_SERVICE}/Genres`);
};
const invoice = (tracks) => {
return axiosInstance.post(
`${INVOICES_SERVICE}/invoice`,
{
tracks,
},
{
headers: { 'content-type': 'application/json' },
}
);
};
const fetchPerson = () => {
return axiosInstance.get(`${USER_SERVICE}/${axiosInstance.defaults.userEntity}`);
};
const confirmPerson = (person) => {
return axiosInstance.put(
`${USER_SERVICE}/${axiosInstance.defaults.userEntity}`,
{
...person,
},
{
headers: { 'content-type': 'application/json' },
}
);
};
const fetchInvoices = () => {
return axiosInstance.get(
`${INVOICES_SERVICE}/Invoices?$expand=invoiceItems($expand=track($expand=album($expand=artist)))`
);
};
const cancelInvoice = (ID) => {
return axiosInstance.post(
`${INVOICES_SERVICE}/cancelInvoice`,
{
ID,
},
{
headers: { 'content-type': 'application/json' },
}
);
};
const fetchAlbumsByName = (substr = '', top) => {
return axiosInstance.get(
`${BROWSE_TRACKS_SERVICE}/Albums?$filter=${`contains(title,'${substr}')&$top=${top}`}`
);
};
const addTrack = (data) => {
return axiosInstance.post(`${MANAGE_STORE}/Tracks`, data, {
headers: { 'content-type': 'application/json;IEEE754Compatible=true' },
});
};
const addArtist = (data) => {
return axiosInstance.post(`${MANAGE_STORE}/Artists`, data, {
headers: { 'content-type': 'application/json' },
});
};
const addAlbum = (data) => {
return axiosInstance.post(`${MANAGE_STORE}/Albums`, data, {
headers: { 'content-type': 'application/json' },
});
};
const fetchArtistsByName = (substr = '', top) => {
return axiosInstance.get(
`${MANAGE_STORE}/Artists?$filter=${`contains(name,'${substr}')&$top=${top}`}`
);
};
const login = (data) => {
return axiosInstance.post(`${USER_SERVICE}/login`, data, {
headers: { 'content-type': 'application/json' },
});
};
const updateTrack = (track) => {
return axiosInstance.put(
`${MANAGE_STORE}/Tracks/${track.ID}`,
{
...track,
},
{
headers: { 'content-type': 'application/json;IEEE754Compatible=true' },
}
);
};
const getTrack = (ID) => {
return axiosInstance.get(
`${BROWSE_TRACKS_SERVICE}/${axiosInstance.defaults.tracksEntity}/${ID}?$expand=genre,album($expand=artist)`
);
};
const deleteTrack = (ID) => {
return axiosInstance.delete(`${MANAGE_STORE}/Tracks(${ID})`);
};
export {
fetchTacks,
countTracks,
fetchGenres,
invoice,
fetchPerson,
confirmPerson,
fetchInvoices,
cancelInvoice,
fetchAlbumsByName,
addTrack,
addArtist,
addAlbum,
fetchArtistsByName,
login,
updateTrack,
getTrack,
deleteTrack,
};

View File

@@ -0,0 +1,49 @@
import React from 'react';
import { useHistory } from 'react-router-dom';
import { isEmpty } from 'lodash';
import { Result, Button } from 'antd';
import { useAppState } from '../hooks/useAppState';
const ErrorPage = () => {
const { error, setError } = useAppState();
const history = useHistory();
const onGoHome = () => {
setError({});
history.push('/');
};
const goLoginPage = () => {
setError({});
history.push('/login');
};
const goHomeButton = (
<Button onClick={onGoHome} key={1} type="primary">
Back Home
</Button>
);
const goLoginButton = (
<Button onClick={goLoginPage} key={2} type="primary">
Login
</Button>
);
const errorResultProps = isEmpty(error)
? {
status: 404,
title: 'Not found',
subTitle: 'Sorry, the page you visited does not exist.',
extra: goHomeButton,
}
: {
status: [404, 403, 500].includes(error.status) ? error.status : 'error',
title: error.statusText,
subTitle: error.message,
extra: error.status === 401 ? [goHomeButton, goLoginButton] : goHomeButton,
};
return <Result {...errorResultProps} />;
};
export default ErrorPage;

View File

@@ -0,0 +1,3 @@
.ant-menu-item .anticon {
margin: 0;
}

View File

@@ -0,0 +1,141 @@
import React from 'react';
import { Menu, Badge, Spin, message } from 'antd';
import { isEmpty } from 'lodash';
import {
CreditCardOutlined,
LogoutOutlined,
LoginOutlined,
LoadingOutlined,
} from '@ant-design/icons';
import { useHistory, useLocation } from 'react-router-dom';
import { useAppState } from '../hooks/useAppState';
import { setLocaleToLS } from '../util/localStorageService';
import { changeLocaleDefaults } from '../api/axiosInstance';
import { emitter } from '../util/EventEmitter';
import './Header.css';
import { requireEmployee, requireCustomer, MESSAGE_TIMEOUT } from '../util/constants';
const { SubMenu } = Menu;
const keys = ['/', '/person', '/login', '/manage', '/invoice', '/invoices'];
const AVAILABLE_LOCALES = ['en', 'fr', 'de'];
const RELOAD_LOCATION_NUMBER = 0;
const Header = () => {
const history = useHistory();
const location = useLocation();
const { user, invoicedItems, locale, setLocale, loading } = useAppState();
const currentKey = [keys.find((key) => key === location.pathname)];
const haveInvoicedItems = !isEmpty(invoicedItems);
const invoicedItemsLength = invoicedItems.length;
const onChangeLocale = (value) => {
setLocaleToLS(value);
changeLocaleDefaults(value);
setLocale(value);
history.go(RELOAD_LOCATION_NUMBER);
};
const localeElements = AVAILABLE_LOCALES.filter((localeName) => localeName !== locale).map(
(curLocale) => (
<Menu.Item key={curLocale} onClick={() => onChangeLocale(curLocale)}>
{curLocale}
</Menu.Item>
)
);
const onUserLogout = () => {
emitter.emit('UPDATE_USER', undefined);
message.warn(
'Now you are not authenticated. Log in to use full functionality',
MESSAGE_TIMEOUT
);
history.push('/');
};
return (
<div
style={{
display: 'flex',
justifyContent: 'baseline',
alignItems: 'center',
paddingLeft: '15vh',
paddingRight: '15vh',
background: 'white',
}}
>
<Menu theme="light" mode="horizontal" style={{ width: '50%' }} selectedKeys={currentKey}>
<Menu.Item key="/" onClick={() => history.push('/')}>
Browse
</Menu.Item>
{!!user && (
<Menu.Item key="/person" onClick={() => history.push('/person')}>
Profile
</Menu.Item>
)}
{requireCustomer(user) && (
<Menu.Item key="/invoices" onClick={() => history.push('/invoices')}>
Invoices
</Menu.Item>
)}
{requireEmployee(user) && (
<Menu.Item key="/manage" onClick={() => history.push('/manage')}>
Manages
</Menu.Item>
)}
</Menu>
<Menu
style={{ width: '50%', display: 'flex', justifyContent: 'flex-end' }}
theme="light"
mode="horizontal"
selectedKeys={currentKey}
>
<Menu.Item>
{loading && <Spin indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />} />}
</Menu.Item>
{haveInvoicedItems && (
<Menu.Item
style={{
width: 40,
display: 'flex',
justifyContent: 'center',
}}
onClick={() => history.push('/invoice')}
key="/invoice"
>
<div
style={{
height: '100%',
}}
>
<Badge
size="default"
style={{ backgroundColor: '#2db7f5' }}
count={invoicedItemsLength}
>
<CreditCardOutlined style={{ fontSize: 16 }} />
</Badge>
</div>
</Menu.Item>
)}
<SubMenu title={locale}>{localeElements}</SubMenu>
{user ? (
<Menu.Item
onClick={onUserLogout}
danger
icon={<LogoutOutlined style={{ fontSize: 16 }} />}
/>
) : (
<Menu.Item
key="/login"
onClick={() => history.push('/login')}
icon={<LoginOutlined style={{ fontSize: 16 }} />}
/>
)}
</Menu>
</div>
);
};
export default Header;

View File

@@ -0,0 +1,101 @@
import React from 'react';
import { Table, Button, message } from 'antd';
import { useHistory } from 'react-router-dom';
import { useAppState } from '../hooks/useAppState';
import { invoice } from '../api/calls';
import { useErrors } from '../hooks/useErrors';
import { MESSAGE_TIMEOUT } from '../util/constants';
const columns = [
{
title: 'Name',
dataIndex: 'name',
},
{
title: 'Artist',
dataIndex: 'artist',
},
{
title: 'Album',
dataIndex: 'albumTitle',
},
{
title: 'Price',
dataIndex: 'unitPrice',
},
];
const InvoicePage = () => {
const history = useHistory();
const { handleError } = useErrors();
const { user, invoicedItems, setInvoicedItems, setLoading } = useAppState();
const data = invoicedItems.map(({ ID, ...otherProps }) => ({
key: `invoiceItem${ID}`,
...otherProps,
}));
const onBuy = () => {
setLoading(true);
invoice(
invoicedItems.map(({ ID }) => ({
ID,
}))
)
.then(() => {
setInvoicedItems([]);
message.success('Invoice successfully completed', MESSAGE_TIMEOUT);
history.push('/invoices');
})
.catch(handleError)
.finally(() => setLoading(false));
};
const onCancel = () => {
setInvoicedItems([]);
history.push('/');
};
const goLogin = () => {
history.push('/login');
};
return (
<div style={{ backgroundColor: 'white', padding: 10 }}>
<Table
bordered={false}
pagination={false}
columns={columns}
dataSource={data}
size="middle"
footer={() => (
<div
style={{
display: 'flex',
justifyContent: 'flex-end',
padding: 5,
}}
>
{user ? (
<>
<Button type="primary" size="large" onClick={onBuy}>
Buy
</Button>
<Button size="large" style={{ marginLeft: 5 }} onClick={onCancel} danger>
Cancel
</Button>
</>
) : (
<section>
<Button type="primary" size="large" onClick={goLogin}>
Login
</Button>
<span> to buy selected</span>
</section>
)}
</div>
)}
/>
</div>
);
};
export default InvoicePage;

View File

@@ -0,0 +1,107 @@
import React from 'react';
import { Form, Input, Button, Checkbox, message } from 'antd';
import { useHistory } from 'react-router-dom';
import { login } from '../api/calls';
import { useAppState } from '../hooks/useAppState';
import { useErrors } from '../hooks/useErrors';
import { MESSAGE_TIMEOUT } from '../util/constants';
import { emitter } from '../util/EventEmitter';
const layout = {
labelCol: {
span: 8,
},
wrapperCol: {
span: 8,
},
};
const tailLayout = {
wrapperCol: {
offset: 8,
span: 8,
},
};
const Login = () => {
const [form] = Form.useForm();
const history = useHistory();
const { setLoading, setInvoicedItems } = useAppState();
const { handleError } = useErrors();
const onFinish = (values) => {
setLoading(true);
login({ email: values.email, password: values.password })
.then(({ data: user }) => {
emitter.emit('UPDATE_USER', user);
if (user.roles.includes('employee')) {
setInvoicedItems([]);
}
history.push('/');
})
.catch((error) => {
console.log(error);
if (error.response && error.response.status === 401) {
form.resetFields();
message.error('Invalid credentials!', MESSAGE_TIMEOUT);
} else {
handleError(error);
}
})
.finally(() => setLoading(false));
};
const onFinishFailed = (errorInfo) => {
console.log('Validation Failed:', errorInfo);
};
return (
<Form
form={form}
{...layout}
name="basic"
initialValues={{
remember: true,
}}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
>
<Form.Item
label="Email"
name="email"
rules={[
{
required: true,
message: 'Please input your email!',
},
]}
>
<Input />
</Form.Item>
<Form.Item
label="Password"
name="password"
rules={[
{
required: true,
message: 'Please input your password!',
},
]}
>
<Input.Password style={{}} />
</Form.Item>
<Form.Item {...tailLayout} name="remember" valuePropName="checked">
<Checkbox>Remember me</Checkbox>
</Form.Item>
<Form.Item {...tailLayout}>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
};
export default Login;

View File

@@ -0,0 +1,115 @@
import React, { useState, useMemo, useEffect } from 'react';
import { Form, Radio, Button, message } from 'antd';
import { TrackForm } from './manage-store/TrackForm';
import { AddArtistForm } from './manage-store/AddArtistForm';
import { AddAlbumForm } from './manage-store/AddAlbumForm';
import { useErrors } from '../hooks/useErrors';
import { useAppState } from '../hooks/useAppState';
import { addTrack, addArtist, addAlbum } from '../api/calls';
import { MESSAGE_TIMEOUT } from '../util/constants';
const FORM_TYPES = {
track: 'track',
artist: 'artist',
album: 'album',
playlist: '',
};
const chooseForm = (type) => {
return (
(type === 'track' && <TrackForm />) ||
(type === 'artist' && <AddArtistForm />) ||
(type === 'album' && <AddAlbumForm />)
);
};
const ManageStore = () => {
const [form] = Form.useForm();
const { handleError } = useErrors();
const { setLoading } = useAppState();
const [formType, setFormType] = useState('track');
useEffect(() => {
form.resetFields();
}, [formType]);
const formElement = useMemo(() => {
return chooseForm(formType);
}, [formType]);
const onChangeForm = (event) => {
setFormType(event.target.value);
};
const sendCreateRequest = ({ type, ...data }) => {
setLoading(true);
let promise;
switch (type) {
case FORM_TYPES.track:
promise = addTrack({
name: data.name,
composer: data.composer,
album: { ID: data.albumID },
genre: { ID: data.genreID },
unitPrice: data.unitPrice.toString(),
});
break;
case FORM_TYPES.artist:
promise = addArtist(data);
break;
case FORM_TYPES.album:
promise = addAlbum({ title: data.name, artist: { ID: data.artistID } });
break;
default:
}
promise
.then(() => {
message.success('Entity successfully created', MESSAGE_TIMEOUT);
form.resetFields();
})
.catch(handleError)
.finally(() => setLoading(false));
};
return (
<Form
style={{ width: 700 }}
form={form}
labelCol={{
span: 4,
}}
wrapperCol={{
span: 14,
}}
layout="horizontal"
initialValues={{
type: formType,
}}
type={formType}
onFinish={sendCreateRequest}
onFinishFailed={() => console.log('Not valid params provided')}
>
<Form.Item label="Entity" name="type">
<Radio.Group onChange={onChangeForm}>
<Radio.Button value="track">Track</Radio.Button>
<Radio.Button value="album">Album</Radio.Button>
<Radio.Button value="artist">Artist</Radio.Button>
</Radio.Group>
</Form.Item>
{formElement}
<Form.Item
type="primary"
wrapperCol={{
span: 14,
offset: 4,
}}
>
<Button onClick={() => form.submit()}>Create</Button>
</Form.Item>
</Form>
);
};
export default ManageStore;

View File

@@ -0,0 +1,170 @@
import React, { useState, useEffect, useMemo, useCallback } from 'react';
import PropTypes from 'prop-types';
import { Button, message, Tag, Collapse, Table, Spin } from 'antd';
import moment from 'moment';
import { useErrors } from '../hooks/useErrors';
import { useAppState } from '../hooks/useAppState';
import { cancelInvoice, fetchInvoices } from '../api/calls';
import { MESSAGE_TIMEOUT } from '../util/constants';
const { Panel } = Collapse;
const INVOICE_STATUS = {
2: {
tagTitle: 'Shipped',
color: 'green',
},
1: {
tagTitle: 'Submitted',
color: 'processing',
canCancel: true,
},
'-1': {
tagTitle: 'Cancelled',
color: 'default',
},
};
const CANCELLED_STATUS = -1;
const DATE_TIME_FORMAT_PATTERN = 'LLLL';
const UTC_DATE_TIME_FORMAT = 'YYYY-MM-DDThh:mm:ssZ';
const INVOICE_ITEMS_COLUMNS = [
{
title: 'Track name',
dataIndex: 'name',
},
{
title: 'Artist',
dataIndex: 'artistName',
},
{
title: 'Album',
dataIndex: 'albumTitle',
},
{
title: 'Price',
dataIndex: 'unitPrice',
},
];
const LEVERAGE_DURATION = 1; // in hours
const STATUSES = { submitted: 1, shipped: 2, canceled: -1 };
const isLeverageTimeExpired = (utcNowTimestamp, invoiceDate) => {
const duration = moment.duration(moment(utcNowTimestamp).diff(moment(invoiceDate).valueOf()));
return duration.asHours() > LEVERAGE_DURATION;
};
const chooseStatus = (utcNowTimestamp, invoiceDate, statusFromDb) => {
if (isLeverageTimeExpired(utcNowTimestamp, invoiceDate) && statusFromDb !== STATUSES.canceled) {
return INVOICE_STATUS[STATUSES.shipped];
}
return INVOICE_STATUS[statusFromDb];
};
const ExtraHeader = ({ ID, invoiceDate, status: initialStatus }) => {
const { loading, setLoading } = useAppState();
const { handleError } = useErrors();
const [loadingHeaderId, setLoadingHeaderId] = useState();
const [status, setStatus] = useState(initialStatus);
const statusConfig = useMemo(() => {
const utcNowTimestamp = moment(moment().utc().format(UTC_DATE_TIME_FORMAT)).valueOf();
return chooseStatus(utcNowTimestamp, invoiceDate, status);
}, [status]);
const onCancelInvoice = (event, ID) => {
event.stopPropagation();
setLoading(true);
setLoadingHeaderId(ID);
cancelInvoice(ID)
.then(() => {
message.success('Invoice successfully cancelled', MESSAGE_TIMEOUT);
setLoadingHeaderId(undefined);
setStatus(CANCELLED_STATUS);
})
.catch(handleError)
.finally(() => setLoading(false));
};
return (
<Spin spinning={loading && loadingHeaderId === ID}>
<Tag color={statusConfig.color}>{statusConfig.tagTitle}</Tag>
{statusConfig.canCancel && (
<Button onClick={(event) => onCancelInvoice(event, ID)} size="small" danger>
Cancel
</Button>
)}
</Spin>
);
};
ExtraHeader.propTypes = {
ID: PropTypes.number.isRequired,
status: PropTypes.number.isRequired,
invoiceDate: PropTypes.string.isRequired,
};
const MyInvoicesPage = () => {
const { handleError } = useErrors();
const { setLoading } = useAppState();
const [invoices, setInvoices] = useState([]);
useEffect(() => {
setLoading(true);
fetchInvoices()
.then(({ data: { value } }) => setInvoices(value))
.catch(handleError)
.finally(() => setLoading(false));
}, []);
const genExtra = useCallback(
(ID, status, invoiceDate) => <ExtraHeader ID={ID} status={status} invoiceDate={invoiceDate} />,
[]
);
const invoiceElements = useMemo(() => {
return invoices.map(({ ID, status, invoiceDate, total, invoiceItems }) => {
const invoiceItemsData = invoiceItems.map(
({
ID,
track: {
name,
unitPrice,
album: {
title: albumTitle,
artist: { name: artistName },
},
},
}) => ({
key: ID,
ID,
name,
unitPrice,
albumTitle,
artistName,
})
);
return (
<Panel
header={moment(invoiceDate).format(DATE_TIME_FORMAT_PATTERN)}
key={ID}
extra={genExtra(ID, status, invoiceDate)}
>
<div>
<Table
bordered={false}
pagination={false}
columns={INVOICE_ITEMS_COLUMNS}
dataSource={invoiceItemsData}
size="middle"
footer={() => <span style={{ fontWeight: 600 }}>{`Total price: ${total}`}</span>}
/>
</div>
</Panel>
);
});
}, [invoices]);
return (
<div>{invoiceElements && <Collapse expandIconPosition="left">{invoiceElements}</Collapse>}</div>
);
};
export default MyInvoicesPage;

View File

@@ -0,0 +1,108 @@
import React, { useState } from 'react';
import { Form, Button, message, Input } from 'antd';
import { omit, map } from 'lodash';
import { fetchPerson, confirmPerson } from '../api/calls';
import { useErrors } from '../hooks/useErrors';
import { useAppState } from '../hooks/useAppState';
import { MESSAGE_TIMEOUT } from '../util/constants';
import { useAbortableEffect } from '../hooks/useAbortableEffect';
const PERSON_PROP = {
address: 'Address ',
city: 'City ',
country: 'Country ',
fax: 'Fax: ',
firstName: 'First name: ',
lastName: 'Last name: ',
phone: 'Phone: ',
postalCode: 'Postal code: ',
state: 'State',
email: 'email',
company: 'Company: ',
};
const PersonPage = () => {
const { setLoading } = useAppState();
const { handleError } = useErrors();
const [form] = Form.useForm();
const [person, setPerson] = useState({
lastName: '',
firstName: '',
city: '',
state: '',
address: '',
country: '',
phone: '',
postalCode: '',
fax: '',
email: '',
company: '',
});
useAbortableEffect((status) => {
setLoading(true);
fetchPerson()
.then(({ data }) => {
const personData = omit(data, '@odata.context', 'ID');
if (!status.aborted) {
setPerson(personData);
}
})
.catch(handleError)
.finally(() => setLoading(false));
}, []);
const onConfirmChanges = (newPerson) => {
setLoading(true);
confirmPerson(newPerson)
.then(() => {
message.success('Person successfully updated', MESSAGE_TIMEOUT);
})
.catch(handleError)
.finally(() => setLoading(false));
};
const personProperties = map(Object.keys(person), (currentKey) => (
<div key={currentKey}>
<Form.Item label={PERSON_PROP[currentKey]} name={currentKey}>
<Input />
</Form.Item>
</div>
));
return (
<>
{person.lastName !== '' && (
<Form
form={form}
labelCol={{
span: 4,
}}
wrapperCol={{
span: 14,
}}
layout="horizontal"
onFinish={onConfirmChanges}
onFinishFailed={() => console.log('Not valid params provided')}
initialValues={{
...person,
}}
>
{personProperties}
<Form.Item
type="primary"
wrapperCol={{
span: 14,
offset: 4,
}}
>
<Button onClick={() => form.submit()}>Confirm changes</Button>
</Form.Item>
</Form>
)}
</>
);
};
export default PersonPage;

View File

@@ -0,0 +1,58 @@
import React from 'react';
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
import { isEmpty } from 'lodash';
import TracksContainer from './TracksPage';
import Header from './Header';
import PersonPage from './PersonPage';
import ErrorPage from './ErrorPage';
import InvoicePage from './InvoicePage';
import ManageStore from './ManageStore';
import MyInvoicesPage from './MyInvoicesPage';
import Login from './Login';
import { withRestrictions } from '../hocs/withRestrictions';
import { requireEmployee } from '../util/constants';
const RestrictedLogin = withRestrictions(Login, ({ user }) => !user);
const RestrictedInvoicePage = withRestrictions(
InvoicePage,
({ user, invoicedItems }) => !requireEmployee(user) && !isEmpty(invoicedItems)
);
const RestrictedPersonPage = withRestrictions(PersonPage, ({ user }) => !!user);
const RestrictedManageStore = withRestrictions(ManageStore, ({ user }) => requireEmployee(user));
const MyRouter = () => {
return (
<Router>
<Header />
<div style={{ padding: '2em 20vh' }}>
<React.Suspense fallback={<div>Loading...</div>}>
<Switch>
<Route exact path={['/index.html', '/tracks', '/']}>
<TracksContainer />
</Route>
<Route exact path="/person">
<RestrictedPersonPage />
</Route>
<Route exact path="/login">
<RestrictedLogin />
</Route>
<Route exact path="/invoice">
<RestrictedInvoicePage />
</Route>
<Route exact path="/invoices">
<MyInvoicesPage />
</Route>
<Route exact path="/manage">
<RestrictedManageStore />
</Route>
<Route path="/">
<ErrorPage />
</Route>
</Switch>
</React.Suspense>
</div>
</Router>
);
};
export { MyRouter };

View File

@@ -0,0 +1,4 @@
.ant-select > div.ant-select-selector {
padding: 5px;
min-width: 300px;
}

View File

@@ -0,0 +1,221 @@
import React, { useState } from 'react';
import { debounce } from 'lodash';
import { Input, Col, Row, Select, Pagination } from 'antd';
import { Track } from './tracks/Track';
import { ManagedTrack } from './tracks/ManagedTrack';
import { useAppState } from '../hooks/useAppState';
import { useErrors } from '../hooks/useErrors';
import { fetchTacks, countTracks, fetchGenres } from '../api/calls';
import { useAbortableEffect } from '../hooks/useAbortableEffect';
import { requireEmployee } from '../util/constants';
import './TracksPage.css';
const { Search } = Input;
const { Option } = Select;
const DEBOUNCE_TIMER = 500;
const DEBOUNCE_OPTIONS = {
leading: true,
trailing: false,
};
const isEven = (value) => {
return value % 2 === 0;
};
const renderGenres = (genres) =>
genres.map(({ ID, name }) => (
<Option key={ID} value={ID.toString()}>
{name}
</Option>
));
const TracksContainer = () => {
const { setLoading, user } = useAppState();
const { handleError } = useErrors();
const [state, setState] = useState({
tracks: [],
genres: [],
pagination: {
currentPage: 1,
totalItems: 0,
pageSize: 20,
},
searchOptions: {
substr: '',
genreIds: [],
},
});
useAbortableEffect((status) => {
setLoading(true);
const countTracksReq = countTracks();
const getTracksRequest = fetchTacks();
const getGenresReq = fetchGenres();
Promise.all([countTracksReq, getTracksRequest, getGenresReq])
.then(
([
{ data: totalItems },
{
data: { value: tracks },
},
{
data: { value: genres },
},
]) => {
if (!status.aborted) {
setState({
...state,
tracks,
genres,
pagination: { ...state.pagination, totalItems },
});
}
}
)
.catch(handleError)
.finally(() => setLoading(false));
}, []);
const onSearch = debounce(
() => {
setLoading(true);
const options = {
$top: state.pagination.pageSize,
substr: state.searchOptions.substr.replace(/'*/g, (value) =>
isEven(value.length) ? value : `${value}'`
),
genreIds: state.searchOptions.genreIds,
};
Promise.all([
fetchTacks(options),
countTracks({
substr: options.substr,
genreIds: options.genreIds,
}),
])
.then(([{ data: { value: tracks } }, { data: totalItems }]) =>
setState({
...state,
tracks,
pagination: { ...state.pagination, totalItems },
})
)
.catch(handleError)
.finally(() => setLoading(false));
},
DEBOUNCE_TIMER,
DEBOUNCE_OPTIONS
);
const onSelectChange = (genres) => {
setState({
...state,
searchOptions: {
...state.searchOptions,
genreIds: genres.map((value) => parseInt(value, 10)),
},
});
};
const onSearchChange = (event) => {
setState({
...state,
searchOptions: { ...state.searchOptions, substr: event.target.value },
});
};
const onChangePage = (pageNumber) => {
document.querySelector('section.ant-layout').scrollTo({ top: 0, left: 0, behavior: 'smooth' });
setLoading(true);
const options = {
$top: state.pagination.pageSize,
substr: state.searchOptions.substr,
genreIds: state.searchOptions.genreIds,
$skip: (pageNumber - 1) * state.pagination.pageSize,
};
fetchTacks(options)
.then((response) =>
setState({
...state,
tracks: response.data.value,
pagination: { ...state.pagination, currentPage: pageNumber },
})
)
.catch(handleError)
.finally(() => setLoading(false));
};
const deleteTrack = (ID) => {
setState({
...state,
tracks: state.tracks.filter(({ ID: curID }) => curID !== ID),
});
};
const renderTracks = (tracks) => {
const isEmployee = requireEmployee(user);
const TrackComponent = isEmployee ? ManagedTrack : Track;
return tracks.map((track) => {
const isAlreadyOrdered = !isEmployee && track.alreadyOrdered;
const onDeleteTrack = isEmployee && ((ID) => deleteTrack(ID));
return (
<Col key={`track-col${track.ID}`} className="gutter-row" span={8}>
<TrackComponent
initialTrack={track}
onDeleteTrack={onDeleteTrack}
isAlreadyOrdered={isAlreadyOrdered}
/>
</Col>
);
});
};
const trackElements = renderTracks(state.tracks);
const genreElements = renderGenres(state.genres);
return (
<>
<div
style={{
display: 'flex',
alignItems: 'start',
maxWidth: 600,
paddingBottom: 10,
}}
>
<Select
mode="multiple"
allowClear
style={{ marginRight: 10, borderRadius: 6 }}
placeholder="Genres"
onChange={(value) => onSelectChange(value)}
>
{genreElements}
</Select>
<Search
style={{
borderRadius: 6,
}}
placeholder="Search tracks"
size="large"
onSearch={onSearch}
onChange={onSearchChange}
/>
</div>
<div>
<Row gutter={[{ xs: 8, sm: 16, md: 24, lg: 32 }, 24]}>{trackElements}</Row>
</div>
<div style={{ display: 'flex', justifyContent: 'center' }}>
<Pagination
showSizeChanger={false}
defaultCurrent={1}
total={state.pagination.totalItems}
pageSize={state.pagination.pageSize}
onChange={onChangePage}
/>
</div>
</>
);
};
export default TracksContainer;

View File

@@ -0,0 +1,62 @@
import React, { useEffect } from 'react';
import { Form, Input, Select } from 'antd';
import { useSearch } from '@umijs/hooks';
import { useErrors } from '../../hooks/useErrors';
import { fetchArtistsByName } from '../../api/calls';
const REQUIRED = [
{
required: true,
message: 'This filed is required!',
},
];
const ARTISTS_LIMIT = 10;
const getArtists = function (value) {
return fetchArtistsByName(value, ARTISTS_LIMIT)
.then((response) => response.data.value)
.catch(this.handleError);
};
const AddAlbumForm = () => {
const { handleError } = useErrors();
const {
data: artists,
loading: isArtistsLoading,
onChange: onChangeArtistInput,
cancel: onArtistCancel,
} = useSearch(getArtists.bind({ handleError }));
useEffect(() => {
onChangeArtistInput();
}, []);
return (
<>
<h3>Add album</h3>
<Form.Item label="Name" name="name" rules={REQUIRED}>
<Input />
</Form.Item>
<Form.Item label="Artist" name="artistID" rules={REQUIRED}>
<Select
showSearch
placeholder="Select artist"
filterOption={false}
onSearch={onChangeArtistInput}
loading={isArtistsLoading}
onBlur={onArtistCancel}
style={{ width: 300 }}
>
{artists &&
artists.map((artist) => (
<Select.Option key={artist.name} value={artist.ID}>
{artist.name}
</Select.Option>
))}
</Select>
</Form.Item>
</>
);
};
export { AddAlbumForm };

View File

@@ -0,0 +1,22 @@
import React from 'react';
import { Form, Input } from 'antd';
const REQUIRED = [
{
required: true,
message: 'This filed is required!',
},
];
const AddArtistForm = () => {
return (
<>
<h3>Add artist</h3>
<Form.Item label="Name" name="name" rules={REQUIRED}>
<Input />
</Form.Item>
</>
);
};
export { AddArtistForm };

View File

@@ -0,0 +1,96 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { Form, Input, Select, InputNumber } from 'antd';
import { head } from 'lodash';
import { useSearch } from '@umijs/hooks';
import { useAppState } from '../../hooks/useAppState';
import { fetchAlbumsByName, fetchGenres } from '../../api/calls';
import { useErrors } from '../../hooks/useErrors';
const ALBUMS_LIMIT = 10;
const REQUIRED = [
{
required: true,
message: 'This filed is required!',
},
];
function getAlbums(value) {
return fetchAlbumsByName(value, ALBUMS_LIMIT)
.then((response) => response.data.value)
.catch(this.handleError);
}
const TrackForm = ({ initialAlbumTitle }) => {
const { handleError } = useErrors();
const {
data: albums,
loading: isAlbumsLoading,
onChange: onChangeAlbumInput,
cancel: onAlbumCancel,
} = useSearch(getAlbums.bind({ handleError }));
const { setLoading } = useAppState();
const [genres, setGenres] = useState([]);
useEffect(() => {
setLoading(true);
Promise.all([fetchGenres(), onChangeAlbumInput(initialAlbumTitle)])
.then((responses) => setGenres(head(responses).data.value))
.catch(handleError)
.finally(() => setLoading(false));
}, []);
return (
<div>
<Form.Item label="Name" name="name" rules={REQUIRED}>
<Input />
</Form.Item>
<Form.Item label="Composer" name="composer" rules={REQUIRED}>
<Input />
</Form.Item>
<Form.Item label="Album" name="albumID" rules={REQUIRED}>
<Select
showSearch
placeholder="Select album"
filterOption={false}
onSearch={onChangeAlbumInput}
loading={isAlbumsLoading}
onBlur={onAlbumCancel}
>
{albums &&
albums.map((album) => (
<Select.Option key={album.title} value={album.ID}>
{album.title}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item label="Genre" name="genreID" rules={REQUIRED}>
<Select showSearch placeholder="Select genre" filterOption={false}>
{genres &&
genres.map((genre) => (
<Select.Option key={genre.name} value={genre.ID}>
{genre.name}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item label="Unit price" name="unitPrice" precision={2} rules={REQUIRED}>
<InputNumber
precision={2}
decimalSeparator="."
parser={(value) => value.replace(/\$\s?|(,*)/g, '')}
/>
</Form.Item>
</div>
);
};
TrackForm.propTypes = {
initialAlbumTitle: PropTypes.string,
};
TrackForm.defaultProps = {
initialAlbumTitle: undefined,
};
export { TrackForm };

View File

@@ -0,0 +1,44 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { Modal, message } from 'antd';
import { DeleteOutlined } from '@ant-design/icons';
import { deleteTrack } from '../../api/calls';
import { useErrors } from '../../hooks/useErrors';
import { MESSAGE_TIMEOUT } from '../../util/constants';
const DeleteAction = ({ ID, onDeleteTrack }) => {
const [modalVisible, setModalVisible] = useState(false);
const { handleError } = useErrors();
const onOk = () => {
setModalVisible(false);
deleteTrack(ID)
.then(() => {
onDeleteTrack();
setModalVisible(false);
message.success('Track successfully deleted!', MESSAGE_TIMEOUT);
})
.catch(handleError);
};
const onCancel = () => setModalVisible(false);
const onOpenModal = () => {
setModalVisible(true);
};
return (
<>
<DeleteOutlined onClick={onOpenModal}>Delete</DeleteOutlined>
<Modal title="Confirm" visible={modalVisible} onOk={onOk} onCancel={onCancel}>
<p>Are You really want to delete this track?</p>
</Modal>
</>
);
};
DeleteAction.propTypes = {
ID: PropTypes.number.isRequired,
onDeleteTrack: PropTypes.func.isRequired,
};
export { DeleteAction };

View File

@@ -0,0 +1,117 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button, Modal, Form, message } from 'antd';
import { EditOutlined, LoadingOutlined } from '@ant-design/icons';
import { useErrors } from '../../hooks/useErrors';
import { TrackForm } from '../manage-store/TrackForm';
import { updateTrack, getTrack } from '../../api/calls';
import { MESSAGE_TIMEOUT } from '../../util/constants';
const EditAction = ({ ID, name, composer, genre, unitPrice, album, afterTrackUpdate }) => {
const [visible, setVisible] = React.useState(false);
const [confirmLoading, setConfirmLoading] = React.useState(false);
const [updateLoading, setUpdateLoading] = React.useState(false);
const [form] = Form.useForm();
const { handleError } = useErrors();
const onShowModal = () => {
setVisible(true);
};
const afterCloseModal = () => {
setUpdateLoading(true);
getTrack(ID)
.then((response) => {
afterTrackUpdate(response.data);
setUpdateLoading(false);
})
.catch(handleError);
};
const onFinish = (value) => {
setConfirmLoading(true);
updateTrack({
ID,
name: value.name,
composer: value.composer,
album: { ID: value.albumID },
genre: { ID: value.genreID },
unitPrice: value.unitPrice.toString(),
})
.then(() => {
message.success('Track successfully updated!', MESSAGE_TIMEOUT);
setConfirmLoading(false);
setVisible(false);
afterCloseModal();
})
.catch(handleError);
};
const handleOk = () => {
form.submit();
};
const handleCancel = () => {
setVisible(false);
};
return (
<>
{updateLoading ? <LoadingOutlined /> : <EditOutlined onClick={onShowModal} />}
<Modal
title="Edit track"
visible={visible}
confirmLoading={confirmLoading}
onOk={handleOk}
onCancel={handleCancel}
width={600}
footer={[
<Button key="back" onClick={handleCancel}>
Cancel
</Button>,
<Button key="submit" type="primary" loading={confirmLoading} onClick={handleOk}>
Submit
</Button>,
]}
>
<Form
form={form}
labelCol={{
span: 4,
}}
wrapperCol={{
span: 14,
}}
layout="horizontal"
onFinish={onFinish}
onFinishFailed={() => console.log('Not valid params provided')}
initialValues={{
name,
composer,
genreID: genre.ID,
albumID: album.ID,
unitPrice,
}}
>
<TrackForm initialAlbumTitle={album.title} />
</Form>
</Modal>
</>
);
};
EditAction.propTypes = {
ID: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
composer: PropTypes.string,
genre: PropTypes.object.isRequired,
unitPrice: PropTypes.number.isRequired,
album: PropTypes.object.isRequired,
afterTrackUpdate: PropTypes.func.isRequired,
};
EditAction.defaultProps = {
composer: undefined,
};
export { EditAction };

View File

@@ -0,0 +1,7 @@
span > span.anticon.anticon-delete:hover {
color: #ff4d4f;
}
.card-element {
transition: opacity 0.5s ease-in-out;
}

View File

@@ -0,0 +1,48 @@
import React, { useState, useRef } from 'react';
import { Card } from 'antd';
import PropTypes from 'prop-types';
import { EditAction } from './EditAction';
import { DeleteAction } from './DeleteAction';
import { TrackCardBody } from './TrackCardBody';
import './ManagedTrack.css';
const ManagedTrack = ({ initialTrack, onDeleteTrack }) => {
const trackElement = useRef();
const [track, setTrack] = useState(initialTrack);
return (
<div className="card-element" ref={trackElement}>
<Card
actions={[
<DeleteAction
ID={track.ID}
onDeleteTrack={() => {
trackElement.current.style.opacity = 0;
setTimeout(() => onDeleteTrack(track.ID), 500);
}}
/>,
<EditAction
ID={track.ID}
name={track.name}
composer={track.composer}
album={track.album}
genre={track.genre}
unitPrice={track.unitPrice}
afterTrackUpdate={(value) => setTrack(value)}
/>,
]}
title={track.name}
bordered={false}
>
<TrackCardBody track={track} />
</Card>
</div>
);
};
ManagedTrack.propTypes = {
initialTrack: PropTypes.object.isRequired,
onDeleteTrack: PropTypes.func.isRequired,
};
export { ManagedTrack };

View File

@@ -0,0 +1,63 @@
import React, { useState, useRef } from 'react';
import PropTypes from 'prop-types';
import { Card, Button } from 'antd';
import { PlusOutlined, MinusOutlined } from '@ant-design/icons';
import { useAppState } from '../../hooks/useAppState';
import { TrackCardBody } from './TrackCardBody';
const Track = ({ initialTrack, isAlreadyOrdered }) => {
const trackElement = useRef();
const { setInvoicedItems, invoicedItems } = useAppState();
const [isJustInvoiced, setIsJustInvoiced] = useState(
invoicedItems.find((curTrack) => curTrack.ID === initialTrack.ID)
);
const onChangedStatus = () => {
const newIsJustInvoiced = !isJustInvoiced;
if (newIsJustInvoiced) {
setInvoicedItems([
...invoicedItems,
{
ID: initialTrack.ID,
name: initialTrack.name,
artist: initialTrack.album.artist.name,
albumTitle: initialTrack.album.title,
unitPrice: initialTrack.unitPrice,
},
]);
} else {
setInvoicedItems(invoicedItems.filter(({ ID: curID }) => curID !== initialTrack.ID));
}
setIsJustInvoiced(newIsJustInvoiced);
};
return (
<div className="card-element" ref={trackElement}>
<Card
actions={[
<>
{!isAlreadyOrdered && (
<Button onClick={onChangedStatus} danger={isJustInvoiced}>
{isJustInvoiced ? <MinusOutlined /> : <PlusOutlined />}
</Button>
)}
</>,
]}
title={initialTrack.name}
bordered={false}
>
<TrackCardBody track={initialTrack} />
</Card>
</div>
);
};
Track.propTypes = {
initialTrack: PropTypes.object.isRequired,
isAlreadyOrdered: PropTypes.bool,
};
Track.defaultProps = {
isAlreadyOrdered: undefined,
};
export { Track };

View File

@@ -0,0 +1,41 @@
import React from 'react';
import PropTypes from 'prop-types';
const TrackCardBody = ({ track }) => {
return (
<>
<div>
Artist:
<span style={{ fontWeight: 600 }}>{track.album.artist.name}</span>
</div>
<div>
Album:
<span style={{ fontWeight: 600 }}>{track.album.title}</span>
</div>
<div>
Genre:
<span style={{ fontWeight: 600 }}>{track.genre.name}</span>
</div>
<div>
{track.composer && (
<span>
Compositor:
<span style={{ fontWeight: 600 }}>{track.composer}</span>
</span>
)}
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span>
Price:
<span style={{ fontWeight: 600 }}>{track.unitPrice}</span>
</span>
</div>
</>
);
};
TrackCardBody.propTypes = {
track: PropTypes.object.isRequired,
};
export { TrackCardBody };

View File

@@ -0,0 +1,66 @@
import React, { useMemo, createContext, useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { getUserFromLS, getLocaleFromLS, setUserToLS } from '../util/localStorageService';
import { changeUserDefaults } from '../api/axiosInstance';
import { emitter } from '../util/EventEmitter';
const globalContext = {
error: {},
loading: true,
user: {
ID: undefined,
roles: [],
email: undefined,
accessToken: undefined,
refreshToken: undefined,
},
locale: undefined,
invoicedItems: [],
notifications: [],
};
const AppStateContext = createContext(globalContext);
const AppStateContextProvider = ({ children }) => {
const [invoicedItems, setInvoicedItems] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState({});
const [user, setUser] = useState(getUserFromLS());
const [locale, setLocale] = useState(getLocaleFromLS());
useEffect(() => {
const updateUser = (newUser) => {
console.log('USER_UPDATE WAS TRIGGERED');
changeUserDefaults(newUser);
setUserToLS(newUser);
setUser(newUser);
};
emitter.on('UPDATE_USER', updateUser);
return () => {
emitter.removeListener('UPDATE_USER', updateUser);
};
}, []);
const value = useMemo(
() => ({
error,
loading,
invoicedItems,
user,
locale,
setLoading,
setError,
setInvoicedItems,
setUser,
setLocale,
}),
[locale, user, loading, error, invoicedItems]
);
return <AppStateContext.Provider value={value}>{children}</AppStateContext.Provider>;
};
AppStateContextProvider.propTypes = {
children: PropTypes.element.isRequired,
};
export { AppStateContextProvider, AppStateContext };

View File

@@ -0,0 +1,16 @@
import React from 'react';
import { Redirect } from 'react-router-dom';
import { useAppState } from '../hooks/useAppState';
const withRestrictions = (Component, isUserMeetRestrictions) => {
return (props) => {
const { user, invoicedItems } = useAppState();
return isUserMeetRestrictions({ user, invoicedItems }) ? (
<Component {...props} />
) : (
<Redirect exact to="/error" />
);
};
};
export { withRestrictions };

View File

@@ -0,0 +1,22 @@
import { useEffect } from 'react';
function useAbortableEffect(effect, dependencies) {
const status = {}; // mutable status object
useEffect(() => {
status.aborted = false;
// pass the mutable object to the effect callback
// store the returned value for cleanup
const cleanUpFn = effect(status);
return () => {
// mutate the object to signal the consumer
// this effect is cleaning up
status.aborted = true;
if (typeof cleanUpFn === 'function') {
// run the cleanup function
cleanUpFn();
}
};
}, [...dependencies]);
}
export { useAbortableEffect };

View File

@@ -0,0 +1,6 @@
import { useContext } from 'react';
import { AppStateContext } from '../contexts/AppStateContext';
const useAppState = () => useContext(AppStateContext);
export { useAppState };

View File

@@ -0,0 +1,34 @@
import { useHistory } from 'react-router-dom';
import { useAppState } from './useAppState';
const useErrors = () => {
const history = useHistory();
const { setError } = useAppState();
const handleError = (error) => {
console.error('Error', error);
if (error.response) {
const { status, statusText, data } = error.response;
setError({
status,
statusText,
message: data.error ? data.error.message : data,
});
} else {
setError({
status: '',
statusText: 'Error',
message: 'Something went wrong. Seems like request is too long',
});
}
history.push('/error');
};
return {
handleError,
};
};
export { useErrors };

View File

@@ -0,0 +1,11 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
// import * as serviceWorker from './serviceWorker';
ReactDOM.render(<App />, 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();

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
<g fill="#61DAFB">
<path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
<circle cx="420.9" cy="296.5" r="45.7"/>
<path d="M520.5 78.1z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

View File

@@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// 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';

View File

@@ -0,0 +1,5 @@
import EventEmitter from 'events';
const emitter = new EventEmitter();
export { emitter };

View File

@@ -0,0 +1,7 @@
export const AVAILABLE_LOCALES = ['en', 'fr', 'de'];
export const MESSAGE_TIMEOUT = 2;
export const requireEmployee = (user) => !!user && user.roles.includes('employee');
export const requireCustomer = (user) => !!user && user.roles.includes('customer');

View File

@@ -0,0 +1,36 @@
import { isValidUser } from './validateUser';
import { AVAILABLE_LOCALES } from './constants';
const setUserToLS = (user) => {
if (user) {
localStorage.setItem('user', JSON.stringify(user));
} else {
localStorage.removeItem('user');
}
};
const getUserFromLS = () => {
let userFromLS;
try {
userFromLS = JSON.parse(localStorage.getItem('user'));
if (isValidUser(userFromLS)) {
return userFromLS;
}
} catch (e) {
console.error('User from local storage are not valid');
}
return undefined;
};
const getLocaleFromLS = () => {
const localeFromLS = localStorage.getItem('locale');
return localeFromLS && localeFromLS !== 'undefined' && AVAILABLE_LOCALES.includes(localeFromLS)
? localeFromLS
: 'en';
};
const setLocaleToLS = (locale) => {
localStorage.setItem('locale', locale);
};
export { setLocaleToLS, getLocaleFromLS, getUserFromLS, setUserToLS };

View File

@@ -0,0 +1,18 @@
import { isArray, isEmpty, isString, isNumber } from 'lodash';
const CUSTOMER_ROLE = 'customer';
const EMPLOYEE_ROLE = 'employee';
const isValidUser = (user) => {
return (
!isEmpty(user) &&
isNumber(user.ID) &&
isArray(user.roles) &&
!!user.roles.some((role) => role === CUSTOMER_ROLE || role === EMPLOYEE_ROLE) &&
isString(user.email) &&
isString(user.accessToken) &&
isString(user.refreshToken)
);
};
export { isValidUser };

View File

@@ -0,0 +1,33 @@
const path = require('path');
const webpack = require('webpack');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyPlugin = require('copy-webpack-plugin');
const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
module.exports = {
plugins: [
new CleanWebpackPlugin({ dangerouslyAllowCleanPatternsOutsideProject: true }),
new HtmlWebpackPlugin({
template: path.join(__dirname, '../public/index.html'),
filename: path.join(__dirname, '../../build/index.html'),
publicPath: '/static/', // for js bundles path
}),
new InterpolateHtmlPlugin(HtmlWebpackPlugin, {
PUBLIC_URL: '',
}),
new CopyPlugin({
patterns: [
{
from: path.join(__dirname, '../public'),
to: path.join(__dirname, '../../build'),
globOptions: {
dot: true,
ignore: ['**/index.html'],
},
},
],
}),
new webpack.ProgressPlugin(),
],
};

View File

@@ -0,0 +1,19 @@
module.exports = {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /(node_modules)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react'],
},
},
},
{
test: /\.(png|jpg)$/,
use: [{ loader: 'url-loader' }],
},
],
};

View File

@@ -0,0 +1,68 @@
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
module.exports = {
entry: {
app: './src/index.jsx',
},
devtool: 'inline-source-map',
devServer: {
contentBase: './dist',
hot: true,
port: 3000,
compress: true, // compress files to gzip to increase download speed
disableHostCheck: false, // by default true, it is not recomended,
// because it makes app vulnerable to DNS rebinding attacks
open: true, // open the browser after server had been started
historyApiFallback: true,
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /(node_modules)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react'],
plugins: ['react-refresh/babel'].filter(Boolean),
},
},
},
{
test: /\.(png|jpg)$/,
use: [{ loader: 'url-loader' }],
},
{
test: /\.css$/,
use: [{ loader: 'style-loader' }, { loader: 'css-loader' }],
},
],
},
plugins: [
new CleanWebpackPlugin({ cleanStaleWebpackAssets: false }),
new HtmlWebpackPlugin({
template: path.join(__dirname, '../public/index.html'),
}),
new InterpolateHtmlPlugin(HtmlWebpackPlugin, {
PUBLIC_URL: '',
}),
new webpack.DefinePlugin({
'process.env.SERVICE_URL': JSON.stringify('http://localhost:4004/'),
}),
new webpack.ProgressPlugin(),
new webpack.HotModuleReplacementPlugin(), // for hot module replacement option of devServer
new ReactRefreshWebpackPlugin(),
].filter(Boolean),
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
resolve: { extensions: ['*', '.js', '.jsx'] },
};

View File

@@ -0,0 +1,25 @@
const path = require('path');
module.exports = {
entry: {
app: './src/index.jsx', // Bundle with our code
react: ['react', 'react-dom'],
lodash: ['lodash'],
moment: ['moment'],
events: ['events'],
axios: ['axios'],
antd: ['antd'],
},
output: {
// [name] - name of the entry (bundle),
// [checksum] or [hash] - to cache different bundles
// from update when developing (doing changes in the files)
filename: '[name].[fullhash].js',
// in this folder path bundles will be placed
path: path.resolve(__dirname, '../../build/static'),
// where you uploaded your bundled files. (Relative to server root)
// needs for react-router-dom
publicPath: '/static/',
},
resolve: { extensions: ['*', '.js', '.jsx'] },
};

View File

@@ -0,0 +1,25 @@
const webpack = require('webpack');
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
const { rules } = require('./common-rules');
const { plugins } = require('./common-plugins');
module.exports = merge(common, {
mode: 'development',
devtool: 'inline-source-map',
plugins: [
...plugins,
new webpack.DefinePlugin({
'process.env.SERVICE_URL': JSON.stringify('http://localhost:4004/'),
}),
],
module: {
rules: [
...rules,
{
test: /\.css$/,
use: [{ loader: 'style-loader' }, { loader: 'css-loader' }],
},
],
},
});

View File

@@ -0,0 +1,40 @@
const webpack = require('webpack');
const { merge } = require('webpack-merge');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const common = require('./webpack.common.js');
const { rules } = require('./common-rules');
const { plugins } = require('./common-plugins');
module.exports = merge(common, {
mode: 'production',
devtool: 'source-map',
plugins: [
...plugins,
new webpack.DefinePlugin({
'process.env.SERVICE_URL': JSON.stringify('api/'),
}),
new MiniCssExtractPlugin({
filename: '[name].css',
chunkFilename: '[id].css',
}),
],
optimization: {
splitChunks: {
// To split up js code to different bundles.
chunks: 'all', // Now bundle with our code will be cleaned up
}, // from vendors imports (2mb ~> 100kb)
minimize: true,
minimizer: [new TerserPlugin(), new CssMinimizerPlugin()], // to minimize file size
},
module: {
rules: [
...rules,
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader'],
},
],
},
});

View File

@@ -0,0 +1,349 @@
ID,title,artist_ID
1,For Those About To Rock We Salute You,1
2,Balls to the Wall,2
3,Restless and Wild,2
4,Let There Be Rock,1
5,Big Ones,3
6,Jagged Little Pill,4
7,Facelift,5
8,Warner 25 Anos,6
9,Plays Metallica By Four Cellos,7
10,Audioslave,8
11,Out Of Exile,8
12,BackBeat Soundtrack,9
13,The Best Of Billy Cobham,10
14,Alcohol Fueled Brewtality Live! [Disc 1],11
15,Alcohol Fueled Brewtality Live! [Disc 2],11
16,Black Sabbath,12
17,Black Sabbath Vol. 4 (Remaster),12
18,Body Count,13
19,Chemical Wedding,14
20,The Best Of Buddy Guy - The Millenium Collection,15
21,Prenda Minha,16
22,Sozinho Remix Ao Vivo,16
23,Minha Historia,17
24,Afrociberdelia,18
25,Da Lama Ao Caos,18
26,Acústico MTV [Live],19
27,Cidade Negra - Hits,19
28,Na Pista,20
29,Axé Bahia 2001,21
30,BBC Sessions [Disc 1] [Live],22
31,Bongo Fury,23
32,Carnaval 2001,21
33,Chill: Brazil (Disc 1),24
34,Chill: Brazil (Disc 2),6
35,Garage Inc. (Disc 1),50
36,Greatest Hits II,51
37,Greatest Kiss,52
38,Heart of the Night,53
39,International Superhits,54
40,Into The Light,55
41,Meus Momentos,56
42,Minha História,57
43,MK III The Final Concerts [Disc 1],58
44,Physical Graffiti [Disc 1],22
45,Sambas De Enredo 2001,21
46,Supernatural,59
47,The Best of Ed Motta,37
48,The Essential Miles Davis [Disc 1],68
49,The Essential Miles Davis [Disc 2],68
50,The Final Concerts (Disc 2),58
51,Up An' Atom,69
52,Vinícius De Moraes - Sem Limite,70
53,Vozes do MPB,21
54,"Chronicle, Vol. 1",76
55,"Chronicle, Vol. 2",76
56,Cássia Eller - Coleção Sem Limite [Disc 2],77
57,Cássia Eller - Sem Limite [Disc 1],77
58,Come Taste The Band,58
59,Deep Purple In Rock,58
60,Fireball,58
61,Knocking at Your Back Door: The Best Of Deep Purple in the 80's,58
62,Machine Head,58
63,Purpendicular,58
64,Slaves And Masters,58
65,Stormbringer,58
66,The Battle Rages On,58
67,Vault: Def Leppard's Greatest Hits,78
68,Outbreak,79
69,Djavan Ao Vivo - Vol. 02,80
70,Djavan Ao Vivo - Vol. 1,80
71,Elis Regina-Minha História,41
72,The Cream Of Clapton,81
73,Unplugged,81
74,Album Of The Year,82
75,Angel Dust,82
76,King For A Day Fool For A Lifetime,82
77,The Real Thing,82
78,Deixa Entrar,83
79,In Your Honor [Disc 1],84
80,In Your Honor [Disc 2],84
81,One By One,84
82,The Colour And The Shape,84
83,My Way: The Best Of Frank Sinatra [Disc 1],85
84,Roda De Funk,86
85,As Canções de Eu Tu Eles,27
86,Quanta Gente Veio Ver (Live),27
87,Quanta Gente Veio ver--Bônus De Carnaval,27
88,Faceless,87
89,American Idiot,54
90,Appetite for Destruction,88
91,Use Your Illusion I,88
92,Use Your Illusion II,88
93,Blue Moods,89
94,A Matter of Life and Death,90
95,A Real Dead One,90
96,A Real Live One,90
97,Brave New World,90
98,Dance Of Death,90
99,Fear Of The Dark,90
100,Iron Maiden,90
101,Killers,90
102,Live After Death,90
103,Live At Donington 1992 (Disc 1),90
104,Live At Donington 1992 (Disc 2),90
105,No Prayer For The Dying,90
106,Piece Of Mind,90
107,Powerslave,90
108,Rock In Rio [CD1],90
109,Rock In Rio [CD2],90
110,Seventh Son of a Seventh Son,90
111,Somewhere in Time,90
112,The Number of The Beast,90
113,The X Factor,90
114,Virtual XI,90
115,Sex Machine,91
116,Emergency On Planet Earth,92
117,Synkronized,92
118,The Return Of The Space Cowboy,92
119,Get Born,93
120,Are You Experienced?,94
121,Surfing with the Alien (Remastered),95
122,Jorge Ben Jor 25 Anos,46
123,Jota Quest-1995,96
124,Cafezinho,97
125,Living After Midnight,98
126,Unplugged [Live],52
127,BBC Sessions [Disc 2] [Live],22
128,Coda,22
129,Houses Of The Holy,22
130,In Through The Out Door,22
131,IV,22
132,Led Zeppelin I,22
133,Led Zeppelin II,22
134,Led Zeppelin III,22
135,Physical Graffiti [Disc 2],22
136,Presence,22
137,The Song Remains The Same (Disc 1),22
138,The Song Remains The Same (Disc 2),22
139,A TempestadeTempestade Ou O Livro Dos Dias,99
140,Mais Do Mesmo,99
141,Greatest Hits,100
142,Lulu Santos - RCA 100 Anos De Música - Álbum 01,101
143,Lulu Santos - RCA 100 Anos De Música - Álbum 02,101
144,Misplaced Childhood,102
145,Barulhinho Bom,103
146,Seek And Shall Find: More Of The Best (1963-1981),104
147,The Best Of Men At Work,105
148,Black Album,50
149,Garage Inc. (Disc 2),50
150,Kill 'Em All,50
151,Load,50
152,Master Of Puppets,50
153,ReLoad,50
154,Ride The Lightning,50
155,St. Anger,50
156,...And Justice For All,50
157,Miles Ahead,68
158,Milton Nascimento Ao Vivo,42
159,Minas,42
160,Ace Of Spades,106
161,Demorou...,108
162,Motley Crue Greatest Hits,109
163,From The Muddy Banks Of The Wishkah [Live],110
164,Nevermind,110
165,Compositores,111
166,Olodum,112
167,Acústico MTV,113
168,Arquivo II,113
169,Arquivo Os Paralamas Do Sucesso,113
170,Bark at the Moon (Remastered),114
171,Blizzard of Ozz,114
172,Diary of a Madman (Remastered),114
173,No More Tears (Remastered),114
174,Tribute,114
175,Walking Into Clarksdale,115
176,Original Soundtracks 1,116
177,The Beast Live,117
178,Live On Two Legs [Live],118
179,Pearl Jam,118
180,Riot Act,118
181,Ten,118
182,Vs.,118
183,Dark Side Of The Moon,120
184,Os Cães Ladram Mas A Caravana Não Pára,121
185,Greatest Hits I,51
186,News Of The World,51
187,Out Of Time,122
188,Green,124
189,New Adventures In Hi-Fi,124
190,The Best Of R.E.M.: The IRS Years,124
191,Cesta Básica,125
192,Raul Seixas,126
193,Blood Sugar Sex Magik,127
194,By The Way,127
195,Californication,127
196,Retrospective I (1974-1980),128
197,Santana - As Years Go By,59
198,Santana Live,59
199,Maquinarama,130
200,O Samba Poconé,130
201,Judas 0: B-Sides and Rarities,131
202,Rotten Apples: Greatest Hits,131
203,A-Sides,132
204,Morning Dance,53
205,In Step,133
206,Core,134
207,Mezmerize,135
208,[1997] Black Light Syndrome,136
209,Live [Disc 1],137
210,Live [Disc 2],137
211,The Singles,138
212,Beyond Good And Evil,139
213,"Pure Cult: The Best Of The Cult (For Rockers, Ravers, Lovers & Sinners) [UK]",139
214,The Doors,140
215,The Police Greatest Hits,141
216,"Hot Rocks, 1964-1971 (Disc 1)",142
217,No Security,142
218,Voodoo Lounge,142
219,Tangents,143
220,Transmission,143
221,My Generation - The Very Best Of The Who,144
222,Serie Sem Limite (Disc 1),145
223,Serie Sem Limite (Disc 2),145
224,Acústico,146
225,Volume Dois,146
226,Battlestar Galactica: The Story So Far,147
227,"Battlestar Galactica, Season 3",147
228,"Heroes, Season 1",148
229,"Lost, Season 3",149
230,"Lost, Season 1",149
231,"Lost, Season 2",149
232,Achtung Baby,150
233,All That You Can't Leave Behind,150
234,B-Sides 1980-1990,150
235,How To Dismantle An Atomic Bomb,150
236,Pop,150
237,Rattle And Hum,150
238,The Best Of 1980-1990,150
239,War,150
240,Zooropa,150
241,UB40 The Best Of - Volume Two [UK],151
242,Diver Down,152
243,"The Best Of Van Halen, Vol. I",152
244,Van Halen,152
245,Van Halen III,152
246,Contraband,153
247,Vinicius De Moraes,72
248,Ao Vivo [IMPORT],155
249,"The Office, Season 1",156
250,"The Office, Season 2",156
251,"The Office, Season 3",156
252,Un-Led-Ed,157
253,"Battlestar Galactica (Classic), Season 1",158
254,Aquaman,159
255,Instant Karma: The Amnesty International Campaign to Save Darfur,150
256,Speak of the Devil,114
257,20th Century Masters - The Millennium Collection: The Best of Scorpions,179
258,House of Pain,180
259,Radio Brasil (O Som da Jovem Vanguarda) - Seleccao de Henrique Amaro,36
260,Cake: B-Sides and Rarities,196
261,"LOST, Season 4",149
262,Quiet Songs,197
263,Muso Ko,198
264,Realize,199
265,Every Kind of Light,200
266,Duos II,201
267,Worlds,202
268,The Best of Beethoven,203
269,Temple of the Dog,204
270,Carry On,205
271,Revelations,8
272,Adorate Deum: Gregorian Chant from the Proper of the Mass,206
273,Allegri: Miserere,207
274,Pachelbel: Canon & Gigue,208
275,Vivaldi: The Four Seasons,209
276,Bach: Violin Concertos,210
277,Bach: Goldberg Variations,211
278,Bach: The Cello Suites,212
279,Handel: The Messiah (Highlights),213
280,The World of Classical Favourites,214
281,Sir Neville Marriner: A Celebration,215
282,Mozart: Wind Concertos,216
283,Haydn: Symphonies 99 - 104,217
284,Beethoven: Symhonies Nos. 5 & 6,218
285,A Soprano Inspired,219
286,Great Opera Choruses,220
287,Wagner: Favourite Overtures,221
288,"Fauré: Requiem, Ravel: Pavane & Others",222
289,Tchaikovsky: The Nutcracker,223
290,The Last Night of the Proms,224
291,Puccini: Madama Butterfly - Highlights,225
292,"Holst: The Planets, Op. 32 & Vaughan Williams: Fantasies",226
293,Pavarotti's Opera Made Easy,227
294,Great Performances - Barber's Adagio and Other Romantic Favorites for Strings,228
295,Carmina Burana,229
296,"A Copland Celebration, Vol. I",230
297,Bach: Toccata & Fugue in D Minor,231
298,Prokofiev: Symphony No.1,232
299,Scheherazade,233
300,Bach: The Brandenburg Concertos,234
301,Chopin: Piano Concertos Nos. 1 & 2,235
302,Mascagni: Cavalleria Rusticana,236
303,Sibelius: Finlandia,237
304,Beethoven Piano Sonatas: Moonlight & Pastorale,238
305,Great Recordings of the Century - Mahler: Das Lied von der Erde,240
306,Elgar: Cello Concerto & Vaughan Williams: Fantasias,241
307,"Adams, John: The Chairman Dances",242
308,"Tchaikovsky: 1812 Festival Overture, Op.49, Capriccio Italien & Beethoven: Wellington's Victory",243
309,Palestrina: Missa Papae Marcelli & Allegri: Miserere,244
310,Prokofiev: Romeo & Juliet,245
311,Strauss: Waltzes,226
312,Berlioz: Symphonie Fantastique,245
313,Bizet: Carmen Highlights,246
314,English Renaissance,247
315,Handel: Music for the Royal Fireworks (Original Version 1749),208
316,Grieg: Peer Gynt Suites & Sibelius: Pelléas et Mélisande,248
317,Mozart Gala: Famous Arias,249
318,SCRIABIN: Vers la flamme,250
319,Armada: Music from the Courts of England and Spain,251
320,Mozart: Symphonies Nos. 40 & 41,248
321,Back to Black,252
322,Frank,252
323,Carried to Dust (Bonus Track Version),253
324,Beethoven: Symphony No. 6 'Pastoral' Etc.,254
325,Bartok: Violin & Viola Concertos,255
326,Mendelssohn: A Midsummer Night's Dream,256
327,Bach: Orchestral Suites Nos. 1 - 4,257
328,"Charpentier: Divertissements, Airs & Concerts",258
329,South American Getaway,259
330,Górecki: Symphony No. 3,260
331,Purcell: The Fairy Queen,261
332,The Ultimate Relexation Album,262
333,Purcell: Music for the Queen Mary,263
334,Weill: The Seven Deadly Sins,264
335,"J.S. Bach: Chaconne, Suite in E Minor, Partita in E Major & Prelude, Fugue and Allegro",265
336,Prokofiev: Symphony No.5 & Stravinksy: Le Sacre Du Printemps,248
337,"Szymanowski: Piano Works, Vol. 1",266
338,Nielsen: The Six Symphonies,267
339,Great Recordings of the Century: Paganini's 24 Caprices,268
340,Liszt - 12 Études D'Execution Transcendante,269
341,"Great Recordings of the Century - Shubert: Schwanengesang, 4 Lieder",270
342,"Locatelli: Concertos for Violin, Strings and Continuo, Vol. 3",271
343,Respighi:Pines of Rome,226
344,Schubert: The Late String Quartets & String Quintet (3 CD's),272
345,Monteverdi: L'Orfeo,273
346,Mozart: Chamber Music,274
347,Koyaanisqatsi (Soundtrack from the Motion Picture),275
348,asdaasdasdsd,3
1 ID title artist_ID
2 1 For Those About To Rock We Salute You 1
3 2 Balls to the Wall 2
4 3 Restless and Wild 2
5 4 Let There Be Rock 1
6 5 Big Ones 3
7 6 Jagged Little Pill 4
8 7 Facelift 5
9 8 Warner 25 Anos 6
10 9 Plays Metallica By Four Cellos 7
11 10 Audioslave 8
12 11 Out Of Exile 8
13 12 BackBeat Soundtrack 9
14 13 The Best Of Billy Cobham 10
15 14 Alcohol Fueled Brewtality Live! [Disc 1] 11
16 15 Alcohol Fueled Brewtality Live! [Disc 2] 11
17 16 Black Sabbath 12
18 17 Black Sabbath Vol. 4 (Remaster) 12
19 18 Body Count 13
20 19 Chemical Wedding 14
21 20 The Best Of Buddy Guy - The Millenium Collection 15
22 21 Prenda Minha 16
23 22 Sozinho Remix Ao Vivo 16
24 23 Minha Historia 17
25 24 Afrociberdelia 18
26 25 Da Lama Ao Caos 18
27 26 Acústico MTV [Live] 19
28 27 Cidade Negra - Hits 19
29 28 Na Pista 20
30 29 Axé Bahia 2001 21
31 30 BBC Sessions [Disc 1] [Live] 22
32 31 Bongo Fury 23
33 32 Carnaval 2001 21
34 33 Chill: Brazil (Disc 1) 24
35 34 Chill: Brazil (Disc 2) 6
36 35 Garage Inc. (Disc 1) 50
37 36 Greatest Hits II 51
38 37 Greatest Kiss 52
39 38 Heart of the Night 53
40 39 International Superhits 54
41 40 Into The Light 55
42 41 Meus Momentos 56
43 42 Minha História 57
44 43 MK III The Final Concerts [Disc 1] 58
45 44 Physical Graffiti [Disc 1] 22
46 45 Sambas De Enredo 2001 21
47 46 Supernatural 59
48 47 The Best of Ed Motta 37
49 48 The Essential Miles Davis [Disc 1] 68
50 49 The Essential Miles Davis [Disc 2] 68
51 50 The Final Concerts (Disc 2) 58
52 51 Up An' Atom 69
53 52 Vinícius De Moraes - Sem Limite 70
54 53 Vozes do MPB 21
55 54 Chronicle, Vol. 1 76
56 55 Chronicle, Vol. 2 76
57 56 Cássia Eller - Coleção Sem Limite [Disc 2] 77
58 57 Cássia Eller - Sem Limite [Disc 1] 77
59 58 Come Taste The Band 58
60 59 Deep Purple In Rock 58
61 60 Fireball 58
62 61 Knocking at Your Back Door: The Best Of Deep Purple in the 80's 58
63 62 Machine Head 58
64 63 Purpendicular 58
65 64 Slaves And Masters 58
66 65 Stormbringer 58
67 66 The Battle Rages On 58
68 67 Vault: Def Leppard's Greatest Hits 78
69 68 Outbreak 79
70 69 Djavan Ao Vivo - Vol. 02 80
71 70 Djavan Ao Vivo - Vol. 1 80
72 71 Elis Regina-Minha História 41
73 72 The Cream Of Clapton 81
74 73 Unplugged 81
75 74 Album Of The Year 82
76 75 Angel Dust 82
77 76 King For A Day Fool For A Lifetime 82
78 77 The Real Thing 82
79 78 Deixa Entrar 83
80 79 In Your Honor [Disc 1] 84
81 80 In Your Honor [Disc 2] 84
82 81 One By One 84
83 82 The Colour And The Shape 84
84 83 My Way: The Best Of Frank Sinatra [Disc 1] 85
85 84 Roda De Funk 86
86 85 As Canções de Eu Tu Eles 27
87 86 Quanta Gente Veio Ver (Live) 27
88 87 Quanta Gente Veio ver--Bônus De Carnaval 27
89 88 Faceless 87
90 89 American Idiot 54
91 90 Appetite for Destruction 88
92 91 Use Your Illusion I 88
93 92 Use Your Illusion II 88
94 93 Blue Moods 89
95 94 A Matter of Life and Death 90
96 95 A Real Dead One 90
97 96 A Real Live One 90
98 97 Brave New World 90
99 98 Dance Of Death 90
100 99 Fear Of The Dark 90
101 100 Iron Maiden 90
102 101 Killers 90
103 102 Live After Death 90
104 103 Live At Donington 1992 (Disc 1) 90
105 104 Live At Donington 1992 (Disc 2) 90
106 105 No Prayer For The Dying 90
107 106 Piece Of Mind 90
108 107 Powerslave 90
109 108 Rock In Rio [CD1] 90
110 109 Rock In Rio [CD2] 90
111 110 Seventh Son of a Seventh Son 90
112 111 Somewhere in Time 90
113 112 The Number of The Beast 90
114 113 The X Factor 90
115 114 Virtual XI 90
116 115 Sex Machine 91
117 116 Emergency On Planet Earth 92
118 117 Synkronized 92
119 118 The Return Of The Space Cowboy 92
120 119 Get Born 93
121 120 Are You Experienced? 94
122 121 Surfing with the Alien (Remastered) 95
123 122 Jorge Ben Jor 25 Anos 46
124 123 Jota Quest-1995 96
125 124 Cafezinho 97
126 125 Living After Midnight 98
127 126 Unplugged [Live] 52
128 127 BBC Sessions [Disc 2] [Live] 22
129 128 Coda 22
130 129 Houses Of The Holy 22
131 130 In Through The Out Door 22
132 131 IV 22
133 132 Led Zeppelin I 22
134 133 Led Zeppelin II 22
135 134 Led Zeppelin III 22
136 135 Physical Graffiti [Disc 2] 22
137 136 Presence 22
138 137 The Song Remains The Same (Disc 1) 22
139 138 The Song Remains The Same (Disc 2) 22
140 139 A TempestadeTempestade Ou O Livro Dos Dias 99
141 140 Mais Do Mesmo 99
142 141 Greatest Hits 100
143 142 Lulu Santos - RCA 100 Anos De Música - Álbum 01 101
144 143 Lulu Santos - RCA 100 Anos De Música - Álbum 02 101
145 144 Misplaced Childhood 102
146 145 Barulhinho Bom 103
147 146 Seek And Shall Find: More Of The Best (1963-1981) 104
148 147 The Best Of Men At Work 105
149 148 Black Album 50
150 149 Garage Inc. (Disc 2) 50
151 150 Kill 'Em All 50
152 151 Load 50
153 152 Master Of Puppets 50
154 153 ReLoad 50
155 154 Ride The Lightning 50
156 155 St. Anger 50
157 156 ...And Justice For All 50
158 157 Miles Ahead 68
159 158 Milton Nascimento Ao Vivo 42
160 159 Minas 42
161 160 Ace Of Spades 106
162 161 Demorou... 108
163 162 Motley Crue Greatest Hits 109
164 163 From The Muddy Banks Of The Wishkah [Live] 110
165 164 Nevermind 110
166 165 Compositores 111
167 166 Olodum 112
168 167 Acústico MTV 113
169 168 Arquivo II 113
170 169 Arquivo Os Paralamas Do Sucesso 113
171 170 Bark at the Moon (Remastered) 114
172 171 Blizzard of Ozz 114
173 172 Diary of a Madman (Remastered) 114
174 173 No More Tears (Remastered) 114
175 174 Tribute 114
176 175 Walking Into Clarksdale 115
177 176 Original Soundtracks 1 116
178 177 The Beast Live 117
179 178 Live On Two Legs [Live] 118
180 179 Pearl Jam 118
181 180 Riot Act 118
182 181 Ten 118
183 182 Vs. 118
184 183 Dark Side Of The Moon 120
185 184 Os Cães Ladram Mas A Caravana Não Pára 121
186 185 Greatest Hits I 51
187 186 News Of The World 51
188 187 Out Of Time 122
189 188 Green 124
190 189 New Adventures In Hi-Fi 124
191 190 The Best Of R.E.M.: The IRS Years 124
192 191 Cesta Básica 125
193 192 Raul Seixas 126
194 193 Blood Sugar Sex Magik 127
195 194 By The Way 127
196 195 Californication 127
197 196 Retrospective I (1974-1980) 128
198 197 Santana - As Years Go By 59
199 198 Santana Live 59
200 199 Maquinarama 130
201 200 O Samba Poconé 130
202 201 Judas 0: B-Sides and Rarities 131
203 202 Rotten Apples: Greatest Hits 131
204 203 A-Sides 132
205 204 Morning Dance 53
206 205 In Step 133
207 206 Core 134
208 207 Mezmerize 135
209 208 [1997] Black Light Syndrome 136
210 209 Live [Disc 1] 137
211 210 Live [Disc 2] 137
212 211 The Singles 138
213 212 Beyond Good And Evil 139
214 213 Pure Cult: The Best Of The Cult (For Rockers, Ravers, Lovers & Sinners) [UK] 139
215 214 The Doors 140
216 215 The Police Greatest Hits 141
217 216 Hot Rocks, 1964-1971 (Disc 1) 142
218 217 No Security 142
219 218 Voodoo Lounge 142
220 219 Tangents 143
221 220 Transmission 143
222 221 My Generation - The Very Best Of The Who 144
223 222 Serie Sem Limite (Disc 1) 145
224 223 Serie Sem Limite (Disc 2) 145
225 224 Acústico 146
226 225 Volume Dois 146
227 226 Battlestar Galactica: The Story So Far 147
228 227 Battlestar Galactica, Season 3 147
229 228 Heroes, Season 1 148
230 229 Lost, Season 3 149
231 230 Lost, Season 1 149
232 231 Lost, Season 2 149
233 232 Achtung Baby 150
234 233 All That You Can't Leave Behind 150
235 234 B-Sides 1980-1990 150
236 235 How To Dismantle An Atomic Bomb 150
237 236 Pop 150
238 237 Rattle And Hum 150
239 238 The Best Of 1980-1990 150
240 239 War 150
241 240 Zooropa 150
242 241 UB40 The Best Of - Volume Two [UK] 151
243 242 Diver Down 152
244 243 The Best Of Van Halen, Vol. I 152
245 244 Van Halen 152
246 245 Van Halen III 152
247 246 Contraband 153
248 247 Vinicius De Moraes 72
249 248 Ao Vivo [IMPORT] 155
250 249 The Office, Season 1 156
251 250 The Office, Season 2 156
252 251 The Office, Season 3 156
253 252 Un-Led-Ed 157
254 253 Battlestar Galactica (Classic), Season 1 158
255 254 Aquaman 159
256 255 Instant Karma: The Amnesty International Campaign to Save Darfur 150
257 256 Speak of the Devil 114
258 257 20th Century Masters - The Millennium Collection: The Best of Scorpions 179
259 258 House of Pain 180
260 259 Radio Brasil (O Som da Jovem Vanguarda) - Seleccao de Henrique Amaro 36
261 260 Cake: B-Sides and Rarities 196
262 261 LOST, Season 4 149
263 262 Quiet Songs 197
264 263 Muso Ko 198
265 264 Realize 199
266 265 Every Kind of Light 200
267 266 Duos II 201
268 267 Worlds 202
269 268 The Best of Beethoven 203
270 269 Temple of the Dog 204
271 270 Carry On 205
272 271 Revelations 8
273 272 Adorate Deum: Gregorian Chant from the Proper of the Mass 206
274 273 Allegri: Miserere 207
275 274 Pachelbel: Canon & Gigue 208
276 275 Vivaldi: The Four Seasons 209
277 276 Bach: Violin Concertos 210
278 277 Bach: Goldberg Variations 211
279 278 Bach: The Cello Suites 212
280 279 Handel: The Messiah (Highlights) 213
281 280 The World of Classical Favourites 214
282 281 Sir Neville Marriner: A Celebration 215
283 282 Mozart: Wind Concertos 216
284 283 Haydn: Symphonies 99 - 104 217
285 284 Beethoven: Symhonies Nos. 5 & 6 218
286 285 A Soprano Inspired 219
287 286 Great Opera Choruses 220
288 287 Wagner: Favourite Overtures 221
289 288 Fauré: Requiem, Ravel: Pavane & Others 222
290 289 Tchaikovsky: The Nutcracker 223
291 290 The Last Night of the Proms 224
292 291 Puccini: Madama Butterfly - Highlights 225
293 292 Holst: The Planets, Op. 32 & Vaughan Williams: Fantasies 226
294 293 Pavarotti's Opera Made Easy 227
295 294 Great Performances - Barber's Adagio and Other Romantic Favorites for Strings 228
296 295 Carmina Burana 229
297 296 A Copland Celebration, Vol. I 230
298 297 Bach: Toccata & Fugue in D Minor 231
299 298 Prokofiev: Symphony No.1 232
300 299 Scheherazade 233
301 300 Bach: The Brandenburg Concertos 234
302 301 Chopin: Piano Concertos Nos. 1 & 2 235
303 302 Mascagni: Cavalleria Rusticana 236
304 303 Sibelius: Finlandia 237
305 304 Beethoven Piano Sonatas: Moonlight & Pastorale 238
306 305 Great Recordings of the Century - Mahler: Das Lied von der Erde 240
307 306 Elgar: Cello Concerto & Vaughan Williams: Fantasias 241
308 307 Adams, John: The Chairman Dances 242
309 308 Tchaikovsky: 1812 Festival Overture, Op.49, Capriccio Italien & Beethoven: Wellington's Victory 243
310 309 Palestrina: Missa Papae Marcelli & Allegri: Miserere 244
311 310 Prokofiev: Romeo & Juliet 245
312 311 Strauss: Waltzes 226
313 312 Berlioz: Symphonie Fantastique 245
314 313 Bizet: Carmen Highlights 246
315 314 English Renaissance 247
316 315 Handel: Music for the Royal Fireworks (Original Version 1749) 208
317 316 Grieg: Peer Gynt Suites & Sibelius: Pelléas et Mélisande 248
318 317 Mozart Gala: Famous Arias 249
319 318 SCRIABIN: Vers la flamme 250
320 319 Armada: Music from the Courts of England and Spain 251
321 320 Mozart: Symphonies Nos. 40 & 41 248
322 321 Back to Black 252
323 322 Frank 252
324 323 Carried to Dust (Bonus Track Version) 253
325 324 Beethoven: Symphony No. 6 'Pastoral' Etc. 254
326 325 Bartok: Violin & Viola Concertos 255
327 326 Mendelssohn: A Midsummer Night's Dream 256
328 327 Bach: Orchestral Suites Nos. 1 - 4 257
329 328 Charpentier: Divertissements, Airs & Concerts 258
330 329 South American Getaway 259
331 330 Górecki: Symphony No. 3 260
332 331 Purcell: The Fairy Queen 261
333 332 The Ultimate Relexation Album 262
334 333 Purcell: Music for the Queen Mary 263
335 334 Weill: The Seven Deadly Sins 264
336 335 J.S. Bach: Chaconne, Suite in E Minor, Partita in E Major & Prelude, Fugue and Allegro 265
337 336 Prokofiev: Symphony No.5 & Stravinksy: Le Sacre Du Printemps 248
338 337 Szymanowski: Piano Works, Vol. 1 266
339 338 Nielsen: The Six Symphonies 267
340 339 Great Recordings of the Century: Paganini's 24 Caprices 268
341 340 Liszt - 12 Études D'Execution Transcendante 269
342 341 Great Recordings of the Century - Shubert: Schwanengesang, 4 Lieder 270
343 342 Locatelli: Concertos for Violin, Strings and Continuo, Vol. 3 271
344 343 Respighi:Pines of Rome 226
345 344 Schubert: The Late String Quartets & String Quintet (3 CD's) 272
346 345 Monteverdi: L'Orfeo 273
347 346 Mozart: Chamber Music 274
348 347 Koyaanisqatsi (Soundtrack from the Motion Picture) 275
349 348 asdaasdasdsd 3

Some files were not shown because too many files have changed in this diff Show More