Compare commits

..

242 Commits

Author SHA1 Message Date
sjvans
c9ecef4e21 Merge branch 'main' into audit-logging 2022-02-08 13:48:35 +01:00
sjvans
46f1be4395 cleanup 2022-02-08 13:47:32 +01:00
sjvans
b932637400 Update manifest.json 2022-02-08 13:45:36 +01:00
sjvans
3c6d49b88e in development, write audit logs to custom sink 2022-02-08 13:41:30 +01:00
Christian Georgi
f4f02f6fe5 Merge pull request #320 from SAP-samples/node16
Node16, dependency updates
2022-02-07 10:38:17 +01:00
Christian Georgi
68c2504fc4 package-lock update 2022-02-07 10:37:00 +01:00
Christian Georgi
51a8762738 Add Node.js 16 to CI 2022-02-07 10:31:08 +01:00
Christian Georgi
ff533754b4 Merge pull request #319 from SAP-samples/data-viewer
Data viewer Misc
2022-02-07 10:30:01 +01:00
Christian Georgi
f6b4d534fc Cosmetics 2022-02-07 10:11:38 +01:00
Christian Georgi
3c5107ba42 Require authentication 2022-02-07 10:02:31 +01:00
Christian Georgi
dbc561d2e3 Show errors in UI, migrate to Vue 3 2022-02-07 09:54:17 +01:00
Christian Georgi
f21488fd71 Merge pull request #318 from seVladimirs/patch-1
adds @sap/cds dependency to package.json
2022-02-07 09:53:17 +01:00
Christian Georgi
0bce8d9ce0 Fix glitch 2022-02-07 09:52:17 +01:00
Vladimirs Semikins
0f03ffd3eb adds @sap/cds dependency to package.json
To avoid further questions (e.g. https://answers.sap.com/questions/13577720/i-push-my-capnodejsto-btp-environment-but-always-c.html) this PR will add missing `@sap/cds` dependency.
2022-02-07 10:38:28 +02:00
Daniel Hutzel
bc03cae550 fix duplicate annotates (#317) 2022-02-04 16:19:54 +01:00
sjvans
8197559065 fix: app crashes if SELECT results in null (#298) 2022-02-03 18:04:10 +01:00
sjvans
6928ae907a initial 2022-02-03 17:57:35 +01:00
Daniel Hutzel
7accf1ae23 Using only bob as reviewer (#315) 2022-02-03 04:35:56 +01:00
Christian Georgi
2ffa1721b3 Merge pull request #314 from SAP-samples/data-browser
Data browser Cosmetics
2022-02-01 19:54:34 +01:00
Christian Georgi
37811381e7 Show errors in UI 2022-02-01 19:52:05 +01:00
Christian Georgi
74f9d5cc1e Sort columns 2022-02-01 19:30:30 +01:00
Christian Georgi
7c0769e059 Stick table headers 2022-02-01 18:42:36 +01:00
Christian Georgi
10dcd6aadd Merge pull request #313 from SAP-samples/browser
Data Browser: UI improvements, cosmetics
2022-01-31 20:24:38 +01:00
Christian Georgi
f8c23f4c54 Better layout; 2022-01-31 20:08:40 +01:00
Christian Georgi
ae6be30aa0 Data browser: cosmetics, reformat 2022-01-31 19:07:00 +01:00
Christian Georgi
e6baa95f74 Data browser: keep data row selection across refresh 2022-01-31 19:03:06 +01:00
Daniel Hutzel
aa28804a51 Basic bookshop w/o reuse deps (#312) 2022-01-29 23:49:30 +01:00
Christian Georgi
ea0773071e Update package-lock 2022-01-27 15:28:04 +01:00
Christian Georgi
1a71a6d28a Simple Data Viewer
- Generic CDS service to fetch data
- Simple Vue.js UI
2022-01-27 15:28:04 +01:00
Christian Georgi
50791bed80 Override tar with newer version 2022-01-17 13:20:10 +01:00
Christian Georgi
d58a71af7f Bump dependencies 2022-01-17 12:13:13 +01:00
Iwona Hahn
c7c979236e Merge pull request #306 from SAP-samples/iwonahahn-update
update year for license in README.md
2022-01-14 15:13:27 +01:00
Iwona Hahn
0675793c87 update year for license in README.md 2022-01-06 12:13:48 +01:00
Daniel
91bca922c4 use common file names for license 2022-01-06 10:24:46 +01:00
Daniel
30403d239a using latest versions 2022-01-06 10:24:46 +01:00
Daniel
589e282288 mocha has to run parallel 2022-01-06 10:24:46 +01:00
Daniel
a8345122ea fixed support for mocha 2022-01-04 13:55:37 +01:00
Daniel
788a101f41 Added .drawio graphics 2021-12-30 05:38:10 +01:00
Daniel
379ddc9fb0 Added npm start script → launching fiori 2021-12-14 10:13:53 +01:00
Daniel
1fde0c928b Fixed submit order -> returned stock was wrong 2021-11-29 15:38:37 +01:00
Christian Georgi
fa854a2411 Add hint on db-ext datasource to code tour
Signed-off-by: Christian Georgi <christian.georgi@sap.com>
2021-11-29 15:33:57 +01:00
Daniel
40b6acee6a Fixing erroneous db.model config 2021-11-29 15:33:57 +01:00
Daniel
a37f1b8e0c Reformatted package.json 2021-11-29 15:33:57 +01:00
Christian Georgi
042ddec41c Adjust code tour 2021-11-29 15:33:57 +01:00
Daniel
8d8aa6c2c9 Fixing copy errors
Signed-off-by: Daniel <daniel.hutzel@sap.com>
2021-11-29 15:33:57 +01:00
Daniel
756ed48ce3 Fiori apps with manage authors
Signed-off-by: Daniel <daniel.hutzel@sap.com>
2021-11-29 15:33:57 +01:00
Daniel
19b680ab3d Moving lifetime + age to where it is used
Signed-off-by: Daniel <daniel.hutzel@sap.com>
2021-11-29 15:33:57 +01:00
Daniel
33380c0792 Restore simple bookshop - no workarounds
Signed-off-by: Daniel <daniel.hutzel@sap.com>
2021-11-29 15:33:57 +01:00
Christian Georgi
0d5b65d93f Merge pull request #291 from gregorwolf/navigation
Demo for Semantic Navigation using the local Fiori Launchpad
2021-11-22 13:08:57 +01:00
Christian Georgi
dcf5ec6068 Merge branch 'main' into navigation 2021-11-18 16:19:57 +01:00
Christian Georgi
fb41dd100b Merge pull request #293 from SAP-samples/bookstore
In Fiori app, use bookstore consistently
2021-11-18 16:12:12 +01:00
Christian Georgi
769aa2b4f5 Adjust samples tour to bookstore 2021-11-18 15:34:59 +01:00
Christian Georgi
f6c5e620c0 In Fiori app, use bookstore consistently
This also fixes validation errors from annotation LSP, which missed some
of the bookstore elements

Signed-off-by: Christian Georgi <christian.georgi@sap.com>
2021-11-18 14:14:09 +01:00
Gregor Wolf
7013cb6296 remove name 2021-11-13 00:48:13 +01:00
Gregor Wolf
6411ce4ea3 remove redundant annotation 2021-11-13 00:48:04 +01:00
Gregor Wolf
cd914a8d87 show ID in LineItem 2021-11-13 00:41:48 +01:00
Gregor Wolf
b35e92b5bc TextArrangement : #TextOnly 2021-11-13 00:41:28 +01:00
Gregor Wolf
8350aff111 correct renameTo 2021-11-13 00:36:22 +01:00
Gregor Wolf
9aa604c5a0 remove @UI.HiddenFilter 2021-11-12 23:56:11 +01:00
Gregor Wolf
f5cb53adf8 add inbounds parameter 2021-11-12 23:48:51 +01:00
Gregor Wolf
9fe16edc55 fix manifest issues 2021-11-12 23:48:26 +01:00
Gregor Wolf
0093883f25 add Author ID column 2021-11-12 23:46:41 +01:00
Gregor Wolf
14024a4837 add semantic object for navigation 2021-11-12 22:31:58 +01:00
Gregor Wolf
2e13b37096 activate automatic load 2021-11-12 22:31:42 +01:00
Gregor Wolf
7407b0aae3 format 2021-11-12 22:04:48 +01:00
Gregor Wolf
152d088ff4 rename author to authorName 2021-11-12 21:49:39 +01:00
Gregor Wolf
19c886d6dc fix implicit redirection issue 2021-11-12 21:41:39 +01:00
Gregor Wolf
575cffe70a migrate to fioriSandboxConfig.json 2021-11-12 21:29:21 +01:00
Gregor Wolf
03869e5896 Fiori UI for Authors 2021-11-12 21:28:49 +01:00
Gregor Wolf
597afeb3d8 format, rename author to authorName 2021-11-12 21:28:23 +01:00
Gregor Wolf
d376e6e063 UI annotations for Authors 2021-11-12 21:27:40 +01:00
Gregor Wolf
2a44cfa9dc add Authors, rename author attribute 2021-11-12 21:25:33 +01:00
Gregor Wolf
7964c9aefd adjust formatting 2021-11-12 20:59:45 +01:00
Christian Georgi
ce752097d0 Merge pull request #290 from dinurp/main
Annotations for Admin.Book service
2021-11-10 08:55:47 +01:00
dinurp
e838f380c5 annotation to make language value list a drop down.
type:#Fixed was not rendering the value list as a drop down.
There was not UI validation for values entered. 
The added annotation does both these: UI validation and renders the a drop down list.
2021-11-10 11:18:41 +04:00
dinurp
e89e012eba Update bookshop/srv/admin-service.cds
Co-authored-by: Christian Georgi <chgeo@users.noreply.github.com>
2021-11-10 11:12:18 +04:00
Christian Georgi
bf0f57efe5 Merge branch 'main' into main 2021-11-09 17:55:55 +01:00
Dinu
40ea057f7f Annotation for value list for languages for locale 2021-11-09 15:46:22 +04:00
Dinu
31d2f4c932 Avoid popup for ID in Fiori preview for Books. 2021-11-09 14:01:53 +04:00
Daniel Hutzel
8cc2db7118 Using managed compositions for Order.Items (#273)
* Using managed compositions for Order.Items
Co-authored-by: sjvans <30337871+sjvans@users.noreply.github.com>
2021-11-08 17:41:33 +01:00
Daniel Hutzel
482b71e60b Remove workaround for integrity check (#173)
* Remove workaround for integrity check
2021-11-08 14:23:41 +01:00
Daniel Hutzel
7536451e29 Enhanced cds.ql tests (#228)
* Enhanced cds.ql tests
2021-11-08 13:49:43 +01:00
Daniel
2703e8b999 Removed authorization workaround 2021-11-08 13:19:12 +01:00
Daniel
ac2e4dd6f9 Updating package-lock to v2 2021-11-08 13:17:31 +01:00
PierreFritsch
c718fa400d import {Request} => import type {Request} 2021-11-08 13:14:02 +01:00
Pierre Fritsch
730da6e435 hello TypeScript: Define type of req object
Make it clearer that `req` is a CDS `Request` object.
2021-11-08 13:14:02 +01:00
Daniel
7d9b4f7a5d Fixes for Node16 2021-11-05 17:01:59 +01:00
Daniel
35d3708592 Fixes for Node16 2021-11-05 15:49:05 +01:00
Daniel
eaaf0d29a5 Updated package-lock.json 2021-11-04 10:37:33 +01:00
Daniel
64fe700d1e ... 2021-11-04 10:37:33 +01:00
Daniel
5c3cec973e Using bookstore as composite app 2021-11-04 10:37:33 +01:00
Daniel
680a6ae68f Introduced bookstore composite app 2021-11-04 10:37:33 +01:00
Daniel
366b0f8f9a Using flat fiori app folder 2021-11-04 10:37:33 +01:00
Daniel
b95df77b9a Fixed access to cds.entities 2021-11-04 10:37:33 +01:00
Christian Georgi
7f6b87171a Merge pull request #284 from SAP-samples/registry
NPM registry: more robust,  work on Windows
2021-10-26 22:21:32 +02:00
Christian Georgi
21e74bbbfb Add test for NPM registry 2021-10-26 22:17:25 +02:00
Christian Georgi
102b15c3cd NPM registry: more robust, work on Windows
- Run `npm rm` in correct dir
- Encode args in `npm conf rm` command for Powershell
- Don't cleanup in exit handler, this might not complete.
  Instead, do it on startup.
- Different cleanup command for Windows and Linux
- Explicitly close server, instead of relying on process.exit()
- Make scope configurable
2021-10-26 19:11:08 +02:00
Dr. David A. Kunz
73db2e96bc Merge pull request #283 from SAP-samples/ql-changes-typo-fix
fix typo in ql changes
2021-10-26 11:57:44 +02:00
Dr. David A. Kunz
7bb58ee2d5 Merge branch 'main' into ql-changes-typo-fix 2021-10-26 11:56:15 +02:00
D065023
27e82d16e0 fix typo 2021-10-26 11:55:33 +02:00
Dr. David A. Kunz
ff9bbe6d8d Merge pull request #282 from SAP-samples/ql-changes
Changes in ql for cds version 5.6.0
2021-10-26 11:32:09 +02:00
Dr. David A. Kunz
5f1b7b8cbf Merge branch 'main' into ql-changes 2021-10-26 11:27:27 +02:00
D065023
0253300557 Changes in ql for cds version 5.6.0 2021-10-26 11:22:43 +02:00
Daniel
139d957495 Added reviewed.count to messsages 2021-10-18 18:04:33 +02:00
Daniel
404427237b Updated reviews samples for messaging GA 2021-10-18 18:04:33 +02:00
Christian Georgi
5f89334403 Remove refs to 'master' branch 2021-10-12 18:46:21 +02:00
Christian Georgi
e612fa97ea Merge pull request #277 from SAP-samples/updated-package.json
Updated package.json
2021-10-08 11:16:10 +02:00
Christian Georgi
da2ea39466 Do not fail on absent cds-swagger-ui-express 2021-10-08 10:23:12 +02:00
Daniel
117000df71 Revert "temporarily using sqlite3 until next release to fix vulnerabilities"
This reverts commit f3ffb69d3a.
2021-10-08 07:50:04 +02:00
Daniel
f3ffb69d3a temporarily using sqlite3 until next release to fix vulnerabilities 2021-10-08 07:41:18 +02:00
Daniel
f908484973 Updated package.json 2021-10-08 06:42:50 +02:00
Christian Georgi
c4529f3cd7 Increase test timeout
At least on slower machines or when file system caches are not filled,
the default 5000 ms are not enough for the tests to finish.
2021-09-29 13:51:49 +02:00
Christian Georgi
0220400484 Avoid mixed spaces/tabs 2021-09-20 15:56:53 +02:00
dependabot[bot]
c1911b6e96 Bump axios from 0.21.1 to 0.21.4
Bumps [axios](https://github.com/axios/axios) from 0.21.1 to 0.21.4.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/master/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.21.1...v0.21.4)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-10 15:42:25 +02:00
Daniel
19083d156e Rename amount -> quantity 2021-09-10 15:39:30 +02:00
Christian Georgi
48d547e6cd Make jest really silent 2021-09-10 15:35:36 +02:00
Daniel
bae491a832 one more more more 2021-08-31 20:36:36 +02:00
Daniel
ca41a2141c one more 2021-08-31 19:30:09 +02:00
Daniel Hutzel
08f409af73 more (#267) 2021-08-31 18:05:22 +02:00
Daniel Hutzel
efa60550fb fix cds.ql (#266) 2021-08-31 13:29:05 +02:00
Daniel Hutzel
f599206bf4 Fixed cds.ql in latest release (#232)
* Fixed cds.ql in latest release

* Requires @sap/cds ^5.1.5

* fixed fixes

* More cdr tests

Co-authored-by: Christian Georgi <chgeo@users.noreply.github.com>
2021-08-31 09:59:06 +02:00
Daniel
2f5d159428 using cds.test 2021-08-30 16:08:25 +02:00
Daniel
2be3d50389 More cds.ql cleanup 2021-08-30 11:09:42 +02:00
Daniel
46b58f1b5c skipping ts test 2021-08-27 14:19:17 +02:00
Daniel
e87527cbcd fixed package-lock 2021-08-27 14:19:17 +02:00
Daniel
091844219b Upgrade to jest 27 2021-08-27 14:19:17 +02:00
Daniel
de796e5a89 Fixed '*' in upcomming release 2021-08-27 10:46:53 +02:00
Daniel
f988088412 '*' is still different 2021-08-26 06:50:13 +02:00
Daniel
c4a51ab719 fixed test 2021-08-26 06:50:13 +02:00
Daniel
f58376607a Prep for cleaning up cds.ql 2021-08-26 06:50:13 +02:00
Dr. David A. Kunz
839048d87c Merge pull request #260 from SAP-samples/fixing-test
Cascade delete test: Yes it should have been deleted
2021-08-19 12:57:56 +02:00
D065023
3b3463f889 skipped 2021-08-19 12:55:48 +02:00
D065023
d3396304ec yes it should be deleted 2021-08-19 12:45:03 +02:00
Pierre Fritsch
ae09caf7ad Enable cds watch hello
by moving the `hello` implementation into the subfolder `srv`
2021-08-03 16:01:10 +02:00
Daniel Hutzel
e1052c209b Removing workaround for glitch in drafts (#254)
* Removing workaround for glitch in drafts

* cosmetics
2021-08-02 17:20:46 +02:00
Christian Georgi
cd3aad59e1 Fix UI warnings
These were caused by the UI check not being aware of the parallel
`common.cds` file.
2021-07-14 10:11:21 +02:00
Christian Georgi
bcbf65f95e Draft for localized: expose Books.texts well
`Books.texts` must be explicitly exposed in order to be drafted
2021-07-13 16:23:44 +02:00
Christian Georgi
1189374860 Update to cds 5.3.1 2021-07-13 15:16:27 +02:00
Christian Georgi
9f2eb8fa83 Merge pull request #248 from SAP-samples/ts-node_support
Ts node support + tests
2021-07-13 14:32:49 +02:00
Christian Georgi
7b18e15e5b Jest config in package.json again 2021-07-13 13:55:05 +02:00
Christian Georgi
00bfc1507e Isolate all TS stuff in hello package 2021-07-13 13:24:29 +02:00
Christian Georgi
47a58e7393 Moved typescript dependencies to hello package
This allows TS specifics to stay local in just one package
2021-07-13 13:09:54 +02:00
Christian Georgi
c04b972d27 Cosmetics 2021-07-13 13:01:50 +02:00
Christian Georgi
85b9b9d3a3 Clear process variable after test 2021-07-13 13:01:50 +02:00
Tobias Steckenborn
3655eef971 fix: 🚑 Add testEnvironment again 2021-07-13 13:01:50 +02:00
Tobias Steckenborn
87dd356204 refactor: 🔥 Add ts-jest preset, remove manual transformation 2021-07-13 13:01:50 +02:00
Christian Georgi
a89ec34f7c Move jest config out of the way 2021-07-13 13:01:50 +02:00
Christian Georgi
444dcacb14 Add typescript package 2021-07-13 13:01:50 +02:00
Christian Georgi
2011a1ca7a Use transform option to make ts test work 2021-07-13 13:01:50 +02:00
Christian Georgi
7e0c36ede5 use cds-ts binary for starting cds with ts-node runtime 2021-07-13 13:01:50 +02:00
Christian Georgi
e8d08d039e Add example for starting cds with ts-node 2021-07-13 13:01:46 +02:00
Christian Georgi
5e3258913e Merge pull request #247 from SAP-samples/openapi
OpenAPI and Swagger UI
2021-07-12 14:04:30 +02:00
Christian Georgi
44e14926e4 Use latest Jest 2021-07-12 14:02:42 +02:00
Christian Georgi
592e945c28 Update package-lock 2021-07-12 14:02:21 +02:00
Christian Georgi
5d5c787365 Merge remote-tracking branch 'origin/master' into openapi 2021-07-05 17:59:00 +02:00
Christian Georgi
bb79def1d9 Merge pull request #239 from SAP-samples/using-package-lock.json
Using package-lock.json
2021-07-05 17:58:25 +02:00
Christian Georgi
bd4a38bf54 Update package-lock 2021-07-05 17:55:10 +02:00
Christian Georgi
abf18d74db Use npm ci instead of npm install 2021-07-05 17:55:10 +02:00
Christian Georgi
f144354229 Merge branch 'origin/master' into using-package-lock.json 2021-07-05 17:50:39 +02:00
Christian Georgi
97670bbc84 Swagger UI with cds-swagger-ui-express
Moved from `bookshop` to `fiori`, as that one already has a `server.js`
2021-07-05 17:46:24 +02:00
Christian Georgi
82d6f54337 Merge remote-tracking branch 'origin/master' into openapi 2021-07-05 17:39:06 +02:00
Heiko Witteborg
a4baee6e3b Merge pull request #243 from MikhailGoncharov/not-simple-args-fluent-api-v2
Sync with cds-runtime
2021-06-10 08:37:14 +02:00
Mikhail Goncharov
95b01e7d7a Do not trim quotes 2021-06-09 13:39:12 +02:00
Mikhail Goncharov
c2437fc419 Handle cds versions 2021-06-09 12:49:39 +02:00
Mikhail Goncharov
9381459458 Sync with cds-runtime 2021-06-09 10:43:10 +02:00
Daniel Hutzel
28a7c16895 Merge branch 'master' into using-package-lock.json 2021-06-09 07:38:42 +02:00
Christian Georgi
c1141e6c87 Remove trailing comma 2021-06-08 16:19:34 +02:00
Daniel
6e686c6c8b adding top-level dependencies to @sap/cds 2021-06-04 11:52:49 +02:00
Daniel
1ccddb2155 . 2021-06-04 11:44:21 +02:00
Daniel
7c8e52d5e9 Using package-lock.json 2021-06-04 11:43:59 +02:00
Christian Georgi
0dc1dc198d Merge pull request #227 from gregorwolf/fiori-currency-format
Fiori currency format
2021-05-28 15:08:15 +02:00
Christian Georgi
20bfb5c5f7 Merge branch 'master' into fiori-currency-format 2021-05-28 15:03:54 +02:00
Pierre Fritsch
be87f5617a Fix typo orderd -> ordered 2021-05-28 14:58:51 +02:00
Christian Georgi
3117df1282 Test with Jest 26
`cds.test` seems to have issues w/ Jest 27.

To be investigated...
2021-05-28 14:56:25 +02:00
Heiko Witteborg
be685437aa Merge pull request #231 from SAP-samples/windows-test-fix
Avoid race condition in customer-handlers.test
2021-05-14 13:41:17 +02:00
d049904
ff3801be71 Avoid race condition 2021-05-12 12:41:31 +02:00
Christian Georgi
702c3245bd Fix broken translation UI
Use `Books.texts` instead of `_texts`, requiring sap/cds 5
2021-05-10 10:33:39 +02:00
Daniel Hutzel
14002300c5 Rename index.cds to services.cds 2021-05-03 21:07:04 +02:00
Gregor Wolf
ad417ec061 fix test for Yen 2021-05-03 09:33:05 +02:00
Gregor Wolf
a6ec296129 add annotation for ISOCurrency 2021-05-03 09:04:38 +02:00
Gregor Wolf
1b10bbbfe4 switch currency of a book 2021-05-03 09:04:13 +02:00
Gregor Wolf
aaa1b2d6c7 add translation for Japanese Yen 2021-05-03 09:03:48 +02:00
Daniel
1cb169e886 fixed indentation 2021-04-27 16:06:59 +02:00
Daniel
9b9bb4c114 No process.chdir 2021-04-24 17:16:02 +02:00
Daniel
2536f36596 Adjust to latest capire docs 2021-04-13 09:55:27 +02:00
Daniel
5f160c0927 Added debug conf 2021-04-10 13:06:15 +02:00
Daniel
0c4658a3ef Fixed and adjusted to cds5 2021-04-10 13:06:15 +02:00
Christian Georgi
33cd70065a Use cds.test 2021-04-06 16:27:36 +02:00
Christian Georgi
450577d5a7 Refer to fiori app instead of bookshop 2021-04-06 16:11:24 +02:00
Daniel
5a23ba0f76 Prepare cds5 2021-04-01 16:47:45 +02:00
Daniel
064ce32b8a npm script cleanup 2021-03-27 19:39:48 +01:00
Daniel
2c064d0200 Adhere to public recommendations 2021-03-27 19:39:24 +01:00
Daniel
51ee6d167c added comment 2021-03-25 01:08:14 +01:00
Marc Becker
049345a39c Replace @assert.enum with @assert.range
@assert.enum has been deprecated since April 2020: https://cap.cloud.sap/docs/releases/apr20#value-range-annotations-for-odata
@assert.range is the correct annotation to use: https://cap.cloud.sap/docs/guides/generic#assertrange-check-constraints
2021-03-23 15:44:49 +01:00
Daniel
a7e0cf61a3 bump versions 2021-03-23 13:09:05 +01:00
Christian Georgi
1b0462f52e Merge pull request #207 from SAP-samples/avoid-index.cds
Avoid central index.cds files to ease extensibility
2021-03-23 13:08:31 +01:00
Daniel
2bae86079e app/-index -> app/services.cds 2021-03-23 11:14:05 +01:00
Christian Georgi
dcab44ab68 Hide license files 2021-03-17 09:06:48 +01:00
Brian Bernard
9f0629eef1 Fixed REUSE/badge 2021-03-12 11:02:35 -08:00
Brian Bernard
f6c938457f License updates
Added folder, removed top level License file
2021-03-12 10:12:55 -08:00
Daniel
647ac72588 Prepare for compiler v2 2021-03-03 10:39:32 +01:00
Daniel
2975038696 Avoid central index.cds files to ease extensibility 2021-03-02 10:37:02 +01:00
Iwona Hahn
861094cc83 Merge pull request #206 from SAP-samples/promote-code-tour
promote code tours
2021-03-02 09:15:19 +01:00
Christian Georgi
2368913612 Dummy change 2021-03-02 09:09:51 +01:00
Iwona Hahn
22ece2e586 typo 2021-03-01 20:44:05 +01:00
Iwona Hahn
ae788414f8 promote code tours 2021-03-01 20:42:43 +01:00
Iwona Hahn
ebc7017ded UA review 2021-03-01 20:42:23 +01:00
Christian Georgi
876ff94782 Merge pull request #205 from SAP-samples/db-codetour
Codetour for DB functions
2021-03-01 17:29:31 +01:00
Iwona Hahn
76d1b1865b minor edits 2021-03-01 17:24:53 +01:00
Christian Georgi
85dd8558f4 Some more explanation around file structure 2021-03-01 14:38:00 +01:00
Iwona Hahn
d0d95f3c42 first review, minor edits 2021-02-26 17:14:49 +01:00
Christian Georgi
c46a82d0f8 Codetour for DB functions 2021-02-26 16:40:57 +01:00
Iwona Hahn
7654012292 cosmetics codetour 2021-02-26 09:29:51 +01:00
Christian Georgi
65c8c82f74 Showcase how to integrate DB-specific functions (#201)
Augment `AdminService.Author` by 2 fields `age` and `lifetime` whose
values are computed by SQLite and HANA-specific functions.

Don't do this in bookshop directly, to keep it simple.  Instead, use
the fiori module, which also allows to integrate the fields in the UI.

Co-authored-by: Daniel <daniel.hutzel@sap.com>
2021-02-24 17:26:29 +01:00
Daniel
d368eb2ff5 fixed dependency -> never use "latest" 2021-02-19 16:34:15 +01:00
Christian Georgi
3cbb199870 Recommend CodeTours extension 2021-02-19 12:55:35 +01:00
Daniel
b51a08bf4e fixed .registry 2021-02-19 12:40:26 +01:00
Iwona Hahn
b31efc8083 minor edits 2021-02-17 14:07:42 +01:00
Christian Georgi
6669b983b1 First code tour with an overview of the sample repo 2021-02-17 14:07:42 +01:00
Daniel
b6e5a2fced Removed demo 2021-02-17 12:32:27 +01:00
Daniel
6de09e0940 Make usage of @capire/common optional 2021-02-17 12:32:27 +01:00
Daniel
b6f3914d79 Minor cleanup to npm registry mock 2021-02-17 12:32:27 +01:00
Daniel
28402c58b3 Simplified redirects to imported apps 2021-02-17 12:32:27 +01:00
Daniel
77de0e445e demo -> done 2021-02-17 12:32:27 +01:00
Daniel
a037d92c97 demo for bhagat 2021-02-17 12:32:27 +01:00
Christian Georgi
5e829ae7bb Merge branch 'origin/master' into openapi 2021-02-17 09:50:04 +01:00
Christian Georgi
b5031588ce Remove Node 10 for now 2021-02-09 17:15:24 +01:00
Christian Georgi
85319d9e8d Pin sqlite3 to 5.0.0
5.0.1 is broken at the moment
2021-02-09 17:15:24 +01:00
Iwona Hahn
39872200ae Update README.md 2021-02-01 14:54:55 +01:00
Iwona Hahn
6a4af929f1 minor updates 2021-01-27 08:49:33 +01:00
Christian Georgi
5b966c503c Add hint for authentication
So that fewer users stumble over the basic authentication popup.
2021-01-18 14:40:24 +01:00
Christian Georgi
75628b6096 Use latest UI5 again 2021-01-18 14:31:57 +01:00
Daniel
c12e516f5d Prep for upcomming releases 2021-01-13 16:30:40 +01:00
Marius Obert
01073fd6a5 Restructure sentence 2021-01-11 10:54:59 +01:00
Christian Georgi
d265a385f8 Some comments 2020-11-17 11:12:32 +01:00
Christian Georgi
80f469b5b6 Remove need for host to swagger UI 2020-11-17 11:06:04 +01:00
Christian Georgi
8e8ae949df Allow swagger.io to call us 2020-11-16 18:21:06 +01:00
Christian Georgi
3d24dc491d Create files for all services 2020-11-16 12:06:29 +01:00
Christian Georgi
f593265687 Cosmetics 2020-11-14 21:56:08 +01:00
Christian Georgi
6d488b042c Create diagram for Open API spec 2020-11-14 21:38:18 +01:00
Christian Georgi
d5ef630743 Link in index.html. Better server URL 2020-11-14 21:18:50 +01:00
Christian Georgi
b47c9d75df Embed Swagger UI 2020-11-13 16:10:07 +01:00
222 changed files with 16868 additions and 21017 deletions

View File

@@ -8,7 +8,7 @@
"mocha": true
},
"parserOptions": {
"ecmaVersion": 2018
"ecmaVersion": 2020
},
"globals": {
"SELECT": true,
@@ -22,6 +22,7 @@
"rules": {
"no-console": "off",
"require-atomic-updates": "off",
"require-await":"warn"
"require-await":"warn",
"no-unused-vars": ["warn", { "argsIgnorePattern": "_" }]
}
}

View File

@@ -5,9 +5,9 @@ name: CI
on:
push:
branches: [ master ]
branches: [ main ]
pull_request:
branches: [ master ]
branches: [ main ]
jobs:
build:
@@ -16,7 +16,7 @@ jobs:
strategy:
matrix:
node-version: [10.x, 12.x, 14.x]
node-version: [16.x, 14.x, 12.x]
steps:
- uses: actions/checkout@v2
@@ -24,5 +24,5 @@ jobs:
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm install
- run: npm ci
- run: npm test

2
.gitignore vendored
View File

@@ -15,3 +15,5 @@ default-env.json
packages/messageBox
reviews/msg-box
reviews/db/test.db
*.openapi3.json

3
.npmrc Normal file
View File

@@ -0,0 +1,3 @@
# Ensure we always use public packages, i.e. avoid using local registries from ~/.npmrc
@sap:registry=https://registry.npmjs.org/
registry=https://registry.npmjs.org/

1
.registry/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
*.tgz

View File

@@ -1,19 +1,25 @@
const { exec } = require ('child_process')
const isWin = process.platform === 'win32'
const express = require ('express')
const fs = require ('fs')
const app = express()
const { PORT=4444 } = process.env
const [,,port=PORT] = process.argv
const [,,port=PORT,scope='@capire'] = process.argv
const cwd = __dirname
// clean up on start (exit handler might not complete on Windows)
exec(isWin ? 'del *.tgz' : 'rm *.tgz', {cwd})
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)
const [, pkg ] = /^\w+-(\w+)/.exec(tarball)
fs.lstat(tarball,(err => {
if (err) exec(`npm pack ../${pkg}`,next)
if (err) console.debug (`npm pack ../${pkg}`)
if (err) exec(`npm pack ../${pkg}`,{cwd},next)
else next()
}))
} catch (e) {
@@ -25,12 +31,14 @@ app.use('/-/:tarball', (req,res,next) => {
app.use('/-', express.static(__dirname))
app.get('/*', (req,res)=>{
const urlRegex = /^\/(@\w+)\/(\w+)/
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`
if (!urlRegex.test(url)) return res.sendStatus(404)
const [, scpe, pkg ] = urlRegex.exec(url)
const package = require (`${scpe}/${pkg}/package.json`)
const tarball = `${scpe.slice(1)}-${pkg}-${package.version}.tgz`
// https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md
res.json({
"name": package.name,
@@ -42,29 +50,30 @@ app.get('/*', (req,res)=>{
"name": package.name,
"version": package.version,
"dist": {
"tarball": `http://localhost:${port}/-/${tarball}`
"tarball": `${server.url}/-/${tarball}`
},
}
},
})
} catch (e) {
console.error(e)
res.sendStatus(404)
if (e.code === 'MODULE_NOT_FOUND') return res.sendStatus(404)
console.error(e); throw e
}
})
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 server = app.listen(port, ()=>{
const url = server.url = `http://localhost:${server.address().port}`
console.log (`npm set ${scope}:registry=${url}`)
exec(`npm set ${scope}:registry=${url}`)
console.log (`${scope} registry listening on ${url}`)
})
const _exit = ()=>{
console.log ('\nnpm conf rm @capire:registry')
exec('npm conf rm @capire:registry')
exec('rm *.tgz')
process.exit()
server.close()
exec(`npm conf rm "${scope}:registry"`, ()=> { process.exit() })
}
process.on ('SIGTERM',_exit)
process.on ('SIGHUP',_exit)
process.on ('SIGINT',_exit)

117
.tours/db-native.tour Normal file
View File

@@ -0,0 +1,117 @@
{
"$schema": "https://aka.ms/codetour-schema",
"title": "Database Functions",
"steps": [
{
"title": "Introduction",
"description": "### Database Functions in CDS Models\n\nIn this tour, you'll learn how to add database-specific functions to CDS models in your application."
},
{
"file": "bookshop/db/schema.cds",
"description": "#### Basic Schema\n\nWe want to add two fields to the `Authors` entity, one for the author's age and one for the span of years that she or he lived.\n\nThese two fields can be computed out of the existing `dateOfBirth` and `dateOfDeath` fields.",
"selection": {
"start": {
"line": 19,
"character": 1
},
"end": {
"line": 21,
"character": 1
}
},
"title": "Base fields in Author"
},
{
"file": "bookshop/srv/admin-service.cds",
"description": "This is how the `Authors` entity gets exposed in an OData or REST service.\n\nIn the next step, you'll see how we extend this projection.",
"selection": {
"start": {
"line": 4,
"character": 1
},
"end": {
"line": 5,
"character": 1
}
},
"title": "Authors service"
},
{
"file": "fiori/db/sqlite/index.cds",
"description": "#### SQLite Implementation\n\nHere's the first implementation for SQLite. It computes the two fields `age` and `lifetime` through SQLite's [strftime](https://sqlite.org/lang_datefunc.html) function.\n\nThrough the [`extend projection`](https://cap.cloud.sap/docs/cds/cdl#extend-view) clause you can add additional fields to projection entities. These are deployed as database views, which is why we can integrate the database functions in the first place.\n",
"selection": {
"start": {
"line": 7,
"character": 1
},
"end": {
"line": 11,
"character": 1
}
},
"title": "SQLite implementation"
},
{
"file": "fiori/db/hana/index.cds",
"description": "#### SAP HANA Implementation\n\nThis is the second implementation for SAP HANA. It computes the same two fields `age` and `lifetime` through the [YEARS_BETWEEN](https://help.sap.com/viewer/7c78579ce9b14a669c1f3295b0d8ca16/Cloud/en-US/7c0d2c161ea34def86de3f5eadd6a0af.html) and [YEAR](https://help.sap.com/viewer/7c78579ce9b14a669c1f3295b0d8ca16/Cloud/en-US/20f5fac6751910148dabd3c6821f907d.html) functions of SAP HANA.\n\n#### File Layout and Code Structure\n\nNote the path of the `.cds` file we are in: it's in a subfolder of `db`, so that it's _not_ automatically picked up when we start the application. The same is true for the SQLite implementation: it's in a separate `db/sqlite/` folder as well. In the next step, you'll see how these files are loaded.\n\nAlso, we choose to implement all of that as an extension of the original bookshop here in the _fiori_ package. See the first [CAP Samples] code tour for more details on the different packages of this repository.",
"selection": {
"start": {
"line": 7,
"character": 1
},
"end": {
"line": 11,
"character": 1
}
},
"title": "SAP HANA implementation"
},
{
"file": "fiori/package.json",
"description": "#### Configuration\n\nThe `cds.requires` section in `package.json` is a place to configure which of the `db/sqlite` and `db/hana` folders are used for which database.\n\nWe use [Node.js profiles](https://cap.cloud.sap/docs/node.js/cds-env#profiles) to separate the configuration.\nIn the `development` profile, you can see that `db/sqlite` is set as the model, while the `db/hana` folder is configured in the `production` profile. `db-ext` is a pseudo datasource, its name doesn't matter.\n\nSee [`cds.resolve`](https://cap.cloud.sap/docs/node.js/cds-compile#cds-resolve) to learn more about how models are found.",
"selection": {
"start": {
"line": 41,
"character": 1
},
"end": {
"line": 48,
"character": 1
}
},
"title": "Configuration"
},
{
"file": "fiori/package.json",
"description": "#### Run with SQLite\n\nTo run with `development` and an in-memory SQLite database, you don't need to do anything special, because it's activated by default. Just run:\n\n>> cds watch fiori\n\nThen open [http://localhost:4004/admin/Authors](http://localhost:4004/admin/Authors) to see the two new fields.\n",
"line": 43,
"title": "Run with SQLite"
},
{
"file": "fiori/package.json",
"description": "#### Deploy the CDS Model to SAP HANA\n\nTo 'activate' SAP HANA through the `production` profile, you can use the global `--production` flag:\n\n>> cd fiori; cds deploy --to hana --production\n\n[Learn more about SAP HANA deployment](https://cap.cloud.sap/docs/guides/databases#get-hana)\n\n#### Run the Application\n\n>> cd fiori; cds watch --production\n\nThe service on [http://localhost:4004/admin/Authors](http://localhost:4004/admin/Authors) is the same as before, but this time the `Authors` entity is backed by a database view with an SAP HANA function.\n\n#### More\n\nIf you don't see data, you can add some in the next step.",
"line": 46,
"title": "Run with SAP HANA"
},
{
"file": "fiori/test/requests.http",
"description": "### Add More Data\n\nOptionally you can add some `Authors` data by clicking on the _Send Request_ link (provided by the [REST client](https://marketplace.visualstudio.com/items?itemName=humao.rest-client) extension).",
"line": 72,
"selection": {
"start": {
"line": 67,
"character": 1
},
"end": {
"line": 73,
"character": 1
}
},
"title": "Add Data"
},
{
"title": "Wrap-up",
"description": "### Summary\n\nThat's it! You have seen: \n- How to integrate database-specific functions in a CDS model.\n- How to switch between the two implementations for SQLite and SAP HANA."
}
]
}

139
.tours/samples.tour Normal file
View File

@@ -0,0 +1,139 @@
{
"$schema": "https://aka.ms/codetour-schema",
"title": "CAP Samples",
"steps": [
{
"title": "Welcome",
"file": "README.md",
"description": "### Welcome to CAP Samples!\n\nThis tour leads you through a collection of samples for the [SAP Cloud Application Programming Model (CAP)](https://cap.cloud.sap).\nYou will learn which features of the programming model are demonstrated in which sample.\n\nLet's start!",
"line": 2,
"selection": {
"start": {
"line": 1,
"character": 1
},
"end": {
"line": 3,
"character": 108
}
}
},
{
"file": "hello/srv/world.cds",
"description": "### Hello World!\n\nThis is a simplistic [Hello World](https://cap.cloud.sap/docs/get-started/hello-world) service using [CDS](https://cap.cloud.sap/docs/cds/) and [cds.services](https://cap.cloud.sap/docs/node.js/api#services-api).",
"line": 2,
"selection": {
"start": {
"line": 1,
"character": 1
},
"end": {
"line": 4,
"character": 1
}
},
"title": "Hello World!"
},
{
"file": "bookshop/db/schema.cds",
"description": "### A Bookshop!\n\nIntroduces:\n- [Project Setup](https://cap.cloud.sap/docs/get-started/) and [Layouts](https://cap.cloud.sap/docs/get-started/projects)\n- [Domain Modeling](https://cap.cloud.sap/docs/guides/domain-models)\n- [Defining Services](https://cap.cloud.sap/docs/guides/providing-services)\n- [Generic Providers](https://cap.cloud.sap/docs/guides/generic-providers)\n- [Adding Custom Logic](https://cap.cloud.sap/docs/guides/service-impl)\n- [Using Databases](https://cap.cloud.sap/docs/guides/databases)\n",
"line": 1,
"selection": {
"start": {
"line": 1,
"character": 1
},
"end": {
"line": 32,
"character": 1
}
},
"title": "Bookshop"
},
{
"file": "common/index.cds",
"description": "### Extend and Reuse\n\nShowcases how to extend [@sap/cds/common](https://cap.cloud.sap/docs/cds/common) thereby covering:\n- Building [extension packages](https://cap.cloud.sap/docs/guides/domain-models#aspects-extensibility)\n- Providing [reuse packages](https://cap.cloud.sap/docs/get-started/projects#sharing-and-reusing-content)\n- [Verticalization](https://cap.cloud.sap/docs/cds/common#adapting-to-your-needs)\n- Using [Aspects](https://cap.cloud.sap/docs/cds/cdl#aspects)\n- Used in the [fiori app sample](#fiori)\n",
"line": 1,
"selection": {
"start": {
"line": 1,
"character": 1
},
"end": {
"line": 46,
"character": 1
}
},
"title": "Common"
},
{
"file": "orders/db/schema.cds",
"description": "### Orders - Compositions and Serving Documents\n\nA standalone orders management service, demonstrating:\n- Using [Compositions](https://cap.cloud.sap/docs/cds/cdl#compositions) in [Domain Models](https://cap.cloud.sap/docs/guides/domain-models), along with\n- [Serving deeply nested documents](https://cap.cloud.sap/docs/guides/generic-providers#serving-structured-data)\n",
"line": 1,
"selection": {
"start": {
"line": 1,
"character": 1
},
"end": {
"line": 27,
"character": 1
}
},
"title": "Orders"
},
{
"file": "reviews/db/schema.cds",
"description": "### Reviews - More Modularity\n\nShows how to implement a modular service to manage product reviews, including:\n- Consuming other services synchronously and asynchronously\n- Serving requests synchronously\n- Emitting events asynchronously\n- Grow as you go, with:\n- Mocking app services\n- Running service meshes\n- Late-cut Micro Services\n- As well as managed data, input validations, and authorization\n",
"line": 1,
"selection": {
"start": {
"line": 1,
"character": 1
},
"end": {
"line": 39,
"character": 1
}
},
"title": "Reviews"
},
{
"title": "Bookstore",
"description": "### Bookstore - Reuse and UI\n\n- A [composite app, reusing and combining](https://cap.cloud.sap/docs/guides/reuse-and-compose) these packages:\n - [@capire/bookshop](bookshop)\n - [@capire/reviews](reviews)\n - [@capire/orders](orders)\n - [@capire/common](common)\n- [The Vue.js app](bookshop/app/vue) imported from bookshop is served as well\n- [The Vue.js app](reviews/app/vue) imported from reviews is served as well\n- [The Fiori app](orders/app) imported from orders is served as well\n- [OpenAPI export + Swagger UI](https://cap.cloud.sap/docs/advanced/openapi)"
},
{
"file": "fiori/app/services.cds",
"description": "### Annotations for SAP Fiori Elements\n\n- [Adds an SAP Fiori elements application](https://cap.cloud.sap/docs/guides/fiori/) to bookstore, thereby introducing to:\n- [OData Annotations](https://cap.cloud.sap/docs/guides/fiori#adding-odata-annotations) in `.cds` files\n- Support for [Fiori Draft](https://cap.cloud.sap/docs/guides/fiori#draft)\n- Support for [Value Helps](https://cap.cloud.sap/docs/guides/fiori#value-help)\n- Serving SAP Fiori apps locally\n",
"line": 1,
"selection": {
"start": {
"line": 1,
"character": 1
},
"end": {
"line": 13,
"character": 1
}
},
"title": "Fiori"
},
{
"file": "package.json",
"description": "### All-in-one Monorepo\n\nEach sample sub directory essentially is a standard npm package, some with standard npm dependencies to other samples. The root folder's [package.json](package.json) has local links to the sub folders, such that an `npm install` populates a local `node_modules` folder acts like a local npm registry to the individual sample packages.\n",
"selection": {
"start": {
"line": 8,
"character": 1
},
"end": {
"line": 16,
"character": 1
}
},
"title": "Packages"
}
],
"isPrimary": true,
"description": "Overview of CAP Samples for Node.js"
}

View File

@@ -4,14 +4,15 @@
// List of extensions which should be recommended for users of this workspace.
"recommendations": [
"SAPSE.vscode-cds",
"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"
"sdras.night-owl",
"vsls-contrib.codetour"
],
// 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,28 +4,31 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Attach by Process ID",
"processId": "${command:PickProcess}",
"request": "attach",
"skipFiles": [
"<node_internals>/**"
],
"type": "pwa-node"
},
{
"name": "bookshop",
"command": "cds watch bookshop",
"request": "launch",
"command": "npx cds watch bookshop",
"type": "node-terminal",
"skipFiles": ["<node_internals>/**"]
"request": "launch",
"skipFiles": [
"<node_internals>/**",
"**/node_modules/**",
"**/cds/lib/lazy.js",
"**/cds/lib/req/cls.js",
"**/odata-v4/okra/**"
]
},
{
"name": "Fiori app",
"command": "cds watch fiori",
"request": "launch",
"name": "Fiori App",
"command": "npx cds watch fiori",
"type": "node-terminal",
"skipFiles": ["<node_internals>/**"]
"request": "launch",
"skipFiles": [
"<node_internals>/**",
"**/node_modules/**",
"**/cds/lib/lazy.js",
"**/cds/lib/req/cls.js",
"**/odata-v4/okra/**"
]
}
],
"inputs": [
@@ -33,7 +36,7 @@
"type": "pickString",
"id": "sample",
"description": "Which sample do you want to start?",
"options": ["bookshop", "fiori", "reviews", "reviews/test/bookshop"],
"options": [ "bookshop", "fiori", "reviews", "reviews" ],
"default": "bookshop"
}
]

16
.vscode/settings.json vendored
View File

@@ -1,6 +1,18 @@
{
"files.exclude": {
".reuse/**": true,
"**/.gitignore": true,
"**/.vscode": true
}
"**/.vscode": true,
"LICENSES/**": true
},
"debug.javascript.terminalOptions": {
"skipFiles": [
"<node_internals>/**",
"**/node_modules/**",
"**/cds/lib/lazy.js",
"**/cds/lib/req/cls.js",
"**/odata-v4/okra/**"
]
},
"mochaExplorer.parallel": true
}

343
LICENSE
View File

@@ -1,208 +1,201 @@
Apache License
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
Version 2.0, January 2004
http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION,
AND DISTRIBUTION
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"License" shall mean the terms and conditions for use, reproduction, and distribution
as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"Licensor" shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Legal Entity" shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, "control" means (i) the power, direct
or indirect, to cause the direction or management of such entity, whether
by contract or otherwise, or (ii) ownership of fifty percent (50%) or more
of the outstanding shares, or (iii) beneficial ownership of such entity.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions
granted by this License.
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Source" form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
"Object" form shall mean any form resulting from mechanical transformation
or translation of a Source form, including but not limited to compiled object
code, generated documentation, and conversions to other media types.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
"Work" shall mean the work of authorship, whether in Source or Object form,
made available under the License, as indicated by a copyright notice that
is included in or attached to the work (an example is provided in the Appendix
below).
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
"Derivative Works" shall mean any work, whether in Source or Object form,
that is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative
Works shall not include works that remain separable from, or merely link (or
bind by name) to the interfaces of, the Work and Derivative Works thereof.
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part 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 Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
"Contribution" shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative
Works thereof, that is intentionally submitted to Licensor for inclusion in
the Work by the copyright owner or by an individual or Legal Entity authorized
to submit on behalf of the copyright owner. For the purposes of this definition,
"submitted" means any form of electronic, verbal, or written communication
sent to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor
for the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as "Not a Contribution."
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently incorporated
within the Work.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
2. Grant of Copyright License. Subject to the terms and conditions of this
License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive,
no-charge, royalty-free, irrevocable copyright license to reproduce, prepare
Derivative Works of, publicly display, publicly perform, sublicense, and distribute
the Work and such Derivative Works in Source or Object form.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
3. Grant of Patent License. Subject to the terms and conditions of this License,
each Contributor hereby grants to You a perpetual, worldwide, non-exclusive,
no-charge, royalty-free, irrevocable (except as stated in this section) patent
license to make, have made, use, offer to sell, sell, import, and otherwise
transfer the Work, where such license applies only to those patent claims
licensable by such Contributor that are necessarily infringed by their Contribution(s)
alone or by combination of their Contribution(s) with the Work to which such
Contribution(s) was submitted. If You institute patent litigation against
any entity (including a cross-claim or counterclaim in a lawsuit) alleging
that the Work or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses granted to You
under this License for that Work shall terminate as of the date such litigation
is filed.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any 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,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
4. Redistribution. You may reproduce and distribute copies of the Work or
Derivative Works thereof in any medium, with or without modifications, and
in Source or Object form, provided that You meet the following conditions:
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
(a) You must give any other recipients of the Work or Derivative Works a copy
of this License; and
END OF TERMS AND CONDITIONS
(b) You must cause any modified files to carry prominent notices stating that
You changed the files; and
APPENDIX: How to apply the Apache License to your work.
(c) You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source
form of the Work, excluding those notices that do not pertain to any part
of the Derivative Works; and
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
(d) If the Work includes a "NOTICE" text file as part of its distribution,
then any Derivative Works that You distribute must include a readable copy
of the attribution notices contained within such NOTICE file, excluding those
notices that do not pertain to any part of the Derivative Works, in at least
one of the following places: within a NOTICE text file distributed as part
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
Works, if and wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works
that You distribute, alongside or as an addendum to the NOTICE text from the
Work, provided that such additional attribution notices cannot be construed
as modifying the License.
Copyright [yyyy] [name of copyright owner]
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction,
or distribution of Your modifications, or for any such Derivative Works as
a whole, provided Your use, reproduction, and distribution of the Work otherwise
complies with the conditions stated in this License.
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
5. Submission of Contributions. Unless You explicitly state otherwise, any
Contribution intentionally submitted for inclusion in the Work by You to the
Licensor shall be under the terms and conditions of this License, without
any additional terms or conditions. Notwithstanding the above, nothing herein
shall supersede or modify the terms of any separate license agreement you
may have executed with Licensor regarding such Contributions.
http://www.apache.org/licenses/LICENSE-2.0
6. Trademarks. This License does not grant permission to use the trade names,
trademarks, service marks, or product names of the Licensor, except as required
for reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or agreed to
in writing, Licensor provides the Work (and each Contributor provides its
Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied, including, without limitation, any warranties
or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR
A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness
of using or redistributing the Work and assume any risks associated with Your
exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory, whether
in tort (including negligence), contract, or otherwise, unless required by
applicable law (such as deliberate and grossly negligent acts) or agreed to
in writing, shall any Contributor be liable to You for damages, including
any direct, indirect, special, incidental, or consequential damages of any
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,
work stoppage, computer failure or malfunction, or any and all other commercial
damages or losses), even if such Contributor has been advised of the possibility
of such damages.
9. Accepting Warranty or Additional Liability. While redistributing the Work
or Derivative Works thereof, You may choose to offer, and charge a fee for,
acceptance of support, warranty, indemnity, or other liability obligations
and/or rights consistent with this License. However, in accepting such obligations,
You may act only on Your own behalf and on Your sole responsibility, not on
behalf of any other Contributor, and only if You agree to indemnify, defend,
and hold each Contributor harmless for any liability incurred by, or claims
asserted against, such Contributor by reason of your accepting any such warranty
or additional liability. END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets "[]" replaced with your own identifying
information. (Don't include the brackets!) The text should be enclosed in
the appropriate comment syntax for the file format. We also recommend that
a file or class name and description of purpose be included on the same "printed
page" as the copyright notice for easier identification within third-party
archives.
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.
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.

View File

@@ -5,19 +5,19 @@ Find here a collection of samples for the [SAP Cloud Application Programming Mod
![](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)
### Preliminaries
1. [Install @sap/cds-dk](https://cap.cloud.sap/docs/get-started/) globally as documented in [capire](https://cap.cloud.sap)
1. Install [**@sap/cds-dk**](https://cap.cloud.sap/docs/get-started/) globally:
```sh
npm i -g @sap/cds-dk
```
2. _Optional:_ [Use Visual Studio Code](https://cap.cloud.sap/docs/get-started/in-vscode)
2. _Optional:_ [Use Visual Studio Code](https://cap.cloud.sap/docs/get-started/tools#vscode)
### 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).
If you've [Git](https://git-scm.com/downloads) installed, clone this repo as shown below, otherwise [download as ZIP file](archive/main.zip).
```sh
git clone https://github.com/sap-samples/cloud-cap-samples samples
@@ -42,37 +42,45 @@ cds watch bookshop
After that open this link in your browser: [http://localhost:4004](http://localhost:4004)
When asked to log in, type `alice` as user and leave the password field blank, which is the [default user](https://cap.cloud.sap/docs/node.js/authentication#mocked).
### 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.
### Serve `npm`
### Serve `npm`
We've simple npm registry mock included which allows you to do an `npm install @capire/<package>` anywhere locally. Use it as follows:
We've included a simple npm registry mock, which allows you to do an `npm install @capire/<package>` 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.
> While running this will have `@capire:registry=http://localhost:4444` set with npmrc.
2. Install one of the @capire packages wherever you like, for example:
2. Install one of the @capire packages wherever you like, e.g.:
```sh
npm add @capire/common @capire/bookshop
```
## Code Tours
Take one of the [guided tours](.tours) in VS Code through our CAP samples and learn which CAP features are showcased by the different parts of the repository. Just install the [CodeTour extension](https://marketplace.visualstudio.com/items?itemName=vsls-contrib.codetour) for VS Code. We'll add more code tours in the future. Stay tuned!
## Get Support
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).
In case you've a question, find a bug, or otherwise need support, use our [community](https://answers.sap.com/tags/9f13aee1-834c-4105-8e43-ee442775e5ce) to get more visibility.
## License
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.
Copyright (c) 2022 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](LICENSE) file.

View File

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

View File

@@ -10,7 +10,7 @@ const books = new Vue ({
data: {
list: [],
book: undefined,
order: { amount:1, succeeded:'', failed:'' }
order: { quantity:1, succeeded:'', failed:'' }
},
methods: {
@@ -26,18 +26,18 @@ const books = new Vue ({
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 }
books.order = { quantity:1 }
setTimeout (()=> $('form > input').focus(), 111)
},
async submitOrder () {
const {book,order} = books, amount = parseInt (order.amount) || 1 // REVISIT: Okra should be less strict
const {book,order} = books, quantity = parseInt (order.quantity) || 1 // REVISIT: Okra should be less strict
try {
const res = await POST(`/submitOrder`, { amount, book: book.ID })
const res = await POST(`/submitOrder`, { quantity, book: book.ID })
book.stock = res.data.stock
books.order = { amount, succeeded: `Successfully orderd ${amount} item(s).` }
books.order = { quantity, succeeded: `Successfully ordered ${quantity} item(s).` }
} catch (e) {
books.order = { amount, failed: e.response.data.error.message }
books.order = { quantity, failed: e.response.data.error.message }
}
}

View File

@@ -34,9 +34,9 @@
<td>{{ book.author }}</td>
<td>{{ book.genre.name }}</td>
<td class="rating-stars">
{{ ('★'.repeat(Math.round(book.rating))+'☆☆☆☆☆').slice(0,5) }}
{{ ('★'.repeat(Math.round(book.rating))+'☆☆☆☆☆').slice(0,5) }} ({{ book.numberOfReviews }})
</td>
<td>{{ book.currency.symbol }} {{ book.price }}</td>
<td>{{ book.currency && book.currency.symbol }} {{ book.price }}</td>
</tr>
</table>
@@ -48,7 +48,7 @@
&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="number" v-model="order.quantity" v-bind:class="{ failed: order.failed }" style="width:5em">
<input type="submit" value="Order:" class="muted-button">
</form>
<h4> {{ book.title }} </h4>

View File

@@ -3,4 +3,4 @@ ID;title;descr;author_ID;stock;price;currency_code;genre_ID
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;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;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;13
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;150;JPY;13
1 ID title descr author_ID stock price currency_code genre_ID
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 150 EUR JPY 13

24
bookshop/db/init.js Normal file
View File

@@ -0,0 +1,24 @@
/**
* In order to keep basic bookshop sample as simple as possible, we don't add
* reuse dependencies. This db/init.js ensures we still have a minimum set of
* currencies, if not obtained through @capire/common.
*/
module.exports = async (db)=>{
const has_common = db.model.definitions['sap.common.Currencies'].elements.numcode
if (has_common) return
const already_filled = await db.exists('sap.common.Currencies',{code:'EUR'})
if (already_filled) return
await INSERT.into ('sap.common.Currencies') .columns (
'code','symbol','name'
) .rows (
[ 'EUR','€','Euro' ],
[ 'USD','$','US Dollar' ],
[ 'GBP','£','British Pound' ],
[ 'ILS','₪','Shekel' ],
[ 'JPY','¥','Yen' ],
)
}

View File

@@ -1 +1,2 @@
exports.CatalogService = require('./srv/cat-service')
const { CatalogService } = require('./srv/cat-service')
module.exports = { CatalogService }

View File

@@ -3,8 +3,7 @@
"version": "1.0.0",
"description": "A simple self-contained bookshop service.",
"dependencies": {
"@capire/common": "*",
"@sap/cds": "^4",
"@sap/cds": "^5.0.4",
"express": "^4.17.1",
"passport": "0.4.1"
},

View File

@@ -5,10 +5,10 @@ This stand-alone sample introduces the essential tasks in the development of CAP
## Hypothetical Use Cases
1. Build a service that allows to browse _Books_ and _Authors_.
2. Books have assigned _Genres_ which are organized hierarchically.
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)
5. End users may order books (the actual order mgmt being out of scope).
## Running the Sample
@@ -20,12 +20,12 @@ npm run watch
| 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) |
| [Project Setup & Layouts](https://cap.cloud.sap/docs/get-started/projects#sharing-and-reusing-content) | [`./`](./) |
| [Domain Modeling with CDS](https://cap.cloud.sap/docs/guides/domain-models) | [`./db/schema.cds`](./db/schema.cds) |
| [Defining Services](https://cap.cloud.sap/docs/guides/services#defining-services) | [`./srv/*.cds`](./srv) |
| [Single-purposed Services](https://cap.cloud.sap/docs/guides/services#single-purposed-services) | [`./srv/*.cds`](./srv) |
| [Providing & Consuming Providers](https://cap.cloud.sap/docs/guides/providing-services) | http://localhost:4004 |
| [Using Databases](https://cap.cloud.sap/docs/guides/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) |
| Adding Tests | [`./test`](./test) |
| [Sharing for Reuse](https://cap.cloud.sap/docs/guides/reuse-and-compose) | [`./index.cds`](./index.cds) |

View File

@@ -1,14 +1,16 @@
using { sap.capire.bookshop as my } from '../db/schema';
service CatalogService @(path:'/browse') {
@readonly entity Books as SELECT from my.Books { *,
/** For displaying lists of Books */
@readonly entity ListOfBooks as projection on Books
excluding { descr };
/** For display in details pages */
@readonly entity Books as projection on 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 };
action submitOrder ( book: Books:ID, quantity: Integer ) returns { stock: Integer };
event OrderedBook : { book: Books:ID; quantity: Integer; buyer: String };
}

View File

@@ -1,25 +1,25 @@
const cds = require('@sap/cds')
const { Books } = cds.entities ('sap.capire.bookshop')
class CatalogService extends cds.ApplicationService { init(){
const { Books } = cds.entities ('sap.capire.bookshop')
// 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}`)
const {book,quantity} = req.data
if (quantity < 1) return req.reject (400,`quantity has to be 1 or more`)
let b = await SELECT `stock` .from (Books,book)
if (!b) return req.error (404,`Book #${book} doesn't exist`)
let {stock} = b
if (quantity > stock) return req.reject (409,`${quantity} exceeds stock for book #${book}`)
await UPDATE (Books,book) .with ({ stock: stock -= quantity })
await this.emit ('OrderedBook', { book, quantity, buyer:req.user.id })
return { stock }
})
// Add some discount for overstocked books
this.after ('READ','Books', each => {
if (each.stock > 111) {
each.title += ` -- 11% discount!`
}
this.after ('READ','ListOfBooks', each => {
if (each.stock > 111) each.title += ` -- 11% discount!`
})
return super.init()

View File

@@ -32,6 +32,19 @@ GET {{server}}/admin/Authors?
# &sap-language=de
Authorization: Basic alice:
### ------------------------------------------------------------------------
# Create Author
POST {{server}}/admin/Authors
Content-Type: application/json;IEEE754Compatible=true
Authorization: Basic alice:
{
"ID": 112,
"name": "Shakespeeeeere",
"age": 22
}
### ------------------------------------------------------------------------
# Create book
POST {{server}}/admin/Books
@@ -71,7 +84,7 @@ POST {{server}}/browse/submitOrder
Content-Type: application/json
{{me}}
{ "book":201, "amount":5 }
{ "book":201, "quantity":5 }
### ------------------------------------------------------------------------

2
bookstore/index.cds Normal file
View File

@@ -0,0 +1,2 @@
namespace sap.capire.bookshop; //> important for reflection
using from './srv/mashup';

34
bookstore/package.json Normal file
View File

@@ -0,0 +1,34 @@
{
"name": "@capire/bookstore",
"version": "1.0.0",
"dependencies": {
"@capire/bookshop": "*",
"@capire/reviews": "*",
"@capire/orders": "*",
"@capire/common": "*",
"@capire/data-viewer": "*",
"@sap/cds": "^5",
"express": "^4.17.1"
},
"cds": {
"requires": {
"ReviewsService": {
"kind": "odata",
"model": "@capire/reviews"
},
"OrdersService": {
"kind": "odata",
"model": "@capire/orders"
},
"messaging": {
"[development]": { "kind": "file-based-messaging" },
"[hybrid]": { "kind": "enterprise-messaging-shared" },
"[production]": { "kind": "enterprise-messaging" }
},
"db": {
"kind": "sql"
}
},
"log": { "service": true }
}
}

22
bookstore/server.js Normal file
View File

@@ -0,0 +1,22 @@
const cds = require ('@sap/cds')
// Add mashup logic
cds.once('served', require('./srv/mashup'))
// Add routes to UIs from imported packages
cds.once('bootstrap',(app)=>{
app.serve ('/bookshop') .from ('@capire/bookshop','app/vue')
app.serve ('/reviews') .from ('@capire/reviews','app/vue')
app.serve ('/orders') .from('@capire/orders','app/orders')
app.serve ('/data') .from('@capire/data-viewer','app/viewer')
})
// Add Swagger UI
require('./srv/swagger-ui')
// Returning cds.server
module.exports = cds.server
// For didactic reasons in capire
const { ReviewsService, OrdersService } = cds.requires
if (!ReviewsService.credentials && !OrdersService.credentials) cds.requires.messaging = false

38
bookstore/srv/mashup.cds Normal file
View File

@@ -0,0 +1,38 @@
////////////////////////////////////////////////////////////////////////////
//
// Enhancing bookshop with Reviews and Orders provided through
// respective reuse packages and services
//
using { sap.capire.bookshop.Books } from '@capire/bookshop';
//
// Extend Books with access to Reviews and average ratings
//
using { ReviewsService.Reviews } from '@capire/reviews';
extend Books with {
reviews : Composition of many Reviews on reviews.subject = $self.ID;
rating : Decimal;
numberOfReviews : Integer;
}
//
// Extend Orders with Books as Products
//
using { sap.capire.orders.Orders } from '@capire/orders';
extend Orders with {
extend Items with {
book : Association to Books on product.ID = book.ID
}
}
// Add orders fiori app (in case of embedded orders service)
using from '@capire/orders/app/fiori';
// Add data browser
using from '@capire/data-viewer';
// Incorporate pre-build extensions from...
using from '@capire/common';

View File

@@ -1,6 +1,6 @@
////////////////////////////////////////////////////////////////////////////
//
// Mashing up provided and required services...
// Mashing up bookshop services with required services...
//
module.exports = async()=>{ // called by server.js
@@ -20,30 +20,29 @@ module.exports = async()=>{ // called by server.js
CatalogService.prepend (srv => srv.on ('READ', 'Books/reviews', (req) => {
console.debug ('> delegating request to ReviewsService')
const [id] = req.params, { columns, limit } = req.query.SELECT
return ReviewsService.tx(req).read ('Reviews',columns).limit(limit).where({subject:String(id)})
return ReviewsService.read ('Reviews',columns).limit(limit).where({subject:String(id)})
}))
//
// Create an order with the OrdersService when CatalogService signals a new order
//
CatalogService.on ('OrderedBook', async (msg) => {
const { book, amount, buyer } = msg.data
const { book, quantity, buyer } = msg.data
const { title, price } = await db.tx(msg).read (Books, book, b => { b.title, b.price })
return OrdersService.tx(msg).create ('Orders').entries({
OrderNo: 'Order at '+ (new Date).toLocaleString(),
Items: [{ product:{ID:`${book}`}, title, price, amount }],
Items: [{ product:{ID:`${book}`}, title, price, quantity }],
buyer, createdBy: buyer
})
})
//
// Update Books' average ratings when ReviewsService signals updatd reviews
// Update Books' average ratings when ReviewsService signals updated reviews
//
ReviewsService.on ('reviewed', (msg) => {
console.debug ('> received:', msg.event, msg.data)
const { subject, rating } = msg.data
return UPDATE(Books,subject).with({rating})
// ^ Note: the framework will execute this and take care for db.tx
const { subject, count, rating } = msg.data
return UPDATE(Books,subject).with({ numberOfReviews:count, rating })
})
//
@@ -51,9 +50,9 @@ module.exports = async()=>{ // called by server.js
//
OrdersService.on ('OrderChanged', (msg) => {
console.debug ('> received:', msg.event, msg.data)
const { product, deltaAmount } = msg.data
const { product, deltaQuantity } = msg.data
return UPDATE (Books) .where ('ID =', product)
.and ('stock >=', deltaAmount)
.set ('stock -=', deltaAmount)
.and ('stock >=', deltaQuantity)
.set ('stock -=', deltaQuantity)
})
}

View File

@@ -0,0 +1,10 @@
// -----------------------------------------------------------------------
// Adding Swagger UI - see https://cap.cloud.sap/docs/advanced/openapi
const cds = require ('@sap/cds')
try {
const cds_swagger = require ('cds-swagger-ui-express')
cds.once ('bootstrap', app => app.use (cds_swagger()) )
} catch (err) {
if (err.code !== 'MODULE_NOT_FOUND') throw err
}

View File

@@ -2,7 +2,7 @@
@bookshop = http://localhost:4004
@reviews-service = {{bookshop}}/reviews
# Uncomment this when running a separate reviews service
@reviews-service = http://localhost:4005/reviews
# @reviews-service = http://localhost:4005/reviews
@@ -38,7 +38,11 @@ GET {{bookshop}}/browse/Books(201)?
&$select=ID,title,rating
&$expand=reviews
###
GET {{bookshop}}/browse/Books?
&$select=title,author&$expand=currency
Accept-Language: de
#################################################
#
@@ -63,3 +67,15 @@ Content-Type: application/json
### Get active order
GET {{bookshop}}/orders/Orders(ID={{newOrderID}},IsActiveEntity=true)
### Create author
POST {{bookshop}}/admin/Authors
Content-Type: application/json
Authorization: Basic alice:
{
"ID": 200,
"name": "William Shakespeare",
"dateOfBirth": "1564-04-26",
"dateOfDeath": "1616-04-23"
}

View File

@@ -1,3 +0,0 @@
# 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
View File

@@ -1,35 +0,0 @@
# 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

View File

@@ -1,20 +0,0 @@
{
// 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": [
]
}

View File

@@ -1,17 +0,0 @@
{
// 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>/**"]
}
]
}

View File

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

View File

@@ -1,25 +0,0 @@
{
// 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": []
}
]
}

View File

@@ -1,96 +0,0 @@
# 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

@@ -1,11 +0,0 @@
{
"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

@@ -1,17 +0,0 @@
{
"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

@@ -1,12 +0,0 @@
{
"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

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

View File

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

View File

@@ -1,43 +0,0 @@
{
"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"
}
}

View File

@@ -1,23 +0,0 @@
# 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

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

View File

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

View File

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

View File

@@ -1,67 +0,0 @@
{
"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

@@ -1,44 +0,0 @@
<!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.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -1,31 +0,0 @@
{
"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

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

View File

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

View File

@@ -1,57 +0,0 @@
@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

@@ -1,18 +0,0 @@
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

@@ -1,168 +0,0 @@
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

@@ -1,164 +0,0 @@
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

@@ -1,49 +0,0 @@
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

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

View File

@@ -1,141 +0,0 @@
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

@@ -1,101 +0,0 @@
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

@@ -1,107 +0,0 @@
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

@@ -1,115 +0,0 @@
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

@@ -1,170 +0,0 @@
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

@@ -1,108 +0,0 @@
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

@@ -1,58 +0,0 @@
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

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

View File

@@ -1,221 +0,0 @@
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

@@ -1,62 +0,0 @@
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

@@ -1,22 +0,0 @@
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

@@ -1,96 +0,0 @@
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

@@ -1,44 +0,0 @@
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

@@ -1,117 +0,0 @@
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

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

View File

@@ -1,48 +0,0 @@
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

@@ -1,63 +0,0 @@
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

@@ -1,41 +0,0 @@
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

@@ -1,66 +0,0 @@
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

@@ -1,16 +0,0 @@
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

@@ -1,22 +0,0 @@
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

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

View File

@@ -1,34 +0,0 @@
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

@@ -1,11 +0,0 @@
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

@@ -1,7 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -1,5 +0,0 @@
// 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

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

View File

@@ -1,7 +0,0 @@
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

@@ -1,36 +0,0 @@
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

@@ -1,18 +0,0 @@
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

@@ -1,33 +0,0 @@
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

@@ -1,19 +0,0 @@
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

@@ -1,68 +0,0 @@
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

@@ -1,25 +0,0 @@
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

@@ -1,25 +0,0 @@
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

@@ -1,40 +0,0 @@
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

@@ -1,349 +0,0 @@
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

View File

@@ -1,276 +0,0 @@
ID,name
1,AC/DC
2,Accept
3,Aerosmith
4,Alanis Morissette
5,Alice In Chains
6,Antônio Carlos Jobim
7,Apocalyptica
8,Audioslave
9,BackBeat
10,Billy Cobham
11,Black Label Society
12,Black Sabbath
13,Body Count
14,Bruce Dickinson
15,Buddy Guy
16,Caetano Veloso
17,Chico Buarque
18,Chico Science & Nação Zumbi
19,Cidade Negra
20,Cláudio Zoli
21,Various Artists
22,Led Zeppelin
23,Frank Zappa & Captain Beefheart
24,Marcos Valle
25,Milton Nascimento & Bebeto
26,Azymuth
27,Gilberto Gil
28,João Gilberto
29,Bebel Gilberto
30,Jorge Vercilo
31,Baby Consuelo
32,Ney Matogrosso
33,Luiz Melodia
34,Nando Reis
35,Pedro Luís & A Parede
36,O Rappa
37,Ed Motta
38,Banda Black Rio
39,Fernanda Porto
40,Os Cariocas
41,Elis Regina
42,Milton Nascimento
43,A Cor Do Som
44,Kid Abelha
45,Sandra De Sá
46,Jorge Ben
47,Hermeto Pascoal
48,Barão Vermelho
49,"Edson, DJ Marky & DJ Patife Featuring Fernanda Porto"
50,Metallica
51,Queen
52,Kiss
53,Spyro Gyra
54,Green Day
55,David Coverdale
56,Gonzaguinha
57,Os Mutantes
58,Deep Purple
59,Santana
60,Santana Feat. Dave Matthews
61,Santana Feat. Everlast
62,Santana Feat. Rob Thomas
63,Santana Feat. Lauryn Hill & Cee-Lo
64,Santana Feat. The Project G&B
65,Santana Feat. Maná
66,Santana Feat. Eagle-Eye Cherry
67,Santana Feat. Eric Clapton
68,Miles Davis
69,Gene Krupa
70,Toquinho & Vinícius
71,Vinícius De Moraes & Baden Powell
72,Vinícius De Moraes
73,Vinícius E Qurteto Em Cy
74,Vinícius E Odette Lara
75,"Vinicius, Toquinho & Quarteto Em Cy"
76,Creedence Clearwater Revival
77,Cássia Eller
78,Def Leppard
79,Dennis Chambers
80,Djavan
81,Eric Clapton
82,Faith No More
83,Falamansa
84,Foo Fighters
85,Frank Sinatra
86,Funk Como Le Gusta
87,Godsmack
88,Guns N' Roses
89,Incognito
90,Iron Maiden
91,James Brown
92,Jamiroquai
93,JET
94,Jimi Hendrix
95,Joe Satriani
96,Jota Quest
97,João Suplicy
98,Judas Priest
99,Legião Urbana
100,Lenny Kravitz
101,Lulu Santos
102,Marillion
103,Marisa Monte
104,Marvin Gaye
105,Men At Work
106,Motörhead
107,Motörhead & Girlschool
108,Mônica Marianno
109,Mötley Crüe
110,Nirvana
111,O Terço
112,Olodum
113,Os Paralamas Do Sucesso
114,Ozzy Osbourne
115,Page & Plant
116,Passengers
117,Paul D'Ianno
118,Pearl Jam
119,Peter Tosh
120,Pink Floyd
121,Planet Hemp
122,R.E.M. Feat. Kate Pearson
123,R.E.M. Feat. KRS-One
124,R.E.M.
125,Raimundos
126,Raul Seixas
127,Red Hot Chili Peppers
128,Rush
129,Simply Red
130,Skank
131,Smashing Pumpkins
132,Soundgarden
133,Stevie Ray Vaughan & Double Trouble
134,Stone Temple Pilots
135,System Of A Down
136,"Terry Bozzio, Tony Levin & Steve Stevens"
137,The Black Crowes
138,The Clash
139,The Cult
140,The Doors
141,The Police
142,The Rolling Stones
143,The Tea Party
144,The Who
145,Tim Maia
146,Titãs
147,Battlestar Galactica
148,Heroes
149,Lost
150,U2
151,UB40
152,Van Halen
153,Velvet Revolver
154,Whitesnake
155,Zeca Pagodinho
156,The Office
157,Dread Zeppelin
158,Battlestar Galactica (Classic)
159,Aquaman
160,Christina Aguilera featuring BigElf
161,Aerosmith & Sierra Leone's Refugee Allstars
162,Los Lonely Boys
163,Corinne Bailey Rae
164,Dhani Harrison & Jakob Dylan
165,Jackson Browne
166,Avril Lavigne
167,Big & Rich
168,Youssou N'Dour
169,Black Eyed Peas
170,Jack Johnson
171,Ben Harper
172,Snow Patrol
173,Matisyahu
174,The Postal Service
175,Jaguares
176,The Flaming Lips
177,Jack's Mannequin & Mick Fleetwood
178,Regina Spektor
179,Scorpions
180,House Of Pain
181,Xis
182,Nega Gizza
183,Gustavo & Andres Veiga & Salazar
184,Rodox
185,Charlie Brown Jr.
186,Pedro Luís E A Parede
187,Los Hermanos
188,Mundo Livre S/A
189,Otto
190,Instituto
191,Nação Zumbi
192,DJ Dolores & Orchestra Santa Massa
193,Seu Jorge
194,Sabotage E Instituto
195,Stereo Maracana
196,Cake
197,Aisha Duo
198,Habib Koité and Bamada
199,Karsh Kale
200,The Posies
201,Luciana Souza/Romero Lubambo
202,Aaron Goldberg
203,Nicolaus Esterhazy Sinfonia
204,Temple of the Dog
205,Chris Cornell
206,Alberto Turco & Nova Schola Gregoriana
207,"Richard Marlow & The Choir of Trinity College, Cambridge"
208,English Concert & Trevor Pinnock
209,"Anne-Sophie Mutter, Herbert Von Karajan & Wiener Philharmoniker"
210,"Hilary Hahn, Jeffrey Kahane, Los Angeles Chamber Orchestra & Margaret Batjer"
211,Wilhelm Kempff
212,Yo-Yo Ma
213,Scholars Baroque Ensemble
214,Academy of St. Martin in the Fields & Sir Neville Marriner
215,Academy of St. Martin in the Fields Chamber Ensemble & Sir Neville Marriner
216,"Berliner Philharmoniker, Claudio Abbado & Sabine Meyer"
217,Royal Philharmonic Orchestra & Sir Thomas Beecham
218,Orchestre Révolutionnaire et Romantique & John Eliot Gardiner
219,"Britten Sinfonia, Ivor Bolton & Lesley Garrett"
220,"Chicago Symphony Chorus, Chicago Symphony Orchestra & Sir Georg Solti"
221,Sir Georg Solti & Wiener Philharmoniker
222,"Academy of St. Martin in the Fields, John Birch, Sir Neville Marriner & Sylvia McNair"
223,London Symphony Orchestra & Sir Charles Mackerras
224,Barry Wordsworth & BBC Concert Orchestra
225,"Herbert Von Karajan, Mirella Freni & Wiener Philharmoniker"
226,Eugene Ormandy
227,Luciano Pavarotti
228,Leonard Bernstein & New York Philharmonic
229,Boston Symphony Orchestra & Seiji Ozawa
230,Aaron Copland & London Symphony Orchestra
231,Ton Koopman
232,Sergei Prokofiev & Yuri Temirkanov
233,Chicago Symphony Orchestra & Fritz Reiner
234,Orchestra of The Age of Enlightenment
235,"Emanuel Ax, Eugene Ormandy & Philadelphia Orchestra"
236,James Levine
237,Berliner Philharmoniker & Hans Rosbaud
238,Maurizio Pollini
239,"Academy of St. Martin in the Fields, Sir Neville Marriner & William Bennett"
240,Gustav Mahler
241,"Felix Schmidt, London Symphony Orchestra & Rafael Frühbeck de Burgos"
242,Edo de Waart & San Francisco Symphony
243,Antal Doráti & London Symphony Orchestra
244,Choir Of Westminster Abbey & Simon Preston
245,Michael Tilson Thomas & San Francisco Symphony
246,"Chor der Wiener Staatsoper, Herbert Von Karajan & Wiener Philharmoniker"
247,The King's Singers
248,Berliner Philharmoniker & Herbert Von Karajan
249,"Sir Georg Solti, Sumi Jo & Wiener Philharmoniker"
250,Christopher O'Riley
251,Fretwork
252,Amy Winehouse
253,Calexico
254,Otto Klemperer & Philharmonia Orchestra
255,Yehudi Menuhin
256,Philharmonia Orchestra & Sir Neville Marriner
257,"Academy of St. Martin in the Fields, Sir Neville Marriner & Thurston Dart"
258,Les Arts Florissants & William Christie
259,The 12 Cellists of The Berlin Philharmonic
260,Adrian Leaper & Doreen de Feis
261,"Roger Norrington, London Classical Players"
262,Charles Dutoit & L'Orchestre Symphonique de Montréal
263,"Equale Brass Ensemble, John Eliot Gardiner & Munich Monteverdi Orchestra and Choir"
264,Kent Nagano and Orchestre de l'Opéra de Lyon
265,Julian Bream
266,Martin Roscoe
267,Göteborgs Symfoniker & Neeme Järvi
268,Itzhak Perlman
269,Michele Campanella
270,Gerald Moore
271,"Mela Tenenbaum, Pro Musica Prague & Richard Kapp"
272,Emerson String Quartet
273,"C. Monteverdi, Nigel Rogers - Chiaroscuro; London Baroque; London Cornett & Sackbu"
274,Nash Ensemble
275,Philip Glass Ensemble
1 ID name
2 1 AC/DC
3 2 Accept
4 3 Aerosmith
5 4 Alanis Morissette
6 5 Alice In Chains
7 6 Antônio Carlos Jobim
8 7 Apocalyptica
9 8 Audioslave
10 9 BackBeat
11 10 Billy Cobham
12 11 Black Label Society
13 12 Black Sabbath
14 13 Body Count
15 14 Bruce Dickinson
16 15 Buddy Guy
17 16 Caetano Veloso
18 17 Chico Buarque
19 18 Chico Science & Nação Zumbi
20 19 Cidade Negra
21 20 Cláudio Zoli
22 21 Various Artists
23 22 Led Zeppelin
24 23 Frank Zappa & Captain Beefheart
25 24 Marcos Valle
26 25 Milton Nascimento & Bebeto
27 26 Azymuth
28 27 Gilberto Gil
29 28 João Gilberto
30 29 Bebel Gilberto
31 30 Jorge Vercilo
32 31 Baby Consuelo
33 32 Ney Matogrosso
34 33 Luiz Melodia
35 34 Nando Reis
36 35 Pedro Luís & A Parede
37 36 O Rappa
38 37 Ed Motta
39 38 Banda Black Rio
40 39 Fernanda Porto
41 40 Os Cariocas
42 41 Elis Regina
43 42 Milton Nascimento
44 43 A Cor Do Som
45 44 Kid Abelha
46 45 Sandra De Sá
47 46 Jorge Ben
48 47 Hermeto Pascoal
49 48 Barão Vermelho
50 49 Edson, DJ Marky & DJ Patife Featuring Fernanda Porto
51 50 Metallica
52 51 Queen
53 52 Kiss
54 53 Spyro Gyra
55 54 Green Day
56 55 David Coverdale
57 56 Gonzaguinha
58 57 Os Mutantes
59 58 Deep Purple
60 59 Santana
61 60 Santana Feat. Dave Matthews
62 61 Santana Feat. Everlast
63 62 Santana Feat. Rob Thomas
64 63 Santana Feat. Lauryn Hill & Cee-Lo
65 64 Santana Feat. The Project G&B
66 65 Santana Feat. Maná
67 66 Santana Feat. Eagle-Eye Cherry
68 67 Santana Feat. Eric Clapton
69 68 Miles Davis
70 69 Gene Krupa
71 70 Toquinho & Vinícius
72 71 Vinícius De Moraes & Baden Powell
73 72 Vinícius De Moraes
74 73 Vinícius E Qurteto Em Cy
75 74 Vinícius E Odette Lara
76 75 Vinicius, Toquinho & Quarteto Em Cy
77 76 Creedence Clearwater Revival
78 77 Cássia Eller
79 78 Def Leppard
80 79 Dennis Chambers
81 80 Djavan
82 81 Eric Clapton
83 82 Faith No More
84 83 Falamansa
85 84 Foo Fighters
86 85 Frank Sinatra
87 86 Funk Como Le Gusta
88 87 Godsmack
89 88 Guns N' Roses
90 89 Incognito
91 90 Iron Maiden
92 91 James Brown
93 92 Jamiroquai
94 93 JET
95 94 Jimi Hendrix
96 95 Joe Satriani
97 96 Jota Quest
98 97 João Suplicy
99 98 Judas Priest
100 99 Legião Urbana
101 100 Lenny Kravitz
102 101 Lulu Santos
103 102 Marillion
104 103 Marisa Monte
105 104 Marvin Gaye
106 105 Men At Work
107 106 Motörhead
108 107 Motörhead & Girlschool
109 108 Mônica Marianno
110 109 Mötley Crüe
111 110 Nirvana
112 111 O Terço
113 112 Olodum
114 113 Os Paralamas Do Sucesso
115 114 Ozzy Osbourne
116 115 Page & Plant
117 116 Passengers
118 117 Paul D'Ianno
119 118 Pearl Jam
120 119 Peter Tosh
121 120 Pink Floyd
122 121 Planet Hemp
123 122 R.E.M. Feat. Kate Pearson
124 123 R.E.M. Feat. KRS-One
125 124 R.E.M.
126 125 Raimundos
127 126 Raul Seixas
128 127 Red Hot Chili Peppers
129 128 Rush
130 129 Simply Red
131 130 Skank
132 131 Smashing Pumpkins
133 132 Soundgarden
134 133 Stevie Ray Vaughan & Double Trouble
135 134 Stone Temple Pilots
136 135 System Of A Down
137 136 Terry Bozzio, Tony Levin & Steve Stevens
138 137 The Black Crowes
139 138 The Clash
140 139 The Cult
141 140 The Doors
142 141 The Police
143 142 The Rolling Stones
144 143 The Tea Party
145 144 The Who
146 145 Tim Maia
147 146 Titãs
148 147 Battlestar Galactica
149 148 Heroes
150 149 Lost
151 150 U2
152 151 UB40
153 152 Van Halen
154 153 Velvet Revolver
155 154 Whitesnake
156 155 Zeca Pagodinho
157 156 The Office
158 157 Dread Zeppelin
159 158 Battlestar Galactica (Classic)
160 159 Aquaman
161 160 Christina Aguilera featuring BigElf
162 161 Aerosmith & Sierra Leone's Refugee Allstars
163 162 Los Lonely Boys
164 163 Corinne Bailey Rae
165 164 Dhani Harrison & Jakob Dylan
166 165 Jackson Browne
167 166 Avril Lavigne
168 167 Big & Rich
169 168 Youssou N'Dour
170 169 Black Eyed Peas
171 170 Jack Johnson
172 171 Ben Harper
173 172 Snow Patrol
174 173 Matisyahu
175 174 The Postal Service
176 175 Jaguares
177 176 The Flaming Lips
178 177 Jack's Mannequin & Mick Fleetwood
179 178 Regina Spektor
180 179 Scorpions
181 180 House Of Pain
182 181 Xis
183 182 Nega Gizza
184 183 Gustavo & Andres Veiga & Salazar
185 184 Rodox
186 185 Charlie Brown Jr.
187 186 Pedro Luís E A Parede
188 187 Los Hermanos
189 188 Mundo Livre S/A
190 189 Otto
191 190 Instituto
192 191 Nação Zumbi
193 192 DJ Dolores & Orchestra Santa Massa
194 193 Seu Jorge
195 194 Sabotage E Instituto
196 195 Stereo Maracana
197 196 Cake
198 197 Aisha Duo
199 198 Habib Koité and Bamada
200 199 Karsh Kale
201 200 The Posies
202 201 Luciana Souza/Romero Lubambo
203 202 Aaron Goldberg
204 203 Nicolaus Esterhazy Sinfonia
205 204 Temple of the Dog
206 205 Chris Cornell
207 206 Alberto Turco & Nova Schola Gregoriana
208 207 Richard Marlow & The Choir of Trinity College, Cambridge
209 208 English Concert & Trevor Pinnock
210 209 Anne-Sophie Mutter, Herbert Von Karajan & Wiener Philharmoniker
211 210 Hilary Hahn, Jeffrey Kahane, Los Angeles Chamber Orchestra & Margaret Batjer
212 211 Wilhelm Kempff
213 212 Yo-Yo Ma
214 213 Scholars Baroque Ensemble
215 214 Academy of St. Martin in the Fields & Sir Neville Marriner
216 215 Academy of St. Martin in the Fields Chamber Ensemble & Sir Neville Marriner
217 216 Berliner Philharmoniker, Claudio Abbado & Sabine Meyer
218 217 Royal Philharmonic Orchestra & Sir Thomas Beecham
219 218 Orchestre Révolutionnaire et Romantique & John Eliot Gardiner
220 219 Britten Sinfonia, Ivor Bolton & Lesley Garrett
221 220 Chicago Symphony Chorus, Chicago Symphony Orchestra & Sir Georg Solti
222 221 Sir Georg Solti & Wiener Philharmoniker
223 222 Academy of St. Martin in the Fields, John Birch, Sir Neville Marriner & Sylvia McNair
224 223 London Symphony Orchestra & Sir Charles Mackerras
225 224 Barry Wordsworth & BBC Concert Orchestra
226 225 Herbert Von Karajan, Mirella Freni & Wiener Philharmoniker
227 226 Eugene Ormandy
228 227 Luciano Pavarotti
229 228 Leonard Bernstein & New York Philharmonic
230 229 Boston Symphony Orchestra & Seiji Ozawa
231 230 Aaron Copland & London Symphony Orchestra
232 231 Ton Koopman
233 232 Sergei Prokofiev & Yuri Temirkanov
234 233 Chicago Symphony Orchestra & Fritz Reiner
235 234 Orchestra of The Age of Enlightenment
236 235 Emanuel Ax, Eugene Ormandy & Philadelphia Orchestra
237 236 James Levine
238 237 Berliner Philharmoniker & Hans Rosbaud
239 238 Maurizio Pollini
240 239 Academy of St. Martin in the Fields, Sir Neville Marriner & William Bennett
241 240 Gustav Mahler
242 241 Felix Schmidt, London Symphony Orchestra & Rafael Frühbeck de Burgos
243 242 Edo de Waart & San Francisco Symphony
244 243 Antal Doráti & London Symphony Orchestra
245 244 Choir Of Westminster Abbey & Simon Preston
246 245 Michael Tilson Thomas & San Francisco Symphony
247 246 Chor der Wiener Staatsoper, Herbert Von Karajan & Wiener Philharmoniker
248 247 The King's Singers
249 248 Berliner Philharmoniker & Herbert Von Karajan
250 249 Sir Georg Solti, Sumi Jo & Wiener Philharmoniker
251 250 Christopher O'Riley
252 251 Fretwork
253 252 Amy Winehouse
254 253 Calexico
255 254 Otto Klemperer & Philharmonia Orchestra
256 255 Yehudi Menuhin
257 256 Philharmonia Orchestra & Sir Neville Marriner
258 257 Academy of St. Martin in the Fields, Sir Neville Marriner & Thurston Dart
259 258 Les Arts Florissants & William Christie
260 259 The 12 Cellists of The Berlin Philharmonic
261 260 Adrian Leaper & Doreen de Feis
262 261 Roger Norrington, London Classical Players
263 262 Charles Dutoit & L'Orchestre Symphonique de Montréal
264 263 Equale Brass Ensemble, John Eliot Gardiner & Munich Monteverdi Orchestra and Choir
265 264 Kent Nagano and Orchestre de l'Opéra de Lyon
266 265 Julian Bream
267 266 Martin Roscoe
268 267 Göteborgs Symfoniker & Neeme Järvi
269 268 Itzhak Perlman
270 269 Michele Campanella
271 270 Gerald Moore
272 271 Mela Tenenbaum, Pro Musica Prague & Richard Kapp
273 272 Emerson String Quartet
274 273 C. Monteverdi, Nigel Rogers - Chiaroscuro; London Baroque; London Cornett & Sackbu
275 274 Nash Ensemble
276 275 Philip Glass Ensemble

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